From 4fa130cb8fe925e7544981b48d86a8ba584e8547 Mon Sep 17 00:00:00 2001 From: devmrko Date: Tue, 23 Jun 2026 15:27:17 +0900 Subject: [PATCH] [Developer] #424 generate object query ORDS handlers --- .../domain/ords/OrdsObjectHandlerResult.java | 9 ++ .../service/OrdsMetadataService.java | 148 +++++++++++++++++- .../web/ProtectedObjectController.java | 23 ++- src/main/resources/static/css/app.css | 11 ++ src/main/resources/templates/objects.html | 19 ++- 5 files changed, 202 insertions(+), 8 deletions(-) create mode 100644 src/main/java/com/cloudhandson/vpdbackoffice/domain/ords/OrdsObjectHandlerResult.java diff --git a/src/main/java/com/cloudhandson/vpdbackoffice/domain/ords/OrdsObjectHandlerResult.java b/src/main/java/com/cloudhandson/vpdbackoffice/domain/ords/OrdsObjectHandlerResult.java new file mode 100644 index 0000000..e9a7674 --- /dev/null +++ b/src/main/java/com/cloudhandson/vpdbackoffice/domain/ords/OrdsObjectHandlerResult.java @@ -0,0 +1,9 @@ +package com.cloudhandson.vpdbackoffice.domain.ords; + +public record OrdsObjectHandlerResult( + long objectId, + String ordsPath, + String moduleName, + String template +) { +} diff --git a/src/main/java/com/cloudhandson/vpdbackoffice/service/OrdsMetadataService.java b/src/main/java/com/cloudhandson/vpdbackoffice/service/OrdsMetadataService.java index 6bd8ed7..21726ca 100644 --- a/src/main/java/com/cloudhandson/vpdbackoffice/service/OrdsMetadataService.java +++ b/src/main/java/com/cloudhandson/vpdbackoffice/service/OrdsMetadataService.java @@ -2,6 +2,9 @@ package com.cloudhandson.vpdbackoffice.service; import com.cloudhandson.vpdbackoffice.domain.ords.OrdsHandlerUpdateCommand; import com.cloudhandson.vpdbackoffice.domain.ords.OrdsHandlerView; +import com.cloudhandson.vpdbackoffice.domain.ords.OrdsObjectHandlerResult; +import com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedColumn; +import com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedObject; import com.cloudhandson.vpdbackoffice.mapper.OrdsMetadataMapper; import java.util.List; import java.util.Locale; @@ -14,10 +17,16 @@ public class OrdsMetadataService { private final OrdsMetadataMapper mapper; private final JdbcTemplate jdbcTemplate; + private final ProtectedObjectService protectedObjectService; - public OrdsMetadataService(OrdsMetadataMapper mapper, JdbcTemplate jdbcTemplate) { + public OrdsMetadataService( + OrdsMetadataMapper mapper, + JdbcTemplate jdbcTemplate, + ProtectedObjectService protectedObjectService + ) { this.mapper = mapper; this.jdbcTemplate = jdbcTemplate; + this.protectedObjectService = protectedObjectService; } public List findHandlers() { @@ -71,4 +80,141 @@ public class OrdsMetadataService { END; """, handler.moduleName(), handler.template(), handler.method(), command.source()); } + + @Transactional + public OrdsObjectHandlerResult createObjectQueryHandler(long objectId) { + ProtectedObject object = protectedObjectService.assertEnabled(objectId); + List columns = protectedObjectService.findColumns(objectId).stream() + .map(ProtectedColumn::columnName) + .toList(); + if (columns.isEmpty()) { + throw new AppException("보호 객체 컬럼이 없어 ORDS 조회 Handler를 만들 수 없습니다."); + } + requireIdentifier(object.owner(), "owner"); + requireIdentifier(object.objectName(), "objectName"); + columns.forEach(column -> requireIdentifier(column, "column")); + + String currentUser = jdbcTemplate.queryForObject("SELECT USER FROM dual", String.class); + if (currentUser == null || !currentUser.equalsIgnoreCase("CB_ORDS")) { + throw new AppException("ORDS 조회 Handler 생성은 ORDS parsing schema(CB_ORDS)로 DB에 연결했을 때만 가능합니다. 현재 연결 사용자: " + + currentUser); + } + + String moduleName = "cb.object.query"; + String basePath = "cb-object-query/"; + String template = object.owner().toLowerCase(Locale.ROOT) + "/" + object.objectName().toLowerCase(Locale.ROOT); + String ordsPath = "cb-ords/" + basePath + template; + String source = objectQuerySource(object, columns); + + jdbcTemplate.update(""" + BEGIN + ORDS.DEFINE_MODULE( + p_module_name => ?, + p_base_path => ?, + p_items_per_page => 25, + p_status => 'PUBLISHED' + ); + ORDS.DEFINE_TEMPLATE( + p_module_name => ?, + p_pattern => ? + ); + ORDS.DEFINE_HANDLER( + p_module_name => ?, + p_pattern => ?, + p_method => 'POST', + p_source_type => ORDS.source_type_plsql, + p_source => ?, + p_items_per_page => 0 + ); + ORDS.DEFINE_PARAMETER( + p_module_name => ?, + p_pattern => ?, + p_method => 'POST', + p_name => 'Authorization', + p_bind_variable_name => 'auth_header', + p_source_type => 'HEADER', + p_param_type => 'STRING', + p_access_method => 'IN' + ); + ORDS.DEFINE_PARAMETER( + p_module_name => ?, + p_pattern => ?, + p_method => 'POST', + p_name => 'limit', + p_bind_variable_name => 'row_limit', + p_source_type => 'QUERY', + p_param_type => 'INT', + p_access_method => 'IN' + ); + COMMIT; + END; + """, + moduleName, basePath, + moduleName, template, + moduleName, template, source, + moduleName, template, + moduleName, template); + protectedObjectService.updateOrdsPath(object.objectId(), ordsPath); + return new OrdsObjectHandlerResult(object.objectId(), ordsPath, moduleName, template); + } + + private String objectQuerySource(ProtectedObject object, List columns) { + String jsonEntries = columns.stream() + .map(column -> "'" + column.toLowerCase(Locale.ROOT) + "' VALUE " + column) + .reduce((left, right) -> left + ",\n " + right) + .orElseThrow(); + String selectColumns = String.join(", ", columns); + return """ + DECLARE + v_auth VARCHAR2(4000); + v_bearer_key VARCHAR2(4000); + v_limit NUMBER := LEAST(GREATEST(NVL(:row_limit, 50), 1), 500); + v_rows_json CLOB; + BEGIN + v_auth := TRIM(:auth_header); + IF v_auth IS NULL OR LOWER(SUBSTR(v_auth, 1, 7)) != 'bearer ' THEN + RAISE_APPLICATION_ERROR(-20101, 'Authorization header must be Bearer '); + END IF; + v_bearer_key := TRIM(SUBSTR(v_auth, 8)); + + admin.cb_agent_ctx_pkg.clear_user; + admin.cb_agent_ctx_pkg.set_user_by_bearer(v_bearer_key); + + SELECT JSON_ARRAYAGG( + JSON_OBJECT( + %s + RETURNING CLOB + ) + RETURNING CLOB + ) + INTO v_rows_json + FROM ( + SELECT %s + FROM %s.%s + WHERE ROWNUM <= v_limit + ); + + IF v_rows_json IS NULL THEN + v_rows_json := '[]'; + END IF; + + :status_code := 200; + OWA_UTIL.MIME_HEADER('application/json', TRUE); + HTP.P(JSON_OBJECT('rows' VALUE v_rows_json FORMAT JSON RETURNING CLOB)); + admin.cb_agent_ctx_pkg.clear_user; + EXCEPTION + WHEN OTHERS THEN + admin.cb_agent_ctx_pkg.clear_user; + :status_code := 403; + OWA_UTIL.MIME_HEADER('application/json', TRUE); + HTP.P(JSON_OBJECT('error' VALUE SQLERRM RETURNING CLOB)); + END; + """.formatted(jsonEntries, selectColumns, object.owner(), object.objectName()); + } + + private void requireIdentifier(String value, String label) { + if (value == null || !value.matches("[A-Z][A-Z0-9_$#]*")) { + throw new AppException("ORDS Handler 생성에 사용할 수 없는 " + label + " 식별자입니다: " + value); + } + } } diff --git a/src/main/java/com/cloudhandson/vpdbackoffice/web/ProtectedObjectController.java b/src/main/java/com/cloudhandson/vpdbackoffice/web/ProtectedObjectController.java index 65f2abe..abc7927 100644 --- a/src/main/java/com/cloudhandson/vpdbackoffice/web/ProtectedObjectController.java +++ b/src/main/java/com/cloudhandson/vpdbackoffice/web/ProtectedObjectController.java @@ -2,7 +2,9 @@ package com.cloudhandson.vpdbackoffice.web; import com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedObjectCreateCommand; import com.cloudhandson.vpdbackoffice.service.AppException; +import com.cloudhandson.vpdbackoffice.service.OrdsMetadataService; import com.cloudhandson.vpdbackoffice.service.ProtectedObjectService; +import org.springframework.dao.DataAccessException; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; @@ -14,9 +16,14 @@ import org.springframework.web.servlet.mvc.support.RedirectAttributes; public class ProtectedObjectController { private final ProtectedObjectService protectedObjectService; + private final OrdsMetadataService ordsMetadataService; - public ProtectedObjectController(ProtectedObjectService protectedObjectService) { + public ProtectedObjectController( + ProtectedObjectService protectedObjectService, + OrdsMetadataService ordsMetadataService + ) { this.protectedObjectService = protectedObjectService; + this.ordsMetadataService = ordsMetadataService; } @GetMapping("/objects") @@ -60,6 +67,20 @@ public class ProtectedObjectController { return "redirect:/objects"; } + @PostMapping("/objects/ords-handler") + public String createOrdsHandler(@RequestParam long objectId, RedirectAttributes redirectAttributes) { + try { + var result = ordsMetadataService.createObjectQueryHandler(objectId); + redirectAttributes.addFlashAttribute("message", "ORDS 조회 Handler를 생성했습니다: " + result.ordsPath()); + } catch (AppException exception) { + redirectAttributes.addFlashAttribute("errorMessage", exception.getMessage()); + } catch (DataAccessException exception) { + RuntimeErrorMessage error = RuntimeErrorMessages.dataAccess(exception); + redirectAttributes.addFlashAttribute("errorMessage", error.message()); + } + return "redirect:/objects"; + } + @PostMapping("/objects/disable") public String disable(@RequestParam long objectId, RedirectAttributes redirectAttributes) { protectedObjectService.disableObject(objectId); diff --git a/src/main/resources/static/css/app.css b/src/main/resources/static/css/app.css index ceb7a68..f00066b 100644 --- a/src/main/resources/static/css/app.css +++ b/src/main/resources/static/css/app.css @@ -143,6 +143,17 @@ body { display: inline; } +.action-stack { + align-items: center; + display: flex; + flex-wrap: wrap; + gap: .4rem; +} + +.action-stack .inline-form { + display: inline-flex; +} + .switch-field { align-items: center; display: flex; diff --git a/src/main/resources/templates/objects.html b/src/main/resources/templates/objects.html index fa55120..cb3058d 100644 --- a/src/main/resources/templates/objects.html +++ b/src/main/resources/templates/objects.html @@ -61,7 +61,7 @@ Owner Object ORDS Path - + Action @@ -78,11 +78,18 @@ -
- - - -
+
+
+ + + +
+
+ + + +
+