[Developer] #424 add backoffice schema initialization action
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.schema;
|
||||
|
||||
public record SchemaActionResult(
|
||||
String objectName,
|
||||
String action,
|
||||
String status,
|
||||
String message
|
||||
) {
|
||||
}
|
||||
@@ -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<SchemaActionResult> initializeSchema() {
|
||||
List<SchemaActionResult> 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<SchemaActionResult> results, String name, int startWith) {
|
||||
runIgnoringAlreadyExists(results, name, "SEQUENCE",
|
||||
"CREATE SEQUENCE " + name + " START WITH " + startWith + " INCREMENT BY 1 NOCACHE");
|
||||
}
|
||||
|
||||
private void createTable(List<SchemaActionResult> results, String name, String ddl) {
|
||||
runIgnoringAlreadyExists(results, name, "TABLE", ddl);
|
||||
}
|
||||
|
||||
private void addColumn(List<SchemaActionResult> 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<SchemaActionResult> 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<SchemaActionResult> 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) + "...";
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,10 @@
|
||||
</div>
|
||||
|
||||
<div class="alert alert-success" th:if="${message}" th:text="${message}"></div>
|
||||
<div class="alert alert-warning" th:if="${runtimeErrorMessage}">
|
||||
<strong th:text="${runtimeErrorTitle}">DB 준비 필요</strong>
|
||||
<div th:text="${runtimeErrorMessage}">지원 테이블을 생성하세요.</div>
|
||||
</div>
|
||||
|
||||
<section class="content-band">
|
||||
<h2>ORDS</h2>
|
||||
@@ -22,6 +26,41 @@
|
||||
<button class="btn rw-btn-primary" type="submit">저장</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="content-band mt-3">
|
||||
<h2>DB 스키마 초기화</h2>
|
||||
<p class="text-muted mb-3">
|
||||
현재 연결된 DB에 백오피스가 사용하는 지원 테이블과 시퀀스를 생성하거나 보강합니다.
|
||||
</p>
|
||||
<form method="post" action="/settings/schema/initialize" class="inline-form">
|
||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
|
||||
<button class="btn btn-outline-primary" type="submit">지원 테이블 생성/확인</button>
|
||||
</form>
|
||||
<div class="table-responsive mt-3" th:if="${schemaResults}">
|
||||
<table class="table table-sm align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Object</th>
|
||||
<th>Action</th>
|
||||
<th>Status</th>
|
||||
<th>Message</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr th:each="result : ${schemaResults}">
|
||||
<td><code th:text="${result.objectName()}">object</code></td>
|
||||
<td th:text="${result.action()}">TABLE</td>
|
||||
<td>
|
||||
<span class="badge"
|
||||
th:classappend="${result.status() == 'CREATED' || result.status() == 'MERGED'} ? ' text-bg-success' : (${result.status() == 'EXISTS'} ? ' text-bg-secondary' : ' text-bg-danger')"
|
||||
th:text="${result.status()}">CREATED</span>
|
||||
</td>
|
||||
<td th:text="${result.message()}">생성했습니다.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user