diff --git a/.env.example b/.env.example index a952162..7df8c81 100644 --- a/.env.example +++ b/.env.example @@ -21,7 +21,7 @@ export ADB_PASSWORD="" # 비워두면 # export ADB_CONN="${ADB_USER}/${ADB_PASSWORD}@${ADB_TNS}" # --- (2b) Spring Boot 백오피스 --- -export BACKOFFICE_ORDS_BASE_URL="" # 예: https:///ords +export BACKOFFICE_ORDS_BASE_URL="https://yh0olybn5pqce4n-d8aukro81636mon0.adb.ap-seoul-1.oraclecloudapps.com/ords" export BACKOFFICE_ORDS_TIMEOUT_SECONDS="10" # --- (3) 데모용 ADB 엔드유저 비밀번호 (sql/adb/07_end_users.sql 에서 사용) --- diff --git a/sql/adb/25_agent_ords_security_backoffice_support.sql b/sql/adb/25_agent_ords_security_backoffice_support.sql index d28ff2a..216ce28 100644 --- a/sql/adb/25_agent_ords_security_backoffice_support.sql +++ b/sql/adb/25_agent_ords_security_backoffice_support.sql @@ -77,6 +77,34 @@ CREATE TABLE cb_ords_probe_audit ( 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 === MERGE INTO cb_protected_object dst USING ( diff --git a/src/main/java/com/cloudhandson/vpdbackoffice/domain/setting/BackofficeSetting.java b/src/main/java/com/cloudhandson/vpdbackoffice/domain/setting/BackofficeSetting.java new file mode 100644 index 0000000..5749fa6 --- /dev/null +++ b/src/main/java/com/cloudhandson/vpdbackoffice/domain/setting/BackofficeSetting.java @@ -0,0 +1,7 @@ +package com.cloudhandson.vpdbackoffice.domain.setting; + +public record BackofficeSetting( + String settingKey, + String settingValue +) { +} diff --git a/src/main/java/com/cloudhandson/vpdbackoffice/mapper/SettingMapper.java b/src/main/java/com/cloudhandson/vpdbackoffice/mapper/SettingMapper.java new file mode 100644 index 0000000..572b70a --- /dev/null +++ b/src/main/java/com/cloudhandson/vpdbackoffice/mapper/SettingMapper.java @@ -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); +} diff --git a/src/main/java/com/cloudhandson/vpdbackoffice/service/OrdsProbeService.java b/src/main/java/com/cloudhandson/vpdbackoffice/service/OrdsProbeService.java index 4f5c95d..af26bd5 100644 --- a/src/main/java/com/cloudhandson/vpdbackoffice/service/OrdsProbeService.java +++ b/src/main/java/com/cloudhandson/vpdbackoffice/service/OrdsProbeService.java @@ -1,6 +1,5 @@ package com.cloudhandson.vpdbackoffice.service; -import com.cloudhandson.vpdbackoffice.config.BackofficeProperties; import com.cloudhandson.vpdbackoffice.domain.audit.AuditEvent; import com.cloudhandson.vpdbackoffice.domain.probe.ProbeCommand; import com.cloudhandson.vpdbackoffice.domain.probe.ProbeResult; @@ -43,7 +42,7 @@ public class OrdsProbeService { private final ProbeErrorClassifier errorClassifier; private final RestTemplate ordsRestTemplate; private final ObjectMapper objectMapper; - private final BackofficeProperties properties; + private final SettingService settingService; private final Clock clock; public OrdsProbeService( @@ -53,7 +52,7 @@ public class OrdsProbeService { ProbeErrorClassifier errorClassifier, RestTemplate ordsRestTemplate, ObjectMapper objectMapper, - BackofficeProperties properties, + SettingService settingService, Clock clock ) { this.tokenService = tokenService; @@ -62,12 +61,13 @@ public class OrdsProbeService { this.errorClassifier = errorClassifier; this.ordsRestTemplate = ordsRestTemplate; this.objectMapper = objectMapper; - this.properties = properties; + this.settingService = settingService; this.clock = clock; } 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( ProbeStatus.ORDS_NOT_CONFIGURED, ProbeStatus.ORDS_NOT_CONFIGURED.name(), @@ -100,7 +100,7 @@ public class OrdsProbeService { String requestHeaders = null; String requestPayload = null; try { - URI uri = buildUri(object.ordsPath(), command.limit()); + URI uri = buildUri(ordsBaseUrl, object.ordsPath(), command.limit()); HttpHeaders headers = new HttpHeaders(); headers.setBearerAuth(command.bearerToken()); requestHeaders = prettyHeaders(maskedRequestHeaders(headers)); @@ -144,8 +144,7 @@ public class OrdsProbeService { } } - private URI buildUri(String ordsPath, int limit) { - String baseUrl = properties.ords().baseUrl(); + private URI buildUri(String baseUrl, String ordsPath, int limit) { String path = ordsPath.startsWith("/") ? ordsPath.substring(1) : ordsPath; return UriComponentsBuilder.fromUriString(baseUrl) .path("/") diff --git a/src/main/java/com/cloudhandson/vpdbackoffice/service/SettingService.java b/src/main/java/com/cloudhandson/vpdbackoffice/service/SettingService.java new file mode 100644 index 0000000..888e047 --- /dev/null +++ b/src/main/java/com/cloudhandson/vpdbackoffice/service/SettingService.java @@ -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; + } +} diff --git a/src/main/java/com/cloudhandson/vpdbackoffice/web/SettingController.java b/src/main/java/com/cloudhandson/vpdbackoffice/web/SettingController.java new file mode 100644 index 0000000..3a65afc --- /dev/null +++ b/src/main/java/com/cloudhandson/vpdbackoffice/web/SettingController.java @@ -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"; + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index b75dd9c..29adfb1 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -29,5 +29,5 @@ backoffice: token: max-days: ${BACKOFFICE_TOKEN_MAX_DAYS:365} 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} diff --git a/src/main/resources/mapper/SettingMapper.xml b/src/main/resources/mapper/SettingMapper.xml new file mode 100644 index 0000000..fe422a0 --- /dev/null +++ b/src/main/resources/mapper/SettingMapper.xml @@ -0,0 +1,25 @@ + + + + + + + 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) + + diff --git a/src/main/resources/static/css/app.css b/src/main/resources/static/css/app.css index d05b45b..d420d30 100644 --- a/src/main/resources/static/css/app.css +++ b/src/main/resources/static/css/app.css @@ -82,6 +82,10 @@ body { font-weight: 600; } +.form-grid .span-2 { + grid-column: span 2; +} + .inline-form { display: inline; } diff --git a/src/main/resources/templates/fragments/layout.html b/src/main/resources/templates/fragments/layout.html index 3cbeb0b..f0c9cbb 100644 --- a/src/main/resources/templates/fragments/layout.html +++ b/src/main/resources/templates/fragments/layout.html @@ -20,6 +20,7 @@ 테이블/뷰 토큰 ORDS 검증 + 설정
diff --git a/src/main/resources/templates/settings.html b/src/main/resources/templates/settings.html new file mode 100644 index 0000000..d15e138 --- /dev/null +++ b/src/main/resources/templates/settings.html @@ -0,0 +1,27 @@ + + + + + +
+
+

설정

+

백오피스에서 사용하는 외부 연동 값을 관리합니다.

+
+ +
+ +
+

ORDS

+ + + + + +
+
+ +