@@ -21,7 +21,7 @@ export ADB_PASSWORD="" # 비워두면
|
|||||||
# export ADB_CONN="${ADB_USER}/${ADB_PASSWORD}@${ADB_TNS}"
|
# export ADB_CONN="${ADB_USER}/${ADB_PASSWORD}@${ADB_TNS}"
|
||||||
|
|
||||||
# --- (2b) Spring Boot 백오피스 ---
|
# --- (2b) Spring Boot 백오피스 ---
|
||||||
export BACKOFFICE_ORDS_BASE_URL="" # 예: https://<actual-ords-domain>/ords
|
export BACKOFFICE_ORDS_BASE_URL="https://yh0olybn5pqce4n-d8aukro81636mon0.adb.ap-seoul-1.oraclecloudapps.com/ords"
|
||||||
export BACKOFFICE_ORDS_TIMEOUT_SECONDS="10"
|
export BACKOFFICE_ORDS_TIMEOUT_SECONDS="10"
|
||||||
|
|
||||||
# --- (3) 데모용 ADB 엔드유저 비밀번호 (sql/adb/07_end_users.sql 에서 사용) ---
|
# --- (3) 데모용 ADB 엔드유저 비밀번호 (sql/adb/07_end_users.sql 에서 사용) ---
|
||||||
|
|||||||
@@ -77,6 +77,34 @@ CREATE TABLE cb_ords_probe_audit (
|
|||||||
created_at TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL
|
created_at TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
|
PROMPT === Creating backoffice settings ===
|
||||||
|
BEGIN
|
||||||
|
EXECUTE IMMEDIATE '
|
||||||
|
CREATE TABLE cb_backoffice_setting (
|
||||||
|
setting_key VARCHAR2(100) PRIMARY KEY,
|
||||||
|
setting_value VARCHAR2(1000),
|
||||||
|
updated_at TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL
|
||||||
|
)';
|
||||||
|
EXCEPTION
|
||||||
|
WHEN OTHERS THEN
|
||||||
|
IF SQLCODE != -955 THEN
|
||||||
|
RAISE;
|
||||||
|
END IF;
|
||||||
|
END;
|
||||||
|
/
|
||||||
|
|
||||||
|
MERGE INTO cb_backoffice_setting dst
|
||||||
|
USING (
|
||||||
|
SELECT 'ORDS_BASE_URL' setting_key,
|
||||||
|
'https://yh0olybn5pqce4n-d8aukro81636mon0.adb.ap-seoul-1.oraclecloudapps.com/ords' setting_value
|
||||||
|
FROM dual
|
||||||
|
) src
|
||||||
|
ON (dst.setting_key = src.setting_key)
|
||||||
|
WHEN MATCHED THEN UPDATE SET dst.setting_value = src.setting_value
|
||||||
|
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);
|
||||||
|
|
||||||
PROMPT === Seeding default VPD ORDS protected object ===
|
PROMPT === Seeding default VPD ORDS protected object ===
|
||||||
MERGE INTO cb_protected_object dst
|
MERGE INTO cb_protected_object dst
|
||||||
USING (
|
USING (
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.cloudhandson.vpdbackoffice.domain.setting;
|
||||||
|
|
||||||
|
public record BackofficeSetting(
|
||||||
|
String settingKey,
|
||||||
|
String settingValue
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.cloudhandson.vpdbackoffice.mapper;
|
||||||
|
|
||||||
|
import com.cloudhandson.vpdbackoffice.domain.setting.BackofficeSetting;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface SettingMapper {
|
||||||
|
|
||||||
|
BackofficeSetting findByKey(@Param("settingKey") String settingKey);
|
||||||
|
|
||||||
|
void upsert(@Param("settingKey") String settingKey, @Param("settingValue") String settingValue);
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.cloudhandson.vpdbackoffice.service;
|
package com.cloudhandson.vpdbackoffice.service;
|
||||||
|
|
||||||
import com.cloudhandson.vpdbackoffice.config.BackofficeProperties;
|
|
||||||
import com.cloudhandson.vpdbackoffice.domain.audit.AuditEvent;
|
import com.cloudhandson.vpdbackoffice.domain.audit.AuditEvent;
|
||||||
import com.cloudhandson.vpdbackoffice.domain.probe.ProbeCommand;
|
import com.cloudhandson.vpdbackoffice.domain.probe.ProbeCommand;
|
||||||
import com.cloudhandson.vpdbackoffice.domain.probe.ProbeResult;
|
import com.cloudhandson.vpdbackoffice.domain.probe.ProbeResult;
|
||||||
@@ -43,7 +42,7 @@ public class OrdsProbeService {
|
|||||||
private final ProbeErrorClassifier errorClassifier;
|
private final ProbeErrorClassifier errorClassifier;
|
||||||
private final RestTemplate ordsRestTemplate;
|
private final RestTemplate ordsRestTemplate;
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
private final BackofficeProperties properties;
|
private final SettingService settingService;
|
||||||
private final Clock clock;
|
private final Clock clock;
|
||||||
|
|
||||||
public OrdsProbeService(
|
public OrdsProbeService(
|
||||||
@@ -53,7 +52,7 @@ public class OrdsProbeService {
|
|||||||
ProbeErrorClassifier errorClassifier,
|
ProbeErrorClassifier errorClassifier,
|
||||||
RestTemplate ordsRestTemplate,
|
RestTemplate ordsRestTemplate,
|
||||||
ObjectMapper objectMapper,
|
ObjectMapper objectMapper,
|
||||||
BackofficeProperties properties,
|
SettingService settingService,
|
||||||
Clock clock
|
Clock clock
|
||||||
) {
|
) {
|
||||||
this.tokenService = tokenService;
|
this.tokenService = tokenService;
|
||||||
@@ -62,12 +61,13 @@ public class OrdsProbeService {
|
|||||||
this.errorClassifier = errorClassifier;
|
this.errorClassifier = errorClassifier;
|
||||||
this.ordsRestTemplate = ordsRestTemplate;
|
this.ordsRestTemplate = ordsRestTemplate;
|
||||||
this.objectMapper = objectMapper;
|
this.objectMapper = objectMapper;
|
||||||
this.properties = properties;
|
this.settingService = settingService;
|
||||||
this.clock = clock;
|
this.clock = clock;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ProbeResult runProbe(ProbeCommand command) {
|
public ProbeResult runProbe(ProbeCommand command) {
|
||||||
if (properties.ords().baseUrl() == null || properties.ords().baseUrl().isBlank()) {
|
String ordsBaseUrl = settingService.ordsBaseUrl();
|
||||||
|
if (ordsBaseUrl == null || ordsBaseUrl.isBlank()) {
|
||||||
return auditAndReturn(command, ProbeResult.blocked(
|
return auditAndReturn(command, ProbeResult.blocked(
|
||||||
ProbeStatus.ORDS_NOT_CONFIGURED,
|
ProbeStatus.ORDS_NOT_CONFIGURED,
|
||||||
ProbeStatus.ORDS_NOT_CONFIGURED.name(),
|
ProbeStatus.ORDS_NOT_CONFIGURED.name(),
|
||||||
@@ -100,7 +100,7 @@ public class OrdsProbeService {
|
|||||||
String requestHeaders = null;
|
String requestHeaders = null;
|
||||||
String requestPayload = null;
|
String requestPayload = null;
|
||||||
try {
|
try {
|
||||||
URI uri = buildUri(object.ordsPath(), command.limit());
|
URI uri = buildUri(ordsBaseUrl, object.ordsPath(), command.limit());
|
||||||
HttpHeaders headers = new HttpHeaders();
|
HttpHeaders headers = new HttpHeaders();
|
||||||
headers.setBearerAuth(command.bearerToken());
|
headers.setBearerAuth(command.bearerToken());
|
||||||
requestHeaders = prettyHeaders(maskedRequestHeaders(headers));
|
requestHeaders = prettyHeaders(maskedRequestHeaders(headers));
|
||||||
@@ -144,8 +144,7 @@ public class OrdsProbeService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private URI buildUri(String ordsPath, int limit) {
|
private URI buildUri(String baseUrl, String ordsPath, int limit) {
|
||||||
String baseUrl = properties.ords().baseUrl();
|
|
||||||
String path = ordsPath.startsWith("/") ? ordsPath.substring(1) : ordsPath;
|
String path = ordsPath.startsWith("/") ? ordsPath.substring(1) : ordsPath;
|
||||||
return UriComponentsBuilder.fromUriString(baseUrl)
|
return UriComponentsBuilder.fromUriString(baseUrl)
|
||||||
.path("/")
|
.path("/")
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package com.cloudhandson.vpdbackoffice.service;
|
||||||
|
|
||||||
|
import com.cloudhandson.vpdbackoffice.config.BackofficeProperties;
|
||||||
|
import com.cloudhandson.vpdbackoffice.domain.setting.BackofficeSetting;
|
||||||
|
import com.cloudhandson.vpdbackoffice.mapper.SettingMapper;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class SettingService {
|
||||||
|
|
||||||
|
public static final String ORDS_BASE_URL = "ORDS_BASE_URL";
|
||||||
|
|
||||||
|
private final SettingMapper settingMapper;
|
||||||
|
private final BackofficeProperties properties;
|
||||||
|
|
||||||
|
public SettingService(SettingMapper settingMapper, BackofficeProperties properties) {
|
||||||
|
this.settingMapper = settingMapper;
|
||||||
|
this.properties = properties;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String ordsBaseUrl() {
|
||||||
|
BackofficeSetting setting = settingMapper.findByKey(ORDS_BASE_URL);
|
||||||
|
if (setting != null && setting.settingValue() != null && !setting.settingValue().isBlank()) {
|
||||||
|
return normalizeUrl(setting.settingValue());
|
||||||
|
}
|
||||||
|
return normalizeUrl(properties.ords().baseUrl());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void updateOrdsBaseUrl(String value) {
|
||||||
|
if (value == null || value.isBlank()) {
|
||||||
|
throw new AppException("ORDS Base URL은 필수입니다.");
|
||||||
|
}
|
||||||
|
String normalized = normalizeUrl(value);
|
||||||
|
if (!normalized.startsWith("https://") && !normalized.startsWith("http://")) {
|
||||||
|
throw new AppException("ORDS Base URL은 http:// 또는 https://로 시작해야 합니다.");
|
||||||
|
}
|
||||||
|
settingMapper.upsert(ORDS_BASE_URL, normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizeUrl(String value) {
|
||||||
|
if (value == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
String trimmed = value.trim();
|
||||||
|
while (trimmed.endsWith("/") && trimmed.length() > "https://".length()) {
|
||||||
|
trimmed = trimmed.substring(0, trimmed.length() - 1);
|
||||||
|
}
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package com.cloudhandson.vpdbackoffice.web;
|
||||||
|
|
||||||
|
import com.cloudhandson.vpdbackoffice.service.SettingService;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.ui.Model;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
public class SettingController {
|
||||||
|
|
||||||
|
private final SettingService settingService;
|
||||||
|
|
||||||
|
public SettingController(SettingService settingService) {
|
||||||
|
this.settingService = settingService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/settings")
|
||||||
|
public String settings(Model model) {
|
||||||
|
model.addAttribute("ordsBaseUrl", settingService.ordsBaseUrl());
|
||||||
|
return "settings";
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/settings/ords")
|
||||||
|
public String updateOrds(
|
||||||
|
@RequestParam String ordsBaseUrl,
|
||||||
|
RedirectAttributes redirectAttributes
|
||||||
|
) {
|
||||||
|
settingService.updateOrdsBaseUrl(ordsBaseUrl);
|
||||||
|
redirectAttributes.addFlashAttribute("message", "ORDS 설정을 저장했습니다.");
|
||||||
|
return "redirect:/settings";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,5 +29,5 @@ backoffice:
|
|||||||
token:
|
token:
|
||||||
max-days: ${BACKOFFICE_TOKEN_MAX_DAYS:365}
|
max-days: ${BACKOFFICE_TOKEN_MAX_DAYS:365}
|
||||||
ords:
|
ords:
|
||||||
base-url: ${BACKOFFICE_ORDS_BASE_URL:}
|
base-url: ${BACKOFFICE_ORDS_BASE_URL:https://yh0olybn5pqce4n-d8aukro81636mon0.adb.ap-seoul-1.oraclecloudapps.com/ords}
|
||||||
timeout-seconds: ${BACKOFFICE_ORDS_TIMEOUT_SECONDS:10}
|
timeout-seconds: ${BACKOFFICE_ORDS_TIMEOUT_SECONDS:10}
|
||||||
|
|||||||
25
src/main/resources/mapper/SettingMapper.xml
Normal file
25
src/main/resources/mapper/SettingMapper.xml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||||
|
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
<mapper namespace="com.cloudhandson.vpdbackoffice.mapper.SettingMapper">
|
||||||
|
<select id="findByKey" resultType="com.cloudhandson.vpdbackoffice.domain.setting.BackofficeSetting">
|
||||||
|
SELECT setting_key, setting_value
|
||||||
|
FROM cb_backoffice_setting
|
||||||
|
WHERE setting_key = #{settingKey,jdbcType=VARCHAR}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<update id="upsert">
|
||||||
|
MERGE INTO cb_backoffice_setting dst
|
||||||
|
USING (
|
||||||
|
SELECT #{settingKey,jdbcType=VARCHAR} setting_key,
|
||||||
|
#{settingValue,jdbcType=VARCHAR} 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
|
||||||
|
WHEN NOT MATCHED THEN INSERT (setting_key, setting_value, updated_at)
|
||||||
|
VALUES (src.setting_key, src.setting_value, SYSTIMESTAMP)
|
||||||
|
</update>
|
||||||
|
</mapper>
|
||||||
@@ -82,6 +82,10 @@ body {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-grid .span-2 {
|
||||||
|
grid-column: span 2;
|
||||||
|
}
|
||||||
|
|
||||||
.inline-form {
|
.inline-form {
|
||||||
display: inline;
|
display: inline;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
<a class="nav-link" href="/objects">테이블/뷰</a>
|
<a class="nav-link" href="/objects">테이블/뷰</a>
|
||||||
<a class="nav-link" href="/tokens">토큰</a>
|
<a class="nav-link" href="/tokens">토큰</a>
|
||||||
<a class="nav-link" href="/probe">ORDS 검증</a>
|
<a class="nav-link" href="/probe">ORDS 검증</a>
|
||||||
|
<a class="nav-link" href="/settings">설정</a>
|
||||||
</div>
|
</div>
|
||||||
<form method="post" action="/logout" class="ms-auto">
|
<form method="post" action="/logout" class="ms-auto">
|
||||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
|
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
|
||||||
|
|||||||
27
src/main/resources/templates/settings.html
Normal file
27
src/main/resources/templates/settings.html
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
|
||||||
|
<head th:replace="~{fragments/layout :: head('설정')}"></head>
|
||||||
|
<body>
|
||||||
|
<nav th:replace="~{fragments/layout :: nav}"></nav>
|
||||||
|
<main class="container py-4">
|
||||||
|
<div class="page-title">
|
||||||
|
<h1>설정</h1>
|
||||||
|
<p>백오피스에서 사용하는 외부 연동 값을 관리합니다.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert alert-success" th:if="${message}" th:text="${message}"></div>
|
||||||
|
|
||||||
|
<section class="content-band">
|
||||||
|
<h2>ORDS</h2>
|
||||||
|
<form method="post" action="/settings/ords" class="form-grid">
|
||||||
|
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
|
||||||
|
<label class="span-2">
|
||||||
|
Base URL
|
||||||
|
<input class="form-control" name="ordsBaseUrl" th:value="${ordsBaseUrl}" required>
|
||||||
|
</label>
|
||||||
|
<button class="btn btn-primary" type="submit">저장</button>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user