@@ -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;
|
||||
|
||||
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("/")
|
||||
|
||||
@@ -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:
|
||||
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}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
.form-grid .span-2 {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
.inline-form {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
<a class="nav-link" href="/objects">테이블/뷰</a>
|
||||
<a class="nav-link" href="/tokens">토큰</a>
|
||||
<a class="nav-link" href="/probe">ORDS 검증</a>
|
||||
<a class="nav-link" href="/settings">설정</a>
|
||||
</div>
|
||||
<form method="post" action="/logout" class="ms-auto">
|
||||
<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