refs #722: externalize backoffice customer configuration

This commit is contained in:
devmrko
2026-07-23 19:14:37 +09:00
parent 7ba173240c
commit 53342e7bc3
47 changed files with 622 additions and 216 deletions

View File

@@ -1,6 +1,7 @@
package com.cloudhandson.vpdbackoffice.service;
import com.cloudhandson.vpdbackoffice.config.BackofficeProperties;
import com.cloudhandson.vpdbackoffice.config.McpProperties;
import com.cloudhandson.vpdbackoffice.domain.mcp.McpToolView;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -9,26 +10,27 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
import java.util.List;
import org.springframework.stereotype.Service;
/** MCP boundary exposing the Smilegate game-data Select AI generation and read-only execution tool. */
/** MCP boundary exposing a configured Select AI generation and read-only execution tool. */
@Service
public class McpSseService {
private static final String SELECT_AI_VPD_QUERY_TOOL = "oracle.select_ai.smilegate_game_text2sql";
private static final String SELECT_AI_VPD_QUERY_PATH = "/mcp (tools/call)";
private static final String DEFAULT_SELECT_AI_PROFILE = "SGMP_POC_OCI_GPT54MINI";
private final SmilegateSelectAiService smilegateSelectAiService;
private final SelectAiService selectAiService;
private final ObjectMapper objectMapper;
private final BackofficeProperties properties;
private final McpProperties mcpProperties;
public McpSseService(
SmilegateSelectAiService smilegateSelectAiService,
SelectAiService selectAiService,
ObjectMapper objectMapper,
BackofficeProperties properties
BackofficeProperties properties,
McpProperties mcpProperties
) {
this.smilegateSelectAiService = smilegateSelectAiService;
this.selectAiService = selectAiService;
this.objectMapper = objectMapper;
this.properties = properties;
this.mcpProperties = mcpProperties;
}
public ObjectNode handle(String contextPath, JsonNode request) {
@@ -93,7 +95,7 @@ public class McpSseService {
private ObjectNode selectAiVpdQueryTool() {
ObjectNode item = objectMapper.createObjectNode();
item.put("name", SELECT_AI_VPD_QUERY_TOOL);
item.put("name", toolName());
item.put("description", selectAiVpdQueryView().description());
ObjectNode schema = objectMapper.createObjectNode();
@@ -102,7 +104,7 @@ public class McpSseService {
ObjectNode prompt = objectMapper.createObjectNode();
prompt.put("type", "string");
prompt.put("description", "Smilegate 게임 로그·서비스 데이터에 대해 조회할 내용을 자연어로 입력합니다.");
prompt.put("description", promptDescription());
prompt.put("maxLength", 4000);
properties.set("prompt", prompt);
@@ -117,7 +119,7 @@ public class McpSseService {
private ObjectNode toolsCallResult(JsonNode params, String vpdBearerToken) {
String toolName = params.path("name").asText("");
if (!SELECT_AI_VPD_QUERY_TOOL.equals(toolName)) {
if (!toolName().equals(toolName)) {
throw new AppException("등록되지 않은 MCP tool입니다: " + toolName);
}
@@ -128,13 +130,13 @@ public class McpSseService {
}
JsonNode response;
try {
response = smilegateSelectAiService.generateAndExecute(token, arguments.path("prompt").asText(""));
response = selectAiService.generateAndExecute(token, arguments.path("prompt").asText(""));
} catch (VpdTokenAccessDeniedException ignored) {
return tokenAccessDeniedResult();
}
ObjectNode payload = objectMapper.createObjectNode();
payload.put("toolName", SELECT_AI_VPD_QUERY_TOOL);
payload.put("toolName", toolName());
payload.put("profile", selectAiProfile());
payload.put("ordsPath", SELECT_AI_VPD_QUERY_PATH);
payload.set("response", response);
@@ -169,10 +171,10 @@ public class McpSseService {
private McpToolView selectAiVpdQueryView() {
String profile = selectAiProfile();
return new McpToolView(
SELECT_AI_VPD_QUERY_TOOL,
profile + " 프로파일로 게임 로그·서비스 데이터용 읽기 전용 SELECT/WITH SQL을 생성하고, 검증 후 읽기 전용 트랜잭션에서 실행합니다. 생성 SQL과 최대 100건의 조회 결과를 함께 반환하며 DDL/DML/잠금/패키지 호출은 실행하지 않습니다.",
toolName(),
profile + " 프로파일로 " + toolDescription(),
-1L,
"Smilegate 게임 데이터 Text2SQL",
toolLabel(),
SELECT_AI_VPD_QUERY_PATH
);
}
@@ -180,11 +182,29 @@ public class McpSseService {
private String selectAiProfile() {
BackofficeProperties.SelectAi selectAi = properties == null ? null : properties.selectAi();
if (selectAi == null || selectAi.profile() == null || selectAi.profile().isBlank()) {
return DEFAULT_SELECT_AI_PROFILE;
return "";
}
return selectAi.profile().trim();
}
private String toolName() {
return mcpProperties == null ? "oracle.select_ai.data_text2sql" : mcpProperties.resolvedToolName();
}
private String toolLabel() {
return mcpProperties == null ? "업무 데이터 Text2SQL" : mcpProperties.resolvedToolLabel();
}
private String toolDescription() {
return mcpProperties == null
? "승인된 업무 데이터용 읽기 전용 SELECT/WITH SQL을 생성하고 검증 후 실행합니다."
: mcpProperties.resolvedToolDescription();
}
private String promptDescription() {
return mcpProperties == null ? "업무 데이터에서 조회할 내용을 자연어로 입력합니다." : mcpProperties.resolvedPromptDescription();
}
private String pretty(Object value) {
try {
return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(value);