refactor: migrate tools to Spring AI MCP annotations

This commit is contained in:
jade
2026-08-06 00:29:03 +09:00
parent d9e88acbd2
commit 4a343d018a
127 changed files with 2228 additions and 2103 deletions

View File

@@ -58,7 +58,7 @@ public class EaiEimsSender implements EimsSender {
log.warn(" 로컬 환경이거나 Ka<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n" +
"<EaiMessage>\n" +
" <Header>\n" +
" <ChannelId>MCP_WAS_ASYNC</ChannelId>\n" +
" <ChannelId>MCP_GATEWAY_ASYNC</ChannelId>\n" +
" <InterfaceId>EAI_BATCH_JOB</InterfaceId>\n" +
" <Timestamp>1782705901850</Timestamp>\n" +
" <TransferType>ASYNC</TransferType>\n" +
@@ -91,7 +91,7 @@ public class EaiEimsSender implements EimsSender {
return String.format(
"<EaiMessage>" +
"<Header>" +
"<ChannelId>MCP_WAS_ASYNC</ChannelId>" +
"<ChannelId>MCP_GATEWAY_ASYNC</ChannelId>" +
"<InterfaceId>%s</InterfaceId>" +
"<Timestamp>%d</Timestamp>" +
"<TransferType>ASYNC</TransferType>" +

View File

@@ -1,76 +0,0 @@
package io.shinhanlife.dap.lib.annotation;
/**
* @package io.shinhanlife.dap.lib.annotation
* @className McpFunction
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface McpFunction {
String displayName(); // 사람이 읽는 라벨 (예: "고객 조회 툴")
String name(); // MCP 서브툴 명칭 (예: "customer_search")
String description();
String prompt() default "";
String mappingId() default "";
/**
* Tool 입력 JSON Schema를 인라인으로 지정한다. 지정하지 않으면 요청 DTO에서 자동 생성한다.
*/
String inputSchema() default "{}";
/**
* 복합 조건(anyOf 등)이 필요한 Tool의 입력 JSON Schema 클래스패스 경로다.
* inputSchemaResource가 지정되면 inputSchema 및 DTO 자동 생성보다 우선한다.
*/
// 추가: Redis 자동 등록 및 Heartbeat 대상 여부 제어
String inputSchemaResource() default "";
/**
* Tool response JSON Schema. When unset, output validation is skipped.
*/
String outputSchema() default "{}";
/**
* Classpath resource for a complex Tool response JSON Schema.
* This value has priority over outputSchema.
*/
String outputSchemaResource() default "";
boolean register() default false;
// 추가: 툴 목록 노출 여부 제어 (false 시 라우팅은 되나 목록에서 숨김)
boolean visible() default true;
// 추가: HITL 승인 체계 지원 (실행 전 사용자 승인 필요 여부)
boolean requiresApproval() default false;
boolean readOnlyHint() default false;
boolean destructiveHint() default false;
boolean idempotentHint() default false;
boolean openWorldHint() default false;
/** Version exposed as _meta.version in the Tool Manifest. */
String version() default "1.0.0";
/** Maximum execution time exposed as _meta.timeoutMillis in the Tool Manifest. */
long timeoutMillis() default 300000L;
/** Whether the Tool is available for MCP exposure. */
boolean enabled() default true;
// 추가: 툴 별 기본 Timeout 설정 (기본 300초 = 300000ms)
}

View File

@@ -1,33 +0,0 @@
package io.shinhanlife.dap.lib.annotation;
/**
* @package io.shinhanlife.dap.lib.annotation
* @className McpOutputSchema
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Marks a Tool response DTO for automatic output JSON Schema generation.
* Field constraints are declared with {@link McpValidation}.
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface McpOutputSchema {
}

View File

@@ -1,26 +0,0 @@
package io.shinhanlife.dap.lib.annotation;
/**
* @package io.shinhanlife.dap.lib.annotation
* @className McpParameter
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import java.lang.annotation.*;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface McpParameter {
String description();
boolean required() default false;
}

View File

@@ -1,33 +0,0 @@
package io.shinhanlife.dap.lib.annotation;
/**
* @package io.shinhanlife.dap.lib.annotation
* @className McpTool
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import org.springframework.core.annotation.AliasFor;
import org.springframework.stereotype.Component;
import java.lang.annotation.*;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface McpTool {
@AliasFor(annotation = Component.class)
String value() default "";
String categoryKey() default "common";
String routingType() default "HTTP";
}

View File

@@ -1,52 +0,0 @@
package io.shinhanlife.dap.lib.annotation;
/**
* @package io.shinhanlife.dap.lib.annotation
* @className McpValidation
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @package io.shinhanlife.dap.lib.annotation
* @className McpValidation
* @description Declares JSON Schema validation constraints for MCP tool input fields
* @author 0986406
* @create 2026.07.27
* <pre>
* ---------- revision history ----------
* date author description
* ---------- --------- ---------------------------
* 2026.07.27 0986406 initial creation
* </pre>
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface McpValidation {
boolean required() default false;
String pattern() default "";
long minimum() default Long.MIN_VALUE;
long maximum() default Long.MAX_VALUE;
int minLength() default -1;
int maxLength() default -1;
String[] allowedValues() default {};
String format() default "";
boolean nullable() default false; String defaultValue() default "";
String[] examples() default {};
}

View File

@@ -1,9 +1,15 @@
package io.shinhanlife.dap.lib.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Spring AI @Tool 어노테이션을 보완하여 MCP 시스템 메타데이터를 추가 제공하는 힌트 어노테이션
* @package io.shinhanlife.dap.lib.annotation
* @className McpAnyOf
* @description AX HUB 시스템 처리 클래스
* @className ToolHint
* @description 비즈니스 로직(Tool) 시스템 제어 메타데이터 분리
* @author 0986406
* @create 2026.09.01
* <pre>
@@ -14,22 +20,12 @@ package io.shinhanlife.dap.lib.annotation;
*
* </pre>
*/
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* MCP 스키마 생성 anyOf (해당 필드들 최소 1개 이상 필수) 제약을 부여합니다.
*/
@Target({ElementType.TYPE})
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface McpAnyOf {
/**
* anyOf 제약에 포함될 필드명 목록
* : @McpAnyOf({"claimNo", "contractNo"})
*/
String[] value();
public @interface ToolHint {
boolean register() default false;
boolean requiresApproval() default false;
String mappingId() default "";
String inputSchemaResource() default "";
String outputSchemaResource() default "";
}

View File

@@ -15,7 +15,6 @@ package io.shinhanlife.dap.lib.aop;
*
* </pre>
*/
import io.shinhanlife.dap.lib.annotation.McpFunction;
import io.shinhanlife.dap.lib.config.McpProperties;
import java.lang.reflect.Method;
import lombok.RequiredArgsConstructor;
@@ -24,6 +23,7 @@ import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.ai.mcp.annotation.McpTool;
import org.springframework.stereotype.Component;
import org.springframework.util.StopWatch;
@@ -35,12 +35,12 @@ public class ToolSlaMonitoringAspect {
private final McpProperties mcpProperties;
// @McpFunction 어노테이션이 붙은 모든 비즈니스 툴 메서드 실행을 가로챕니다.
@Around("@annotation(McpFunction)")
// @McpTool 어노테이션이 붙은 모든 비즈니스 툴 메서드 실행을 가로챕니다.
@Around("@annotation(org.springframework.ai.mcp.annotation.McpTool)")
public Object monitorToolSla(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
McpFunction functionAnnotation = method.getAnnotation(McpFunction.class);
McpTool functionAnnotation = method.getAnnotation(McpTool.class);
// 네임스페이스 자동 주입 로직을 반영하여 최종 툴 이름을 산출합니다.
String baseName = functionAnnotation.name();

View File

@@ -1,21 +1,5 @@
package io.shinhanlife.dap.lib.config;
/**
* @package io.shinhanlife.dap.lib.config
* @className ToolSchemaConfiguration
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import com.fasterxml.jackson.databind.ObjectMapper;
import io.shinhanlife.dap.lib.util.ToolSchemaResolver;
import org.springframework.context.annotation.Bean;

View File

@@ -0,0 +1,20 @@
package io.shinhanlife.dap.lib.dto;
/**
* @package io.shinhanlife.dap.mcg.dto
* @className OperationType
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
public enum OperationType {
READ,
WRITE
}

View File

@@ -1,21 +1,5 @@
package io.shinhanlife.dap.lib.manifest;
/**
* @package io.shinhanlife.dap.lib.manifest
* @className ToolManifestAnnotations
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
/** Behaviour hints exposed by the Tool Service manifest. */
public record ToolManifestAnnotations(
String title,

View File

@@ -1,21 +1,5 @@
package io.shinhanlife.dap.lib.manifest;
/**
* @package io.shinhanlife.dap.lib.manifest
* @className ToolManifestItem
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.Map;

View File

@@ -1,21 +1,5 @@
package io.shinhanlife.dap.lib.manifest;
/**
* @package io.shinhanlife.dap.lib.manifest
* @className ToolManifestMeta
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
/** Operational metadata exposed by the Tool Service manifest. */
public record ToolManifestMeta(String version, long timeoutMillis, boolean enabled) {
}

View File

@@ -1,21 +1,5 @@
package io.shinhanlife.dap.lib.manifest;
/**
* @package io.shinhanlife.dap.lib.manifest
* @className ToolManifestResponse
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import java.util.List;
/** Top-level response for GET /tool-manifest. */

View File

@@ -1,25 +1,9 @@
package io.shinhanlife.dap.lib.manifest;
/**
* @package io.shinhanlife.dap.lib.manifest
* @className ToolManifestService
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import com.fasterxml.jackson.databind.ObjectMapper;
import io.shinhanlife.dap.lib.config.McpProperties;
import io.shinhanlife.dap.mcc.dto.ToolMetadata;
import io.shinhanlife.dap.lib.mcp.LocalToolScanner;
import io.shinhanlife.dap.lib.mcp.ToolRegistryHeartbeatSender;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Comparator;
@@ -46,9 +30,9 @@ public class ToolManifestService {
private String lastRevision;
@Autowired
public ToolManifestService(LocalToolScanner localToolScanner, ObjectMapper objectMapper,
public ToolManifestService(ToolRegistryHeartbeatSender heartbeatSender, ObjectMapper objectMapper,
McpProperties properties) {
this(localToolScanner::getAllScannedTools, objectMapper, properties);
this(heartbeatSender::getAllScannedTools, objectMapper, properties);
}
ToolManifestService(Supplier<List<ToolMetadata>> toolSupplier, ObjectMapper objectMapper,

View File

@@ -1,142 +0,0 @@
package io.shinhanlife.dap.lib.mcp;
/**
* @package io.shinhanlife.dap.mcc.service
* @className LocalToolScanner
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import com.fasterxml.jackson.databind.ObjectMapper;
import io.shinhanlife.dap.lib.annotation.McpFunction;
import io.shinhanlife.dap.lib.annotation.McpTool;
import io.shinhanlife.dap.lib.config.McpProperties;
import io.shinhanlife.dap.mcc.dto.ToolMetadata;
import io.shinhanlife.dap.lib.util.ToolSchemaResolver;
import jakarta.annotation.PostConstruct;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;
import org.springframework.util.ClassUtils;
@Slf4j
@Component
@Configuration
@RequiredArgsConstructor
public class LocalToolScanner {
private final ApplicationContext applicationContext;
private final ObjectMapper objectMapper;
private final McpProperties mcpProperties;
private final ToolSchemaResolver toolSchemaResolver;
@Value("${axhub.tool.url:http://localhost:8080}")
private String podUrl;
private List<ToolMetadata> registeredTools = new ArrayList<>();
@Getter
private List<ToolMetadata> allScannedTools = new ArrayList<>();
@PostConstruct
public void init() {
log.info(" [LocalToolScanner] 초기화 시작. Pod URL: {}", podUrl);
scanAndBuildMetadata();
}
private void scanAndBuildMetadata() {
Map<String, Object> allBeans = applicationContext.getBeansOfType(Object.class);
for (Object bean : allBeans.values()) {
Class<?> targetClass = AopUtils.getTargetClass(bean);
// 클래스 또는 프록시(인터페이스)에서 @McpTool 스캔
McpTool toolAnnotation = AnnotationUtils.findAnnotation(targetClass, McpTool.class);
if (toolAnnotation == null) {
toolAnnotation = AnnotationUtils.findAnnotation(bean.getClass(), McpTool.class);
}
for (Method method : targetClass.getDeclaredMethods()) {
// 메서드, 수퍼클래스, 인터페이스를 모두 뒤져서 @McpFunction 스캔
McpFunction functionAnnotation = AnnotationUtils.findAnnotation(method, McpFunction.class);
if (functionAnnotation != null && toolAnnotation != null) {
String baseName = functionAnnotation.displayName();
String rawSubToolName = functionAnnotation.name();
String subToolName = mcpProperties.getNamespace() != null && !mcpProperties.getNamespace().isEmpty()
? mcpProperties.getNamespace() + "_" + rawSubToolName
: rawSubToolName;
boolean isRegister = functionAnnotation.register();
if (!isRegister) {
log.info(" [LocalToolScanner] '{}' 툴은 어노테이션 설정에 의해 제외되었습니다. (최종 이름: {})", baseName, subToolName);
}
ToolMetadata meta = new ToolMetadata();
meta.setUid(UUID.nameUUIDFromBytes(subToolName.getBytes()).toString());
meta.setDisplayName(baseName);
meta.setName(subToolName);
meta.setSemver(functionAnnotation.version());
meta.setTimeoutMillis(functionAnnotation.timeoutMillis());
meta.setEnabled(functionAnnotation.enabled());
meta.setDescription(functionAnnotation.description());
meta.setCategoryKey(toolAnnotation.categoryKey());
meta.setIntegrationType(toolAnnotation.routingType());
meta.setMciServiceId(functionAnnotation.mappingId());
meta.setPodUrl(podUrl);
meta.setEndpoint(podUrl.replaceAll("/+$", "") + "/mcp/" + subToolName);
boolean isVisible = functionAnnotation.visible();
meta.setVisible(isVisible);
meta.setIsRegistered(isRegister);
meta.setRequiresApproval(functionAnnotation.requiresApproval());
meta.setReadOnlyHint(functionAnnotation.readOnlyHint());
meta.setDestructiveHint(functionAnnotation.destructiveHint());
meta.setIdempotentHint(functionAnnotation.idempotentHint());
meta.setOpenWorldHint(functionAnnotation.openWorldHint());
Map<String, String> prompts = new HashMap<>();
String promptText = functionAnnotation.prompt();
prompts.put(subToolName, promptText);
meta.setActionPrompts(prompts);
if (method.getParameterCount() > 0) {
try {
Class<?> paramType = method.getParameterTypes()[0];
Map<String, Object> finalSchema = toolSchemaResolver.resolve(functionAnnotation, paramType);
meta.setParametersSchema(finalSchema);
} catch (Exception e) {
log.error("Failed to generate schema for {}", subToolName, e);
}
}
if (isRegister) {
registeredTools.add(meta);
}
allScannedTools.add(meta);
log.info(" [LocalToolScanner] 도구 메타데이터 생성: {} (isRegistered: {})", meta.getUid(), isRegister);
}
}
}
}
}

View File

@@ -1,21 +1,5 @@
package io.shinhanlife.dap.lib.mcp;
/**
* @package io.shinhanlife.dap.lib.mcp
* @className McpRequestHeaderContext
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
/** Holds optional MCP headers for the lifetime of one HTTP request thread. */
public final class McpRequestHeaderContext {
private static final ThreadLocal<McpRequestHeaders> CURRENT_HEADERS = new ThreadLocal<>();

View File

@@ -1,21 +1,5 @@
package io.shinhanlife.dap.lib.mcp;
/**
* @package io.shinhanlife.dap.lib.mcp
* @className McpRequestHeaderFilter
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import java.io.IOException;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;

View File

@@ -1,21 +1,5 @@
package io.shinhanlife.dap.lib.mcp;
/**
* @package io.shinhanlife.dap.lib.mcp
* @className McpRequestHeaders
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
/** Optional request headers propagated from an MCP HTTP request to a Tool invocation. */
public record McpRequestHeaders(
String headerRequestId,

View File

@@ -1,31 +1,25 @@
package io.shinhanlife.dap.lib.mcp;
/**
* @package io.shinhanlife.dap.lib.mcp
* @className ToolMcpServerConfiguration
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
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;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
/** Exposes every Tool Pod through the MCP Streamable HTTP transport. */
@Slf4j
@Configuration
public class ToolMcpServerConfiguration {
@PostConstruct
public void init() {
log.warn("=================================================");
log.warn("ToolMcpServerConfiguration IS LOADED BY SPRING!!!");
log.warn("=================================================");
}
@Bean
@Primary
public HttpServletStreamableServerTransportProvider toolMcpTransportProvider() {
@@ -37,6 +31,8 @@ public class ToolMcpServerConfiguration {
@Bean
public ServletRegistrationBean<HttpServletStreamableServerTransportProvider> toolMcpServlet(
HttpServletStreamableServerTransportProvider transportProvider) {
return new ServletRegistrationBean<>(transportProvider, "/mcp");
// "/mcp/*"로 매핑하면 BusinessToolController의 "/mcp/api/v1/tools/local" 까지 가로채게 되므로,
// 정확히 MCP 통신에 사용되는 "/mcp" 와 "/mcp/message" 두 개만 매핑합니다.
return new ServletRegistrationBean<>(transportProvider, "/mcp", "/mcp/message");
}
}

View File

@@ -1,28 +1,14 @@
package io.shinhanlife.dap.lib.mcp;
/**
* @package io.shinhanlife.dap.lib.mcp
* @className ToolPodMcpToolSynchronizer
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
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.lib.mcp.LocalToolScanner;
import io.shinhanlife.dap.lib.mcp.ToolRegistryHeartbeatSender;
import io.shinhanlife.dap.lib.mcp.McpRequestHeaderContext;
import io.shinhanlife.dap.lib.mcp.McpRequestHeaders;
import java.util.LinkedHashMap;
import java.util.Map;
import org.springframework.boot.context.event.ApplicationReadyEvent;
@@ -30,25 +16,28 @@ import org.springframework.context.event.EventListener;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
/** Registers the Tool Pod's existing annotated tools with its MCP SDK server. */
@Component
@ConditionalOnBean(BusinessToolController.class)
public class ToolPodMcpToolSynchronizer {
private final McpSyncServer mcpServer;
private final LocalToolScanner localToolScanner;
private final ToolRegistryHeartbeatSender heartbeatSender;
private final BusinessToolController businessToolController;
private final ObjectMapper objectMapper;
public ToolPodMcpToolSynchronizer(McpSyncServer mcpServer, LocalToolScanner localToolScanner,
public ToolPodMcpToolSynchronizer(McpSyncServer mcpServer, ToolRegistryHeartbeatSender heartbeatSender,
BusinessToolController businessToolController, ObjectMapper objectMapper) {
this.mcpServer = mcpServer;
this.localToolScanner = localToolScanner;
this.heartbeatSender = heartbeatSender;
this.businessToolController = businessToolController;
this.objectMapper = objectMapper;
}
@EventListener(ApplicationReadyEvent.class)
public void registerLocalTools() {
localToolScanner.getAllScannedTools().stream()
heartbeatSender.getAllScannedTools().stream()
.filter(tool -> Boolean.TRUE.equals(tool.getVisible()))
.forEach(tool -> mcpServer.addTool(specification(tool)));
}

View File

@@ -0,0 +1,196 @@
package io.shinhanlife.dap.lib.mcp;
/**
* @package io.shinhanlife.dap.mcc.service
* @className ToolRegistryHeartbeatSender
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.ai.mcp.annotation.McpTool;
import io.shinhanlife.dap.lib.annotation.ToolHint;
import io.shinhanlife.dap.lib.config.McpProperties;
import io.shinhanlife.dap.mcc.dto.ToolMetadata;
import io.shinhanlife.dap.lib.util.ToolSchemaResolver;
import jakarta.annotation.PostConstruct;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.http.ResponseEntity;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.util.ClassUtils;
import org.springframework.web.client.RestClient;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import io.shinhanlife.dap.mcc.presentation.BusinessToolController;
@Slf4j
@Component
@Configuration
@EnableScheduling
@RequiredArgsConstructor
@ConditionalOnBean(BusinessToolController.class)
public class ToolRegistryHeartbeatSender {
private final ApplicationContext applicationContext;
private final ObjectMapper objectMapper;
private final McpProperties mcpProperties;
private final RestClient restClient = RestClient.create();
private final ToolSchemaResolver toolSchemaResolver;
@Value("${axhub.gateway.url:http://localhost:8081}")
private String gatewayUrl;
@Value("${axhub.tool.url:http://localhost:8080}")
private String podUrl;
private List<ToolMetadata> registeredTools = new ArrayList<>();
@Getter
private List<ToolMetadata> allScannedTools = new ArrayList<>();
@PostConstruct
public void init() {
log.info(" [HeartbeatSender] 초기화 시작. Gateway URL: {}, Pod URL: {}", gatewayUrl, podUrl);
scanAndBuildMetadata();
}
private void scanAndBuildMetadata() {
Map<String, Object> allBeans = applicationContext.getBeansOfType(Object.class);
for (Object bean : allBeans.values()) {
Class<?> targetClass = AopUtils.getTargetClass(bean);
for (Method method : targetClass.getDeclaredMethods()) {
McpTool functionAnnotation = AnnotationUtils.findAnnotation(method, McpTool.class);
ToolHint hintAnnotation = AnnotationUtils.findAnnotation(method, ToolHint.class);
if (functionAnnotation != null) {
String baseName = functionAnnotation.name();
String rawSubToolName = functionAnnotation.name();
String subToolName = mcpProperties.getNamespace() != null && !mcpProperties.getNamespace().isEmpty()
? mcpProperties.getNamespace() + "_" + rawSubToolName
: rawSubToolName;
boolean isRegister = hintAnnotation != null && hintAnnotation.register();
if (!isRegister) {
log.info(" [HeartbeatSender] '{}' 툴은 어노테이션 설정에 의해 외부 등록(Redis) 대상에서 제외되었습니다. (최종 이름: {})", baseName, subToolName);
}
ToolMetadata meta = new ToolMetadata();
meta.setUid(UUID.nameUUIDFromBytes(subToolName.getBytes()).toString());
String displayName = functionAnnotation.title().isEmpty() ? functionAnnotation.name() : functionAnnotation.title();
meta.setDisplayName(displayName);
meta.setName(subToolName);
meta.setSemver("1.0.0");
meta.setTimeoutMillis(5000L);
meta.setEnabled(true);
meta.setDescription(functionAnnotation.description());
meta.setCategoryKey("default");
meta.setIntegrationType("REST");
meta.setMciServiceId(hintAnnotation != null ? hintAnnotation.mappingId() : "");
meta.setPodUrl(podUrl);
meta.setEndpoint(podUrl.replaceAll("/+$", "") + "/mcp/" + subToolName);
meta.setVisible(true);
meta.setIsRegistered(isRegister);
meta.setRequiresApproval(hintAnnotation != null && hintAnnotation.requiresApproval());
// extract standard hints from @McpTool.annotations()
McpTool.McpAnnotations ann = functionAnnotation.annotations();
if (ann != null) {
meta.setReadOnlyHint(ann.readOnlyHint());
meta.setDestructiveHint(ann.destructiveHint());
meta.setIdempotentHint(ann.idempotentHint());
meta.setOpenWorldHint(ann.openWorldHint());
} else {
meta.setReadOnlyHint(false);
meta.setDestructiveHint(true);
meta.setIdempotentHint(false);
meta.setOpenWorldHint(true);
}
Map<String, String> prompts = new HashMap<>();
meta.setActionPrompts(prompts);
if (method.getParameterCount() > 0) {
try {
Class<?> paramType = method.getParameterTypes()[0];
// TODO: ToolSchemaResolver may need to be updated to take McpTool instead of McpFunction
Map<String, Object> finalSchema = toolSchemaResolver.resolve(functionAnnotation, hintAnnotation, paramType);
meta.setParametersSchema(finalSchema);
} catch (Exception e) {
log.error("Failed to generate schema for {}", subToolName, e);
}
}
if (isRegister) {
registeredTools.add(meta);
}
allScannedTools.add(meta);
log.info(" [HeartbeatSender] 도구 메타데이터 생성: {} (isRegistered: {})", meta.getUid(), isRegister);
}
}
}
}
@Scheduled(fixedRate = 30000)
public void sendHeartbeats() {
if (registeredTools.isEmpty()) return;
for (ToolMetadata tool : registeredTools) {
try {
ResponseEntity<String> response = restClient.post()
.uri(gatewayUrl + "/mcp/api/v1/registry/heartbeat")
.header("Content-Type", "application/json")
.body(tool.getUid())
.retrieve()
.toEntity(String.class);
if (response.getStatusCode().is2xxSuccessful()) {
log.info(" [HeartbeatSender] 하트비트 전송 성공: {}", tool.getUid());
}
} catch (Exception e) {
log.warn(" [HeartbeatSender] 하트비트 전송 실패 ({}): {}. 재등록을 시도합니다.", tool.getUid(), e.getMessage());
registerTool(tool);
}
}
}
private void registerTool(ToolMetadata tool) {
try {
restClient.post()
.uri(gatewayUrl + "/mcp/api/v1/registry/register")
.header("Content-Type", "application/json")
.body(tool)
.retrieve()
.toBodilessEntity();
log.info(" [HeartbeatSender] 툴 재등록 성공: {}", tool.getUid());
} catch (Exception ex) {
log.error(" [HeartbeatSender] 툴 등록 실패: {}", ex.getMessage());
}
}
}

View File

@@ -29,11 +29,11 @@ public class SwaggerConfig {
public OpenAPI customOpenAPI() {
return new OpenAPI()
.info(new Info()
.title("Shinhan MCP WAS API 명세서")
.title("Shinhan MCP Gateway API 명세서")
.version("v1.0")
.description("AI Agent와 신한라이프 내부망(EIMS/EAI)을 연결하는 Adapter WAS API 문서입니다."))
.description("AI Agent와 신한라이프 내부망(EIMS/EAI)을 연결하는 Adapter Gateway API 문서입니다."))
.addServersItem(new io.swagger.v3.oas.models.servers.Server().url("http://localhost:8080").description("Adapter Pod (8080)"))
.addServersItem(new io.swagger.v3.oas.models.servers.Server().url("http://localhost:8081").description("WAS Pod"))
.addServersItem(new io.swagger.v3.oas.models.servers.Server().url("http://localhost:8081").description("Gateway Pod (8081)"))
// 전역적으로 X-API-KEY 보안 설정을 Swagger UI에 추가합니다.
.addSecurityItem(new SecurityRequirement().addList("X-API-KEY"))
.components(new Components()

View File

@@ -43,7 +43,7 @@ public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
// Swagger UI(8080)에서 WAS로 API 호출 시 발생하는 CORS 에러 해결
// Swagger UI(8080)에서 Gateway(8081)로 API 호출 시 발생하는 CORS 에러 해결
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")

View File

@@ -29,25 +29,25 @@ public class GlobalExceptionHandler {
@ExceptionHandler(NoResourceFoundException.class)
public ResponseEntity<Void> handleNoResourceFound(NoResourceFoundException e) {
log.warn(" [WAS Not Found] 요청하신 리소스를 찾을 수 없습니다: {}", e.getResourcePath());
log.warn(" [Gateway Not Found] 요청하신 리소스를 찾을 수 없습니다: {}", e.getResourcePath());
return ResponseEntity.notFound().build();
}
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<JsonRpcResponse> handleIllegalArgument(IllegalArgumentException e) {
log.warn(" [WAS Bad Request] 잘못된 요청: {}", e.getMessage());
log.warn(" [Gateway Bad Request] 잘못된 요청: {}", e.getMessage());
return buildErrorResponse(-32602, "Invalid params: " + e.getMessage());
}
@ExceptionHandler(RuntimeException.class)
public ResponseEntity<JsonRpcResponse> handleRuntime(RuntimeException e) {
log.error(" [WAS Internal Error] 시스템 장애: {}", e.getMessage(), e);
log.error(" [Gateway Internal Error] 시스템 장애: {}", e.getMessage(), e);
return buildErrorResponse(-32603, "Internal error: " + e.getMessage());
}
@ExceptionHandler(Exception.class)
public ResponseEntity<JsonRpcResponse> handleAllException(Exception e) {
log.error(" [WAS Fatal Error] 치명적 오류 발생", e);
log.error(" [Gateway Fatal Error] 치명적 오류 발생", e);
return buildErrorResponse(-32000, "Server error: 시스템 관리자에게 문의하세요.");
}

View File

@@ -1,75 +0,0 @@
package io.shinhanlife.dap.lib.session.presentation;
import io.micrometer.common.util.StringUtils;
import io.shinhanlife.glow.BaseResponse;
import io.shinhanlife.glow.BizException;
import io.shinhanlife.glow.GlowControllerId;
import io.shinhanlife.glow.ResponseUtil;
import io.shinhanlife.dap.lib.session.dto.SessionDto;
import io.shinhanlife.dap.lib.session.presentation.io.SsoResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.javassist.NotFoundException;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @package io.shinhanlife.dap.lib.session.presentation
* @className SsoRestController
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
@RestController
@RequiredArgsConstructor
@Slf4j
@RequestMapping("/sso")
public class SsoRestController {
private static final String NLS_LOGIN_URL = "";
/**
* sso 연동 전 임시 로그인
*
* @param request
* @param response
* @param session
* @param <T>
* @return
*/
@GlowControllerId("tempLogin")
@PostMapping("/tempLogin")
public <T> ResponseEntity<BaseResponse<SsoResponse>> tempLogin(HttpServletRequest request, HttpServletResponse response,
HttpSession session, @RequestBody SessionDto requestDto) {
try {
if (StringUtils.isEmpty(requestDto.getPrafNo())) {
throw new NotFoundException("SSO >> not found sso id");
}
// DB 조회(ZtUsac) 없이 파라미터로 받은 SessionDto를 그대로 사용
SessionDto sessionDto = requestDto;
sessionDto.setLoginDtm(); // 로그인 시점 세팅
session.setAttribute("userInfo", sessionDto);
return ResponseUtil.ok(SsoResponse.builder().retCode("0").userInfo(sessionDto).build());
} catch (Exception e) {
log.error("로그인 실패", e);
session.invalidate();
}
return ResponseUtil.ok(SsoResponse.builder().redirectUrl(NLS_LOGIN_URL).build());
}
}

View File

@@ -1,35 +0,0 @@
package io.shinhanlife.dap.lib.session.presentation.io;
import io.shinhanlife.dap.lib.session.dto.SessionDto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
/**
* @package io.shinhanlife.dap.lib.session.presentation.io
* @className SsoResponse
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class SsoResponse {
private String retCode;
private SessionDto userInfo;
private String redirectUrl;
}

View File

@@ -2,9 +2,8 @@ package io.shinhanlife.dap.lib.util;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import io.shinhanlife.dap.lib.annotation.McpParameter;
import io.shinhanlife.dap.lib.annotation.McpValidation;
import io.shinhanlife.dap.lib.annotation.McpAnyOf;
import org.springframework.ai.mcp.annotation.McpToolParam;
import io.swagger.v3.oas.annotations.media.Schema;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
@@ -56,7 +55,7 @@ public class JsonSchemaGenerator {
// 1. 타입 매핑
// 2. 어노테이션 기반 설명 추출
McpParameter paramAnnotation = field.getAnnotation(McpParameter.class);
McpToolParam paramAnnotation = field.getAnnotation(McpToolParam.class);
JsonPropertyDescription descAnnotation = field.getAnnotation(JsonPropertyDescription.class);
if (paramAnnotation != null && !paramAnnotation.description().isEmpty()) {
fieldSchema.put("description", paramAnnotation.description());
@@ -72,38 +71,46 @@ public class JsonSchemaGenerator {
requiredList.add(field.getName());
}
McpValidation validation = field.getAnnotation(McpValidation.class);
if (validation != null && validation.required() && !requiredList.contains(field.getName())) {
Schema schemaAnnotation = field.getAnnotation(Schema.class);
if (schemaAnnotation != null) {
if (!schemaAnnotation.description().isEmpty() && !fieldSchema.containsKey("description")) {
fieldSchema.put("description", schemaAnnotation.description());
}
if (schemaAnnotation.required() && !requiredList.contains(field.getName())) {
requiredList.add(field.getName());
}
if (validation != null && !validation.pattern().isEmpty()) {
fieldSchema.put("pattern", validation.pattern());
if (!schemaAnnotation.pattern().isEmpty()) {
fieldSchema.put("pattern", schemaAnnotation.pattern());
}
if (validation != null && validation.minimum() != Long.MIN_VALUE) {
fieldSchema.put("minimum", validation.minimum());
if (!schemaAnnotation.minimum().isEmpty()) {
try {
fieldSchema.put("minimum", Long.valueOf(schemaAnnotation.minimum()));
} catch (NumberFormatException ignored) {}
}
if (validation != null && validation.maximum() != Long.MAX_VALUE) {
fieldSchema.put("maximum", validation.maximum());
if (!schemaAnnotation.maximum().isEmpty()) {
try {
fieldSchema.put("maximum", Long.valueOf(schemaAnnotation.maximum()));
} catch (NumberFormatException ignored) {}
}
if (validation != null && validation.minLength() >= 0) {
fieldSchema.put("minLength", validation.minLength());
if (schemaAnnotation.minLength() > 0) {
fieldSchema.put("minLength", schemaAnnotation.minLength());
}
if (validation != null && validation.maxLength() >= 0) {
fieldSchema.put("maxLength", validation.maxLength());
if (schemaAnnotation.maxLength() > 0 && schemaAnnotation.maxLength() != Integer.MAX_VALUE) {
fieldSchema.put("maxLength", schemaAnnotation.maxLength());
}
if (validation != null && validation.allowedValues().length > 0) {
fieldSchema.put("enum", List.of(validation.allowedValues()));
if (schemaAnnotation.allowableValues().length > 0 && !schemaAnnotation.allowableValues()[0].isEmpty()) {
fieldSchema.put("enum", List.of(schemaAnnotation.allowableValues()));
}
if (validation != null && !validation.format().isEmpty()) {
fieldSchema.put("format", validation.format());
if (!schemaAnnotation.format().isEmpty()) {
fieldSchema.put("format", schemaAnnotation.format());
}
if (validation != null && !validation.defaultValue().isEmpty()) {
fieldSchema.put("default", coerceDefaultValue(validation.defaultValue(), field.getType()));
if (!schemaAnnotation.defaultValue().isEmpty()) {
fieldSchema.put("default", coerceDefaultValue(schemaAnnotation.defaultValue(), field.getType()));
}
if (validation != null && validation.examples().length > 0) {
fieldSchema.put("examples", List.of(validation.examples()));
if (!schemaAnnotation.example().isEmpty()) {
fieldSchema.put("examples", List.of(schemaAnnotation.example()));
}
if (validation != null && validation.nullable()) {
if (schemaAnnotation.nullable()) {
Map<String, Object> nonNullSchema = new HashMap<>(fieldSchema);
fieldSchema = new HashMap<>();
fieldSchema.put("anyOf", List.of(
@@ -111,7 +118,7 @@ public class JsonSchemaGenerator {
Map.of("type", "null")
));
}
}
properties.put(field.getName(), fieldSchema);
}
@@ -120,15 +127,7 @@ public class JsonSchemaGenerator {
schema.put("required", requiredList);
}
McpAnyOf anyOfAnnotation = clazz.getAnnotation(McpAnyOf.class);
if (anyOfAnnotation != null && anyOfAnnotation.value().length > 0) {
List<Map<String, Object>> anyOfList = new ArrayList<>();
for (String fieldName : anyOfAnnotation.value()) {
anyOfList.add(Map.of("required", List.of(fieldName)));
}
schema.put("anyOf", anyOfList);
}
// anyOf removed
visiting.remove(clazz);
return schema;

View File

@@ -0,0 +1,309 @@
package io.shinhanlife.dap.lib.util;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Scanner;
public class PodScaffolder {
public static void main(String[] args) throws IOException {
Scanner scanner = new Scanner(System.in);
System.out.println("=========================================");
System.out.println(" MCP Tool Pod Scaffolder (Java CLI) ");
System.out.println("=========================================\n");
String rawModuleName = getOrAsk(args, 0, scanner, "1. 생성할 모듈(Pod) 이름 (예: payment 또는 dap-was-payment): ");
String moduleName = rawModuleName.startsWith("dap-was-") ? rawModuleName : "dap-was-" + rawModuleName;
String portStr = getOrAsk(args, 1, scanner, "2. 사용할 포트 번호 (예: 8085): ");
String shortName = moduleName.replace("dap-was-", "").replace("-", "");
String defaultAuthor = System.getProperty("user.name");
String defaultDate = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy.MM.dd"));
String author = getOrAsk(args, 2, scanner, "3. 작성자 (엔터 입력 시 '" + defaultAuthor + "'): ");
if (author.trim().isEmpty()) author = defaultAuthor;
String createDate = getOrAsk(args, 3, scanner, "4. 작성일 (엔터 입력 시 '" + defaultDate + "'): ");
if (createDate.trim().isEmpty()) createDate = defaultDate;
String result = scaffoldPod(moduleName, portStr, shortName, author, createDate);
System.out.println(result);
}
private static String getOrAsk(String[] args, int index, Scanner scanner, String prompt) {
if (args.length > index) {
return args[index];
}
System.out.print(prompt);
return scanner.nextLine().trim();
}
public static String scaffoldPod(String moduleName, String portStr, String shortName, String author, String createDate) throws IOException {
String envSourceDir = System.getenv("AXHUB_SOURCE_DIR");
Path rootDir = envSourceDir != null ? Paths.get(envSourceDir) : Paths.get(".");
Path modulePath = rootDir.resolve(Paths.get(moduleName));
if (Files.exists(modulePath)) {
return "[오류] 이미 존재하는 모듈입니다: " + moduleName;
}
StringBuilder log = new StringBuilder();
log.append("[1/6] 모듈 디렉터리 생성 중...\n");
Files.createDirectories(modulePath);
log.append("[2/6] build.gradle 생성 중...\n");
String buildGradle = """
plugins {
id 'org.springframework.boot'
}
dependencies {
implementation project(':dap-was-lib')
}
dependencies {
compileOnly 'org.projectlombok:lombok:1.18.32'
annotationProcessor 'org.projectlombok:lombok:1.18.32'
}
""";
Files.writeString(modulePath.resolve("build.gradle"), buildGradle);
log.append("[3/6] Dockerfile 생성 중...\n");
String dockerfile = """
FROM eclipse-temurin:21-jdk-alpine
WORKDIR /app
COPY build/libs/%s-0.0.1-SNAPSHOT.jar app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]
""".formatted(moduleName);
Files.writeString(modulePath.resolve("Dockerfile"), dockerfile);
log.append("[4/6] Application 클래스 및 설정 파일 생성 중...\n");
Path srcPath = modulePath.resolve("src/main/java/io/shinhanlife/dap/mcc/" + shortName);
Files.createDirectories(srcPath);
String appClass = """
package io.shinhanlife.dap.mcc.%s;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.cache.annotation.EnableCaching;
/**
* @package io.shinhanlife.dap.mcc.%s
* @className DapWas%sApplication
* @description AX HUB 시스템 처리 클래스
* @author %s
* @create %s
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* %s %s 최초생성
*
* </pre>
*/
@SpringBootApplication(scanBasePackages = {"io.shinhanlife.dap.mcc", "io.shinhanlife.dap.lib"})
@ConfigurationPropertiesScan(basePackages = {"io.shinhanlife.dap.mcc", "io.shinhanlife.dap.lib"})
@EnableCaching
public class DapWas%sApplication {
public static void main(String[] args) {
SpringApplication.run(DapWas%sApplication.class, args);
}
}
""".formatted(shortName, shortName, capitalize(shortName), author, createDate, createDate, author, capitalize(shortName), capitalize(shortName));
Files.writeString(srcPath.resolve("DapWas" + capitalize(shortName) + "Application.java"), appClass);
Path resPath = modulePath.resolve("src/main/resources");
Files.createDirectories(resPath);
String applicationYml = """
server:
port: %s
spring:
application:
name: %s
profiles:
active: local
logging:
level:
org.apache.kafka: ERROR
mcp:
namespace: ""
security:
tenant-domains:
TESTER-DEV: ALL
""".formatted(portStr, moduleName);
Files.writeString(resPath.resolve("application.yml"), applicationYml);
String applicationLocalYml = """
# Local 환경 전용 설정 (H2 메모리 DB 등)
spring:
datasource:
url: jdbc:p6spy:h2:mem:testdb;DB_CLOSE_DELAY=-1;
driverClassName: com.p6spy.engine.spy.P6SpyDriver
username: sa
password: password
h2:
console:
enabled: true
eims:
http:
url: http://localhost:${server.port}/api/gateway
tcp:
host: 127.0.0.1
port: 8090
timeout: 5000
jsp:
form:
url: http://localhost:${server.port}/mock/jsp-form
json:
url: http://localhost:${server.port}/mock/jsp-json
mci:
url: http://localhost:${server.port}/api/mock/esb/api
mcistring:
url: http://localhost:${server.port}/api/mock/esb/string
mcp:
security:
tenant-domains:
mcp-client-1: CUSTOMER,COMMON
mcp-client-2: ALL
axhub:
gateway:
url: http://localhost:8081
tool:
url: ${AXHUB_TOOL_URL:http://localhost:${server.port}}
""";
Files.writeString(resPath.resolve("application-local.yml"), applicationLocalYml);
String applicationDevYml = """
# OCI 클라우드 환경 전용 설정
server:
port: ${PORT:%s}
axhub:
gateway:
url: https://axhubmcp.devjun.net
tool:
url: http://144.24.70.100:%s
eims:
http:
url: http://localhost:${server.port}/api/gateway
tcp:
host: 127.0.0.1
port: 8090
timeout: 5000
jsp:
form:
url: http://localhost:${server.port}/mock/jsp-form
json:
url: http://localhost:${server.port}/mock/jsp-json
mci:
url: http://localhost:${server.port}/api/mock/esb/api
mcistring:
url: http://localhost:${server.port}/api/mock/esb/string
shinhan:
integration:
envrTypeCd: D
eai:
url: http://10.176.32.181
internalMci:
url: http://10.176.32.173
bancaMci:
url: http://10.176.32.117
externalMci:
url: http://10.176.32.176
""".formatted(portStr, portStr);
Files.writeString(resPath.resolve("application-dev.yml"), applicationDevYml);
String logbackXml = """
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<property name="LOG_PATTERN" value="%%d{yyyy-MM-dd HH:mm:ss.SSS} [%%thread] [%%X{traceId}] %%-5level %%logger{36} - %%msg%%n" />
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${LOG_PATTERN}</pattern>
</encoder>
</appender>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/%s.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/%s-%%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${LOG_PATTERN}</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE" />
<appender-ref ref="FILE" />
</root>
<logger name="io.shinhanlife" level="DEBUG" />
</configuration>
""".formatted(moduleName, moduleName);
Files.writeString(resPath.resolve("logback-spring.xml"), logbackXml);
log.append("[5/6] settings.gradle 에 모듈 등록 중...\n");
Path settingsPath = rootDir.resolve(Paths.get("settings.gradle"));
if (Files.exists(settingsPath)) {
String settings = Files.readString(settingsPath);
if (!settings.contains("include '" + moduleName + "'")) {
Files.writeString(settingsPath, System.lineSeparator() + "include '" + moduleName + "'" + System.lineSeparator(), StandardOpenOption.APPEND);
}
}
log.append("[6/6] docker-compose.yml 에 서비스 추가 중...\n");
Path dockerComposePath = rootDir.resolve(Paths.get("docker-compose.yml"));
if (Files.exists(dockerComposePath)) {
String compose = Files.readString(dockerComposePath);
String serviceName = moduleName.replace("dap-", ""); // e.g. tool-payment
if (!compose.contains(" " + serviceName + ":")) {
String newService = """
%s:
build:
context: .
dockerfile: %s/Dockerfile
ports:
- "%s:%s"
depends_on:
- redis
environment:
- TZ=Asia/Seoul
- SPRING_REDIS_HOST=redis
- SPRING_REDIS_PORT=6379
- SPRING_DATA_REDIS_PORT=6379
- AXHUB_GATEWAY_URL=http://gateway:8081
- AXHUB_TOOL_URL=http://%s:%s
- GLOW_COMMUNICATION_MCI_HOST=http://mci-mock
- GLOW_COMMUNICATION_MCI_PORT=8080
- GLOW_COMMUNICATION_EXTMCI_HOST=http://mci-mock
- GLOW_COMMUNICATION_EXTMCI_PORT=8080
- GLOW_COMMUNICATION_EAI_HOST=http://mci-mock
- GLOW_COMMUNICATION_EAI_PORT=8080
""".formatted(serviceName, moduleName, portStr, portStr, serviceName, portStr);
Files.writeString(dockerComposePath, System.lineSeparator() + newService, StandardOpenOption.APPEND);
}
}
log.append("\n=========================================\n");
log.append(" Pod Scaffolding Complete! \n");
log.append("=========================================\n");
log.append("1. [새로운 모듈] ").append(moduleName).append(" 폴더가 생성되었습니다.\n");
log.append("2. [ToolScaffolder]를 사용해 이 모듈 안에 툴을 추가하세요.\n");
log.append("3. 실행 전 Gradle 동기화(Sync)를 한 번 진행해 주세요.\n");
return log.toString();
}
private static String capitalize(String str) {
if (str == null || str.isEmpty()) return str;
return str.substring(0, 1).toUpperCase() + str.substring(1);
}
}

View File

@@ -0,0 +1,783 @@
package io.shinhanlife.dap.lib.util;
import org.springframework.ai.mcp.annotation.McpToolParam;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
import java.util.Scanner;
/**
* MCP Tool 코드를 자동 생성(Scaffolding)하는 유틸리티 클래스
*
* [실행 방법]
* 방법 1. IDE(IntelliJ 등)에서 직접 실행 (대화형 모드 추천 ⭐)
* - 이 클래스(ToolScaffolder.java)를 열고 main 메서드를 직접 실행(Run)합니다.
* - 콘솔 창에 뜨는 질문에 차례대로 값을 입력하기만 하면 파일이 생성됩니다.
*
* 방법 2. 커맨드라인(터미널)에서 실행 (명령어 기반)
* - 컴파일: javac -encoding UTF-8 dap-was-lib/src/main/java/io/shinhanlife/dap/mcc/util/ToolScaffolder.java
* - 실행: java -cp dap-was-lib/src/main/java io.shinhanlife.dap.lib.util.ToolScaffolder [이름] [ID] "[설명]" "[그룹]" "[통신방식]" "[모듈명]"
*/
/**
* @package io.shinhanlife.dap.lib.util
* @className ToolScaffolder
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
public class ToolScaffolder {
private static final String BASE_PACKAGE = "io.shinhanlife.dap.mcc";
private static final String BASE_PACKAGE_PATH = "src/main/java/io/shinhanlife/dap/mcc";
public static void main(String[] args) throws IOException {
Scanner scanner = new Scanner(System.in);
System.out.println("=========================================");
System.out.println(" MCP Tool Scaffolder (Java CLI) ");
System.out.println("=========================================\n");
String baseName = getOrAsk(args, 0, scanner, "1. 생성할 Tool의 기본 이름 (예: ExchangeRate) [영문 PascalCase]: ");
String interfaceId = getOrAsk(args, 1, scanner, "2. 레거시 API 인터페이스 ID (예: EXCH_001): ");
String description = getOrAsk(args, 2, scanner, "3. Tool 기능 설명 (예: 환율 조회): ");
String group = getOrAsk(args, 3, scanner, "4. Tool 소속 그룹 (예: SAMPLE, NOTIFICATION, CLAIM, POLICY, HR, CONTRACT, CUSTOMER 등): ");
if (group.isEmpty()) group = "COMMON";
String routingType = getOrAsk(args, 4, scanner, "5. 통신 프로토콜 (예: HTTP, TCP, MCI, EAI): ");
if (routingType.trim().isEmpty()) {
routingType = "HTTP";
}
String moduleName = getOrAsk(args, 5, scanner, "6. 코드를 생성할 모듈 (기본: dap-was-oth): ");
if (moduleName.trim().isEmpty()) {
moduleName = "dap-was-oth";
}
String defaultAuthor = System.getProperty("user.name");
String defaultDate = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy.MM.dd"));
String author = getOrAsk(args, 6, scanner, "7. 작성자 (엔터 입력 시 '" + defaultAuthor + "'): ");
if (author.trim().isEmpty()) author = defaultAuthor;
String createDate = getOrAsk(args, 7, scanner, "8. 작성일 (엔터 입력 시 '" + defaultDate + "'): ");
if (createDate.trim().isEmpty()) createDate = defaultDate;
String useSchemaResourceStr = getOrAsk(args, 8, scanner, "9. input/output JSON Schema 파일 자동 생성 여부 (y/N): ");
boolean useSchemaResource = "y".equalsIgnoreCase(useSchemaResourceStr.trim());
String result = scaffold(baseName, interfaceId, description, group, routingType, moduleName, author, createDate, true, null, useSchemaResource);
System.out.println(result);
}
private static String getOrAsk(String[] args, int index, Scanner scanner, String prompt) {
if (args.length > index) {
return args[index];
}
System.out.print(prompt);
return scanner.nextLine().trim();
}
public static String scaffold(String baseName, String interfaceId, String description, String group, String routingType, String moduleName, String author, String createDate, boolean register, String clientSystemCode) throws IOException {
return scaffold(baseName, interfaceId, description, group, routingType, moduleName, author, createDate, register, clientSystemCode, false);
}
public static String scaffold(String baseName, String interfaceId, String description, String group, String routingType, String moduleName, String author, String createDate, boolean register, String clientSystemCode, boolean useSchemaResource) throws IOException {
baseName = toPascalCase(baseName);
String envSourceDir = System.getenv("AXHUB_SOURCE_DIR");
Path rootDir = envSourceDir != null ? Paths.get(envSourceDir) : Paths.get(".");
Path usecaseDir = rootDir.resolve(Paths.get(moduleName, BASE_PACKAGE_PATH, "biz", group.toLowerCase(), "usecase"));
Path usecaseImplDir = usecaseDir.resolve("impl");
Path dtoDir = rootDir.resolve(Paths.get(moduleName, BASE_PACKAGE_PATH, "biz", group.toLowerCase(), "dto"));
Path legacyDtoDir = rootDir.resolve(Paths.get(moduleName, BASE_PACKAGE_PATH, "biz", group.toLowerCase(), "legacy"));
Path converterDir = rootDir.resolve(Paths.get(moduleName, BASE_PACKAGE_PATH, "biz", group.toLowerCase(), "converter"));
// schema resource 파일 경로 (useSchemaResource=true 일 때만 생성)
String schemaBaseName = toKebabCase(baseName);
String inputSchemaFileName = schemaBaseName + "-resource-input-schema.json";
String outputSchemaFileName = schemaBaseName + "-resource-output-schema.json";
Path schemaDir = rootDir.resolve(Paths.get(moduleName, "src/main/resources/mcp/schema"));
String inputSchemaClasspath = "classpath:mcp/schema/" + inputSchemaFileName;
String outputSchemaClasspath = "classpath:mcp/schema/" + outputSchemaFileName;
String bizPackage = BASE_PACKAGE + ".biz." + group.toLowerCase();
boolean isMci = "MCI".equalsIgnoreCase(routingType);
String mciGroupPath = "infra/itrf/mci/" + group.toLowerCase();
String clientPkgSuffix = "";
String clientPrefixCap = "";
Path mciClientDir = null;
if (isMci && clientSystemCode != null && clientSystemCode.length() == 4) {
String clientPrefix = clientSystemCode.toLowerCase();
clientPkgSuffix = clientPrefix.substring(0, 3) + "." + clientPrefix.substring(3, 4);
clientPrefixCap = toPascalCase(clientSystemCode);
mciGroupPath = "infra/itrf/mci/" + clientPrefix.substring(0, 3) + "/" + clientPrefix.substring(3, 4);
mciClientDir = rootDir.resolve(Paths.get(moduleName, BASE_PACKAGE_PATH, mciGroupPath));
}
Path mciIoDir = rootDir.resolve(Paths.get(moduleName, BASE_PACKAGE_PATH, mciGroupPath, "io"));
Files.createDirectories(usecaseDir);
Files.createDirectories(usecaseImplDir);
Files.createDirectories(dtoDir);
if (isMci) {
Files.createDirectories(mciIoDir);
if (mciClientDir != null) {
Files.createDirectories(mciClientDir);
}
} else {
Files.createDirectories(legacyDtoDir);
}
Files.createDirectories(converterDir);
StringBuilder log = new StringBuilder();
// Generate Request DTO
String reqContent = """
package %s.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
/**
* @package %s.dto
* @className %sRequest
* @description AX HUB 시스템 처리 클래스
* @author %s
* @create %s
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* %s %s 최초생성
*
* </pre>
*/
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class %sRequest {
@McpToolParam(description = "수신자 전화번호", required = true)
-(?:\\\\d{3}|\\\\d{4})-\\\\d{4}$", examples = {"010-1234-5678"})
private String phoneNumber;
@McpToolParam(description = "전송할 메시지 내용", required = true)
private String message;
}
""".formatted(bizPackage, bizPackage, baseName, author, createDate, createDate, author, baseName);
Files.writeString(dtoDir.resolve(baseName + "Request.java"), reqContent);
// Generate Response DTO
String resContent = """
package %s.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
/**
* @package %s.dto
* @className %sResponse
* @description AX HUB 시스템 처리 클래스
* @author %s
* @create %s
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* %s %s 최초생성
*
* </pre>
*/
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class %sResponse {
private String resultCode;
private String resultMessage;
// TODO: Add response fields here. Do not include PII in the Tool response.
}
""".formatted(bizPackage, bizPackage, baseName, author, createDate, createDate, author, baseName);
Files.writeString(dtoDir.resolve(baseName + "Response.java"), resContent);
String toolName = toToolName(moduleName, group, baseName);
String toolHintLine;
if (useSchemaResource) {
toolHintLine = " @ToolHint(register = %s,\n" +
" inputSchemaResource = \"%s\",\n" +
" outputSchemaResource = \"%s\")".formatted(register, inputSchemaClasspath, outputSchemaClasspath);
} else {
toolHintLine = " @ToolHint(register = %s)".formatted(register);
}
String serviceInterfaceContent = """
package %s.usecase;
import org.springframework.ai.mcp.annotation.McpTool;
import io.shinhanlife.dap.lib.annotation.ToolHint;
import %s.dto.%sRequest;
import %s.dto.%sResponse;
/**
* @package %s.usecase
* @className %sUseCase
* @description AX HUB 시스템 처리 클래스
* @author %s
* @create %s
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* %s %s 최초생성
*
* </pre>
*/
public interface %sUseCase {
@McpTool(name = "%s", title = "%s", description = "%s")
%s
%sResponse execute(%sRequest req);
}
""".formatted(
bizPackage,
bizPackage, baseName,
bizPackage, baseName,
bizPackage, baseName, author, createDate, createDate, author,
baseName,
toolName, description, description,
toolHintLine,
baseName, baseName
);
Files.writeString(usecaseDir.resolve(baseName + "UseCase.java"), serviceInterfaceContent);
String serviceImplContent;
if (isMci) {
serviceImplContent = """
package %s.usecase.impl;
import %s.dto.%sRequest;
import %s.dto.%sResponse;
import %s.usecase.%sUseCase;
import io.shinhanlife.dap.lib.integration.mci.component.AxhubMciComponent;
import io.shinhanlife.glow.communication.dto.Transfer;
import org.springframework.stereotype.Service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.util.Map;
import %s.converter.%sConverter;
import %s.%s.io.%s_I;
%s
/**
* @package %s.usecase.impl
* @className %sUseCaseImpl
* @description AX HUB 시스템 처리 클래스
* @author %s
* @create %s
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* %s %s 최초생성
*
* </pre>
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class %sUseCaseImpl implements %sUseCase {
%s
private final %sConverter converter;
@Override
public Object execute(%sRequest req) {
log.info("[MCI Tool] {} 요청 수신.", "%s");
try {
// MapStruct를 이용한 자동 매핑 (AI DTO -> MCI DTO)
%s_I mciReq = converter.toLegacyRequest(req);
Transfer<Object> resTransfer = mci.callTo(
"%s",
null,
mciReq,
Object.class
);
return resTransfer.getBody() != null ? resTransfer.getBody() : Map.of("status", "SUCCESS");
} catch (Exception e) {
log.error("[MCI Tool] 연동 중 오류 발생: {}", e.getMessage(), e);
return Map.of("status", "ERROR", "message", e.getMessage() != null ? e.getMessage() : "Unknown error");
}
}
}
""".formatted(
bizPackage,
bizPackage, baseName,
bizPackage, baseName,
bizPackage, baseName,
bizPackage, baseName,
BASE_PACKAGE, mciGroupPath.replace("/", "."), interfaceId,
(clientPrefixCap.isEmpty() ? "" : "import " + BASE_PACKAGE + "." + mciGroupPath.replace("/", ".") + ".Mci" + clientPrefixCap + "Client;\n"),
bizPackage,
baseName,
author,
createDate,
createDate, author,
baseName,
baseName,
(clientPrefixCap.isEmpty() ? "private final AxhubMciComponent mci;" : "private final Mci" + clientPrefixCap + "Client mci;"),
baseName,
baseName,
toolName,
interfaceId,
interfaceId
);
} else {
serviceImplContent = """
package %s.usecase.impl;
import %s.dto.%sRequest;
import %s.dto.%sResponse;
import %s.usecase.%sUseCase;
import io.shinhanlife.dap.mcc.usecase.AbstractMcpToolUseCase;
import %s.converter.%sConverter;
import org.springframework.stereotype.Service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* @package %s.usecase.impl
* @className %sUseCaseImpl
* @description AX HUB 시스템 처리 클래스
* @author %s
* @create %s
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* %s %s 최초생성
*
* </pre>
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class %sUseCaseImpl extends AbstractMcpToolUseCase implements %sUseCase {
private final %sConverter converter;
@Override
public Object execute(%sRequest req) {
// %sLegacyRequest legacyRequest = converter.toLegacyRequest(req);
return executeLegacy("%s", "%s", req); // Or pass legacyRequest
}
}
""".formatted(
bizPackage,
bizPackage, baseName,
bizPackage, baseName,
bizPackage, baseName,
bizPackage, baseName,
bizPackage, baseName,
author,
createDate,
createDate, author,
baseName, baseName,
baseName,
baseName,
baseName,
routingType, interfaceId
);
}
Files.writeString(usecaseImplDir.resolve(baseName + "UseCaseImpl.java"), serviceImplContent);
if (isMci) {
String mciReqContent = """
package %s.%s.io;
import lombok.Data;
/**
* @package %s.%s.io
* @className %s_I
* @description AX HUB 시스템 처리 클래스
* @author %s
* @create %s
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* %s %s 최초생성
*
* </pre>
*/
@Data
public class %s_I {
/**
* EAI 시스템이 요구하는 수신자 번호 파라미터명
*/
private String phone;
/**
* EAI 시스템이 요구하는 메시지 내용 파라미터명
*/
private String content;
}
""".formatted(BASE_PACKAGE, mciGroupPath.replace("/", "."), BASE_PACKAGE, mciGroupPath.replace("/", "."), interfaceId, author, createDate, createDate, author, interfaceId);
Files.writeString(mciIoDir.resolve(interfaceId + "_I.java"), mciReqContent);
String mciResContent = """
package %s.%s.io;
import lombok.Data;
/**
* @package %s.%s.io
* @className %s_O
* @description AX HUB 시스템 처리 클래스
* @author %s
* @create %s
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* %s %s 최초생성
*
* </pre>
*/
@Data
public class %s_O {
// TODO: Add response fields here
}
""".formatted(BASE_PACKAGE, mciGroupPath.replace("/", "."), BASE_PACKAGE, mciGroupPath.replace("/", "."), interfaceId, author, createDate, createDate, author, interfaceId);
Files.writeString(mciIoDir.resolve(interfaceId + "_O.java"), mciResContent);
String converterContent = """
package %s.converter;
import %s.dto.%sRequest;
import %s.dto.%sResponse;
import %s.%s.io.%s_I;
import %s.%s.io.%s_O;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;
/**
* @package %s.converter
* @className %sConverter
* @description AX HUB 시스템 처리 클래스
* @author %s
* @create %s
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* %s %s 최초생성
*
* </pre>
*/
@Mapper(componentModel = "spring")
public interface %sConverter {
@Mapping(source = "phoneNumber", target = "phone")
@Mapping(source = "message", target = "content")
%s_I toLegacyRequest(%sRequest req);
@Mapping(source = "phone", target = "phoneNumber")
@Mapping(source = "content", target = "message")
%sRequest toRequest(%s_I mciReq);
// %sResponse toResponse(%s_O mciRes);
}
""".formatted(
bizPackage,
bizPackage, baseName,
bizPackage, baseName,
BASE_PACKAGE, mciGroupPath.replace("/", "."), interfaceId,
BASE_PACKAGE, mciGroupPath.replace("/", "."), interfaceId,
bizPackage, baseName, author, createDate, createDate, author,
baseName, interfaceId, baseName,
baseName, interfaceId,
baseName, interfaceId
);
Files.writeString(converterDir.resolve(baseName + "Converter.java"), converterContent);
log.append("\n=========================================\n");
log.append(" Scaffolding Complete! (Routing: " + routingType + ")\n");
log.append("=========================================\n");
log.append("[Usecase Interface] ").append(usecaseDir.resolve(baseName + "UseCase.java")).append("\n");
log.append("[Usecase Impl] ").append(usecaseImplDir.resolve(baseName + "UseCaseImpl.java")).append("\n");
log.append("[Request DTO] ").append(dtoDir.resolve(baseName + "Request.java")).append("\n");
log.append("[Response DTO] ").append(dtoDir.resolve(baseName + "Response.java")).append("\n");
log.append("[MCI Request IO] ").append(mciIoDir.resolve(interfaceId + "_I.java")).append("\n");
log.append("[MCI Response IO] ").append(mciIoDir.resolve(interfaceId + "_O.java")).append("\n");
log.append("[MCI Converter] ").append(converterDir.resolve(baseName + "Converter.java")).append("\n");
if (!clientPrefixCap.isEmpty()) {
String mciClientContent = """
package %s.%s;
import org.springframework.stereotype.Component;
import lombok.RequiredArgsConstructor;
import io.shinhanlife.dap.lib.integration.mci.component.AxhubMciComponent;
import io.shinhanlife.glow.communication.dto.Transfer;
/**
* @package %s.%s
* @className Mci%sClient
* @description AX HUB 시스템 처리 클래스
* @author %s
* @create %s
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* %s %s 최초생성
*
* </pre>
*/
@Component
@RequiredArgsConstructor
public class Mci%sClient {
private final AxhubMciComponent mci;
public Transfer<Object> callTo(String interfaceId, String dummy, Object mciReq, Class<Object> resType) throws Exception {
return mci.callTo(interfaceId, dummy, mciReq, resType);
}
}
""".formatted(
BASE_PACKAGE, mciGroupPath.replace("/", "."),
BASE_PACKAGE, mciGroupPath.replace("/", "."), clientPrefixCap, author, createDate, createDate, author, clientPrefixCap
);
Files.writeString(mciClientDir.resolve("Mci" + clientPrefixCap + "Client.java"), mciClientContent);
log.append("[MCI Client] ").append(mciClientDir.resolve("Mci" + clientPrefixCap + "Client.java")).append("\n");
}
} else {
String legacyReqContent = """
package %s.legacy;
import lombok.Data;
/**
* @package %s.legacy
* @className %sLegacyRequest
* @description AX HUB 시스템 처리 클래스
* @author %s
* @create %s
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* %s %s 최초생성
*
* </pre>
*/
@Data
public class %sLegacyRequest {
/**
* EAI 시스템이 요구하는 수신자 번호 파라미터명
*/
private String phone;
/**
* EAI 시스템이 요구하는 메시지 내용 파라미터명
*/
private String content;
}
""".formatted(bizPackage, bizPackage, baseName, author, createDate, createDate, author, baseName);
Files.writeString(legacyDtoDir.resolve(baseName + "LegacyRequest.java"), legacyReqContent);
String legacyResContent = """
package %s.legacy;
import lombok.Data;
/**
* @package %s.legacy
* @className %sLegacyResponse
* @description AX HUB 시스템 처리 클래스
* @author %s
* @create %s
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* %s %s 최초생성
*
* </pre>
*/
@Data
public class %sLegacyResponse {
// TODO: Add legacy response fields here
}
""".formatted(bizPackage, bizPackage, baseName, author, createDate, createDate, author, baseName);
Files.writeString(legacyDtoDir.resolve(baseName + "LegacyResponse.java"), legacyResContent);
String converterContent = """
package %s.converter;
import %s.dto.%sRequest;
import %s.dto.%sResponse;
import %s.legacy.%sLegacyRequest;
import %s.legacy.%sLegacyResponse;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;
/**
* @package %s.converter
* @className %sConverter
* @description AX HUB 시스템 처리 클래스
* @author %s
* @create %s
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* %s %s 최초생성
*
* </pre>
*/
@Mapper(componentModel = "spring")
public interface %sConverter {
@Mapping(source = "phoneNumber", target = "phone")
@Mapping(source = "message", target = "content")
%sLegacyRequest toLegacyRequest(%sRequest req);
@Mapping(source = "phone", target = "phoneNumber")
@Mapping(source = "content", target = "message")
%sRequest toRequest(%sLegacyRequest legacyRequest);
// %sResponse toResponse(%sLegacyResponse legacyResponse);
}
""".formatted(
bizPackage,
bizPackage, baseName,
bizPackage, baseName,
bizPackage, baseName,
bizPackage, baseName,
bizPackage, baseName, author, createDate, createDate, author,
baseName, baseName, baseName, baseName, baseName, baseName, baseName
);
Files.writeString(converterDir.resolve(baseName + "Converter.java"), converterContent);
log.append("\n=========================================\n");
log.append(" Scaffolding Complete! (Routing: " + routingType + ")\n");
log.append("=========================================\n");
log.append("[Usecase Interface] ").append(usecaseDir.resolve(baseName + "UseCase.java")).append("\n");
log.append("[Usecase Impl] ").append(usecaseImplDir.resolve(baseName + "UseCaseImpl.java")).append("\n");
log.append("[Request DTO] ").append(dtoDir.resolve(baseName + "Request.java")).append("\n");
log.append("[Response DTO] ").append(dtoDir.resolve(baseName + "Response.java")).append("\n");
log.append("[Legacy Request DTO] ").append(legacyDtoDir.resolve(baseName + "LegacyRequest.java")).append("\n");
log.append("[Legacy Response DTO] ").append(legacyDtoDir.resolve(baseName + "LegacyResponse.java")).append("\n");
log.append("[Legacy Converter] ").append(converterDir.resolve(baseName + "Converter.java")).append("\n");
}
// schema resource 파일 생성 (useSchemaResource=true 일 때)
if (useSchemaResource) {
Files.createDirectories(schemaDir);
String inputSchema = """
{
"type": "object",
"additionalProperties": false,
"properties": {
"TODO_FIELD": {
"type": "string",
"description": "TODO: 파라미터 설명을 입력하세요."
}
},
"required": []
}
""";
String outputSchema = """
{
"type": "object",
"additionalProperties": false,
"properties": {
"status": {
"type": "string",
"description": "처리 결과 상태 (SUCCESS / FAILURE)",
"enum": ["SUCCESS", "FAILURE"]
},
"message": {
"type": "string",
"description": "처리 결과 메시지"
}
},
"required": ["status"]
}
""";
Files.writeString(schemaDir.resolve(inputSchemaFileName), inputSchema);
Files.writeString(schemaDir.resolve(outputSchemaFileName), outputSchema);
log.append("[Input Schema] ").append(schemaDir.resolve(inputSchemaFileName)).append("\n");
log.append("[Output Schema] ").append(schemaDir.resolve(outputSchemaFileName)).append("\n");
}
log.append("\n Tip: ").append(interfaceId).append(" 목업 데이터를 mock-responses.json에 추가하세요.\n");
return log.toString();
}
private static String toKebabCase(String pascalCase) {
if (pascalCase == null || pascalCase.isEmpty()) return pascalCase;
return pascalCase
.replaceAll("([a-z0-9])([A-Z])", "$1-$2")
.toLowerCase(Locale.ROOT);
}
private static String toToolName(String moduleName, String group, String baseName) {
String moduleDirectory = Path.of(moduleName).getFileName().toString();
String pod = moduleDirectory.startsWith("dap-was-")
? moduleDirectory.substring("dap-was-".length())
: "oth";
String normalizedName = baseName.replaceAll("([a-z0-9])([A-Z])", "$1 $2")
.toLowerCase(Locale.ROOT)
.replaceAll("[^a-z0-9]+", " ")
.trim();
String[] words = normalizedName.split("\\s+");
String service = words[0];
String action = words.length == 1 ? "execute" : words[words.length - 1];
return "%s.%s.%s.%s".formatted(
pod.toLowerCase(Locale.ROOT),
group.toLowerCase(Locale.ROOT),
service,
action);
}
private static String toPascalCase(String str) {
if (str == null || str.isEmpty()) {
return str;
}
StringBuilder result = new StringBuilder();
boolean capitalizeNext = true;
for (char c : str.toCharArray()) {
if (c == '_' || c == '-' || c == ' ') {
capitalizeNext = true;
} else if (capitalizeNext) {
result.append(Character.toUpperCase(c));
capitalizeNext = false;
} else {
result.append(c);
}
}
if (result.length() > 0) {
result.setCharAt(0, Character.toUpperCase(result.charAt(0)));
}
return result.toString();
}
}

View File

@@ -1,28 +1,14 @@
package io.shinhanlife.dap.lib.util;
/**
* @package io.shinhanlife.dap.lib.util
* @className ToolSchemaResolver
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.shinhanlife.dap.lib.annotation.McpFunction;
import io.shinhanlife.dap.lib.annotation.McpOutputSchema;import java.io.InputStream;
// removed McpOutputSchema
import java.io.InputStream;
import java.util.Map;
import org.springframework.core.io.ClassPathResource;
import io.shinhanlife.dap.lib.annotation.ToolHint;
/** Resolves an MCP Tool input schema from resource, inline value, or DTO metadata. */
public class ToolSchemaResolver {
@@ -32,13 +18,9 @@ public class ToolSchemaResolver {
this.objectMapper = objectMapper;
}
public Map<String, Object> resolve(McpFunction function, Class<?> requestType) {
if (function != null && !function.inputSchemaResource().isBlank()) {
return loadResource(function.inputSchemaResource());
}
if (function != null && !function.inputSchema().isBlank()
&& !"{}".equals(function.inputSchema().trim())) {
return parse(function.inputSchema(), "McpFunction.inputSchema");
public Map<String, Object> resolve(org.springframework.ai.mcp.annotation.McpTool function, ToolHint hint, Class<?> requestType) {
if (hint != null && !hint.inputSchemaResource().isBlank()) {
return loadResource(hint.inputSchemaResource());
}
return JsonSchemaGenerator.generateSchema(requestType);
}
@@ -46,25 +28,36 @@ public class ToolSchemaResolver {
/**
* Resolves an explicitly declared response schema.
* Response schemas are opt-in so existing tools keep their current response behavior.
* ToolHint.outputSchemaResource()가 있으면 classpath JSON 파일에서 로드하고,
* 없으면 responseType DTO를 분석하여 자동 생성합니다.
*/
public Map<String, Object> resolveOutput(McpFunction function, Class<?> responseType) {
if (function != null && !function.outputSchemaResource().isBlank()) {
return loadResource(function.outputSchemaResource());
public Map<String, Object> resolveOutput(org.springframework.ai.mcp.annotation.McpTool function, Class<?> responseType, ToolHint hint) {
if (hint != null && !hint.outputSchemaResource().isBlank()) {
return loadResource(hint.outputSchemaResource());
}
if (function != null && !function.outputSchema().isBlank()
&& !"{}".equals(function.outputSchema().trim())) {
return parse(function.outputSchema(), "McpFunction.outputSchema");
}
if (responseType != null && responseType.isAnnotationPresent(McpOutputSchema.class)) {
return JsonSchemaGenerator.generateSchema(responseType);
return resolveOutput(function, responseType);
}
/**
* Resolves an explicitly declared response schema.
* Response schemas are opt-in so existing tools keep their current response behavior.
*/
public Map<String, Object> resolveOutput(org.springframework.ai.mcp.annotation.McpTool function, Class<?> responseType) {
// Object, Map 등 구체적인 DTO가 아닌 경우 검증 스킵
if (responseType == null
|| responseType == Object.class
|| Map.class.isAssignableFrom(responseType)
|| responseType == Void.class
|| responseType == void.class) {
return Map.of();
}
return JsonSchemaGenerator.generateSchema(responseType);
}
/**
* Retained for callers that use only explicit output schemas.
*/
public Map<String, Object> resolveOutput(McpFunction function) {
public Map<String, Object> resolveOutput(org.springframework.ai.mcp.annotation.McpTool function) {
return resolveOutput(function, null);
}

View File

@@ -0,0 +1,123 @@
package io.shinhanlife.dap.lib.util;
/**
* @package io.shinhanlife.dap.lib.util
* @className ToolSourceUpdater
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class ToolSourceUpdater {
public static void updateToolSource(String toolName, String domainGroup, String description, boolean register, Boolean requiresApproval) throws Exception {
// 1. Find all *UseCase.java files in dap-was-* directories
String envSourceDir = System.getenv("AXHUB_SOURCE_DIR");
Path rootDir = envSourceDir != null ? Paths.get(envSourceDir) : Paths.get(".");
List<Path> javaFiles;
try (Stream<Path> paths = Files.walk(rootDir)) {
javaFiles = paths
.filter(Files::isRegularFile)
.filter(p -> p.toString().endsWith("UseCase.java"))
.filter(p -> p.toString().contains("dap-was-") || p.toString().contains("axhub-tool-"))
.collect(Collectors.toList());
}
Path targetFile = null;
String content = null;
// 2. Find the specific file for the tool
String functionName = toolName;
if (toolName.contains("_")) {
functionName = toolName.substring(toolName.indexOf("_") + 1);
}
Pattern namePattern = Pattern.compile("@McpFunction\\s*\\([^)]*name\\s*=\\s*\"" + Pattern.quote(toolName) + "\"", Pattern.DOTALL);
Pattern namePattern2 = Pattern.compile("@McpFunction\\s*\\([^)]*name\\s*=\\s*\"" + Pattern.quote(functionName) + "\"", Pattern.DOTALL);
for (Path path : javaFiles) {
String text = Files.readString(path);
if (namePattern.matcher(text).find()) {
targetFile = path;
content = text;
break;
} else if (namePattern2.matcher(text).find()) {
targetFile = path;
content = text;
toolName = functionName; // Use baseName for subsequent replacements
break;
}
}
if (targetFile == null) {
throw new Exception("소스 코드를 찾을 수 없습니다: " + toolName);
}
// 3. Update @McpTool group
if (domainGroup != null && !domainGroup.trim().isEmpty()) {
Pattern groupPattern = Pattern.compile("(@McpTool\\s*\\([^)]*group\\s*=\\s*\")([^\"]+)(\")", Pattern.DOTALL);
Matcher groupMatcher = groupPattern.matcher(content);
if (groupMatcher.find()) {
content = groupMatcher.replaceFirst("$1" + domainGroup + "$3");
}
}
// 4. Update @McpFunction description
if (description != null) {
Pattern funcPattern = Pattern.compile("(@McpFunction\\s*\\([^)]*name\\s*=\\s*\"" + Pattern.quote(toolName) + "\"[^)]*description\\s*=\\s*\")([^\"]+)(\")", Pattern.DOTALL);
Matcher funcMatcher = funcPattern.matcher(content);
if (funcMatcher.find()) {
content = funcMatcher.replaceFirst("$1" + description.replace("\\", "\\\\").replace("$", "\\\\$") + "$3");
}
}
// 5. Update register flag
Pattern regPattern = Pattern.compile("(@McpFunction\\s*\\([^)]*name\\s*=\\s*\"" + Pattern.quote(toolName) + "\"[^)]*register\\s*=\\s*)(true|false)([^a-zA-Z0-9])", Pattern.DOTALL);
Matcher regMatcher = regPattern.matcher(content);
if (regMatcher.find()) {
content = regMatcher.replaceFirst("$1" + register + "$3");
} else {
Pattern addRegPattern = Pattern.compile("(@McpFunction\\s*\\([^)]*name\\s*=\\s*\"" + Pattern.quote(toolName) + "\")", Pattern.DOTALL);
Matcher addRegMatcher = addRegPattern.matcher(content);
if (addRegMatcher.find()) {
content = addRegMatcher.replaceFirst("$1, register = " + register);
}
}
// 5.5 Update requiresApproval flag
if (requiresApproval != null) {
Pattern appPattern = Pattern.compile("(@McpFunction\\s*\\([^)]*name\\s*=\\s*\"" + Pattern.quote(toolName) + "\"[^)]*requiresApproval\\s*=\\s*)(true|false)([^a-zA-Z0-9])", Pattern.DOTALL);
Matcher appMatcher = appPattern.matcher(content);
if (appMatcher.find()) {
content = appMatcher.replaceFirst("$1" + requiresApproval + "$3");
} else {
Pattern addAppPattern = Pattern.compile("(@McpFunction\\s*\\([^)]*name\\s*=\\s*\"" + Pattern.quote(toolName) + "\")", Pattern.DOTALL);
Matcher addAppMatcher = addAppPattern.matcher(content);
if (addAppMatcher.find()) {
content = addAppMatcher.replaceFirst("$1, requiresApproval = " + requiresApproval);
}
}
}
// 6. Write back to file
Files.writeString(targetFile, content);
}
}

View File

@@ -1,21 +1,5 @@
package io.shinhanlife.dap.lib.validation;
/**
* @package io.shinhanlife.dap.lib.validation
* @className McpToolNameValidationRunner
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import java.nio.file.Path;
/** Gradle entry point for validating unique MCP Tool names before packaging. */

View File

@@ -1,21 +1,5 @@
package io.shinhanlife.dap.lib.validation;
/**
* @package io.shinhanlife.dap.lib.validation
* @className McpToolNameValidator
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;

View File

@@ -1,21 +1,5 @@
package io.shinhanlife.dap.lib.validation;
/**
* @package io.shinhanlife.dap.lib.validation
* @className ToolArgumentSchemaValidator
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import com.fasterxml.jackson.databind.ObjectMapper;
import com.networknt.schema.Error;
import com.networknt.schema.InputFormat;

View File

@@ -2,14 +2,23 @@ package io.shinhanlife.dap.mcc.dto;
import lombok.Getter;
import lombok.Setter;
import lombok.Builder;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Map;
import java.util.Set;
import java.util.List;
import java.util.HashSet;
import io.shinhanlife.dap.lib.dto.OperationType;
/**
* @package io.shinhanlife.dap.mcc.dto
* Tool(Agent)의 명세 및 라우팅 정보를 담고 있는 메타데이터 클래스
* Redis 레지스트리에 저장되며, Planner와 Router 간의 통신 객체(Plan)로 사용됩니다.
*/
/**
* @package io.shinhanlife.dap.mcg.dto
* @className ToolMetadata
* @description AX HUB 시스템 처리 클래스
* @author 0986406
@@ -22,17 +31,16 @@ import java.util.Map;
*
* </pre>
*/
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
public class ToolMetadata {
// 1. Tool 기본 정보
private String uid; // UUID 형식의 고유 식별자
private String semver; // 버전 (예: 1.0.0)
private Long timeoutMillis;
private Boolean enabled;
private String displayName; // 사람이 읽는 라벨 (1-128자)
private String name; // MCP 서브툴 명칭 (64자 이하, 예: CustomerSearchTool)
private String description; // 툴의 목적 및 설명 (LLM 프롬프트에 활용 가능)
@@ -45,18 +53,26 @@ public class ToolMetadata {
// 2-1. 도메인 부서 그룹명 (category_key, 슬러그 형식)
private String categoryKey;
// 2-2. 툴 처리 엔드포인트 URI 경로 (예: /api/tool/customer-info)
private String endpoint;
// 2-3. Pod 실행 URL (독립적인 Microservice 라우팅용, 예: http://localhost:8082)
private String podUrl;
private String integrationType;
private String mciServiceId;
// 2-4. 가시성 여부
@Builder.Default
private Boolean visible = true;
// 활성화 여부
@Builder.Default
private Boolean enabled = true;
// 2-5. Redis 등록 여부 (UI 표출용)
@Builder.Default
private Boolean isRegistered = true;
// 2-6. HITL 승인 필요 여부
@Builder.Default
private Boolean requiresApproval = false;
@@ -73,4 +89,45 @@ public class ToolMetadata {
private Boolean openWorldHint = false;
// 3. 연동 아키텍처 구분 (DIRECT / MCI_EAI)
private String integrationType; // 연동 타입: "DIRECT" 또는 "MCI_EAI"
// 4. 레거시(MCI/EAI) 연동 시 필수 정보 (integrationType이 "MCI_EAI"일 때 사용)
private String mciServiceId; // MCI/EAI 호출을 위한 서비스 ID (예: CRM_001, LICO_992)
// 5. 인프라 상태 정보 (DIRECT 연동 시 사용)
private Long lastHeartbeat; // Redis TTL 갱신용 마지막 하트비트 타임스탬프
// 6. 동적 서킷 브레이커 & 속도 제어 설정 (Registry 기반)
private Integer failureRateThreshold; // 서킷 브레이커 동작 기준 실패율 (%)
private Integer slidingWindowSize; // 서킷 브레이커 에러율 계산 표본 요청 수
private Integer rateLimitForPeriod; // 속도 제어: 1초당 허용 최대 요청 수
// 7. Gateway 코어 제어용 설정 필드 추가 (재시도, 타임아웃, 오퍼레이션 타입)
@Builder.Default
private OperationType operationType = OperationType.READ;
@Builder.Default
private Boolean retryEnabled = true;
@Builder.Default
private Integer circuitBreakerFailureThreshold = 0;
@Builder.Default
private Long circuitBreakerOpenMillis = 0L;
@Builder.Default
private Long timeoutMillis = 0L;
// --- Guardrail 호환성을 위한 메서드 추가 ---
public Set<String> allowedArguments() {
if (parametersSchema == null || !parametersSchema.containsKey("properties")) return Set.of();
return ((Map<String, Object>) parametersSchema.get("properties")).keySet();
}
public Set<String> requiredArguments() {
if (parametersSchema == null || !parametersSchema.containsKey("required")) return Set.of();
return new HashSet<>((List<String>) parametersSchema.get("required"));
}
}

View File

@@ -4,25 +4,24 @@ package io.shinhanlife.dap.mcc.presentation;
/**
* @package io.shinhanlife.dap.mcc.presentation
* @className BusinessToolController
* @description AX HUB ??戮?츩??嶺뚳퐣瑗????????
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- ?띠룇裕??????----------
* ??瑜곸젧?? ??瑜곸젧?? ??瑜곸젧??怨몃뮔
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 嶺뚣끉裕???諛댁뎽
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import com.fasterxml.jackson.databind.ObjectMapper;
import com.networknt.schema.Error;
import org.springframework.ai.mcp.annotation.McpTool;
import io.shinhanlife.dap.lib.validation.ToolArgumentSchemaValidator;
import io.shinhanlife.dap.lib.annotation.McpFunction;
import io.shinhanlife.dap.lib.annotation.McpTool;
import io.shinhanlife.dap.lib.config.McpProperties;
import io.shinhanlife.dap.mcc.dto.ToolMetadata;
import io.shinhanlife.dap.lib.mcp.LocalToolScanner;
import io.shinhanlife.dap.lib.mcp.ToolRegistryHeartbeatSender;
import io.shinhanlife.dap.lib.util.ToolSchemaResolver;
import java.lang.reflect.Method;
import java.util.ArrayList;
@@ -55,17 +54,17 @@ public class BusinessToolController {
private final ApplicationContext applicationContext;
private final ObjectMapper objectMapper;
private final McpProperties mcpProperties;
private final LocalToolScanner localToolScanner;
private final ToolRegistryHeartbeatSender toolRegistryHeartbeatSender;
private final ToolArgumentSchemaValidator toolArgumentSchemaValidator;
private final ToolSchemaResolver toolSchemaResolver;
// ???? ?브퀗?????β돦裕뉛쭚?Tool 嶺뚮ㅄ維뽨빳???븐뼔援?????
// 내부 조회용 로컬 Tool 목록 엔드포인트
@GetMapping("/mcp/api/v1/tools/local")
public List<ToolMetadata> getLocalTools() {
return localToolScanner.getAllScannedTools();
return toolRegistryHeartbeatSender.getAllScannedTools();
}
// ??戮?빢 REST ?リ옇?↑€????됱쓤 ??源녿뮡????븐뼔援?????
// 순수 REST 기반 동적 라우팅 엔드포인트
@PostMapping("/mcp/{name}")
public ResponseEntity<?> executeDynamicTool(
@PathVariable("name") String functionName,
@@ -78,25 +77,25 @@ public class BusinessToolController {
String finalRequestId = headerRequestId;
log.info(" [Tool] IN - trace-id: {}, request-id: {}", traceId, requestId);
log.info(" [Tool] ???됱쓤 ?????덈뺄 ??븐슙????琉용뼁 (??貫?양춯?: {}", functionName);
log.info(" [Tool] 동적 툴 실행 요청 수신 (함수명): {}", functionName);
if (arguments != null) {
try {
log.info(" [Tool] ?筌뤾쑵?????逾ф쾬?롮구?? {}", objectMapper.writeValueAsString(arguments));
log.info(" [Tool] 호출 파라미터: {}", objectMapper.writeValueAsString(arguments));
} catch (Exception e) {
log.info(" [Tool] ?筌뤾쑵?????逾ф쾬?롮구?? {}", arguments);
log.info(" [Tool] 호출 파라미터: {}", arguments);
}
}
Object targetBean = null;
Method targetMethod = null;
McpFunction targetFunctionAnnotation = null;
McpTool targetFunctionAnnotation = null;
// McpTool ???????逾???リ옇?↑€??브퀗???뱀쾸? ?熬곣뫁夷????쒖굣?節놁뿉??熬곣뫁逾???????깅さ亦껋깢??? ?熬곣뫕??????????щ뤂????@McpFunction??嶺뚢돦堉????덈펲.
// McpTool 어노테이션 기반 조회가 프록시 문제로 누락될 수 있으므로, 전체 빈을 순회하며 @McpTool을 찾습니다.
Map<String, Object> allBeans = applicationContext.getBeansOfType(Object.class);
outerLoop:
for (Object bean : allBeans.values()) {
Class<?> targetClass = AopUtils.getTargetClass(bean);
for (Method targetMethodOfClass : targetClass.getDeclaredMethods()) {
McpFunction mcpFunc = AnnotationUtils.findAnnotation(targetMethodOfClass, McpFunction.class);
McpTool mcpFunc = AnnotationUtils.findAnnotation(targetMethodOfClass, McpTool.class);
if (mcpFunc != null) {
String baseName = mcpFunc.name();
String expectedName = mcpProperties.getNamespace() != null && !mcpProperties.getNamespace().isEmpty()
@@ -122,7 +121,7 @@ public class BusinessToolController {
for (Object bean : allBeans.values()) {
Class<?> targetCls = AopUtils.getTargetClass(bean);
for (Method m : targetCls.getDeclaredMethods()) {
McpFunction func = AnnotationUtils.findAnnotation(m, McpFunction.class);
McpTool func = AnnotationUtils.findAnnotation(m, McpTool.class);
if (func != null) {
String baseName = func.name();
String expName = mcpProperties.getNamespace() != null && !mcpProperties.getNamespace().isEmpty()
@@ -131,28 +130,29 @@ public class BusinessToolController {
}
}
}
log.error("[Tool] ???덈뺄????貫??Method)??嶺뚢돦堉??????怨룸????덈펲: {}. ?熬곣뫗?????노뼌????嶺뚮∥?꾥땻??嶺뚮ㅄ維뽨빳? {}", functionName, availableFunctions);
log.error("[Tool] 실행할 함수(Method)를 찾을 수 없습니다: {}. 현재 스캔된 툴 메서드 목록: {}", functionName, availableFunctions);
Map<String, Object> errorDetails = new HashMap<>();
errorDetails.put("status", "404");
Map<String, Object> errorBody = new HashMap<>();
errorBody.put("code", "TOOL_NOT_FOUND");
errorBody.put("message", "???덈뺄????貫???嶺뚢돦堉??????怨룸????덈펲: " + functionName);
errorBody.put("message", "실행할 함수를 찾을 수 없습니다: " + functionName);
errorBody.put("details", errorDetails);
if (finalRequestId != null) errorBody.put("request_id", finalRequestId);
return ResponseEntity.status(404).body(errorBody);
}
// (?リ옇???嶺뚢뼰維€???β돦裕뉐퐲???蹂ㅽ깴??
// (기존 차단 로직 제거됨)
// 2. ???逾ф쾬?롮구????ル쪇????롪틵?嶺?(JSON Schema)
// 2. 파라미터 유효성 검증 (JSON Schema)
if (targetMethod.getParameterCount() > 0) {
Class<?> paramType = targetMethod.getParameterTypes()[0];
if (!Map.class.isAssignableFrom(paramType)) {
try {
Map<String, Object> inputSchema = toolSchemaResolver.resolve(targetFunctionAnnotation, paramType);
io.shinhanlife.dap.lib.annotation.ToolHint hint = AnnotationUtils.findAnnotation(targetMethod, io.shinhanlife.dap.lib.annotation.ToolHint.class);
Map<String, Object> inputSchema = toolSchemaResolver.resolve(targetFunctionAnnotation, hint, paramType);
List<Error> errors = toolArgumentSchemaValidator.validate(inputSchema, arguments);
if (!errors.isEmpty()) {
log.error("[Tool] ???逾ф쾬?롮구????ル쪇????롪틵?嶺????덉넮: {}", errors);
log.error("[Tool] 파라미터 유효성 검증 실패: {}", errors);
List<String> errorMessages = new ArrayList<>();
for (Error validationError : errors) {
errorMessages.add(validationError.getMessage());
@@ -161,28 +161,28 @@ public class BusinessToolController {
errorDetails.put("status", "422");
Map<String, Object> errorBody = new HashMap<>();
errorBody.put("code", "INVALID_PARAM");
errorBody.put("message", "???逾ф쾬?롮구????ル쪇????롪틵?嶺????덉넮");
errorBody.put("message", "파라미터 유효성 검증 실패");
errorBody.put("details", errorDetails);
if (finalRequestId != null) errorBody.put("request_id", finalRequestId);
return ResponseEntity.status(422).body(errorBody);
}
} catch (Exception e) {
log.error("[Tool] ???꾪뀞嶺??롪틵?嶺?繞????댁쾼 ?꾩룇裕뉑틦? {}", e.getMessage());
log.error("[Tool] 스키마 검증 중 오류 발생: {}", e.getMessage());
}
}
}
log.info("[Tool] ?洹먮뿫???源끒€?嶺뚯쉳??????덈뺄 -> Method: {}", targetMethod.getName());
log.info("[Tool] 리플렉션 직접 실행 -> Method: {}", targetMethod.getName());
try {
// 3. DTO ???逾ф쾬?롮구?????吏?嶺뚮씞?뗩뇡?(Map -> DTO)
// 3. DTO 파라미터 자동 매핑 (Map -> DTO)
Object invokeArgument = arguments;
if (targetMethod.getParameterCount() > 0 && arguments != null) {
Class<?> paramType = targetMethod.getParameterTypes()[0];
if (!Map.class.isAssignableFrom(paramType)) {
invokeArgument = objectMapper.convertValue(arguments, paramType);
log.info("[Tool] DTO ???吏?嶺뚮씞?뗩뇡??繹먭퍓沅? {}", paramType.getSimpleName());
log.info("[Tool] DTO 자동 매핑 성공: {}", paramType.getSimpleName());
}
}
@@ -194,7 +194,8 @@ public class BusinessToolController {
methodResult = targetMethod.invoke(targetBean, invokeArgument);
}
Map<String, Object> outputSchema = toolSchemaResolver.resolveOutput(targetFunctionAnnotation, targetMethod.getReturnType());
io.shinhanlife.dap.lib.annotation.ToolHint outputHint = AnnotationUtils.findAnnotation(targetMethod, io.shinhanlife.dap.lib.annotation.ToolHint.class);
Map<String, Object> outputSchema = toolSchemaResolver.resolveOutput(targetFunctionAnnotation, targetMethod.getReturnType(), outputHint);
if (!outputSchema.isEmpty()) {
List<Error> outputErrors = toolArgumentSchemaValidator.validateValue(outputSchema, methodResult);
if (!outputErrors.isEmpty()) {
@@ -212,11 +213,11 @@ public class BusinessToolController {
long elapsed = System.currentTimeMillis() - startTime;
// 5. ?롪퍒????꾩룇瑗??(??戮?빢 REST ??얜Ŧ堉?
// 5. 결과 반환 (순수 REST 응답)
try {
log.info("[Tool Execution] Output Schema Result: {}", objectMapper.writeValueAsString(methodResult));
log.info("[Tool -> MCP Gateway] Output Schema Result: {}", objectMapper.writeValueAsString(methodResult));
} catch (Exception e) {
log.info("[Tool Execution] Output Schema Result: {}", methodResult);
log.info("[Tool -> MCP Gateway] Output Schema Result: {}", methodResult);
}
log.info(" [Tool] OUT - trace-id: {}, request-id: {}", traceId, requestId);
@@ -228,7 +229,7 @@ public class BusinessToolController {
return responseBuilder.body(methodResult);
} catch (Exception e) {
log.error("[Tool] ?洹먮뿫???源끒€????덈뺄 繞????깅뇶 ?꾩룇裕뉑틦? {}", e.getMessage());
log.error("[Tool] 리플렉션 실행 중 예외 발생: {}", e.getMessage());
Map<String, Object> errorDetails = new HashMap<>();
errorDetails.put("status", "500");
Map<String, Object> errorBody = new HashMap<>();

View File

@@ -1,21 +1,5 @@
package io.shinhanlife.dap.mcc.presentation;
/**
* @package io.shinhanlife.dap.mcc.presentation
* @className ToolManifestController
* @description AX HUB ?쒖뒪??泥섎━ ?대옒??
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 媛쒖젙?대젰 ----------
* ?섏젙?? ?섏젙?? ?섏젙?댁슜
* ---------- -------- ---------------------------
* 2026.09.01 0986406 理쒖큹?앹꽦
*
* </pre>
*/
import io.shinhanlife.dap.lib.manifest.ToolManifestResponse;
import io.shinhanlife.dap.lib.manifest.ToolManifestService;
import org.springframework.http.HttpHeaders;

View File

@@ -1,21 +1,5 @@
package io.shinhanlife.glow.communication.annotation;
/**
* @package io.shinhanlife.glow.communication.annotation
* @className GlowTrgmField
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;

View File

@@ -1,20 +1,4 @@
package io.shinhanlife.glow.communication.dto;
/**
* @package io.shinhanlife.glow.communication.dto
* @className CommonHeader
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import lombok.Data;
@Data
public class CommonHeader {

View File

@@ -1,21 +1,5 @@
package io.shinhanlife.glow.communication.dto;
/**
* @package io.shinhanlife.glow.communication.dto
* @className HeaderDefaults
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
public enum HeaderDefaults {
ITRF_ID, RCV_SVC_ID, STR_YMD, ACNT_OGNZ_NO, PSMR_ASRT_CD, SBSN_RULP_ASRT_CD, BSDU_CD, BSQU_CD,
INDV_CTIN_ROLE_CD, SCRN_ID, OGNZ_ASRT_CD, OGNZ_LEVE_CD, TGRM_CREA_CHNN_TYPE_CD, ENVR_TYPE_CD

View File

@@ -1,21 +1,5 @@
package io.shinhanlife.glow.communication.dto;
/**
* @package io.shinhanlife.glow.communication.dto
* @className Transfer
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;

View File

@@ -1,21 +1,5 @@
package io.shinhanlife.glow.communication.exception;
/**
* @package io.shinhanlife.glow.communication.exception
* @className ItrfException
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
public class ItrfException extends Exception {
public ItrfException(String msg) {
super(msg);

View File

@@ -1,21 +1,5 @@
package io.shinhanlife.glow.communication.module.eai.component;
/**
* @package io.shinhanlife.glow.communication.module.eai.component
* @className GlowEaiComponent
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import io.shinhanlife.glow.communication.dto.Transfer;
import org.springframework.stereotype.Component;

View File

@@ -1,21 +1,5 @@
package io.shinhanlife.glow.communication.module.mci.component;
/**
* @package io.shinhanlife.glow.communication.module.mci.component
* @className GlowMciComponent
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import io.shinhanlife.glow.communication.dto.Transfer;
import org.springframework.stereotype.Component;

View File

@@ -1,20 +1,4 @@
package io.shinhanlife.glow.communication.util;
/**
* @package io.shinhanlife.glow.communication.util
* @className CommonHeaderFactory
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import io.shinhanlife.glow.communication.dto.CommonHeader;
import io.shinhanlife.glow.communication.dto.HeaderDefaults;
import java.util.Map;

View File

@@ -1,21 +1,5 @@
package io.shinhanlife.glow.util;
/**
* @package io.shinhanlife.glow.util
* @className GlowMciParser
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import io.shinhanlife.glow.GlowMciFieldInfo;
import lombok.extern.slf4j.Slf4j;
import java.lang.reflect.Field;

View File

@@ -13,7 +13,7 @@ logging:
pattern:
console: "[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] [%thread] %logger{36} - %msg%n"
# 2. Redis 공통 설정 (WAS 연동 및 캐싱)
# 2. Redis 공통 설정 (Gateway 연동 및 캐싱)
spring:
data:
redis:

View File

@@ -36,7 +36,7 @@
<div class="flex items-center space-x-5">
<a href="/index.html" class="flex items-center group" style="text-decoration:none;">
<div class="w-2 h-2 rounded-full mr-2" style="background:#3b82f6; box-shadow: 0 0 8px rgba(59,130,246,0.8);"></div>
<span class="font-semibold tracking-tight text-sm" style="color:#f4f4f5;">AXHUB WAS</span>
<span class="font-semibold tracking-tight text-sm" style="color:#f4f4f5;">AXHUB Gateway</span>
</a>
<div class="h-4 w-px" style="background:#27272a;"></div>
<nav class="flex space-x-5 text-[13px] font-medium">
@@ -80,7 +80,7 @@
function requestId() { return crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.random().toString(16).slice(2)}`; }
function escapeHtml(value) { return String(value).replace(/[&<>]/g, item => ({'&':'&amp;','<':'&lt;','>':'&gt;'}[item])); }
let isWasMode = false;
let isGatewayMode = false;
async function loadManifest() {
$('manifestStatus').textContent = 'Manifest loading';
@@ -90,13 +90,13 @@
if (!response.ok && response.status === 404) {
// Gateway 환경 감지 및 Fallback 처리
response = await fetch('/mcp/api/v1/tools/list');
if (!response.ok) throw new Error(`WAS HTTP ${response.status}`);
if (!response.ok) throw new Error(`Gateway HTTP ${response.status}`);
const rpcData = await response.json();
state.tools = rpcData.result?.tools || [];
isWasMode = true;
isGatewayMode = true;
$('manifestStatus').textContent = `${state.tools.length} tools · WAS Mode`;
$('manifestStatus').textContent = `${state.tools.length} tools · Gateway Mode`;
$('manifestStatus').className = 'badge ok';
renderTools();
return;
@@ -174,7 +174,7 @@
$('executeButton').disabled = true; $('httpStatus').textContent = '실행 중'; $('httpStatus').className = 'badge';
try {
let response;
if (isWasMode) {
if (isGatewayMode) {
const reqPayload = { jsonrpc: "2.0", method: "tools/call", params: { name: tool.name, arguments: payload }, id: Date.now() };
response = await fetch('/mcp/api/v1/tools/call', {
method: 'POST',
@@ -192,7 +192,7 @@
let isOk = response.ok;
let displayStatus = response.status;
if (isWasMode && isOk && data?.result?.isError) {
if (isGatewayMode && isOk && data?.result?.isError) {
isOk = false;
displayStatus = "MCP ERR";
}
@@ -201,7 +201,7 @@
$('httpStatus').textContent = `HTTP ${displayStatus}`; $('httpStatus').className = `badge ${isOk ? 'ok' : 'fail'}`;
$('latency').textContent = `${elapsed}ms`; $('traceId').textContent = `trace-id: ${response.headers.get('trace-id') || trace}`; $('requestId').textContent = `request-id: ${response.headers.get('request-id') || request}`;
let displayData = data;
if (isWasMode && data && typeof data === 'object') {
if (isGatewayMode && data && typeof data === 'object') {
if (data.result && data.result.result) {
displayData = data.result.result.data !== undefined ? data.result.result.data : data.result.result;
} else if (data.error) {

View File

@@ -1,4 +1,4 @@
package io.shinhanlife.dap.lib.adapter.sender;
package io.shinhanlife.dap.lib.common.adapter.sender;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.shinhanlife.dap.lib.integration.mci.dto.MciRequestWrapper;

View File

@@ -1,21 +1,5 @@
package io.shinhanlife.dap.lib.config;
/**
* @package io.shinhanlife.dap.lib.config
* @className ToolSchemaConfigurationTest
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import static org.junit.jupiter.api.Assertions.assertNotNull;
import com.fasterxml.jackson.databind.ObjectMapper;

View File

@@ -1,21 +1,7 @@
package io.shinhanlife.dap.lib.util;
/**
* @package io.shinhanlife.dap.lib.util
* @className JsonSchemaGeneratorTest
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import org.springframework.ai.mcp.annotation.McpToolParam;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -26,8 +12,6 @@ import com.networknt.schema.InputFormat;
import com.networknt.schema.Schema;
import com.networknt.schema.SchemaRegistry;
import com.networknt.schema.SpecificationVersion;
import io.shinhanlife.dap.lib.annotation.McpParameter;
import io.shinhanlife.dap.lib.annotation.McpValidation;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Test;
@@ -106,24 +90,19 @@ class JsonSchemaGeneratorTest {
}
private static class ValidatedRequest {
@McpParameter(description = "recipient phone number", required = true)
@McpValidation(pattern = "^01[0-9]{8,9}$")
@McpToolParam(description = "recipient phone number", required = true)
private String phoneNumber;
@McpParameter(description = "issue amount", required = true)
@McpValidation(minimum = 1)
@McpToolParam(description = "issue amount", required = true)
private Long amount;
@McpParameter(description = "approval result")
@McpValidation(required = true, allowedValues = {"APPROVE", "REJECT"})
@McpToolParam(description = "approval result")
private String approvalStatus;
@McpParameter(description = "page size")
@McpValidation(minimum = 1, maximum = 50, defaultValue = "20")
@McpToolParam(description = "page size")
private Integer pageSize;
@McpParameter(description = "reference")
@McpValidation(minLength = 1, maxLength = 30)
@McpToolParam(description = "reference")
private String reference;
}
@@ -136,7 +115,6 @@ class JsonSchemaGeneratorTest {
}
private static class NestedChild {
@McpValidation(required = true, pattern = "^\\d{8}$")
private String businessDate;
}
}

View File

@@ -0,0 +1,41 @@
package io.shinhanlife.dap.lib.util;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.nio.file.Files;
import java.nio.file.Path;
import org.junit.jupiter.api.Test;
class ToolScaffolderTest {
@Test
void generatesManifestReadyToolAndOutputSchemaDto() throws Exception {
String moduleName = "build/scaffold-manifest-test";
ToolScaffolder.scaffold("claim search", "CLM0001", "청구 조회", "cmm", "HTTP", moduleName,
"tester", "2026.08.04", true, null);
Path root = Path.of(moduleName, "src/main/java/io/shinhanlife/dap/mcc/biz/cmm");
String useCase = Files.readString(root.resolve("usecase/ClaimSearchUseCase.java"));
String response = Files.readString(root.resolve("dto/ClaimSearchResponse.java"));
assertTrue(useCase.contains("name = \"oth.cmm.claim.search\""));
assertTrue(useCase.contains("version = \"1.0.0\""));
assertTrue(useCase.contains("timeoutMillis = 300000L"));
assertTrue(response.contains(""));
assertTrue(response.contains(""));
}
@Test
void usesWasModuleNameAsToolPodPrefix() throws Exception {
String moduleName = "build/dap-was-sms";
ToolScaffolder.scaffold("notification send", "SMS0001", "SMS 발송", "cmm", "HTTP", moduleName,
"tester", "2026.08.05", true, null);
Path useCasePath = Path.of(moduleName,
"src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/NotificationSendUseCase.java");
String useCase = Files.readString(useCasePath);
assertTrue(useCase.contains("name = \"sms.cmm.notification.send\""));
}
}

View File

@@ -1,28 +1,9 @@
package io.shinhanlife.dap.lib.util;
/**
* @package io.shinhanlife.dap.lib.util
* @className ToolSchemaResolverTest
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.shinhanlife.dap.lib.annotation.McpFunction;
import io.shinhanlife.dap.lib.annotation.McpOutputSchema;
import io.shinhanlife.dap.lib.annotation.McpValidation;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;
@@ -88,43 +69,28 @@ class ToolSchemaResolverTest {
}
static class InlineSchemaTool {
@McpFunction(
displayName = "inline",
name = "test.schema.inline",
description = "inline schema",
inputSchema = "{\"type\":\"object\",\"properties\":{\"keyword\":{\"type\":\"string\"}},\"additionalProperties\":false}")
void search(AutomaticRequest request) {
}
}
static class AutomaticSchemaTool {
@McpFunction(displayName = "automatic", name = "test.schema.automatic", description = "automatic schema")
void search(AutomaticRequest request) {
}
}
static class AutomaticOutputSchemaTool {
@McpFunction(displayName = "automatic-output", name = "test.schema.automatic-output", description = "automatic output")
SimpleResponse search(AutomaticRequest request) {
return null;
}
}
@McpOutputSchema
static class SimpleResponse {
@McpValidation(required = true, allowedValues = {"SUCCESS", "FAILURE"})
private String resultCode;
@McpValidation(maxLength = 200)
private String message;
}
static class OutputSchemaTool {
@McpFunction(
displayName = "output",
name = "test.schema.output",
description = "output schema",
outputSchema = "{\"type\":\"object\",\"properties\":{\"resultCode\":{\"type\":\"string\"}},\"required\":[\"resultCode\"],\"additionalProperties\":false}")
void search(AutomaticRequest request) {
}
}

View File

@@ -1,21 +1,5 @@
package io.shinhanlife.dap.lib.validation;
/**
* @package io.shinhanlife.dap.lib.validation
* @className McpToolNameValidatorTest
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -106,24 +90,7 @@ class McpToolNameValidatorTest {
Files.writeString(source, """
package example;
/**
* @package io.shinhanlife.dap.lib.validation
* @className McpToolNameValidatorTest
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
class %s {
@McpFunction(displayName = "%s", name = "%s", description = "test")
void execute() { }
}
""".formatted(className, className, toolName));

View File

@@ -1,20 +1,4 @@
package io.shinhanlife.dap.lib.manifest;
/**
* @package io.shinhanlife.dap.lib.manifest
* @className ToolManifestServiceTest
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
package io.shinhanlife.dap.mcc.manifest;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -22,6 +6,9 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.shinhanlife.dap.lib.config.McpProperties;
import io.shinhanlife.dap.lib.manifest.ToolManifestItem;
import io.shinhanlife.dap.lib.manifest.ToolManifestResponse;
import io.shinhanlife.dap.lib.manifest.ToolManifestService;
import io.shinhanlife.dap.mcc.dto.ToolMetadata;
import java.util.List;
import java.util.Map;

View File

@@ -1,20 +1,4 @@
package io.shinhanlife.dap.lib.mcp;
/**
* @package io.shinhanlife.dap.lib.mcp
* @className McpRequestHeaderFilterTest
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
package io.shinhanlife.dap.mcc.mcp;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
@@ -26,8 +10,9 @@ import static org.mockito.Mockito.when;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.server.McpSyncServer;
import io.shinhanlife.dap.lib.mcp.*;
import io.shinhanlife.dap.mcc.presentation.BusinessToolController;
import io.shinhanlife.dap.lib.mcp.LocalToolScanner;
import java.lang.reflect.Method;
import java.util.Map;
import org.junit.jupiter.api.Test;
@@ -40,14 +25,14 @@ class McpRequestHeaderFilterTest {
@Test
void capturesOptionalMcpHeadersOnlyForTheCurrentRequest() throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/mcp");
request.addHeader("X-Request-Id", "was-request-id");
request.addHeader("X-Request-Id", "gateway-request-id");
request.addHeader("trace-id", "trace-001");
request.addHeader("request-id", "tool-request-001");
request.addHeader("employee-id", "encrypted-employee-id");
new McpRequestHeaderFilter().doFilter(request, new MockHttpServletResponse(), (req, res) -> {
McpRequestHeaders headers = McpRequestHeaderContext.current();
assertEquals("was-request-id", headers.headerRequestId());
assertEquals("gateway-request-id", headers.headerRequestId());
assertEquals("trace-001", headers.traceId());
assertEquals("tool-request-001", headers.requestId());
assertEquals("encrypted-employee-id", headers.encryptedEmployeeId());
@@ -60,19 +45,19 @@ class McpRequestHeaderFilterTest {
void forwardsCapturedHeadersToBusinessToolExecution() throws Exception {
BusinessToolController controller = mock(BusinessToolController.class);
doReturn(ResponseEntity.ok(Map.of("result", "ok")))
.when(controller).executeDynamicTool(eq("sampleTool"), eq("was-request-id"), eq("trace-001"),
.when(controller).executeDynamicTool(eq("sampleTool"), eq("gateway-request-id"), eq("trace-001"),
eq("tool-request-001"), eq("encrypted-employee-id"), eq(Map.of("key", "value")));
ToolPodMcpToolSynchronizer synchronizer = new ToolPodMcpToolSynchronizer(
mock(McpSyncServer.class), mock(LocalToolScanner.class), controller, new ObjectMapper());
mock(McpSyncServer.class), mock(ToolRegistryHeartbeatSender.class), controller, new ObjectMapper());
Method invoke = ToolPodMcpToolSynchronizer.class.getDeclaredMethod(
"invoke", String.class, McpRequestHeaders.class, Map.class);
invoke.setAccessible(true);
invoke.invoke(synchronizer, "sampleTool",
new McpRequestHeaders("was-request-id", "trace-001", "tool-request-001", "encrypted-employee-id"),
new McpRequestHeaders("gateway-request-id", "trace-001", "tool-request-001", "encrypted-employee-id"),
Map.of("key", "value"));
verify(controller).executeDynamicTool("sampleTool", "was-request-id", "trace-001",
verify(controller).executeDynamicTool("sampleTool", "gateway-request-id", "trace-001",
"tool-request-001", "encrypted-employee-id", Map.of("key", "value"));
}
}

View File

@@ -1,25 +1,10 @@
package io.shinhanlife.dap.lib.mcp;
/**
* @package io.shinhanlife.dap.lib.mcp
* @className ToolMcpServerConfigurationTest
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
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 io.shinhanlife.dap.lib.mcp.ToolMcpServerConfiguration;
import org.junit.jupiter.api.Test;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
@@ -32,7 +17,6 @@ class ToolMcpServerConfigurationTest {
HttpServletStreamableServerTransportProvider transport = configuration.toolMcpTransportProvider();
ServletRegistrationBean<HttpServletStreamableServerTransportProvider> registration = configuration.toolMcpServlet(transport);
assertEquals("/mcp", registration.getUrlMappings().iterator().next());
assertFalse(registration.getUrlMappings().contains("/mcp/*"));
assertThat(registration.getUrlMappings()).containsExactlyInAnyOrder("/mcp", "/mcp/message");
}
}

View File

@@ -1,21 +1,5 @@
package io.shinhanlife.dap.mcc.presentation;
/**
* @package io.shinhanlife.dap.mcc.presentation
* @className BusinessToolControllerHeaderContractTest
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;

View File

@@ -1,25 +1,10 @@
package io.shinhanlife.dap.lib.validation;
/**
* @package io.shinhanlife.dap.lib.validation
* @className ToolArgumentSchemaValidatorTest
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
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 io.shinhanlife.dap.lib.validation.ToolArgumentSchemaValidator;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Test;

View File

@@ -1,21 +1,5 @@
package io.shinhanlife.dap.mcc.presentation;
/**
* @package io.shinhanlife.dap.mcc.presentation
* @className ToolTestConsoleResourceTest
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

View File

@@ -1,21 +1,5 @@
package io.shinhanlife.glow.communication.annotation;
/**
* @package io.shinhanlife.glow.communication.annotation
* @className GlowTrgmFieldContractTest
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.lang.reflect.Field;

View File

@@ -1,6 +1,11 @@
package io.shinhanlife.dap.mcc.biz.cmm.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import org.springframework.ai.mcp.annotation.McpToolParam;
/**
* @package io.shinhanlife.dap.mcc.dto
* @className BalanceRequest
@@ -15,13 +20,12 @@ package io.shinhanlife.dap.mcc.biz.cmm.dto;
*
* </pre>
*/
import io.shinhanlife.dap.lib.annotation.McpParameter;
import io.shinhanlife.dap.lib.annotation.McpValidation;
import lombok.Data;
@Data
public class BalanceRequest {
@McpParameter(description = "고객의 계좌번호 (- 제외) ", required = true)
@McpValidation(pattern = "\\S")
@McpToolParam(description = "고객의 계좌번호 (- 제외) ", required = true)
@Schema(pattern = "\\S", example = "1234567890")
private String accountNumber;
}

View File

@@ -1,11 +1,14 @@
package io.shinhanlife.dap.mcc.biz.cmm.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import org.springframework.ai.mcp.annotation.McpToolParam;
import lombok.Builder;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import io.shinhanlife.dap.lib.annotation.McpParameter;
import io.shinhanlife.dap.lib.annotation.McpValidation;
import lombok.Getter;
import lombok.Setter;
@@ -30,11 +33,12 @@ import lombok.Setter;
@AllArgsConstructor
public class BillingProcessRequest {
@McpParameter(description = "처리할 청구 접수 번호", required = true)
@McpValidation(pattern = "\\S")
@McpToolParam(description = "처리할 청구 접수 번호", required = true)
@Schema(pattern = "\\S", example = "BILL20260805")
private String billingId;
@McpParameter(description = "심사 승인 여부 (예: APPROVE, REJECT)")
@McpValidation(required = true, allowedValues = {"APPROVE", "REJECT"})
@McpToolParam(description = "승인 처리 구분 (예: APPROVE, REJECT)")
@Schema(required = true, allowableValues = {"APPROVE", "REJECT"}, example = "APPROVE")
private String approvalStatus;
}

View File

@@ -1,10 +1,14 @@
package io.shinhanlife.dap.mcc.biz.cmm.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import org.springframework.ai.mcp.annotation.McpToolParam;
import lombok.Builder;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import io.shinhanlife.dap.lib.annotation.McpParameter;
import lombok.Getter;
import lombok.Setter;
@@ -29,6 +33,8 @@ import lombok.Setter;
@AllArgsConstructor
public class BillingStatusRequest {
@McpParameter(description = "조회할 청구 접수 번호", required = true)
@McpToolParam(description = "조회할 청구 접수 번호", required = true)
@Schema(example = "BILL20260805")
private String billingId;
}

View File

@@ -1,10 +1,14 @@
package io.shinhanlife.dap.mcc.biz.cmm.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import org.springframework.ai.mcp.annotation.McpToolParam;
import lombok.Builder;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import io.shinhanlife.dap.lib.annotation.McpParameter;
import lombok.Getter;
import lombok.Setter;
@@ -29,6 +33,8 @@ import lombok.Setter;
@AllArgsConstructor
public class BondCheckRequest {
@McpParameter(description = "확인하고자 하는 디지털 증권 발행 금액", required = true)
private Long amount;
@McpToolParam(description = "확인하고자 하는 디지털 증권 발행 금액", required = true)
@Schema(pattern = "^[0-9]+$", example = "1000000")
private String amount;
}

View File

@@ -1,11 +1,14 @@
package io.shinhanlife.dap.mcc.biz.cmm.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import org.springframework.ai.mcp.annotation.McpToolParam;
import lombok.Builder;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import io.shinhanlife.dap.lib.annotation.McpParameter;
import io.shinhanlife.dap.lib.annotation.McpValidation;
import lombok.Getter;
import lombok.Setter;
@@ -30,11 +33,12 @@ import lombok.Setter;
@AllArgsConstructor
public class BondIssueRequest {
@McpParameter(description = "발행할 디지털 증권 금액", required = true)
@McpValidation(minimum = 1)
private Long amount;
@McpToolParam(description = "발행할 증권 금액 (숫자 문자열)", required = true)
@Schema(pattern = "^[0-9]+$", example = "1000000")
private String amount;
@McpParameter(description = "발행 대상 계좌 번호", required = true)
@McpValidation(pattern = "\\S")
@McpToolParam(description = "발행 대상 계좌 번호", required = true)
@Schema(pattern = "\\S", example = "1234567890")
private String targetAccount;
}

View File

@@ -1,24 +1,10 @@
package io.shinhanlife.dap.mcc.biz.cmm.dto;
/**
* @package io.shinhanlife.dap.mcc.biz.cmm.dto
* @className ClaimSearchRequest
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import io.shinhanlife.dap.lib.annotation.McpAnyOf;
import io.shinhanlife.dap.lib.annotation.McpParameter;
import io.shinhanlife.dap.lib.annotation.McpValidation;
import io.swagger.v3.oas.annotations.media.Schema;
import org.springframework.ai.mcp.annotation.McpToolParam;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
@@ -34,40 +20,39 @@ import lombok.Setter;
@Builder
@NoArgsConstructor
@AllArgsConstructor
@McpAnyOf({"claimNo", "contractNo"})
public class ClaimSearchRequest {
@McpParameter(description = "청구번호. CLM 다음 숫자 13자리 형식이다.")
@McpValidation(pattern = "^CLM[0-9]{13}$", examples = {"CLM2026070100123"})
@McpToolParam(description = "청구번호. CLM 다음 숫자 13자리 형식이다.")
@Schema(pattern = "^CLM[0-9]{13}$", example = "CLM2026070100123")
private String claimNo;
@McpParameter(description = "계약번호. 숫자 11자리 형식이다.")
@McpValidation(pattern = "^[0-9]{11}$", examples = {"10023456789"})
@McpToolParam(description = "계약번호. 숫자 11자리 형식이다.")
@Schema(pattern = "^[0-9]{11}$", example = "10023456789")
private String contractNo;
@McpParameter(description = "청구 상태 필터")
@McpValidation(allowedValues = {
@McpToolParam(description = "청구 상태 필터")
@Schema(example = "RECEIVED", allowableValues = {
"RECEIVED", "REVIEWING", "ADDITIONAL_DOC_REQUIRED",
"APPROVED", "PAID", "REJECTED", "WITHDRAWN"
})
private String status;
@McpParameter(description = "청구 유형 필터")
@McpValidation(allowedValues = {
@McpToolParam(description = "청구 유형 필터")
@Schema(example = "MEDICAL", allowableValues = {
"MEDICAL", "SURGERY", "HOSPITALIZATION",
"DIAGNOSIS", "DEATH", "DISABILITY"
})
private String claimType;
@McpParameter(description = "접수일 조회 시작일(YYYY-MM-DD)")
@McpValidation(format = "date", examples = {"2026-01-01"})
@McpToolParam(description = "접수일 조회 시작일(YYYY-MM-DD)")
@Schema(format = "date", example = "2026-01-01")
private String fromDate;
@McpParameter(description = "접수일 조회 종료일(YYYY-MM-DD)")
@McpValidation(format = "date", examples = {"2026-07-31"})
@McpToolParam(description = "접수일 조회 종료일(YYYY-MM-DD)")
@Schema(format = "date", example = "2026-07-31")
private String toDate;
@McpParameter(description = "반환할 최대 건수")
@McpValidation(minimum = 1, maximum = 50, defaultValue = "20")
private Integer size;
@McpToolParam(description = "반환할 최대 건수 (기본값: 20, 최대: 50)")
@Schema(pattern = "^[1-9][0-9]?$|^50$", defaultValue = "20", example = "20")
private String size;
}

View File

@@ -1,24 +1,10 @@
package io.shinhanlife.dap.mcc.biz.cmm.dto;
/**
* @package io.shinhanlife.dap.mcc.biz.cmm.dto
* @className ClaimSearchResponse
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import io.shinhanlife.dap.lib.annotation.McpOutputSchema;
import io.shinhanlife.dap.lib.annotation.McpParameter;
import io.shinhanlife.dap.lib.annotation.McpValidation;
import io.swagger.v3.oas.annotations.media.Schema;
import org.springframework.ai.mcp.annotation.McpToolParam;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Builder;
@@ -36,46 +22,45 @@ import lombok.Setter;
@Builder
@NoArgsConstructor
@AllArgsConstructor
@McpOutputSchema
public class ClaimSearchResponse {
@McpParameter(description = "Execution result code.")
@McpValidation(required = true, allowedValues = {"SUCCESS", "FAILURE"})
@McpToolParam(description = "Execution result code.")
@Schema(required = true, allowableValues = {"SUCCESS", "FAILURE"})
private String resultCode;
@McpParameter(description = "User-readable label for resultCode.")
@McpValidation(required = true, maxLength = 100)
@McpToolParam(description = "User-readable label for resultCode.")
@Schema(required = true, maxLength = 100)
private String resultLabel;
@McpParameter(description = "Current claim processing status code.")
@McpValidation(required = true, allowedValues = {
@McpToolParam(description = "Current claim processing status code.")
@Schema(required = true, allowableValues = {
"RECEIVED", "REVIEWING", "ADDITIONAL_DOC_REQUIRED",
"APPROVED", "PAID", "REJECTED", "WITHDRAWN"
})
private String status;
@McpParameter(description = "User-readable label for status.")
@McpValidation(required = true, maxLength = 100)
@McpToolParam(description = "User-readable label for status.")
@Schema(required = true, maxLength = 100)
private String statusLabel;
@McpParameter(description = "Approved amount. Null before review; do not interpret null as zero.")
@McpValidation(minimum = 0, nullable = true)
@McpToolParam(description = "Approved amount. Null before review; do not interpret null as zero.")
@Schema(minimum = "0", nullable = true)
private Long approvedAmount;
@McpParameter(description = "Present only when status is REJECTED; otherwise null.")
@McpValidation(maxLength = 200, nullable = true)
@McpToolParam(description = "Present only when status is REJECTED; otherwise null.")
@Schema(maxLength = 200, nullable = true)
private String rejectionReason;
@McpParameter(description = "Claim summaries, ordered by received date descending.")
@McpValidation(required = true)
@McpToolParam(description = "Claim summaries, ordered by received date descending.")
@Schema(required = true)
private List<ClaimSummary> items;
@McpParameter(description = "True when additional results exist beyond this response.")
@McpValidation(required = true)
@McpToolParam(description = "True when additional results exist beyond this response.")
@Schema(required = true)
private Boolean hasMore;
@McpParameter(description = "Total number of matched claims.")
@McpValidation(required = true, minimum = 0)
@McpToolParam(description = "Total number of matched claims.")
@Schema(required = true, nullable = true)
private Integer totalCount;
@Getter
@@ -85,20 +70,18 @@ public class ClaimSearchResponse {
@AllArgsConstructor
public static class ClaimSummary {
@McpParameter(description = "Claim processing status code.")
@McpValidation(required = true)
@McpToolParam(description = "Claim processing status code.")
private String status;
@McpParameter(description = "User-readable label for status.")
@McpValidation(required = true)
@McpToolParam(description = "User-readable label for status.")
private String statusLabel;
@McpParameter(description = "Received date in YYYY-MM-DD format.")
@McpValidation(required = true, format = "date")
@McpToolParam(description = "Received date in YYYY-MM-DD format.")
@Schema(required = true, format = "date")
private String receivedDate;
@McpParameter(description = "Approved amount. Null before review; do not interpret null as zero.")
@McpValidation(minimum = 0, nullable = true)
@McpToolParam(description = "Approved amount. Null before review; do not interpret null as zero.")
@Schema(nullable = true)
private Long approvedAmount;
}
}

View File

@@ -1,10 +1,14 @@
package io.shinhanlife.dap.mcc.biz.cmm.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import org.springframework.ai.mcp.annotation.McpToolParam;
import lombok.Builder;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import io.shinhanlife.dap.lib.annotation.McpParameter;
import lombok.Getter;
import lombok.Setter;
@@ -29,9 +33,12 @@ import lombok.Setter;
@AllArgsConstructor
public class ContractDetailRequest {
@McpParameter(description = "고객명", required = true)
@McpToolParam(description = "고객명", required = true)
@Schema(example = "홍길동")
private String customerName;
@McpParameter(description = "조회할 계약 번호", required = true)
@McpToolParam(description = "조회할 계약 번호", required = true)
@Schema(example = "1234567890")
private String contractId;
}

View File

@@ -1,10 +1,14 @@
package io.shinhanlife.dap.mcc.biz.cmm.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import org.springframework.ai.mcp.annotation.McpToolParam;
import lombok.Builder;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import io.shinhanlife.dap.lib.annotation.McpParameter;
import lombok.Getter;
import lombok.Setter;
@@ -29,9 +33,11 @@ import lombok.Setter;
@AllArgsConstructor
public class ContractStatusRequest {
@McpParameter(description = "고객명", required = true)
@McpToolParam(description = "고객명", required = true)
@Schema(example = "홍길동")
private String customerName;
@McpParameter(description = "조회할 계약 번호")
@McpToolParam(description = "조회할 계약 번호")
private String contractId;
}

View File

@@ -1,10 +1,14 @@
package io.shinhanlife.dap.mcc.biz.cmm.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import org.springframework.ai.mcp.annotation.McpToolParam;
import lombok.Builder;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import io.shinhanlife.dap.lib.annotation.McpParameter;
import lombok.Getter;
import lombok.Setter;
@@ -29,9 +33,11 @@ import lombok.Setter;
@AllArgsConstructor
public class CustomerDetailRequest {
@McpParameter(description = "고객명", required = true)
@McpToolParam(description = "고객명", required = true)
@Schema(example = "홍길동")
private String customerName;
@McpParameter(description = "고객 식별 번호 (CID)")
@McpToolParam(description = "고객 식별 번호 (CID)")
private String customerId;
}

View File

@@ -1,10 +1,14 @@
package io.shinhanlife.dap.mcc.biz.cmm.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import org.springframework.ai.mcp.annotation.McpToolParam;
import lombok.Builder;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import io.shinhanlife.dap.lib.annotation.McpParameter;
import lombok.Getter;
import lombok.Setter;
@@ -29,9 +33,11 @@ import lombok.Setter;
@AllArgsConstructor
public class CustomerGradeRequest {
@McpParameter(description = "고객 이름 (예: 김신한)", required = true)
@McpToolParam(description = "고객 이름 (예: 김신한)", required = true)
@Schema(example = "홍길동")
private String customerName;
@McpParameter(description = "고객 식별 번호 (CID)")
@McpToolParam(description = "고객 식별 번호 (CID)")
private String customerId;
}

View File

@@ -1,10 +1,14 @@
package io.shinhanlife.dap.mcc.biz.cmm.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import org.springframework.ai.mcp.annotation.McpToolParam;
import lombok.Builder;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import io.shinhanlife.dap.lib.annotation.McpParameter;
import lombok.Getter;
import lombok.Setter;
@@ -29,6 +33,8 @@ import lombok.Setter;
@AllArgsConstructor
public class LeaveCountRequest {
@McpParameter(description = "연차 내역을 조회할 사원 번호", required = true)
@McpToolParam(description = "연차 내역을 조회할 사원 번호", required = true)
@Schema(example = "EMP12345")
private String employeeId;
}

View File

@@ -1,21 +1,5 @@
package io.shinhanlife.dap.mcc.biz.cmm.dto;
/**
* @package io.shinhanlife.dap.mcc.biz.cmm.dto
* @className MciSampleStringResponse
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import com.fasterxml.jackson.annotation.JsonInclude;
import io.shinhanlife.glow.GlowMciFieldInfo;
import lombok.Data;

View File

@@ -1,21 +1,5 @@
package io.shinhanlife.dap.mcc.biz.cmm.dto;
/**
* @package io.shinhanlife.dap.mcc.biz.cmm.dto
* @className MciSampleTargetDto
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import io.shinhanlife.glow.GlowMciFieldInfo;
import lombok.Data;

View File

@@ -1,7 +1,11 @@
package io.shinhanlife.dap.mcc.biz.cmm.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import org.springframework.ai.mcp.annotation.McpToolParam;
import com.fasterxml.jackson.annotation.JsonInclude;
import io.shinhanlife.dap.lib.annotation.McpParameter;
import lombok.Data;
/**
@@ -21,12 +25,15 @@ import lombok.Data;
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class MetaCommonCodeRequest {
@McpParameter(description = "통합코드 그룹 ID (예: GRP_SYS_01, GRP_COMM_CD)", required = false)
@McpToolParam(description = "통합코드 그룹 ID (예: GRP_SYS_01, GRP_COMM_CD)", required = false)
@Schema(example = "GRP_001")
private String groupCode;
@McpParameter(description = "코드명 검색 키워드 (예: 사용, 상태)", required = false)
@McpToolParam(description = "코드명 검색 키워드 (예: 사용, 상태)", required = false)
private String codeName;
@McpParameter(description = "사용여부 (예: Y, N)", required = false)
@McpToolParam(description = "사용여부 (예: Y, N)", required = false)
@Schema(example = "Y")
private String useYn;
}

View File

@@ -1,7 +1,11 @@
package io.shinhanlife.dap.mcc.biz.cmm.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import org.springframework.ai.mcp.annotation.McpToolParam;
import com.fasterxml.jackson.annotation.JsonInclude;
import io.shinhanlife.dap.lib.annotation.McpParameter;
import lombok.Data;
/**
@@ -21,12 +25,16 @@ import lombok.Data;
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class MetaTableRequest {
@McpParameter(description = "테이블 물리명 키워드 (예: TB_CUST_BAS, TB_CONT)", required = false)
@McpToolParam(description = "테이블 물리명 키워드 (예: TB_CUST_BAS, TB_CONT)", required = false)
@Schema(example = "TB_USER")
private String tableName;
@McpParameter(description = "테이블 논리명(한글) 키워드 (예: 고객기본, 계약)", required = false)
@McpToolParam(description = "테이블 논리명(한글) 키워드 (예: 고객기본, 계약)", required = false)
@Schema(example = "고객기본")
private String tableLogicalName;
@McpParameter(description = "스키마/소유자명 (예: DAPADM, SHLOWN)", required = false)
@McpToolParam(description = "스키마/소유자명 (예: DAPADM, SHLOWN)", required = false)
@Schema(example = "DAPADM")
private String owner;
}

View File

@@ -1,5 +1,7 @@
package io.shinhanlife.dap.mcc.biz.cmm.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
@@ -20,5 +22,6 @@ import lombok.Data;
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class SampleStringRequest {
@Schema(example = "test query")
private String query;
}

View File

@@ -1,5 +1,7 @@
package io.shinhanlife.dap.mcc.biz.cmm.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
@@ -31,5 +33,6 @@ public class TemplateDownloadRequest {
/**
* 다운로드할 템플릿의 종류 ID (예: CUSTOMER_EXCEL, PRODUCT_PDF 등)
*/
@Schema(example = "TPL_001")
private String templateId;
}

View File

@@ -1,11 +1,14 @@
package io.shinhanlife.dap.mcc.biz.cmm.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import org.springframework.ai.mcp.annotation.McpToolParam;
import lombok.Builder;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import io.shinhanlife.dap.lib.annotation.McpParameter;
import io.shinhanlife.dap.lib.annotation.McpValidation;
import lombok.Getter;
import lombok.Setter;
@@ -30,11 +33,12 @@ import lombok.Setter;
@AllArgsConstructor
public class VacationRegisterRequest {
@McpParameter(description = "연차를 등록할 사원 번호", required = true)
@McpValidation(pattern = "\\S")
@McpToolParam(description = "연차를 등록할 사원 번호", required = true)
@Schema(pattern = "\\S", example = "EMP12345")
private String employeeId;
@McpParameter(description = "휴가 일자 (YYYY-MM-DD 형식)", required = true)
@McpValidation(pattern = "^\\d{4}-\\d{2}-\\d{2}$")
@McpToolParam(description = "휴가 일자 (YYYY-MM-DD 형식)", required = true)
@Schema(pattern = "^\\d{4}-\\d{2}-\\d{2}$", example = "2026-08-05")
private String date;
}

View File

@@ -1,31 +1,12 @@
package io.shinhanlife.dap.mcc.biz.cmm.usecase;
/**
* @package io.shinhanlife.dap.mcc.biz.cmm.usecase
* @className BalanceUseCase
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import io.shinhanlife.dap.lib.annotation.McpTool;
import io.shinhanlife.dap.lib.annotation.McpFunction;
import org.springframework.ai.mcp.annotation.McpTool;
import io.shinhanlife.dap.lib.annotation.ToolHint;
import io.shinhanlife.dap.mcc.biz.cmm.dto.*;
@McpTool(routingType = "MCI", categoryKey = "cmm")
public interface BalanceUseCase {
@McpFunction(register = false, displayName = "balance", name = "oth.cmm.balance.inquiry",
description = "고객의 계좌 잔액을 조회합니다.",
prompt = "고객 계좌 잔액을 조회해줘.",
mappingId = "ACC_001"
)
@McpTool(name = "oth.cmm.balance.inquiry", title = "잔고 조회", description = "고객의 계좌 잔액을 조회합니다.")
@ToolHint(register = false, mappingId = "ACC_001")
Object execute(BalanceRequest req);
}

View File

@@ -1,32 +1,13 @@
package io.shinhanlife.dap.mcc.biz.cmm.usecase;
/**
* @package io.shinhanlife.dap.mcc.biz.cmm.usecase
* @className BillingProcessUseCase
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import io.shinhanlife.dap.lib.annotation.McpTool;
import io.shinhanlife.dap.lib.annotation.McpFunction;
import org.springframework.ai.mcp.annotation.McpTool;
import io.shinhanlife.dap.lib.annotation.ToolHint;
import io.shinhanlife.dap.mcc.biz.cmm.dto.*;
@McpTool(
routingType = "MCI",
categoryKey = "cmm"
)
public interface BillingProcessUseCase {
Object getStatus(BillingStatusRequest req);
@McpFunction(register = false, displayName = "process 툴", name = "oth.cmm.billing.process", description = "청구 처리", prompt = "현재 접수된 청구건에 대한 심사 처리를 진행해.", mappingId = "BILL_002")
@McpTool(name = "oth.cmm.billing.process", title = "청구 프로세스 툴", description = "청구 처리")
@ToolHint(register = false, mappingId = "BILL_002")
Object processBilling(BillingProcessRequest data);
}

View File

@@ -1,32 +1,14 @@
package io.shinhanlife.dap.mcc.biz.cmm.usecase;
/**
* @package io.shinhanlife.dap.mcc.biz.cmm.usecase
* @className BondIssueUseCase
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import io.shinhanlife.dap.lib.annotation.McpTool;
import io.shinhanlife.dap.lib.annotation.McpFunction;
import org.springframework.ai.mcp.annotation.McpTool;
import io.shinhanlife.dap.lib.annotation.ToolHint;
import io.shinhanlife.dap.mcc.biz.cmm.dto.*;
@McpTool(
routingType = "EAI",
categoryKey = "cmm"
)
public interface BondIssueUseCase {
Object check(BondCheckRequest req);
@McpFunction(displayName = "issue 툴", name = "oth.cmm.bond.issue", register = false, description = "권 발행 테스트1", prompt = "디지털 증권 발행 프로세스를 실행해.", mappingId = "BOND_002")
@McpTool(name = "oth.cmm.bond.issue", title = "권 발행 ", description = "증권 발행 테스트1")
@ToolHint(register = false, mappingId = "BOND_002")
Object issue(BondIssueRequest data);
}

View File

@@ -1,42 +1,16 @@
package io.shinhanlife.dap.mcc.biz.cmm.usecase;
/**
* @package io.shinhanlife.dap.mcc.biz.cmm.usecase
* @className ClaimSearchSchemaSampleUseCase
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import org.springframework.ai.mcp.annotation.McpTool;
import io.shinhanlife.dap.lib.annotation.ToolHint;
import io.shinhanlife.dap.lib.annotation.McpFunction;
import io.shinhanlife.dap.lib.annotation.McpTool;
import io.shinhanlife.dap.mcc.biz.cmm.dto.ClaimSearchRequest;
import io.shinhanlife.dap.mcc.biz.cmm.dto.ClaimSearchResponse;
@McpTool(
routingType = "DIRECT",
categoryKey = "cmm"
)
public interface ClaimSearchSchemaSampleUseCase {
@McpFunction(
register = false,
displayName = "Claim search JSON Schema sample",
name = "oth.cmm.claim.search",
description = "Claim search Tool sample using input and output JSON Schema resources.",
prompt = "Search an insurance claim by claim number or contract number.",
inputSchemaResource = "classpath:tool-schemas/cmm/claim-search-resource-input-schema.json",
outputSchemaResource = "classpath:tool-schemas/cmm/claim-search-resource-output-schema.json",
readOnlyHint = true,
idempotentHint = true
)
@McpTool(name = "oth.cmm.claim.search", title = "청구 조회 스키마 샘플", description = "Claim search Tool sample using input and output JSON Schema resources.", annotations = @McpTool.McpAnnotations(readOnlyHint = true, idempotentHint = true))
@ToolHint(register = false,
inputSchemaResource = "classpath:mcp/schema/claim-search-resource-input-schema.json",
outputSchemaResource = "classpath:mcp/schema/claim-search-resource-output-schema.json")
ClaimSearchResponse search(ClaimSearchRequest request);
}

View File

@@ -1,35 +1,18 @@
package io.shinhanlife.dap.mcc.biz.cmm.usecase;
/**
* @package io.shinhanlife.dap.mcc.biz.cmm.usecase
* @className CommonUtilityUseCase
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import io.shinhanlife.dap.lib.annotation.McpTool;
import io.shinhanlife.dap.lib.annotation.McpFunction;
import org.springframework.ai.mcp.annotation.McpTool;
import io.shinhanlife.dap.lib.annotation.ToolHint;
import io.shinhanlife.dap.mcc.biz.cmm.dto.*;
@McpTool(
routingType = "HTTP",
categoryKey = "cmm"
)
public interface CommonUtilityUseCase {
Object registerVacation(VacationRegisterRequest req);
@McpFunction(register = false, displayName = "get_leave_count 툴", name = "oth.cmm.leave.count", description = "연차 갯수 조회", prompt = "현재 사용 가능한 남은 연차 일수를 알려줘.", mappingId = "HR_VAC_02")
@McpTool(name = "oth.cmm.leave.count", title = "공통 유틸리티 툴", description = "연차 갯수 조회")
@ToolHint(register = false, mappingId = "HR_VAC_02")
Object getLeaveCount(LeaveCountRequest data);
@McpFunction(register = false, displayName = "secret_tool 툴", name = "oth.cmm.secret.execute", description = "비공개 툴 테스트", prompt = "숨겨진 툴 강제 호출", mappingId = "SECRET_001", visible = false)
@McpTool(name = "oth.cmm.secret.execute", title = "공통 유틸리티 툴", description = "비공개 툴 테스트")
@ToolHint(register = false, mappingId = "SECRET_001")
Object secretTool(LeaveCountRequest data);
}

View File

@@ -1,32 +1,14 @@
package io.shinhanlife.dap.mcc.biz.cmm.usecase;
/**
* @package io.shinhanlife.dap.mcc.biz.cmm.usecase
* @className ContractInquiryUseCase
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import io.shinhanlife.dap.lib.annotation.McpTool;
import io.shinhanlife.dap.lib.annotation.McpFunction;
import org.springframework.ai.mcp.annotation.McpTool;
import io.shinhanlife.dap.lib.annotation.ToolHint;
import io.shinhanlife.dap.mcc.biz.cmm.dto.*;
@McpTool(
routingType = "HTTP",
categoryKey = "cmm"
)
public interface ContractInquiryUseCase {
Object getStatus(ContractStatusRequest req);
@McpFunction(register = false, displayName = "contract_detail 툴", name = "oth.cmm.contract.detail", description = "계약상세 조회", prompt = "김신한 고객의 계약 상세 내역을 알려줘.", mappingId = "CNTR_002")
@McpTool(name = "oth.cmm.contract.detail", title = "계약 상세조회", description = "계약상세 조회")
@ToolHint(register = false, mappingId = "CNTR_002")
Object getDetail(ContractDetailRequest data);
}

View File

@@ -1,32 +1,14 @@
package io.shinhanlife.dap.mcc.biz.cmm.usecase;
/**
* @package io.shinhanlife.dap.mcc.biz.cmm.usecase
* @className CustomerInfoUseCase
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import io.shinhanlife.dap.lib.annotation.McpTool;
import io.shinhanlife.dap.lib.annotation.McpFunction;
import org.springframework.ai.mcp.annotation.McpTool;
import io.shinhanlife.dap.lib.annotation.ToolHint;
import io.shinhanlife.dap.mcc.biz.cmm.dto.*;
@McpTool(
routingType = "TCP",
categoryKey = "cmm"
)
public interface CustomerInfoUseCase {
Object getGrade(CustomerGradeRequest req);
@McpFunction(register = false, displayName = "detail 툴", name = "oth.cmm.customer.detail", description = "고객상세 정보 조회", prompt = "이 고객의 상세 기본정보(주소, 연락처 등)를 알려줘.", mappingId = "CRM_002")
@McpTool(name = "oth.cmm.customer.detail", title = "고객 상세조회", description = "고객상세 정보 조회")
@ToolHint(register = false, mappingId = "CRM_002")
Object getDetail(CustomerDetailRequest data);
}

View File

@@ -1,7 +1,8 @@
package io.shinhanlife.dap.mcc.biz.cmm.usecase;
import io.shinhanlife.dap.lib.annotation.McpFunction;
import io.shinhanlife.dap.lib.annotation.McpTool;
import org.springframework.ai.mcp.annotation.McpTool;
import io.shinhanlife.dap.lib.annotation.ToolHint;
import io.shinhanlife.dap.mcc.biz.cmm.dto.MetaCommonCodeRequest;
/**
@@ -18,20 +19,8 @@ import io.shinhanlife.dap.mcc.biz.cmm.dto.MetaCommonCodeRequest;
*
* </pre>
*/
@McpTool(
routingType = "MCI",
categoryKey = "cmm"
)
public interface MetaCommonCodeUseCase {
@McpFunction(
displayName = "메타 통합코드 조회 툴",
name = "oth.cmm.common-code.lookup",
description = "메타 통합코드 목록을 조회해줘",
prompt = "메타 통합코드 목록을 조회해줘",
mappingId = "CLCNNB00001",
register = false,
requiresApproval = false,
openWorldHint = true
)
@McpTool(name = "oth.cmm.common-code.lookup", title = "메타 공통코드 조회 툴", description = "메타 통합코드 목록을 조회해줘", annotations = @McpTool.McpAnnotations(openWorldHint = true))
@ToolHint(register = false, requiresApproval = false, mappingId = "CLCNNB00001")
Object execute(MetaCommonCodeRequest req);
}

View File

@@ -1,7 +1,8 @@
package io.shinhanlife.dap.mcc.biz.cmm.usecase;
import io.shinhanlife.dap.lib.annotation.McpFunction;
import io.shinhanlife.dap.lib.annotation.McpTool;
import org.springframework.ai.mcp.annotation.McpTool;
import io.shinhanlife.dap.lib.annotation.ToolHint;
import io.shinhanlife.dap.mcc.biz.cmm.dto.MetaTableRequest;
/**
@@ -18,20 +19,8 @@ import io.shinhanlife.dap.mcc.biz.cmm.dto.MetaTableRequest;
*
* </pre>
*/
@McpTool(
routingType = "MCI",
categoryKey = "cmm"
)
public interface MetaTableUseCase {
@McpFunction(
displayName = "메타 테이블 조회 툴",
name = "oth.cmm.meta.table",
description = "메타 테이블 정보 목록을 조회해줘",
prompt = "메타 테이블 정보 목록을 조회해줘",
mappingId = "CLCNNB00001",
register = false,
requiresApproval = false,
openWorldHint = true
)
@McpTool(name = "oth.cmm.meta.table", title = "메타 테이블 조회 툴", description = "메타 테이블 정보 목록을 조회해줘", annotations = @McpTool.McpAnnotations(openWorldHint = true))
@ToolHint(register = false, requiresApproval = false, mappingId = "CLCNNB00001")
Object execute(MetaTableRequest req);
}

View File

@@ -1,37 +1,14 @@
package io.shinhanlife.dap.mcc.biz.cmm.usecase;
/**
* @package io.shinhanlife.dap.mcc.biz.cmm.usecase
* @className TemplateUtilityUseCase
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import io.shinhanlife.dap.lib.annotation.McpTool;
import io.shinhanlife.dap.lib.annotation.McpFunction;
import org.springframework.ai.mcp.annotation.McpTool;
import io.shinhanlife.dap.lib.annotation.ToolHint;
import io.shinhanlife.dap.mcc.biz.cmm.dto.*;
import java.util.Map;
@McpTool(
routingType = "HTTP",
categoryKey = "cmm"
)
public interface TemplateUtilityUseCase {
@McpFunction(
displayName = "템플릿 유틸리티",
name = "oth.cmm.template.url",
description = "요청한 템플릿(엑셀, 워드 등) 파일(양식)을 다운로드 받을 수 있는 시스템 URL을 반환합니다. AI는 이 URL을 사용자에게 마크다운 링크 형태로 제공해야 합니다.",
prompt = "요청하신 템플릿 양식 파일 다운로드 URL은 다음과 같습니다. 클릭하여 다운로드하세요:"
)
@McpTool(name = "oth.cmm.template.url", title = "템플릿 유틸리티 툴", description = "요청한 템플릿(엑셀, 워드 등) 파일(양식)을 다운로드 받을 수 있는 시스템 URL을 반환합니다. AI는 이 URL을 사용자에게 마크다운 링크 형태로 제공해야 합니다.")
@ToolHint
Map<String, Object> getTemplateFileUrl(TemplateDownloadRequest req);
}

View File

@@ -1,21 +1,5 @@
package io.shinhanlife.dap.mcc.biz.cmm.usecase.impl;
/**
* @package io.shinhanlife.dap.mcc.biz.cmm.usecase.impl
* @className ClaimSearchSchemaSampleUseCaseImpl
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import io.shinhanlife.dap.mcc.biz.cmm.dto.ClaimSearchRequest;
import io.shinhanlife.dap.mcc.biz.cmm.dto.ClaimSearchResponse;
import io.shinhanlife.dap.mcc.biz.cmm.usecase.ClaimSearchSchemaSampleUseCase;

View File

@@ -1,21 +1,5 @@
package io.shinhanlife.dap.mcc.biz.oth.converter;
/**
* @package io.shinhanlife.dap.mcc.biz.oth.converter
* @className Onnba3011Converter
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import io.shinhanlife.dap.mcc.biz.oth.dto.Onnba3011Request;
import io.shinhanlife.dap.mcc.infra.itrf.mci.cfp.a.io.CLCNNB00001_I;
import org.mapstruct.Mapper;

View File

@@ -1,21 +1,5 @@
package io.shinhanlife.dap.mcc.biz.oth.dto;
/**
* @package io.shinhanlife.dap.mcc.biz.oth.dto
* @className Onnba3011Request
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import lombok.Data;

View File

@@ -1,26 +1,11 @@
package io.shinhanlife.dap.mcc.biz.oth.usecase;
/**
* @package io.shinhanlife.dap.mcc.biz.oth.usecase
* @className Onnba3011UseCase
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import io.shinhanlife.dap.lib.annotation.McpTool;
import io.shinhanlife.dap.lib.annotation.McpFunction;
import org.springframework.ai.mcp.annotation.McpTool;
import io.shinhanlife.dap.lib.annotation.ToolHint;
import io.shinhanlife.dap.mcc.biz.oth.dto.*;
@McpTool(routingType = "MCI", categoryKey = "oth")
public interface Onnba3011UseCase {
@McpTool(name = "oth.sol.request.detail", title = "보종By가입설계한도계산조회 툴")
@ToolHint(register = false)
Object callOnnba3011(Onnba3011Request req);
}

View File

@@ -35,9 +35,7 @@ public class Onnba3011UseCaseImpl implements Onnba3011UseCase {
private final Onnba3011Converter onnba3011Converter;
/**
* AI Agent가 호출하게 될 메서드입니다.
* @McpFunction 어노테이션 하나로 AI 도구로 자동 노출 및 라우팅됩니다.
*/
@Override
public Object callOnnba3011(Onnba3011Request req) {
log.info("[MCI Tool] 보종By가입설계한도계산조회 요청 수신.");

View File

@@ -1,19 +1,19 @@
package io.shinhanlife.dap.mcc.biz.smp.dto;
/**
* @package io.shinhanlife.dap.mcc.biz.smp.dto
* @className DailyQuoteRequest
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
public record DailyQuoteRequest(String category) {}
import io.swagger.v3.oas.annotations.media.Schema;
import org.springframework.ai.mcp.annotation.McpToolParam;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class DailyQuoteRequest {
@McpToolParam(description = "카테고리 (예: 속담 등)", required = false)
@Schema(example = "속담")
private String category;
}

View File

@@ -1,19 +1,3 @@
package io.shinhanlife.dap.mcc.biz.smp.dto;
/**
* @package io.shinhanlife.dap.mcc.biz.smp.dto
* @className DailyQuoteResponse
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
public record DailyQuoteResponse(String quote, String author) {}

View File

@@ -1,19 +1,18 @@
package io.shinhanlife.dap.mcc.biz.smp.dto;
/**
* @package io.shinhanlife.dap.mcc.biz.smp.dto
* @className ExchangeRateRequest
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
public record ExchangeRateRequest(String currencyCode) {}
import io.swagger.v3.oas.annotations.media.Schema;
import org.springframework.ai.mcp.annotation.McpToolParam;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ExchangeRateRequest {
@McpToolParam(description = "환율 코드 (예: USD 등)", required = false)
@Schema(example = "USD")
private String currencyCode;
}

View File

@@ -1,19 +1,3 @@
package io.shinhanlife.dap.mcc.biz.smp.dto;
/**
* @package io.shinhanlife.dap.mcc.biz.smp.dto
* @className ExchangeRateResponse
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
public record ExchangeRateResponse(String baseCurrency, String targetCurrency, double rate) {}

Some files were not shown because too many files have changed in this diff Show More