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) + "..."; } }