feat: OCI 배포 듀얼 LLM 스위칭 및 신규 툴 DTO 의존성 주입 병합
This commit is contained in:
@@ -11,6 +11,7 @@ dependencies {
|
|||||||
// Spring AI MCP Server
|
// Spring AI MCP Server
|
||||||
implementation 'org.springframework.ai:spring-ai-starter-mcp-server-webmvc'
|
implementation 'org.springframework.ai:spring-ai-starter-mcp-server-webmvc'
|
||||||
implementation 'org.springframework.ai:spring-ai-starter-model-openai'
|
implementation 'org.springframework.ai:spring-ai-starter-model-openai'
|
||||||
|
implementation 'org.springframework.ai:spring-ai-openai:2.0.0'
|
||||||
// MyBatis & DB
|
// MyBatis & DB
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
|
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
|
||||||
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3'
|
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3'
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import org.springframework.ai.tool.definition.ToolDefinition;
|
|||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
|
|
||||||
@@ -60,21 +61,90 @@ public class ChatController {
|
|||||||
Map<String, Object> resultMap = (Map<String, Object>) toolsResponse.getResult();
|
Map<String, Object> resultMap = (Map<String, Object>) toolsResponse.getResult();
|
||||||
if (resultMap.containsKey("tools")) {
|
if (resultMap.containsKey("tools")) {
|
||||||
List<ToolMetadata> allTools = (List<ToolMetadata>) resultMap.get("tools");
|
List<ToolMetadata> allTools = (List<ToolMetadata>) resultMap.get("tools");
|
||||||
|
Set<String> addedToolNames = new HashSet<>();
|
||||||
for (ToolMetadata meta : allTools) {
|
for (ToolMetadata meta : allTools) {
|
||||||
if (Boolean.TRUE.equals(meta.getVisible())) {
|
if (Boolean.TRUE.equals(meta.getVisible()) && meta.getName() != null) {
|
||||||
callbacks.add(new DynamicMcpToolCallback(meta, executeService, objectMapper, effectiveTenantId));
|
String cleanName = meta.getName().replaceAll("[^a-zA-Z0-9_-]", "_");
|
||||||
|
if (!addedToolNames.contains(cleanName)) {
|
||||||
|
callbacks.add(new DynamicMcpToolCallback(meta, executeService, objectMapper, effectiveTenantId));
|
||||||
|
addedToolNames.add(cleanName);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ChatClient chatClient = chatClientBuilder
|
String selectedModel = request.getOrDefault("model", "gemini-flash-latest").trim();
|
||||||
|
if (selectedModel.isEmpty()) {
|
||||||
|
selectedModel = "gemini-flash-latest";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Google Gemini 무료 티어 직접 API 연동 (사용량 및 컴파일 충돌 차단용 Short-circuit)
|
||||||
|
if (selectedModel.equals("gemini-flash-latest")) {
|
||||||
|
String geminiKey = "AQ.Ab8RN6KFZggsQf8iooY1v_3h3vp2TIjiYB54dV4Yay3vVKEMtg";
|
||||||
|
String geminiUrl = "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions";
|
||||||
|
|
||||||
|
Map<String, Object> body = new HashMap<>();
|
||||||
|
body.put("model", "gemini-1.5-flash");
|
||||||
|
body.put("stream", true);
|
||||||
|
body.put("messages", List.of(Map.of("role", "user", "content", message)));
|
||||||
|
|
||||||
|
try {
|
||||||
|
java.net.http.HttpClient httpClient = java.net.http.HttpClient.newHttpClient();
|
||||||
|
java.net.http.HttpRequest httpRequest = java.net.http.HttpRequest.newBuilder()
|
||||||
|
.uri(java.net.URI.create(geminiUrl))
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
.header("Authorization", "Bearer " + geminiKey)
|
||||||
|
.POST(java.net.http.HttpRequest.BodyPublishers.ofString(objectMapper.writeValueAsString(body)))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
httpClient.sendAsync(httpRequest, java.net.http.HttpResponse.BodyHandlers.ofLines())
|
||||||
|
.thenAccept(response -> {
|
||||||
|
response.body().forEach(line -> {
|
||||||
|
try {
|
||||||
|
if (line.startsWith("data: ")) {
|
||||||
|
String jsonStr = line.substring(6).trim();
|
||||||
|
if (!jsonStr.equals("[DONE]")) {
|
||||||
|
JsonNode node = objectMapper.readTree(jsonStr);
|
||||||
|
String text = node.path("choices").path(0).path("delta").path("content").asText("");
|
||||||
|
if (!text.isEmpty()) {
|
||||||
|
emitter.send(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Gemini stream parse error: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
emitter.complete();
|
||||||
|
})
|
||||||
|
.exceptionally(ex -> {
|
||||||
|
log.error("Gemini stream connection failed", ex);
|
||||||
|
try {
|
||||||
|
emitter.send("\n\n⚠️ **Gemini API 호출에 실패했습니다.** (" + ex.getMessage() + ")");
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
emitter.completeWithError(ex);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
log.info("[Real AI Chat] 구글 제미나이 다이렉트 API 스트리밍 개시 완료!");
|
||||||
|
return emitter;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Gemini direct API setup error", e);
|
||||||
|
emitter.completeWithError(e);
|
||||||
|
return emitter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. OpenRouter 무료 모델 처리 (Spring AI 빌드된 ChatClient 및 MCP 툴 호출 완벽 지원!)
|
||||||
|
ChatClient activeChatClient = chatClientBuilder
|
||||||
.defaultSystem("You are AX HUB Assistant, a highly capable enterprise AI agent. You must use the provided tools to answer user questions when necessary. Always answer politely in Korean.")
|
.defaultSystem("You are AX HUB Assistant, a highly capable enterprise AI agent. You must use the provided tools to answer user questions when necessary. Always answer politely in Korean.")
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
Flux<String> responseStream = chatClient.prompt()
|
Flux<String> responseStream = activeChatClient.prompt()
|
||||||
.user(message)
|
.user(message)
|
||||||
.tools((Object[]) callbacks.toArray(new ToolCallback[0])) // Spring AI 2.0 uses tools()
|
.tools((Object[]) callbacks.toArray(new ToolCallback[0])) // Spring AI 2.0 uses tools()
|
||||||
|
.options(org.springframework.ai.openai.OpenAiChatOptions.builder()
|
||||||
|
.model(selectedModel))
|
||||||
.stream()
|
.stream()
|
||||||
.content();
|
.content();
|
||||||
|
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ spring:
|
|||||||
timeout-per-shutdown-phase: 20s
|
timeout-per-shutdown-phase: 20s
|
||||||
ai:
|
ai:
|
||||||
openai:
|
openai:
|
||||||
api-key: AQ.Ab8RN6KFZggsQf8iooY1v_3h3vp2TIjiYB54dV4Yay3vVKEMtg
|
api-key: ${OPENROUTER_API_KEY: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}
|
||||||
base-url: https://generativelanguage.googleapis.com/v1beta/openai/
|
base-url: https://openrouter.ai/api/v1
|
||||||
chat:
|
chat:
|
||||||
options:
|
options:
|
||||||
model: gemini-flash-latest
|
model: google/gemma-4-31b-it:free
|
||||||
temperature: 0.3
|
temperature: 0.3
|
||||||
|
|
||||||
server:
|
server:
|
||||||
|
|||||||
@@ -43,12 +43,23 @@
|
|||||||
<p class="text-[11px] text-slate-500">Powered by Mock Engine & MCP Tools</p>
|
<p class="text-[11px] text-slate-500">Powered by Mock Engine & MCP Tools</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-4">
|
||||||
<span class="relative flex h-2.5 w-2.5">
|
<!-- 모델 선택 Select Box 신설 -->
|
||||||
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
|
<select id="model-select" class="bg-[#1e2128] text-slate-300 text-xs px-3 py-1.5 rounded-lg border border-white/10 focus:outline-none focus:border-emerald-500/50 cursor-pointer">
|
||||||
<span class="relative inline-flex rounded-full h-2.5 w-2.5 bg-emerald-500"></span>
|
<option value="gemini-flash-latest">Gemini 1.5 Flash (기본 - Google)</option>
|
||||||
</span>
|
<option value="google/gemma-4-31b-it:free">Gemma 4 31B (무료 - OpenRouter)</option>
|
||||||
<span class="text-xs text-slate-400">Online</span>
|
<option value="inclusionai/ling-3.0-flash:free">Ling 3.0 Flash (무료 - OpenRouter)</option>
|
||||||
|
<option value="openai/gpt-oss-20b:free">GPT-OSS 20B (무료 - OpenRouter)</option>
|
||||||
|
<option value="cohere/north-mini-code:free">Cohere North Mini (무료 - OpenRouter)</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="relative flex h-2.5 w-2.5">
|
||||||
|
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
|
||||||
|
<span class="relative inline-flex rounded-full h-2.5 w-2.5 bg-emerald-500"></span>
|
||||||
|
</span>
|
||||||
|
<span class="text-xs text-slate-400">Online</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -173,14 +184,12 @@
|
|||||||
.replace(/'/g, "'");
|
.replace(/'/g, "'");
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeMarkdownEmphasis(text) {
|
|
||||||
return text.replace(/\*\*/g, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
async function sendMessage() {
|
async function sendMessage() {
|
||||||
const text = chatInput.value.trim();
|
const text = chatInput.value.trim();
|
||||||
if (!text || isLoading) return;
|
if (!text || isLoading) return;
|
||||||
|
|
||||||
|
const selectedModel = document.getElementById('model-select').value;
|
||||||
|
|
||||||
// 1. 유저 메시지 추가
|
// 1. 유저 메시지 추가
|
||||||
appendUserMessage(text);
|
appendUserMessage(text);
|
||||||
chatInput.value = '';
|
chatInput.value = '';
|
||||||
@@ -197,7 +206,7 @@
|
|||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'X-Agent-Id': 'TESTER-DEV' // 권한 통과를 위한 테스트 Agent ID
|
'X-Agent-Id': 'TESTER-DEV' // 권한 통과를 위한 테스트 Agent ID
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ message: text })
|
body: JSON.stringify({ message: text, model: selectedModel })
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) throw new Error('Network response was not ok');
|
if (!response.ok) throw new Error('Network response was not ok');
|
||||||
@@ -209,7 +218,6 @@
|
|||||||
// 5. 스트림 읽기
|
// 5. 스트림 읽기
|
||||||
const reader = response.body.getReader();
|
const reader = response.body.getReader();
|
||||||
const decoder = new TextDecoder("utf-8");
|
const decoder = new TextDecoder("utf-8");
|
||||||
let responseText = '';
|
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
const { done, value } = await reader.read();
|
const { done, value } = await reader.read();
|
||||||
@@ -220,8 +228,7 @@
|
|||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
if (line.startsWith('data:')) {
|
if (line.startsWith('data:')) {
|
||||||
const data = line.substring(5);
|
const data = line.substring(5);
|
||||||
responseText += data;
|
textContainer.innerHTML += escapeHtml(data);
|
||||||
textContainer.textContent = removeMarkdownEmphasis(responseText);
|
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ dependencies {
|
|||||||
api 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.17.1'
|
api 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.17.1'
|
||||||
api 'com.fasterxml.jackson.core:jackson-databind:2.17.1'
|
api 'com.fasterxml.jackson.core:jackson-databind:2.17.1'
|
||||||
api 'com.networknt:json-schema-validator:1.4.0'
|
api 'com.networknt:json-schema-validator:1.4.0'
|
||||||
|
api 'org.springframework.ai:spring-ai-starter-mcp-server-webmvc'
|
||||||
api 'org.springframework.kafka:spring-kafka:3.2.0'
|
api 'org.springframework.kafka:spring-kafka:3.2.0'
|
||||||
api 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0'
|
api 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package io.shinhanlife.dap.mcc.mcp;
|
||||||
|
|
||||||
|
import io.modelcontextprotocol.server.transport.HttpServletStreamableServerTransportProvider;
|
||||||
|
import org.springframework.boot.web.servlet.ServletRegistrationBean;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.context.annotation.Primary;
|
||||||
|
|
||||||
|
/** Exposes every Tool Pod through the MCP Streamable HTTP transport. */
|
||||||
|
@Configuration
|
||||||
|
public class ToolMcpServerConfiguration {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@Primary
|
||||||
|
public HttpServletStreamableServerTransportProvider toolMcpTransportProvider() {
|
||||||
|
return HttpServletStreamableServerTransportProvider.builder()
|
||||||
|
.mcpEndpoint("/mcp")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public ServletRegistrationBean<HttpServletStreamableServerTransportProvider> toolMcpServlet(
|
||||||
|
HttpServletStreamableServerTransportProvider transportProvider) {
|
||||||
|
return new ServletRegistrationBean<>(transportProvider, "/mcp");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
package io.shinhanlife.dap.mcc.mcp;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import io.modelcontextprotocol.server.McpServerFeatures;
|
||||||
|
import io.modelcontextprotocol.server.McpSyncServer;
|
||||||
|
import io.modelcontextprotocol.spec.McpSchema;
|
||||||
|
import io.shinhanlife.dap.mcc.dto.ToolMetadata;
|
||||||
|
import io.shinhanlife.dap.mcc.presentation.BusinessToolController;
|
||||||
|
import io.shinhanlife.dap.mcc.usecase.ToolRegistryHeartbeatSender;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||||
|
import org.springframework.context.event.EventListener;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/** Registers the Tool Pod's existing annotated tools with its MCP SDK server. */
|
||||||
|
@Component
|
||||||
|
public class ToolPodMcpToolSynchronizer {
|
||||||
|
private final McpSyncServer mcpServer;
|
||||||
|
private final ToolRegistryHeartbeatSender heartbeatSender;
|
||||||
|
private final BusinessToolController businessToolController;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
public ToolPodMcpToolSynchronizer(McpSyncServer mcpServer, ToolRegistryHeartbeatSender heartbeatSender,
|
||||||
|
BusinessToolController businessToolController, ObjectMapper objectMapper) {
|
||||||
|
this.mcpServer = mcpServer;
|
||||||
|
this.heartbeatSender = heartbeatSender;
|
||||||
|
this.businessToolController = businessToolController;
|
||||||
|
this.objectMapper = objectMapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
@EventListener(ApplicationReadyEvent.class)
|
||||||
|
public void registerLocalTools() {
|
||||||
|
heartbeatSender.getAllScannedTools().stream()
|
||||||
|
.filter(tool -> Boolean.TRUE.equals(tool.getVisible()))
|
||||||
|
.forEach(tool -> mcpServer.addTool(specification(tool)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private McpServerFeatures.SyncToolSpecification specification(ToolMetadata tool) {
|
||||||
|
McpSchema.Tool mcpTool = McpSchema.Tool.builder()
|
||||||
|
.name(tool.getName())
|
||||||
|
.description(tool.getDescription() == null || tool.getDescription().isBlank() ? tool.getName() + " Tool" : tool.getDescription())
|
||||||
|
.inputSchema(tool.getParametersSchema() == null ? emptySchema() : tool.getParametersSchema())
|
||||||
|
.annotations(McpSchema.ToolAnnotations.builder()
|
||||||
|
.readOnlyHint(Boolean.TRUE.equals(tool.getReadOnlyHint()))
|
||||||
|
.destructiveHint(Boolean.TRUE.equals(tool.getDestructiveHint()))
|
||||||
|
.idempotentHint(Boolean.TRUE.equals(tool.getIdempotentHint()))
|
||||||
|
.openWorldHint(Boolean.TRUE.equals(tool.getOpenWorldHint())).build())
|
||||||
|
.build();
|
||||||
|
return McpServerFeatures.SyncToolSpecification.builder().tool(mcpTool)
|
||||||
|
.callHandler((context, request) -> invoke(tool.getName(), request.arguments())).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private McpSchema.CallToolResult invoke(String toolName, Map<String, Object> arguments) {
|
||||||
|
ResponseEntity<?> response = businessToolController.executeDynamicTool(toolName, null, null, null, arguments);
|
||||||
|
boolean failed = !response.getStatusCode().is2xxSuccessful();
|
||||||
|
Object body = response.getBody();
|
||||||
|
try {
|
||||||
|
return McpSchema.CallToolResult.builder().addTextContent(objectMapper.writeValueAsString(body))
|
||||||
|
.structuredContent(body).isError(failed).build();
|
||||||
|
} catch (Exception error) {
|
||||||
|
return McpSchema.CallToolResult.builder().addTextContent(String.valueOf(body)).isError(failed).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> emptySchema() {
|
||||||
|
Map<String, Object> schema = new LinkedHashMap<>();
|
||||||
|
schema.put("type", "object");
|
||||||
|
schema.put("properties", Map.of());
|
||||||
|
schema.put("additionalProperties", false);
|
||||||
|
return schema;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package io.shinhanlife.dap.mcc.presentation;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.networknt.schema.JsonSchema;
|
||||||
|
import com.networknt.schema.JsonSchemaFactory;
|
||||||
|
import com.networknt.schema.SpecVersion;
|
||||||
|
import com.networknt.schema.ValidationMessage;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.Map;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/** Validates tool arguments with the NetworkNT version selected by the MCP SDK. */
|
||||||
|
@Component
|
||||||
|
public class ToolArgumentSchemaValidator {
|
||||||
|
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
public ToolArgumentSchemaValidator(ObjectMapper objectMapper) {
|
||||||
|
this.objectMapper = objectMapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<ValidationMessage> validate(Map<String, Object> schemaDefinition, Map<String, Object> arguments) throws Exception {
|
||||||
|
JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7);
|
||||||
|
JsonNode schemaNode = objectMapper.valueToTree(schemaDefinition);
|
||||||
|
JsonSchema schema = factory.getSchema(schemaNode);
|
||||||
|
JsonNode argumentsNode = objectMapper.valueToTree(arguments);
|
||||||
|
return schema.validate(argumentsNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package io.shinhanlife.dap.mcc.mcp;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
|
||||||
|
import io.modelcontextprotocol.server.transport.HttpServletStreamableServerTransportProvider;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.boot.web.servlet.ServletRegistrationBean;
|
||||||
|
|
||||||
|
class ToolMcpServerConfigurationTest {
|
||||||
|
|
||||||
|
private final ToolMcpServerConfiguration configuration = new ToolMcpServerConfiguration();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void exposesOnlyExactMcpEndpointSoLegacyMcpApiPathsRemainAvailable() {
|
||||||
|
HttpServletStreamableServerTransportProvider transport = configuration.toolMcpTransportProvider();
|
||||||
|
ServletRegistrationBean<HttpServletStreamableServerTransportProvider> registration = configuration.toolMcpServlet(transport);
|
||||||
|
|
||||||
|
assertEquals("/mcp", registration.getUrlMappings().iterator().next());
|
||||||
|
assertFalse(registration.getUrlMappings().contains("/mcp/*"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package io.shinhanlife.dap.mcc.presentation;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
class ToolArgumentSchemaValidatorTest {
|
||||||
|
|
||||||
|
private final ToolArgumentSchemaValidator validator = new ToolArgumentSchemaValidator(new ObjectMapper());
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void validatesDraft7SchemaWithTheRuntimeNetworkntVersion() throws Exception {
|
||||||
|
Map<String, Object> schema = Map.of(
|
||||||
|
"type", "object",
|
||||||
|
"properties", Map.of("name", Map.of("type", "string")),
|
||||||
|
"required", List.of("name"));
|
||||||
|
|
||||||
|
assertTrue(validator.validate(schema, Map.of("name", "Hong")).isEmpty());
|
||||||
|
assertFalse(validator.validate(schema, Map.of()).isEmpty());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package io.shinhanlife.dap.mcc.biz.cmm.converter;
|
||||||
|
|
||||||
|
import io.shinhanlife.dap.mcc.biz.cmm.dto.MetaCommonCodeRequest;
|
||||||
|
import io.shinhanlife.dap.mcc.biz.cmm.dto.MetaCommonCodeResponse;
|
||||||
|
import io.shinhanlife.dap.mcc.infra.itrf.mci.cfp.a.io.CLCNNB00001_I;
|
||||||
|
import io.shinhanlife.dap.mcc.infra.itrf.mci.cfp.a.io.CLCNNB00001_O;
|
||||||
|
import org.mapstruct.Mapper;
|
||||||
|
import org.mapstruct.Mapping;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @package io.shinhanlife.dap.mcc.biz.cmm.converter
|
||||||
|
* @className MetaCommonCodeConverter
|
||||||
|
* @description AX HUB 시스템 처리 클래스
|
||||||
|
* @author 09863409
|
||||||
|
* @create 2026.07.29
|
||||||
|
* <pre>
|
||||||
|
* ---------- 개정이력 ----------
|
||||||
|
* 수정일 수정자 수정내용
|
||||||
|
* ---------- -------- ---------------------------
|
||||||
|
* 2026.07.29 09863409 최초생성
|
||||||
|
*
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
@Mapper(componentModel = "spring")
|
||||||
|
public interface MetaCommonCodeConverter {
|
||||||
|
@Mapping(target = "csNo", source = "groupCode", defaultValue = "GRP_COMM_CD")
|
||||||
|
CLCNNB00001_I toLegacyRequest(MetaCommonCodeRequest req);
|
||||||
|
|
||||||
|
@Mapping(target = "codeList", ignore = true)
|
||||||
|
MetaCommonCodeResponse toResponse(CLCNNB00001_O mciRes);
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package io.shinhanlife.dap.mcc.biz.cmm.converter;
|
||||||
|
|
||||||
|
import io.shinhanlife.dap.mcc.biz.cmm.dto.MetaTableRequest;
|
||||||
|
import io.shinhanlife.dap.mcc.biz.cmm.dto.MetaTableResponse;
|
||||||
|
import io.shinhanlife.dap.mcc.infra.itrf.mci.cfp.a.io.CLCNNB00001_I;
|
||||||
|
import io.shinhanlife.dap.mcc.infra.itrf.mci.cfp.a.io.CLCNNB00001_O;
|
||||||
|
import org.mapstruct.Mapper;
|
||||||
|
import org.mapstruct.Mapping;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @package io.shinhanlife.dap.mcc.biz.cmm.converter
|
||||||
|
* @className MetaTableConverter
|
||||||
|
* @description AX HUB 시스템 처리 클래스
|
||||||
|
* @author 09863409
|
||||||
|
* @create 2026.07.29
|
||||||
|
* <pre>
|
||||||
|
* ---------- 개정이력 ----------
|
||||||
|
* 수정일 수정자 수정내용
|
||||||
|
* ---------- -------- ---------------------------
|
||||||
|
* 2026.07.29 09863409 최초생성
|
||||||
|
*
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
@Mapper(componentModel = "spring")
|
||||||
|
public interface MetaTableConverter {
|
||||||
|
@Mapping(target = "csNo", source = "tableName", defaultValue = "TB_META_BAS")
|
||||||
|
CLCNNB00001_I toLegacyRequest(MetaTableRequest req);
|
||||||
|
|
||||||
|
@Mapping(target = "tableList", ignore = true)
|
||||||
|
MetaTableResponse toResponse(CLCNNB00001_O mciRes);
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package io.shinhanlife.dap.mcc.biz.cmm.dto;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
|
import io.shinhanlife.dap.lib.annotation.McpParameter;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @package io.shinhanlife.dap.mcc.biz.cmm.dto
|
||||||
|
* @className MetaCommonCodeRequest
|
||||||
|
* @description AX HUB 시스템 처리 클래스
|
||||||
|
* @author 09863409
|
||||||
|
* @create 2026.07.29
|
||||||
|
* <pre>
|
||||||
|
* ---------- 개정이력 ----------
|
||||||
|
* 수정일 수정자 수정내용
|
||||||
|
* ---------- -------- ---------------------------
|
||||||
|
* 2026.07.29 09863409 최초생성
|
||||||
|
*
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
|
public class MetaCommonCodeRequest {
|
||||||
|
@McpParameter(description = "통합코드 그룹 ID (예: GRP_SYS_01, GRP_COMM_CD)", required = false)
|
||||||
|
private String groupCode;
|
||||||
|
|
||||||
|
@McpParameter(description = "코드명 검색 키워드 (예: 사용, 상태)", required = false)
|
||||||
|
private String codeName;
|
||||||
|
|
||||||
|
@McpParameter(description = "사용여부 (예: Y, N)", required = false)
|
||||||
|
private String useYn;
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package io.shinhanlife.dap.mcc.biz.cmm.dto;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @package io.shinhanlife.dap.mcc.biz.cmm.dto
|
||||||
|
* @className MetaCommonCodeResponse
|
||||||
|
* @description AX HUB 시스템 처리 클래스
|
||||||
|
* @author 09863409
|
||||||
|
* @create 2026.07.29
|
||||||
|
* <pre>
|
||||||
|
* ---------- 개정이력 ----------
|
||||||
|
* 수정일 수정자 수정내용
|
||||||
|
* ---------- -------- ---------------------------
|
||||||
|
* 2026.07.29 09863409 최초생성
|
||||||
|
*
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
|
public class MetaCommonCodeResponse {
|
||||||
|
private List<MetaCommonCodeItem> codeList;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
|
public static class MetaCommonCodeItem {
|
||||||
|
private String groupCode;
|
||||||
|
private String code;
|
||||||
|
private String codeName;
|
||||||
|
private String codeDesc;
|
||||||
|
private Integer sortSeq;
|
||||||
|
private String useYn;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package io.shinhanlife.dap.mcc.biz.cmm.dto;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
|
import io.shinhanlife.dap.lib.annotation.McpParameter;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @package io.shinhanlife.dap.mcc.biz.cmm.dto
|
||||||
|
* @className MetaTableRequest
|
||||||
|
* @description AX HUB 시스템 처리 클래스
|
||||||
|
* @author 09863409
|
||||||
|
* @create 2026.07.29
|
||||||
|
* <pre>
|
||||||
|
* ---------- 개정이력 ----------
|
||||||
|
* 수정일 수정자 수정내용
|
||||||
|
* ---------- -------- ---------------------------
|
||||||
|
* 2026.07.29 09863409 최초생성
|
||||||
|
*
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
|
public class MetaTableRequest {
|
||||||
|
@McpParameter(description = "테이블 물리명 키워드 (예: TB_CUST_BAS, TB_CONT)", required = false)
|
||||||
|
private String tableName;
|
||||||
|
|
||||||
|
@McpParameter(description = "테이블 논리명(한글) 키워드 (예: 고객기본, 계약)", required = false)
|
||||||
|
private String tableLogicalName;
|
||||||
|
|
||||||
|
@McpParameter(description = "스키마/소유자명 (예: DAPADM, SHLOWN)", required = false)
|
||||||
|
private String owner;
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package io.shinhanlife.dap.mcc.biz.cmm.dto;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @package io.shinhanlife.dap.mcc.biz.cmm.dto
|
||||||
|
* @className MetaTableResponse
|
||||||
|
* @description AX HUB 시스템 처리 클래스
|
||||||
|
* @author 09863409
|
||||||
|
* @create 2026.07.29
|
||||||
|
* <pre>
|
||||||
|
* ---------- 개정이력 ----------
|
||||||
|
* 수정일 수정자 수정내용
|
||||||
|
* ---------- -------- ---------------------------
|
||||||
|
* 2026.07.29 09863409 최초생성
|
||||||
|
*
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
|
public class MetaTableResponse {
|
||||||
|
private List<MetaTableItem> tableList;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
|
public static class MetaTableItem {
|
||||||
|
private String owner;
|
||||||
|
private String tableName;
|
||||||
|
private String tableLogicalName;
|
||||||
|
private String tableDesc;
|
||||||
|
private Integer columnCount;
|
||||||
|
private Long rowCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package io.shinhanlife.dap.mcc.biz.cmm.usecase;
|
||||||
|
|
||||||
|
import io.shinhanlife.dap.lib.annotation.McpFunction;
|
||||||
|
import io.shinhanlife.dap.lib.annotation.McpTool;
|
||||||
|
import io.shinhanlife.dap.mcc.biz.cmm.dto.MetaCommonCodeRequest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @package io.shinhanlife.dap.mcc.biz.cmm.usecase
|
||||||
|
* @className MetaCommonCodeUseCase
|
||||||
|
* @description AX HUB 시스템 처리 클래스
|
||||||
|
* @author 09863409
|
||||||
|
* @create 2026.07.29
|
||||||
|
* <pre>
|
||||||
|
* ---------- 개정이력 ----------
|
||||||
|
* 수정일 수정자 수정내용
|
||||||
|
* ---------- -------- ---------------------------
|
||||||
|
* 2026.07.29 09863409 최초생성
|
||||||
|
*
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
@McpTool(
|
||||||
|
routingType = "MCI",
|
||||||
|
categoryKey = "cmm"
|
||||||
|
)
|
||||||
|
public interface MetaCommonCodeUseCase {
|
||||||
|
@McpFunction(
|
||||||
|
displayName = "메타 통합코드 조회 툴",
|
||||||
|
name = "metaCommonCode",
|
||||||
|
description = "메타 통합코드 목록을 조회해줘",
|
||||||
|
prompt = "메타 통합코드 목록을 조회해줘",
|
||||||
|
mappingId = "CLCNNB00001",
|
||||||
|
register = false,
|
||||||
|
requiresApproval = false,
|
||||||
|
openWorldHint = true
|
||||||
|
)
|
||||||
|
Object execute(MetaCommonCodeRequest req);
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package io.shinhanlife.dap.mcc.biz.cmm.usecase;
|
||||||
|
|
||||||
|
import io.shinhanlife.dap.lib.annotation.McpFunction;
|
||||||
|
import io.shinhanlife.dap.lib.annotation.McpTool;
|
||||||
|
import io.shinhanlife.dap.mcc.biz.cmm.dto.MetaTableRequest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @package io.shinhanlife.dap.mcc.biz.cmm.usecase
|
||||||
|
* @className MetaTableUseCase
|
||||||
|
* @description AX HUB 시스템 처리 클래스
|
||||||
|
* @author 09863409
|
||||||
|
* @create 2026.07.29
|
||||||
|
* <pre>
|
||||||
|
* ---------- 개정이력 ----------
|
||||||
|
* 수정일 수정자 수정내용
|
||||||
|
* ---------- -------- ---------------------------
|
||||||
|
* 2026.07.29 09863409 최초생성
|
||||||
|
*
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
@McpTool(
|
||||||
|
routingType = "MCI",
|
||||||
|
categoryKey = "cmm"
|
||||||
|
)
|
||||||
|
public interface MetaTableUseCase {
|
||||||
|
@McpFunction(
|
||||||
|
displayName = "메타 테이블 조회 툴",
|
||||||
|
name = "metaTable",
|
||||||
|
description = "메타 테이블 정보 목록을 조회해줘",
|
||||||
|
prompt = "메타 테이블 정보 목록을 조회해줘",
|
||||||
|
mappingId = "CLCNNB00001",
|
||||||
|
register = false,
|
||||||
|
requiresApproval = false,
|
||||||
|
openWorldHint = true
|
||||||
|
)
|
||||||
|
Object execute(MetaTableRequest req);
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
package io.shinhanlife.dap.mcc.biz.cmm.usecase.impl;
|
||||||
|
|
||||||
|
import io.shinhanlife.dap.mcc.biz.cmm.converter.MetaCommonCodeConverter;
|
||||||
|
import io.shinhanlife.dap.mcc.biz.cmm.dto.MetaCommonCodeRequest;
|
||||||
|
import io.shinhanlife.dap.mcc.biz.cmm.dto.MetaCommonCodeResponse;
|
||||||
|
import io.shinhanlife.dap.mcc.biz.cmm.dto.MetaCommonCodeResponse.MetaCommonCodeItem;
|
||||||
|
import io.shinhanlife.dap.mcc.biz.cmm.usecase.MetaCommonCodeUseCase;
|
||||||
|
import io.shinhanlife.dap.mcc.infra.itrf.mci.cfp.a.MciCfpaClient;
|
||||||
|
import io.shinhanlife.dap.mcc.infra.itrf.mci.cfp.a.io.CLCNNB00001_I;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @package io.shinhanlife.dap.mcc.biz.cmm.usecase.impl
|
||||||
|
* @className MetaCommonCodeUseCaseImpl
|
||||||
|
* @description AX HUB 시스템 처리 클래스
|
||||||
|
* @author 09863409
|
||||||
|
* @create 2026.07.29
|
||||||
|
* <pre>
|
||||||
|
* ---------- 개정이력 ----------
|
||||||
|
* 수정일 수정자 수정내용
|
||||||
|
* ---------- -------- ---------------------------
|
||||||
|
* 2026.07.29 09863409 최초생성
|
||||||
|
*
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class MetaCommonCodeUseCaseImpl implements MetaCommonCodeUseCase {
|
||||||
|
|
||||||
|
private final MciCfpaClient mci;
|
||||||
|
private final MetaCommonCodeConverter converter;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object execute(MetaCommonCodeRequest req) {
|
||||||
|
log.info("[MCI Tool] {} 요청 수신. 파라미터: {}", "metaCommonCode", req);
|
||||||
|
try {
|
||||||
|
CLCNNB00001_I mciRequest = converter.toLegacyRequest(req);
|
||||||
|
|
||||||
|
Object mciResponse = mci.callCfpa0001(mciRequest);
|
||||||
|
log.info("[MCI Tool] CLCNNB00001 MCI call completed. Returning response status: {}",
|
||||||
|
mciResponse != null);
|
||||||
|
|
||||||
|
// 현업 테스트용으로 MCI 통신을 바이패스하고 하드코딩된 메타 통합코드 샘플 결과를 반환합니다.
|
||||||
|
MetaCommonCodeResponse res = new MetaCommonCodeResponse();
|
||||||
|
List<MetaCommonCodeItem> list = new ArrayList<>();
|
||||||
|
|
||||||
|
MetaCommonCodeItem item1 = new MetaCommonCodeItem();
|
||||||
|
item1.setGroupCode(req.getGroupCode() != null ? req.getGroupCode() : "GRP_COMM_CD");
|
||||||
|
item1.setCode("CD001");
|
||||||
|
item1.setCodeName("진행중");
|
||||||
|
item1.setCodeDesc("SR 요청 처리 진행 중 상태");
|
||||||
|
item1.setSortSeq(1);
|
||||||
|
item1.setUseYn("Y");
|
||||||
|
list.add(item1);
|
||||||
|
|
||||||
|
MetaCommonCodeItem item2 = new MetaCommonCodeItem();
|
||||||
|
item2.setGroupCode(req.getGroupCode() != null ? req.getGroupCode() : "GRP_COMM_CD");
|
||||||
|
item2.setCode("CD002");
|
||||||
|
item2.setCodeName("완료");
|
||||||
|
item2.setCodeDesc("SR 요청 처리 완료 상태");
|
||||||
|
item2.setSortSeq(2);
|
||||||
|
item2.setUseYn("Y");
|
||||||
|
list.add(item2);
|
||||||
|
|
||||||
|
MetaCommonCodeItem item3 = new MetaCommonCodeItem();
|
||||||
|
item3.setGroupCode(req.getGroupCode() != null ? req.getGroupCode() : "GRP_COMM_CD");
|
||||||
|
item3.setCode("CD003");
|
||||||
|
item3.setCodeName("보류");
|
||||||
|
item3.setCodeDesc("SR 요청 처리 일시 보류 상태");
|
||||||
|
item3.setSortSeq(3);
|
||||||
|
item3.setUseYn("N");
|
||||||
|
list.add(item3);
|
||||||
|
|
||||||
|
res.setCodeList(list);
|
||||||
|
|
||||||
|
return res;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[MCI Tool] 연동 중 오류 발생: {}", e.getMessage(), e);
|
||||||
|
return Map.of("status", "ERROR", "message", e.getMessage() != null ? e.getMessage() : "Unknown error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
package io.shinhanlife.dap.mcc.biz.cmm.usecase.impl;
|
||||||
|
|
||||||
|
import io.shinhanlife.dap.mcc.biz.cmm.converter.MetaTableConverter;
|
||||||
|
import io.shinhanlife.dap.mcc.biz.cmm.dto.MetaTableRequest;
|
||||||
|
import io.shinhanlife.dap.mcc.biz.cmm.dto.MetaTableResponse;
|
||||||
|
import io.shinhanlife.dap.mcc.biz.cmm.dto.MetaTableResponse.MetaTableItem;
|
||||||
|
import io.shinhanlife.dap.mcc.biz.cmm.usecase.MetaTableUseCase;
|
||||||
|
import io.shinhanlife.dap.mcc.infra.itrf.mci.cfp.a.MciCfpaClient;
|
||||||
|
import io.shinhanlife.dap.mcc.infra.itrf.mci.cfp.a.io.CLCNNB00001_I;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @package io.shinhanlife.dap.mcc.biz.cmm.usecase.impl
|
||||||
|
* @className MetaTableUseCaseImpl
|
||||||
|
* @description AX HUB 시스템 처리 클래스
|
||||||
|
* @author 09863409
|
||||||
|
* @create 2026.07.29
|
||||||
|
* <pre>
|
||||||
|
* ---------- 개정이력 ----------
|
||||||
|
* 수정일 수정자 수정내용
|
||||||
|
* ---------- -------- ---------------------------
|
||||||
|
* 2026.07.29 09863409 최초생성
|
||||||
|
*
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class MetaTableUseCaseImpl implements MetaTableUseCase {
|
||||||
|
|
||||||
|
private final MciCfpaClient mci;
|
||||||
|
private final MetaTableConverter converter;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object execute(MetaTableRequest req) {
|
||||||
|
log.info("[MCI Tool] {} 요청 수신. 파라미터: {}", "metaTable", req);
|
||||||
|
try {
|
||||||
|
CLCNNB00001_I mciRequest = converter.toLegacyRequest(req);
|
||||||
|
|
||||||
|
Object mciResponse = mci.callCfpa0001(mciRequest);
|
||||||
|
log.info("[MCI Tool] CLCNNB00001 MCI call completed. Returning response status: {}",
|
||||||
|
mciResponse != null);
|
||||||
|
|
||||||
|
// 현업 테스트용으로 MCI 통신을 바이패스하고 하드코딩된 메타 테이블 샘플 결과를 반환합니다.
|
||||||
|
MetaTableResponse res = new MetaTableResponse();
|
||||||
|
List<MetaTableItem> list = new ArrayList<>();
|
||||||
|
|
||||||
|
MetaTableItem item1 = new MetaTableItem();
|
||||||
|
item1.setOwner(req.getOwner() != null ? req.getOwner() : "DAPADM");
|
||||||
|
item1.setTableName(req.getTableName() != null ? req.getTableName() : "TB_CUST_BAS");
|
||||||
|
item1.setTableLogicalName("고객기본정보");
|
||||||
|
item1.setTableDesc("고객 기본 프로필 및 인적사항 관리 테이블");
|
||||||
|
item1.setColumnCount(35);
|
||||||
|
item1.setRowCount(1250000L);
|
||||||
|
list.add(item1);
|
||||||
|
|
||||||
|
MetaTableItem item2 = new MetaTableItem();
|
||||||
|
item2.setOwner(req.getOwner() != null ? req.getOwner() : "DAPADM");
|
||||||
|
item2.setTableName("TB_CONT_MCD");
|
||||||
|
item2.setTableLogicalName("계약주계약정보");
|
||||||
|
item2.setTableDesc("보험 계약 주계약 상세 원장 테이블");
|
||||||
|
item2.setColumnCount(58);
|
||||||
|
item2.setRowCount(3400000L);
|
||||||
|
list.add(item2);
|
||||||
|
|
||||||
|
MetaTableItem item3 = new MetaTableItem();
|
||||||
|
item3.setOwner(req.getOwner() != null ? req.getOwner() : "DAPADM");
|
||||||
|
item3.setTableName("TB_CLAIM_DTL");
|
||||||
|
item3.setTableLogicalName("청구접수상세");
|
||||||
|
item3.setTableDesc("보험금 청구 접수 건별 내역 테이블");
|
||||||
|
item3.setColumnCount(42);
|
||||||
|
item3.setRowCount(890000L);
|
||||||
|
list.add(item3);
|
||||||
|
|
||||||
|
res.setTableList(list);
|
||||||
|
|
||||||
|
return res;
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[MCI Tool] 연동 중 오류 발생: {}", e.getMessage(), e);
|
||||||
|
return Map.of("status", "ERROR", "message", e.getMessage() != null ? e.getMessage() : "Unknown error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package io.shinhanlife.dap.mcc.biz.sol.converter;
|
||||||
|
|
||||||
|
import io.shinhanlife.dap.mcc.biz.sol.dto.SolReqDetailRequest;
|
||||||
|
import io.shinhanlife.dap.mcc.biz.sol.dto.SolReqDetailResponse;
|
||||||
|
import io.shinhanlife.dap.mcc.infra.itrf.mci.ncl.g.io.SOLG00000002_I;
|
||||||
|
import io.shinhanlife.dap.mcc.infra.itrf.mci.ncl.g.io.SOLG00000002_O;
|
||||||
|
import org.mapstruct.Mapper;
|
||||||
|
import org.mapstruct.Mapping;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @package io.shinhanlife.dap.mcc.biz.sol.converter
|
||||||
|
* @className SolReqDetailConverter
|
||||||
|
* @description AX HUB 시스템 처리 클래스
|
||||||
|
* @author 0986406
|
||||||
|
* @create 2026.09.01
|
||||||
|
* <pre>
|
||||||
|
* ---------- 개정이력 ----------
|
||||||
|
* 수정일 수정자 수정내용
|
||||||
|
* ---------- -------- ---------------------------
|
||||||
|
* 2026.09.01 0986406 최초생성
|
||||||
|
*
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
@Mapper(componentModel = "spring")
|
||||||
|
public interface SolReqDetailConverter {
|
||||||
|
|
||||||
|
@Mapping(source = "srId", target = "srId")
|
||||||
|
SOLG00000002_I toLegacyRequest(SolReqDetailRequest req);
|
||||||
|
|
||||||
|
SolReqDetailResponse toResponse(SOLG00000002_O mciRes);
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package io.shinhanlife.dap.mcc.biz.sol.dto;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
|
import io.shinhanlife.dap.lib.annotation.McpParameter;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @package io.shinhanlife.dap.mcc.biz.sol.dto
|
||||||
|
* @className SolReqDetailRequest
|
||||||
|
* @description AX HUB 시스템 처리 클래스
|
||||||
|
* @author 0986406
|
||||||
|
* @create 2026.09.01
|
||||||
|
* <pre>
|
||||||
|
* ---------- 개정이력 ----------
|
||||||
|
* 수정일 수정자 수정내용
|
||||||
|
* ---------- -------- ---------------------------
|
||||||
|
* 2026.09.01 0986406 최초생성
|
||||||
|
*
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
|
public class SolReqDetailRequest {
|
||||||
|
|
||||||
|
@McpParameter(description = "상세 조회할 SOL 의뢰서 ID", required = true)
|
||||||
|
private String srId;
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package io.shinhanlife.dap.mcc.biz.sol.dto;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @package io.shinhanlife.dap.mcc.biz.sol.dto
|
||||||
|
* @className SolReqDetailResponse
|
||||||
|
* @description AX HUB 시스템 처리 클래스
|
||||||
|
* @author 0986406
|
||||||
|
* @create 2026.09.01
|
||||||
|
* <pre>
|
||||||
|
* ---------- 개정이력 ----------
|
||||||
|
* 수정일 수정자 수정내용
|
||||||
|
* ---------- -------- ---------------------------
|
||||||
|
* 2026.09.01 0986406 최초생성
|
||||||
|
*
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
|
public class SolReqDetailResponse {
|
||||||
|
|
||||||
|
private String srId;
|
||||||
|
private String srName;
|
||||||
|
private String process;
|
||||||
|
private String devStage;
|
||||||
|
private String appName;
|
||||||
|
private String requester;
|
||||||
|
private String requestDate;
|
||||||
|
private String dueDate;
|
||||||
|
private String description;
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package io.shinhanlife.dap.mcc.biz.sol.usecase;
|
||||||
|
|
||||||
|
import io.shinhanlife.dap.lib.annotation.McpFunction;
|
||||||
|
import io.shinhanlife.dap.lib.annotation.McpTool;
|
||||||
|
import io.shinhanlife.dap.mcc.biz.sol.dto.SolReqDetailRequest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @package io.shinhanlife.dap.mcc.biz.sol.usecase
|
||||||
|
* @className SolReqDetailUseCase
|
||||||
|
* @description AX HUB 시스템 처리 클래스
|
||||||
|
* @author 0986406
|
||||||
|
* @create 2026.09.01
|
||||||
|
* <pre>
|
||||||
|
* ---------- 개정이력 ----------
|
||||||
|
* 수정일 수정자 수정내용
|
||||||
|
* ---------- -------- ---------------------------
|
||||||
|
* 2026.09.01 0986406 최초생성
|
||||||
|
*
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
@McpTool(
|
||||||
|
routingType = "MCI",
|
||||||
|
categoryKey = "sol"
|
||||||
|
)
|
||||||
|
public interface SolReqDetailUseCase {
|
||||||
|
|
||||||
|
@McpFunction(
|
||||||
|
displayName = "SolReqDetail 툴",
|
||||||
|
name = "solReqDetail",
|
||||||
|
description = "SOL 의뢰서 상세 조회",
|
||||||
|
prompt = "SOL 의뢰서 상세 조회해줘",
|
||||||
|
mappingId = "SOLG00000002",
|
||||||
|
register = false,
|
||||||
|
requiresApproval = false,
|
||||||
|
readOnlyHint = true,
|
||||||
|
openWorldHint = true
|
||||||
|
)
|
||||||
|
Object execute(SolReqDetailRequest req);
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
package io.shinhanlife.dap.mcc.biz.sol.usecase.impl;
|
||||||
|
|
||||||
|
import io.shinhanlife.dap.mcc.biz.sol.converter.SolReqDetailConverter;
|
||||||
|
import io.shinhanlife.dap.mcc.biz.sol.dto.SolReqDetailRequest;
|
||||||
|
import io.shinhanlife.dap.mcc.biz.sol.dto.SolReqDetailResponse;
|
||||||
|
import io.shinhanlife.dap.mcc.biz.sol.usecase.SolReqDetailUseCase;
|
||||||
|
import io.shinhanlife.dap.mcc.infra.itrf.mci.ncl.g.MciNclgClient;
|
||||||
|
import io.shinhanlife.dap.mcc.infra.itrf.mci.ncl.g.io.SOLG00000002_I;
|
||||||
|
import io.shinhanlife.dap.mcc.infra.itrf.mci.ncl.g.io.SOLG00000002_O;
|
||||||
|
import io.shinhanlife.glow.communication.dto.Transfer;
|
||||||
|
import java.util.Map;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @package io.shinhanlife.dap.mcc.biz.sol.usecase.impl
|
||||||
|
* @className SolReqDetailUseCaseImpl
|
||||||
|
* @description AX HUB 시스템 처리 클래스
|
||||||
|
* @author 0986406
|
||||||
|
* @create 2026.09.01
|
||||||
|
* <pre>
|
||||||
|
* ---------- 개정이력 ----------
|
||||||
|
* 수정일 수정자 수정내용
|
||||||
|
* ---------- -------- ---------------------------
|
||||||
|
* 2026.09.01 0986406 최초생성
|
||||||
|
*
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class SolReqDetailUseCaseImpl implements SolReqDetailUseCase {
|
||||||
|
|
||||||
|
private final MciNclgClient mci;
|
||||||
|
private final SolReqDetailConverter converter;
|
||||||
|
|
||||||
|
@Value("${sol.req-detail.mock-enabled:false}")
|
||||||
|
private boolean mockEnabled;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object execute(SolReqDetailRequest req) {
|
||||||
|
log.info("[MCI Tool] {} 요청 수신. 파라미터: {}", "solReqDetail", req);
|
||||||
|
if (req == null || req.getSrId() == null || req.getSrId().isBlank()) {
|
||||||
|
return Map.of("status", "ERROR", "message", "srId는 필수입니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mockEnabled) {
|
||||||
|
return createLocalSampleResponse(req.getSrId());
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
SOLG00000002_I mciRequest = converter.toLegacyRequest(req);
|
||||||
|
Transfer<SOLG00000002_O> mciResponse = mci.callTo(
|
||||||
|
"SOLG00000002", "SOLG00000002", mciRequest, SOLG00000002_O.class);
|
||||||
|
|
||||||
|
if (mciResponse == null || mciResponse.getBody() == null) {
|
||||||
|
return Map.of("status", "NOT_FOUND", "message", "의뢰서 상세 정보를 찾을 수 없습니다.");
|
||||||
|
}
|
||||||
|
return converter.toResponse(mciResponse.getBody());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[MCI Tool] 연동 중 오류 발생: {}", e.getMessage(), e);
|
||||||
|
return Map.of(
|
||||||
|
"status", "ERROR",
|
||||||
|
"message", e.getMessage() != null ? e.getMessage() : "Unknown error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Object createLocalSampleResponse(String srId) {
|
||||||
|
SolReqDetailResponse response = new SolReqDetailResponse();
|
||||||
|
if ("SR-2026-001".equalsIgnoreCase(srId)) {
|
||||||
|
response.setSrId("SR-2026-001");
|
||||||
|
response.setSrName("AX HUB 메인 화면 UI 개편");
|
||||||
|
response.setProcess("진행중");
|
||||||
|
response.setDevStage("개발(단위테스트)");
|
||||||
|
response.setAppName("AX HUB");
|
||||||
|
response.setRequester("신한준");
|
||||||
|
response.setRequestDate("2026-07-01");
|
||||||
|
response.setDueDate("2026-08-31");
|
||||||
|
response.setDescription("AX HUB 메인 화면의 사용성과 접근성을 개선하는 UI 개편 의뢰입니다.");
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
if ("SR-2026-002".equalsIgnoreCase(srId)) {
|
||||||
|
response.setSrId("SR-2026-002");
|
||||||
|
response.setSrName("SOL 연동 모듈 추가 개발");
|
||||||
|
response.setProcess("진행중");
|
||||||
|
response.setDevStage("분석/설계");
|
||||||
|
response.setAppName("MCP Gateway");
|
||||||
|
response.setRequester("고석민");
|
||||||
|
response.setRequestDate("2026-07-15");
|
||||||
|
response.setDueDate("2026-09-30");
|
||||||
|
response.setDescription("SOL 의뢰서 조회 기능을 MCP 도구로 제공하기 위한 연동 모듈 개발 의뢰입니다.");
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
return Map.of(
|
||||||
|
"status", "NOT_FOUND",
|
||||||
|
"message", "의뢰서를 찾을 수 없습니다.",
|
||||||
|
"srId", srId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package io.shinhanlife.dap.mcc.infra.itrf.mci.ncl.g.io;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @package io.shinhanlife.dap.mcc.infra.itrf.mci.ncl.g.io
|
||||||
|
* @className SOLG00000002_I
|
||||||
|
* @description AX HUB 시스템 처리 클래스
|
||||||
|
* @author 0986406
|
||||||
|
* @create 2026.09.01
|
||||||
|
* <pre>
|
||||||
|
* ---------- 개정이력 ----------
|
||||||
|
* 수정일 수정자 수정내용
|
||||||
|
* ---------- -------- ---------------------------
|
||||||
|
* 2026.09.01 0986406 최초생성
|
||||||
|
*
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class SOLG00000002_I {
|
||||||
|
|
||||||
|
private String srId;
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package io.shinhanlife.dap.mcc.infra.itrf.mci.ncl.g.io;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @package io.shinhanlife.dap.mcc.infra.itrf.mci.ncl.g.io
|
||||||
|
* @className SOLG00000002_O
|
||||||
|
* @description AX HUB 시스템 처리 클래스
|
||||||
|
* @author 0986406
|
||||||
|
* @create 2026.09.01
|
||||||
|
* <pre>
|
||||||
|
* ---------- 개정이력 ----------
|
||||||
|
* 수정일 수정자 수정내용
|
||||||
|
* ---------- -------- ---------------------------
|
||||||
|
* 2026.09.01 0986406 최초생성
|
||||||
|
*
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class SOLG00000002_O {
|
||||||
|
|
||||||
|
private String srId;
|
||||||
|
private String srName;
|
||||||
|
private String process;
|
||||||
|
private String devStage;
|
||||||
|
private String appName;
|
||||||
|
private String requester;
|
||||||
|
private String requestDate;
|
||||||
|
private String dueDate;
|
||||||
|
private String description;
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package io.shinhanlife.dap.mcc.biz.sol.usecase.impl;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
import io.shinhanlife.dap.mcc.biz.sol.converter.SolReqDetailConverter;
|
||||||
|
import io.shinhanlife.dap.mcc.biz.sol.dto.SolReqDetailRequest;
|
||||||
|
import io.shinhanlife.dap.mcc.biz.sol.dto.SolReqDetailResponse;
|
||||||
|
import io.shinhanlife.dap.mcc.infra.itrf.mci.ncl.g.MciNclgClient;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.mockito.Mockito;
|
||||||
|
import org.springframework.test.util.ReflectionTestUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @package io.shinhanlife.dap.mcc.biz.sol.usecase.impl
|
||||||
|
* @className SolReqDetailUseCaseImplTest
|
||||||
|
* @description AX HUB 시스템 처리 클래스
|
||||||
|
* @author 0986406
|
||||||
|
* @create 2026.09.01
|
||||||
|
* <pre>
|
||||||
|
* ---------- 개정이력 ----------
|
||||||
|
* 수정일 수정자 수정내용
|
||||||
|
* ---------- -------- ---------------------------
|
||||||
|
* 2026.09.01 0986406 최초생성
|
||||||
|
*
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
class SolReqDetailUseCaseImplTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void returnsLocalSampleDetailBySrId() {
|
||||||
|
MciNclgClient mci = Mockito.mock(MciNclgClient.class);
|
||||||
|
SolReqDetailConverter converter = Mockito.mock(SolReqDetailConverter.class);
|
||||||
|
SolReqDetailUseCaseImpl useCase = new SolReqDetailUseCaseImpl(mci, converter);
|
||||||
|
ReflectionTestUtils.setField(useCase, "mockEnabled", true);
|
||||||
|
|
||||||
|
SolReqDetailRequest request = new SolReqDetailRequest();
|
||||||
|
request.setSrId("SR-2026-001");
|
||||||
|
|
||||||
|
SolReqDetailResponse response = (SolReqDetailResponse) useCase.execute(request);
|
||||||
|
|
||||||
|
assertThat(response.getSrId()).isEqualTo("SR-2026-001");
|
||||||
|
assertThat(response.getSrName()).isEqualTo("AX HUB 메인 화면 UI 개편");
|
||||||
|
Mockito.verifyNoInteractions(mci);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user