diff --git a/src/main/java/com/cloudhandson/vpdbackoffice/domain/schema/SchemaActionResult.java b/src/main/java/com/cloudhandson/vpdbackoffice/domain/schema/SchemaActionResult.java new file mode 100644 index 0000000..cbc4958 --- /dev/null +++ b/src/main/java/com/cloudhandson/vpdbackoffice/domain/schema/SchemaActionResult.java @@ -0,0 +1,9 @@ +package com.cloudhandson.vpdbackoffice.domain.schema; + +public record SchemaActionResult( + String objectName, + String action, + String status, + String message +) { +} diff --git a/src/main/java/com/cloudhandson/vpdbackoffice/service/BackofficeSchemaService.java b/src/main/java/com/cloudhandson/vpdbackoffice/service/BackofficeSchemaService.java new file mode 100644 index 0000000..49be87e --- /dev/null +++ b/src/main/java/com/cloudhandson/vpdbackoffice/service/BackofficeSchemaService.java @@ -0,0 +1,207 @@ +package com.cloudhandson.vpdbackoffice.service; + +import com.cloudhandson.vpdbackoffice.config.BackofficeProperties; +import com.cloudhandson.vpdbackoffice.domain.schema.SchemaActionResult; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class BackofficeSchemaService { + + private final JdbcTemplate jdbcTemplate; + private final BackofficeProperties properties; + + public BackofficeSchemaService(JdbcTemplate jdbcTemplate, BackofficeProperties properties) { + this.jdbcTemplate = jdbcTemplate; + this.properties = properties; + } + + @Transactional + public List initializeSchema() { + List results = new ArrayList<>(); + createSequence(results, "cb_agent_bearer_key_seq", 1000); + createSequence(results, "cb_permission_seq", 1000); + createSequence(results, "cb_permission_rule_seq", 10000); + createSequence(results, "cb_ords_probe_audit_seq", 1); + + createTable(results, "cb_app_user", """ + CREATE TABLE cb_app_user ( + user_id NUMBER PRIMARY KEY, + user_name VARCHAR2(100) NOT NULL UNIQUE, + employee_no VARCHAR2(50), + dept_code VARCHAR2(50), + can_read_contents CHAR(1) DEFAULT 'N' CHECK (can_read_contents IN ('Y','N')) NOT NULL, + active CHAR(1) DEFAULT 'Y' CHECK (active IN ('Y','N')) NOT NULL + ) + """); + createTable(results, "cb_app_role", """ + CREATE TABLE cb_app_role ( + role_id NUMBER PRIMARY KEY, + role_name VARCHAR2(100) NOT NULL UNIQUE + ) + """); + createTable(results, "cb_user_role", """ + CREATE TABLE cb_user_role ( + user_id NUMBER NOT NULL, + role_id NUMBER NOT NULL, + CONSTRAINT cb_user_role_pk PRIMARY KEY (user_id, role_id) + ) + """); + createTable(results, "cb_permission", """ + CREATE TABLE cb_permission ( + perm_id NUMBER PRIMARY KEY, + role_id NUMBER NOT NULL, + target_name VARCHAR2(128) NOT NULL, + action_name VARCHAR2(30) DEFAULT 'SELECT' NOT NULL + ) + """); + createTable(results, "cb_permission_rule", """ + CREATE TABLE cb_permission_rule ( + rule_id NUMBER PRIMARY KEY, + perm_id NUMBER NOT NULL, + rule_column VARCHAR2(128), + rule_type VARCHAR2(30) NOT NULL, + rule_value VARCHAR2(4000) + ) + """); + addColumn(results, "cb_permission_rule", "rule_column", "ALTER TABLE cb_permission_rule ADD (rule_column VARCHAR2(128))"); + createTable(results, "cb_permission_column", """ + CREATE TABLE cb_permission_column ( + permission_id NUMBER NOT NULL, + column_name VARCHAR2(128) NOT NULL, + CONSTRAINT cb_permission_column_pk PRIMARY KEY (permission_id, column_name) + ) + """); + createTable(results, "cb_agent_bearer_key", """ + CREATE TABLE cb_agent_bearer_key ( + key_id NUMBER PRIMARY KEY, + user_id NUMBER NOT NULL, + key_prefix VARCHAR2(30) NOT NULL, + key_hash VARCHAR2(128) NOT NULL UNIQUE, + expires_at TIMESTAMP NOT NULL, + revoked_at TIMESTAMP, + description VARCHAR2(200) + ) + """); + addColumn(results, "cb_agent_bearer_key", "description", + "ALTER TABLE cb_agent_bearer_key ADD (description VARCHAR2(200))"); + createTable(results, "cb_protected_object", """ + CREATE TABLE cb_protected_object ( + object_id NUMBER PRIMARY KEY, + owner VARCHAR2(128) NOT NULL, + object_name VARCHAR2(128) NOT NULL UNIQUE, + ords_path VARCHAR2(300) NOT NULL, + enabled_yn CHAR(1) DEFAULT 'Y' CHECK (enabled_yn IN ('Y','N')) NOT NULL + ) + """); + createTable(results, "cb_protected_column", """ + CREATE TABLE cb_protected_column ( + column_id NUMBER PRIMARY KEY, + object_id NUMBER NOT NULL, + column_name VARCHAR2(128) NOT NULL, + sensitive_yn CHAR(1) DEFAULT 'N' CHECK (sensitive_yn IN ('Y','N')) NOT NULL, + visible_role_id NUMBER, + CONSTRAINT cb_protected_column_uk UNIQUE (object_id, column_name) + ) + """); + createTable(results, "cb_ords_probe_audit", """ + CREATE TABLE cb_ords_probe_audit ( + audit_id NUMBER PRIMARY KEY, + event_type VARCHAR2(50) NOT NULL, + key_id NUMBER, + object_id NUMBER, + status VARCHAR2(50), + row_count NUMBER, + error_code VARCHAR2(100), + message VARCHAR2(500), + created_at TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL + ) + """); + createTable(results, "cb_backoffice_setting", """ + CREATE TABLE cb_backoffice_setting ( + setting_key VARCHAR2(100) PRIMARY KEY, + setting_value VARCHAR2(1000), + updated_at TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL + ) + """); + seedDefaultSettings(results); + return results; + } + + private void createSequence(List results, String name, int startWith) { + runIgnoringAlreadyExists(results, name, "SEQUENCE", + "CREATE SEQUENCE " + name + " START WITH " + startWith + " INCREMENT BY 1 NOCACHE"); + } + + private void createTable(List results, String name, String ddl) { + runIgnoringAlreadyExists(results, name, "TABLE", ddl); + } + + private void addColumn(List results, String table, String column, String ddl) { + try { + jdbcTemplate.execute(ddl); + results.add(new SchemaActionResult(table + "." + column, "COLUMN", "CREATED", "컬럼을 추가했습니다.")); + } catch (RuntimeException exception) { + if (oracleErrorCode(exception) == 1430) { + results.add(new SchemaActionResult(table + "." + column, "COLUMN", "EXISTS", "이미 존재합니다.")); + return; + } + results.add(new SchemaActionResult(table + "." + column, "COLUMN", "FAILED", safeMessage(exception))); + throw exception; + } + } + + private void runIgnoringAlreadyExists(List results, String name, String action, String ddl) { + try { + jdbcTemplate.execute(ddl); + results.add(new SchemaActionResult(name, action, "CREATED", "생성했습니다.")); + } catch (RuntimeException exception) { + if (oracleErrorCode(exception) == 955) { + results.add(new SchemaActionResult(name, action, "EXISTS", "이미 존재합니다.")); + return; + } + results.add(new SchemaActionResult(name, action, "FAILED", safeMessage(exception))); + throw exception; + } + } + + private void seedDefaultSettings(List results) { + jdbcTemplate.update(""" + MERGE INTO cb_backoffice_setting dst + USING ( + SELECT 'ORDS_BASE_URL' setting_key, ? setting_value + FROM dual + ) src + ON (dst.setting_key = src.setting_key) + WHEN MATCHED THEN UPDATE SET dst.setting_value = src.setting_value, dst.updated_at = SYSTIMESTAMP + WHERE dst.setting_value IS NULL OR TRIM(dst.setting_value) IS NULL + WHEN NOT MATCHED THEN INSERT (setting_key, setting_value, updated_at) + VALUES (src.setting_key, src.setting_value, SYSTIMESTAMP) + """, properties.ords().baseUrl()); + results.add(new SchemaActionResult("ORDS_BASE_URL", "SETTING", "MERGED", "기본 ORDS URL 설정을 보강했습니다.")); + } + + private int oracleErrorCode(RuntimeException exception) { + Throwable current = exception; + while (current != null) { + if (current instanceof SQLException sqlException) { + return Math.abs(sqlException.getErrorCode()); + } + current = current.getCause(); + } + return 0; + } + + private String safeMessage(Throwable throwable) { + String message = throwable.getMessage(); + if (message == null || message.isBlank()) { + return throwable.getClass().getSimpleName(); + } + String normalized = message.replaceAll("\\s+", " ").trim(); + return normalized.length() <= 300 ? normalized : normalized.substring(0, 300) + "..."; + } +} diff --git a/src/main/java/com/cloudhandson/vpdbackoffice/web/SettingController.java b/src/main/java/com/cloudhandson/vpdbackoffice/web/SettingController.java index 3a65afc..523641b 100644 --- a/src/main/java/com/cloudhandson/vpdbackoffice/web/SettingController.java +++ b/src/main/java/com/cloudhandson/vpdbackoffice/web/SettingController.java @@ -1,6 +1,8 @@ package com.cloudhandson.vpdbackoffice.web; +import com.cloudhandson.vpdbackoffice.service.BackofficeSchemaService; import com.cloudhandson.vpdbackoffice.service.SettingService; +import org.springframework.dao.DataAccessException; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; @@ -12,14 +14,23 @@ import org.springframework.web.servlet.mvc.support.RedirectAttributes; public class SettingController { private final SettingService settingService; + private final BackofficeSchemaService backofficeSchemaService; - public SettingController(SettingService settingService) { + public SettingController(SettingService settingService, BackofficeSchemaService backofficeSchemaService) { this.settingService = settingService; + this.backofficeSchemaService = backofficeSchemaService; } @GetMapping("/settings") public String settings(Model model) { - model.addAttribute("ordsBaseUrl", settingService.ordsBaseUrl()); + try { + model.addAttribute("ordsBaseUrl", settingService.ordsBaseUrl()); + } catch (DataAccessException exception) { + RuntimeErrorMessage error = RuntimeErrorMessages.dataAccess(exception); + model.addAttribute("ordsBaseUrl", ""); + model.addAttribute("runtimeErrorTitle", error.title()); + model.addAttribute("runtimeErrorMessage", error.message()); + } return "settings"; } @@ -32,4 +43,11 @@ public class SettingController { redirectAttributes.addFlashAttribute("message", "ORDS 설정을 저장했습니다."); return "redirect:/settings"; } + + @PostMapping("/settings/schema/initialize") + public String initializeSchema(RedirectAttributes redirectAttributes) { + redirectAttributes.addFlashAttribute("schemaResults", backofficeSchemaService.initializeSchema()); + redirectAttributes.addFlashAttribute("message", "백오피스 지원 테이블 DDL을 실행했습니다."); + return "redirect:/settings"; + } } diff --git a/src/main/resources/templates/settings.html b/src/main/resources/templates/settings.html index f63d909..f61b7dd 100644 --- a/src/main/resources/templates/settings.html +++ b/src/main/resources/templates/settings.html @@ -10,6 +10,10 @@
+
+ DB 준비 필요 +
지원 테이블을 생성하세요.
+

ORDS

@@ -22,6 +26,41 @@
+ +
+

DB 스키마 초기화

+

+ 현재 연결된 DB에 백오피스가 사용하는 지원 테이블과 시퀀스를 생성하거나 보강합니다. +

+
+ + +
+
+ + + + + + + + + + + + + + + + + +
ObjectActionStatusMessage
objectTABLE + CREATED + 생성했습니다.
+
+