diff --git a/README.md b/README.md index 3bed7c26..bfb0acd7 100644 --- a/README.md +++ b/README.md @@ -144,7 +144,7 @@ UI에서 사용하는 Tailwind CSS와 Chart.js는 `dap-gateway/src/main/resource "jsonrpc": "2.0", "method": "tools/call", "params": { - "name": "oth_cmm_claim_search", + "name": "cmm_claim_schema_search", "arguments": { "claimNo": "CLM2026070100120" } @@ -171,7 +171,7 @@ Tool 함수명은 아래 4단계 규칙을 사용합니다. ```text pod_domain_service_action -예: oth_cmm_claim_search +예: cmm_claim_schema_search ``` - `pod`: Tool Pod 식별자 (`oth`, `sms` 등) @@ -266,6 +266,6 @@ https://dev-ichmci.shinhanlife.co.kr/ntl_mci/clc_rcv Tool 관련 공통 기능은 `dap-was-*` 모듈명만 기준으로 동작합니다. - `validateMcpToolNames`는 `dap-was-*` Tool Pod를 탐색하여 이름 규칙과 전역 중복을 검사합니다. -- Tool Scaffold는 `dap-was-sms`처럼 선택한 Pod 이름을 Tool 함수명 첫 번째 구간에 반영합니다. 예: `sms_cmm_notification_send` +- Tool Scaffold는 Pod 이름을 Tool 함수명에 포함하지 않습니다. 함수명은 `도메인_비즈니스_행위` 형식입니다. 예: `cmm_notification_send` - Pod Scaffold와 Gateway Scaffold 화면/API의 모듈 목록도 `dap-was-*` 명칭으로 통일되어 있습니다. - Tool Source Update 기능은 `dap-was-*` 아래의 `*UseCase.java`를 검색합니다. \ No newline at end of file diff --git a/dap-gateway/src/main/java/io/shinhanlife/dap/mcg/presentation/ScaffoldingController.java b/dap-gateway/src/main/java/io/shinhanlife/dap/mcg/presentation/ScaffoldingController.java index c905bcf0..97816ea8 100644 --- a/dap-gateway/src/main/java/io/shinhanlife/dap/mcg/presentation/ScaffoldingController.java +++ b/dap-gateway/src/main/java/io/shinhanlife/dap/mcg/presentation/ScaffoldingController.java @@ -56,6 +56,8 @@ public class ScaffoldingController { try { String baseName = req.get("baseName"); String interfaceId = req.get("interfaceId"); + String title = req.get("title"); + if (title == null || title.isBlank()) title = baseName; String description = req.get("description"); String group = req.getOrDefault("categoryKey", req.getOrDefault("group", "COMMON")); String routingType = req.getOrDefault("routingType", "HTTP"); @@ -66,6 +68,7 @@ public class ScaffoldingController { if (date == null || date.trim().isEmpty()) date = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy.MM.dd")); boolean register = Boolean.parseBoolean(req.getOrDefault("register", "true")); String clientSystemCode = req.get("clientSystemCode"); + String httpApiName = req.getOrDefault("httpApiName", "sample"); String inputSchemaResource = req.get("inputSchemaResource"); String outputSchemaResource = req.get("outputSchemaResource"); List inputFields = parseFields(req.get("inputFields")); @@ -74,7 +77,7 @@ public class ScaffoldingController { inputFields = List.of(new ToolScaffolder.FieldDefinition("query", "String", "Search query", "example", false)); } - return ToolScaffolder.scaffold(baseName, interfaceId, description, group, routingType, moduleName, author, date, register, clientSystemCode, inputSchemaResource, outputSchemaResource, inputFields, outputFields); + return ToolScaffolder.scaffold(baseName, interfaceId, title, description, group, routingType, moduleName, author, date, register, clientSystemCode, inputSchemaResource, outputSchemaResource, inputFields, outputFields, httpApiName); } catch (Exception e) { return "오류 발생: " + e.getMessage(); } diff --git a/dap-gateway/src/main/resources/static/admin/scaffold.html b/dap-gateway/src/main/resources/static/admin/scaffold.html index 976fedba..fe1a622f 100644 --- a/dap-gateway/src/main/resources/static/admin/scaffold.html +++ b/dap-gateway/src/main/resources/static/admin/scaffold.html @@ -455,11 +455,17 @@ - -
- - -
Critical for AI agent intent matching. Be descriptive.
+
+
+ + +
Short, human-facing name shown in tool lists and the Portal.
+
+
+ + +
LLM call guidance: purpose, when to use it, required conditions, and exclusions.
+
@@ -539,6 +545,11 @@
+ + +
HTTP only. Must match glow.communication.http.api-list[].name; URL and method are read from Glow YAML.
+
+
Required for MCI Protocol (4 letters).
diff --git a/dap-gateway/src/test/java/io/shinhanlife/dap/biz/mcp/gateway/tool/large/PaginationRequestValidatorTest.java b/dap-gateway/src/test/java/io/shinhanlife/dap/biz/mcp/gateway/tool/large/PaginationRequestValidatorTest.java index 65deacfa..296189b2 100644 --- a/dap-gateway/src/test/java/io/shinhanlife/dap/biz/mcp/gateway/tool/large/PaginationRequestValidatorTest.java +++ b/dap-gateway/src/test/java/io/shinhanlife/dap/biz/mcp/gateway/tool/large/PaginationRequestValidatorTest.java @@ -18,6 +18,8 @@ package io.shinhanlife.dap.mcg.tool.large; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import io.shinhanlife.dap.mcg.config.McpGatewayProperties; +import io.shinhanlife.dap.mcc.dto.ToolMetadata; +import java.util.Map; import io.shinhanlife.dap.mcg.resilience.ToolExecutionException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -42,7 +44,7 @@ class PaginationRequestValidatorTest { @Test void shouldSetDefaultPageSizeIfMissing() { ObjectNode arguments = json.createObjectNode(); - ObjectNode normalized = validator.normalize(arguments); + ObjectNode normalized = validator.normalize(paginationTool(), arguments); assertEquals(100, normalized.get("pageSize").asInt()); } @@ -51,7 +53,7 @@ class PaginationRequestValidatorTest { void shouldKeepValidPageSize() { ObjectNode arguments = json.createObjectNode(); arguments.put("pageSize", 200); - ObjectNode normalized = validator.normalize(arguments); + ObjectNode normalized = validator.normalize(paginationTool(), arguments); assertEquals(200, normalized.get("pageSize").asInt()); } @@ -62,7 +64,7 @@ class PaginationRequestValidatorTest { arguments.put("pageSize", 600); ToolExecutionException exception = assertThrows(ToolExecutionException.class, () -> { - validator.normalize(arguments); + validator.normalize(paginationTool(), arguments); }); assertTrue(exception.getMessage().contains("PAGE_SIZE_EXCEEDED")); @@ -74,9 +76,14 @@ class PaginationRequestValidatorTest { arguments.put("cursor", "a".repeat(3000)); ToolExecutionException exception = assertThrows(ToolExecutionException.class, () -> { - validator.normalize(arguments); + validator.normalize(paginationTool(), arguments); }); assertTrue(exception.getMessage().contains("INVALID_CURSOR")); } + private ToolMetadata paginationTool() { + return ToolMetadata.builder() + .parametersSchema(Map.of("properties", Map.of("pageSize", Map.of()))) + .build(); + } } diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/config/AxhubHttpConfiguration.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/config/AxhubHttpConfiguration.java index d124b376..2566ed5f 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/config/AxhubHttpConfiguration.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/config/AxhubHttpConfiguration.java @@ -1,9 +1,11 @@ package io.shinhanlife.dap.lib.config; import io.shinhanlife.glow.communication.module.http.component.GlowHttpComponent; +import java.net.http.HttpClient; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.client.JdkClientHttpRequestFactory; import org.springframework.web.client.RestClient; /** @@ -16,6 +18,12 @@ public class AxhubHttpConfiguration { @Bean @ConditionalOnMissingBean(GlowHttpComponent.class) public GlowHttpComponent glowHttpComponent(RestClient.Builder restClientBuilder) { - return new GlowHttpComponent(restClientBuilder); + // WireMock and legacy internal endpoints can only support HTTP/1.1. + // Avoid JDK HTTP/2 negotiation that may cause RST_STREAM responses. + HttpClient http11Client = HttpClient.newBuilder() + .version(HttpClient.Version.HTTP_1_1) + .build(); + JdkClientHttpRequestFactory requestFactory = new JdkClientHttpRequestFactory(http11Client); + return new GlowHttpComponent(restClientBuilder.requestFactory(requestFactory)); } } \ No newline at end of file diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/integration/http/component/AxhubHttpComponent.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/integration/http/component/AxhubHttpComponent.java index 37323b7f..501151b4 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/integration/http/component/AxhubHttpComponent.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/integration/http/component/AxhubHttpComponent.java @@ -9,17 +9,16 @@ import io.shinhanlife.glow.communication.module.http.component.GlowHttpComponent import io.shinhanlife.glow.communication.module.http.dto.HttpBody; import io.shinhanlife.glow.communication.module.http.dto.HttpHeader; import io.shinhanlife.glow.communication.module.http.dto.HttpTransfer; -import java.util.Map; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Component; /** - * Tool Pod outbound HTTP component modelled after the ShinhanLife HTTP component. - * It resolves an API domain from configuration and delegates the assembled HttpTransfer to Glow. + * Tool Pod outbound HTTP component using the Glow HTTP client. + * Target URL, HTTP method, content type and Pod-to-Pod behaviour are resolved from + * {@code glow.communication.http.api-list}; business source code does not own endpoint values. */ @Slf4j @Component @@ -33,47 +32,74 @@ public class AxhubHttpComponent { private final GlowCommunicationProperties communicationProperties; private final AxhubHttpProperties properties; - public R call(AxhubHttpDomain domain, String uri, T inputDto, Class responseBodyClass) { - return call(domain, uri, inputDto, responseBodyClass, 0); + /** Calls the exact URL configured for the API name. */ + public R call(String apiName, T inputDto, Class responseBodyClass) { + return call(apiName, "", inputDto, responseBodyClass, 0); } + /** Calls the configured URL with an optional resource suffix. */ + public R call(String apiName, String uri, T inputDto, Class responseBodyClass) { + return call(apiName, uri, inputDto, responseBodyClass, 0); + } + + public R call(String apiName, String uri, T inputDto, Class responseBodyClass, int timeout) { + AxhubHttpProperties.ApiDefinition api = resolveApi(apiName); + return execute(api, uri, inputDto, responseBodyClass, timeout); + } + + /** Backward-compatible enum overload. */ + public R call(AxhubHttpDomain domain, String uri, T inputDto, Class responseBodyClass) { + return call(domain.getCode(), uri, inputDto, responseBodyClass, 0); + } + + /** Backward-compatible enum overload. */ public R call(AxhubHttpDomain domain, String uri, T inputDto, Class responseBodyClass, int timeout) { - if (uri == null || uri.isBlank()) { - throw new IllegalArgumentException("URI is required."); + return call(domain.getCode(), uri, inputDto, responseBodyClass, timeout); + } + + /** Calls the configured URL only when the target is marked as a business Pod. */ + public R callBizPod(String apiName, T inputDto, Class responseBodyClass) { + AxhubHttpProperties.ApiDefinition api = resolveApi(apiName); + if (!api.bizPod()) { + throw new IllegalArgumentException("Configured API is not a business Pod: " + apiName); } - AxhubHttpProperties.ApiDefinition api = resolveApi(domain); + return execute(api, "", inputDto, responseBodyClass, 0); + } + + /** Backward-compatible enum overload. */ + public R callBizPod(AxhubHttpDomain domain, String uri, T inputDto, Class responseBodyClass) { + AxhubHttpProperties.ApiDefinition api = resolveApi(domain.getCode()); + if (!api.bizPod()) { + throw new IllegalArgumentException("Configured API is not a business Pod: " + domain.getCode()); + } + return execute(api, uri, inputDto, responseBodyClass, 0); + } + + private R execute(AxhubHttpProperties.ApiDefinition api, String uri, T inputDto, + Class responseBodyClass, int timeout) { HttpHeader header = createHeader(api, timeout); - String requestUri = joinPath(api.path(), uri); + String requestUri = joinPath(api.url(), uri); HttpTransfer request = HttpTransfer.http() .header(header) .domain(api.domain()) .uri(requestUri) .method(api.method()) - .contentType(MediaType.APPLICATION_JSON) + .contentType(contentType(api)) .responseEntity(responseBodyClass) .body(inputDto) .build(); - log.info("[AxhubHttpComponent] Glow HTTP call. domain={}, method={}, uri={}", - domain.getCode(), api.method(), requestUri); + log.info("[AxhubHttpComponent] Glow HTTP call. apiName={}, method={}, uri={}", + api.name(), api.method(), requestUri); ResponseEntity response = http.sync(request); return convertResponse(response.getBody(), responseBodyClass); } - /** Convenience method for APIs configured as internal business Pods. */ - public R callBizPod(AxhubHttpDomain domain, String uri, T inputDto, Class responseBodyClass) { - AxhubHttpProperties.ApiDefinition api = resolveApi(domain); - if (!api.bizPod()) { - throw new IllegalArgumentException("Configured API is not a business Pod: " + domain.getCode()); - } - return call(domain, uri, inputDto, responseBodyClass); - } - - private AxhubHttpProperties.ApiDefinition resolveApi(AxhubHttpDomain domain) { + private AxhubHttpProperties.ApiDefinition resolveApi(String apiName) { return properties.getApiList().stream() - .filter(api -> domain.getCode().equals(api.name())) + .filter(api -> apiName != null && apiName.equals(api.name())) .findFirst() - .orElseThrow(() -> new IllegalArgumentException("No HTTP API configuration for domain: " + domain.getCode())); + .orElseThrow(() -> new IllegalArgumentException("No HTTP API configuration for name: " + apiName)); } private HttpHeader createHeader(AxhubHttpProperties.ApiDefinition api, int timeout) { @@ -98,6 +124,12 @@ public class AxhubHttpComponent { ? 0 : communicationProperties.getHttp().getReadTimeout(); } + private MediaType contentType(AxhubHttpProperties.ApiDefinition api) { + return api.contentType() == null || api.contentType().isBlank() + ? MediaType.APPLICATION_JSON + : MediaType.parseMediaType(api.contentType()); + } + private R convertResponse(HttpBody responseBody, Class responseBodyClass) { String content = responseBody == null ? null : responseBody.content(); if (responseBodyClass == String.class) { @@ -116,9 +148,12 @@ public class AxhubHttpComponent { } } - private String joinPath(String basePath, String uri) { - String left = basePath == null ? "" : basePath.replaceAll("/+$", ""); - String right = uri.startsWith("/") ? uri : "/" + uri; + private String joinPath(String configuredUrl, String suffix) { + String left = configuredUrl == null ? "" : configuredUrl.replaceAll("/+$", ""); + if (suffix == null || suffix.isBlank()) { + return left.isEmpty() ? "/" : left; + } + String right = suffix.startsWith("/") ? suffix : "/" + suffix; return left + right; } } \ No newline at end of file diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/integration/http/component/AxhubHttpProperties.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/integration/http/component/AxhubHttpProperties.java index f183fa05..b07835ed 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/integration/http/component/AxhubHttpProperties.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/integration/http/component/AxhubHttpProperties.java @@ -8,7 +8,12 @@ import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.http.HttpMethod; import org.springframework.stereotype.Component; -/** Domain-to-endpoint configuration for outbound Tool HTTP calls. */ +/** + * Glow HTTP target catalog. + * + *

Each target follows the ShinhanLife standard: name, domain, url, method, + * content-type, and biz-pod. Target-specific values belong in application-glow*.yml.

+ */ @Getter @Setter @Component @@ -17,6 +22,13 @@ public class AxhubHttpProperties { private List apiList = new ArrayList<>(); - public record ApiDefinition(String name, String domain, String path, HttpMethod method, boolean bizPod) { + public record ApiDefinition( + String name, + String domain, + String url, + HttpMethod method, + String contentType, + boolean bizPod + ) { } } \ No newline at end of file diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/util/ToolScaffolder.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/util/ToolScaffolder.java index 19d7ec8d..4b2c2284 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/util/ToolScaffolder.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/util/ToolScaffolder.java @@ -14,28 +14,28 @@ import java.util.Set; import java.util.Scanner; /** - * MCP Tool 코드를 자동 생성(Scaffolding)하는 유틸리티 클래스 + * MCP Tool 肄붾뱶瑜??먮룞 ?앹꽦(Scaffolding)?섎뒗 ?좏떥由ы떚 ?대옒?? * - * [실행 방법] - * 방법 1. IDE(IntelliJ 등)에서 직접 실행 (대화형 모드 추천 ⭐) - * - 이 클래스(ToolScaffolder.java)를 열고 main 메서드를 직접 실행(Run)합니다. - * - 콘솔 창에 뜨는 질문에 차례대로 값을 입력하기만 하면 파일이 생성됩니다. + * [?ㅽ뻾 諛⑸쾿] + * 諛⑸쾿 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] "[설명]" "[그룹]" "[통신방식]" "[모듈명]" + * 諛⑸쾿 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] "[?ㅻ챸]" "[洹몃9]" "[?듭떊諛⑹떇]" "[紐⑤뱢紐?" */ /** * @package io.shinhanlife.dap.lib.util * @className ToolScaffolder - * @description AX HUB 시스템 처리 클래스 + * @description AX HUB ?쒖뒪??泥섎━ ?대옒?? * @author 0986406 * @create 2026.09.01 *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
+ * ---------- 媛쒖젙?대젰 ----------
+ * ?섏젙??     ?섏젙??   ?섏젙?댁슜
  * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
+ * 2026.09.01  0986406    理쒖큹?앹꽦
  * 
  * 
*/ @@ -54,16 +54,17 @@ public class ToolScaffolder { 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 등): "); + String baseName = getOrAsk(args, 0, scanner, "1. ?앹꽦??Tool??湲곕낯 ?대쫫 (?? ExchangeRate) [?곷Ц PascalCase]: "); + String interfaceId = getOrAsk(args, 1, scanner, "2. ?덇굅??API ?명꽣?섏씠??ID (?? EXCH_001): "); + String title = getOrAsk(args, 2, scanner, "3. Tool title: "); + String description = getOrAsk(args, 3, scanner, "4. Tool description for LLM: "); + String group = getOrAsk(args, 4, scanner, "5. Tool category: "); if (group.isEmpty()) group = "COMMON"; - String routingType = getOrAsk(args, 4, scanner, "5. 통신 프로토콜 (예: HTTP, TCP, MCI, EAI): "); + String routingType = getOrAsk(args, 5, scanner, "6. Routing type (HTTP, TCP, MCI, EAI): "); if (routingType.trim().isEmpty()) { routingType = "HTTP"; } - String moduleName = getOrAsk(args, 5, scanner, "6. 코드를 생성할 모듈 (기본: dap-was-oth): "); + String moduleName = getOrAsk(args, 6, scanner, "7. Target module (default dap-was-oth): "); if (moduleName.trim().isEmpty()) { moduleName = "dap-was-oth"; } @@ -71,19 +72,19 @@ public class ToolScaffolder { String defaultAuthor = System.getProperty("user.name"); String defaultDate = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy.MM.dd")); - String author = getOrAsk(args, 6, scanner, "7. 작성자 (엔터 입력 시 '" + defaultAuthor + "'): "); + String author = getOrAsk(args, 7, scanner, "7. ?묒꽦??(?뷀꽣 ?낅젰 ??'" + defaultAuthor + "'): "); if (author.trim().isEmpty()) author = defaultAuthor; - String createDate = getOrAsk(args, 7, scanner, "8. 작성일 (엔터 입력 시 '" + defaultDate + "'): "); + String createDate = getOrAsk(args, 8, scanner, "8. ?묒꽦??(?뷀꽣 ?낅젰 ??'" + defaultDate + "'): "); if (createDate.trim().isEmpty()) createDate = defaultDate; - String useSchemaResourceStr = getOrAsk(args, 8, scanner, "9. input/output JSON Schema 파일 자동 생성 여부 (y/N): "); + String useSchemaResourceStr = getOrAsk(args, 9, scanner, "9. input/output JSON Schema ?뚯씪 ?먮룞 ?앹꽦 ?щ? (y/N): "); boolean useSchemaResource = "y".equalsIgnoreCase(useSchemaResourceStr.trim()); String schemaResourceDirectory = "classpath:tool-schemas/" + group.toLowerCase() + "/"; String inputSchemaResource = useSchemaResource ? schemaResourceDirectory + toKebabCase(baseName) + "-resource-input-schema.json" : null; String outputSchemaResource = useSchemaResource ? schemaResourceDirectory + toKebabCase(baseName) + "-resource-output-schema.json" : null; - String result = scaffold(baseName, interfaceId, description, group, routingType, moduleName, author, createDate, false, null, inputSchemaResource, outputSchemaResource); + String result = scaffold(baseName, interfaceId, title, description, group, routingType, moduleName, author, createDate, false, null, inputSchemaResource, outputSchemaResource, List.of(new FieldDefinition("query", "String", "Search query", "example", false)), List.of()); System.out.println(result); } @@ -106,7 +107,27 @@ public class ToolScaffolder { } public static String scaffold(String baseName, String interfaceId, String description, String group, String routingType, String moduleName, String author, String createDate, boolean register, String clientSystemCode, String inputSchemaResource, String outputSchemaResource, List inputFields, List outputFields) throws IOException { + return scaffold(baseName, interfaceId, description, description, group, routingType, moduleName, author, createDate, + register, clientSystemCode, inputSchemaResource, outputSchemaResource, inputFields, outputFields); + } + + /** + * Generates a Tool with a human-facing title and an LLM-facing description. + * Existing overloads keep their previous behavior by using the description as the title. + */ + public static String scaffold(String baseName, String interfaceId, String title, String description, String group, String routingType, String moduleName, String author, String createDate, boolean register, String clientSystemCode, String inputSchemaResource, String outputSchemaResource, List inputFields, List outputFields) throws IOException { + return scaffold(baseName, interfaceId, title, description, group, routingType, moduleName, author, createDate, + register, clientSystemCode, inputSchemaResource, outputSchemaResource, inputFields, outputFields, "sample"); + } + + /** + * Generates a Tool using an HTTP API name that is resolved from glow.communication.http.api-list. + */ + public static String scaffold(String baseName, String interfaceId, String title, String description, String group, String routingType, String moduleName, String author, String createDate, boolean register, String clientSystemCode, String inputSchemaResource, String outputSchemaResource, List inputFields, List outputFields, String httpApiName) throws IOException { baseName = toPascalCase(baseName); + title = title == null || title.isBlank() ? baseName : title.trim(); + description = description == null ? "" : description.trim(); + httpApiName = httpApiName == null || httpApiName.isBlank() ? "sample" : httpApiName.trim(); String envSourceDir = System.getenv("AXHUB_SOURCE_DIR"); Path rootDir = envSourceDir != null ? Paths.get(envSourceDir) : Paths.get("."); @@ -117,7 +138,7 @@ public class ToolScaffolder { 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 일 때만 생성) + // schema resource ?뚯씪 寃쎈줈 (useSchemaResource=true ???뚮쭔 ?앹꽦) boolean useSchemaResource = (inputSchemaResource != null && !inputSchemaResource.trim().isEmpty()) || (outputSchemaResource != null && !outputSchemaResource.trim().isEmpty()); String schemaBaseName = toKebabCase(baseName); String inputSchemaFileName = schemaBaseName + "-resource-input-schema.json"; @@ -128,6 +149,7 @@ public class ToolScaffolder { String bizPackage = BASE_PACKAGE + ".biz." + group.toLowerCase(); boolean isMci = "MCI".equalsIgnoreCase(routingType); + boolean isHttp = "HTTP".equalsIgnoreCase(routingType); String mciGroupPath = "infra/itrf/mci/" + group.toLowerCase(); String clientPrefixCap = ""; Path mciClientDir = null; @@ -140,6 +162,11 @@ public class ToolScaffolder { } Path mciIoDir = rootDir.resolve(Paths.get(moduleName, BASE_PACKAGE_PATH, mciGroupPath, "io")); + String httpApiPackage = toPackageSegment(httpApiName); + String httpApiClass = toPascalCase(httpApiName); + String httpGroupPath = "infra/itrf/http/" + httpApiPackage; + Path httpClientDir = rootDir.resolve(Paths.get(moduleName, BASE_PACKAGE_PATH, httpGroupPath)); + Path httpIoDir = httpClientDir.resolve("io"); Files.createDirectories(usecaseDir); Files.createDirectories(usecaseImplDir); @@ -149,6 +176,8 @@ public class ToolScaffolder { if (mciClientDir != null) { Files.createDirectories(mciClientDir); } + } else if (isHttp) { + Files.createDirectories(httpIoDir); } else { Files.createDirectories(legacyDtoDir); } @@ -166,25 +195,25 @@ public class ToolScaffolder { /** * @package %s.dto * @className %sRequest - * @description AX HUB 시스템 처리 클래스 + * @description AX HUB ?쒖뒪??泥섎━ ?대옒?? * @author %s * @create %s *
-             * ---------- 개정이력 ----------
-             * 수정일      수정자    수정내용
+             * ---------- 媛쒖젙?대젰 ----------
+             * ?섏젙??     ?섏젙??   ?섏젙?댁슜
              * ---------- -------- ---------------------------
-             * %s  %s    최초생성
+             * %s  %s    理쒖큹?앹꽦
              * 
              * 
*/ @Data @JsonInclude(JsonInclude.Include.NON_NULL) public class %sRequest { - @McpToolParam(description = "수신자 전화번호", required = true) + @McpToolParam(description = "?섏떊???꾪솕踰덊샇", required = true) -(?:\\\\d{3}|\\\\d{4})-\\\\d{4}$", examples = {"010-1234-5678"}) private String phoneNumber; - @McpToolParam(description = "전송할 메시지 내용", required = true) + @McpToolParam(description = "?꾩넚??硫붿떆吏€ ?댁슜", required = true) private String message; } """.formatted(bizPackage, bizPackage, baseName, author, createDate, createDate, author, baseName); @@ -194,7 +223,7 @@ public class ToolScaffolder { .replaceAll("(?m)^\\s*@McpToolParam\\([^\\r\\n]*\\)\\R", "") .replaceAll("(?m)^\\s*-\\(\\?:[^\\r\\n]*\\R", "") .replace("private String phoneNumber;", "@Schema(example = \"01012345678\")\n private String phoneNumber;") - .replace("private String message;", "@Schema(example = \"테스트 메시지입니다.\")\n private String message;"); + .replace("private String message;", "@Schema(example = \"?뚯뒪??硫붿떆吏€?낅땲??\")\n private String message;"); reqContent = dtoContent(bizPackage + ".dto", baseName + "Request", inputFields, author, createDate, true); Files.writeString(dtoDir.resolve(baseName + "Request.java"), reqContent); @@ -208,14 +237,14 @@ public class ToolScaffolder { /** * @package %s.dto * @className %sResponse - * @description AX HUB 시스템 처리 클래스 + * @description AX HUB ?쒖뒪??泥섎━ ?대옒?? * @author %s * @create %s *
-             * ---------- 개정이력 ----------
-             * 수정일      수정자    수정내용
+             * ---------- 媛쒖젙?대젰 ----------
+             * ?섏젙??     ?섏젙??   ?섏젙?댁슜
              * ---------- -------- ---------------------------
-             * %s  %s    최초생성
+             * %s  %s    理쒖큹?앹꽦
              * 
              * 
*/ @@ -254,14 +283,14 @@ public class ToolScaffolder { /** * @package %s.usecase * @className %sUseCase - * @description AX HUB 시스템 처리 클래스 + * @description AX HUB ?쒖뒪??泥섎━ ?대옒?? * @author %s * @create %s *
-             * ---------- 개정이력 ----------
-             * 수정일      수정자    수정내용
+             * ---------- 媛쒖젙?대젰 ----------
+             * ?섏젙??     ?섏젙??   ?섏젙?댁슜
              * ---------- -------- ---------------------------
-             * %s  %s    최초생성
+             * %s  %s    理쒖큹?앹꽦
              *
              * 
*/ @@ -277,7 +306,7 @@ public class ToolScaffolder { bizPackage, baseName, bizPackage, baseName, author, createDate, createDate, author, baseName, - toolName, description, description, + toolName, title, description, toolHintLine, baseName, baseName ); @@ -305,14 +334,14 @@ public class ToolScaffolder { /** * @package %s.usecase.impl * @className %sUseCaseImpl - * @description AX HUB 시스템 처리 클래스 + * @description AX HUB ?쒖뒪??泥섎━ ?대옒?? * @author %s * @create %s *
-                 * ---------- 개정이력 ----------
-                 * 수정일      수정자    수정내용
+                 * ---------- 媛쒖젙?대젰 ----------
+                 * ?섏젙??     ?섏젙??   ?섏젙?댁슜
                  * ---------- -------- ---------------------------
-                 * %s  %s    최초생성
+                 * %s  %s    理쒖큹?앹꽦
                  * 
                  * 
*/ @@ -326,9 +355,9 @@ public class ToolScaffolder { @Override public %sResponse execute(%sRequest req) { - log.info("[MCI Tool] {} 요청 수신.", "%s"); + log.info("[MCI Tool] {} ?붿껌 ?섏떊.", "%s"); try { - // MapStruct를 이용한 자동 매핑 (AI DTO -> MCI DTO) + // MapStruct瑜??댁슜???먮룞 留ㅽ븨 (AI DTO -> MCI DTO) %s_I mciReq = converter.toLegacyRequest(req); Transfer resTransfer = mci.callTo( @@ -344,7 +373,7 @@ public class ToolScaffolder { : "MCI call completed without a response body."); return response; } catch (Exception e) { - log.error("[MCI Tool] 연동 중 오류 발생: {}", e.getMessage(), e); + log.error("[MCI Tool] ?곕룞 以??ㅻ쪟 諛쒖깮: {}", e.getMessage(), e); %sResponse response = new %sResponse(); response.setResultCode("ERROR"); response.setResultMessage(e.getMessage() != null ? e.getMessage() : "Unknown error"); @@ -388,8 +417,8 @@ public class ToolScaffolder { .replace("response.setResultCode(\"SUCCESS\");", "if (resTransfer.getBody() != null) {\n response = converter.toResponse(resTransfer.getBody());\n }\n response.setResultCode(\"SUCCESS\");"); } else { - serviceImplContent = "HTTP".equalsIgnoreCase(routingType) - ? httpUseCaseImplContent(bizPackage, baseName, interfaceId, author, createDate) + serviceImplContent = isHttp + ? httpUseCaseImplContent(bizPackage, baseName, httpGroupPath.replace("/", "."), httpApiClass, author, createDate) : """ package %s.usecase.impl; @@ -405,14 +434,14 @@ public class ToolScaffolder { /** * @package %s.usecase.impl * @className %sUseCaseImpl - * @description AX HUB 시스템 처리 클래스 + * @description AX HUB ?쒖뒪??泥섎━ ?대옒?? * @author %s * @create %s *
-                 * ---------- 개정이력 ----------
-                 * 수정일      수정자    수정내용
+                 * ---------- 媛쒖젙?대젰 ----------
+                 * ?섏젙??     ?섏젙??   ?섏젙?댁슜
                  * ---------- -------- ---------------------------
-                 * %s  %s    최초생성
+                 * %s  %s    理쒖큹?앹꽦
                  * 
                  * 
*/ @@ -469,26 +498,26 @@ public class ToolScaffolder { /** * @package %s.%s.io * @className %s_I - * @description AX HUB 시스템 처리 클래스 + * @description AX HUB ?쒖뒪??泥섎━ ?대옒?? * @author %s * @create %s *
-                 * ---------- 개정이력 ----------
-                 * 수정일      수정자    수정내용
+                 * ---------- 媛쒖젙?대젰 ----------
+                 * ?섏젙??     ?섏젙??   ?섏젙?댁슜
                  * ---------- -------- ---------------------------
-                 * %s  %s    최초생성
+                 * %s  %s    理쒖큹?앹꽦
                  * 
                  * 
*/ @Data public class %s_I { /** - * EAI 시스템이 요구하는 수신자 번호 파라미터명 + * EAI ?쒖뒪?쒖씠 ?붽뎄?섎뒗 ?섏떊??踰덊샇 ?뚮씪誘명꽣紐? */ private String phone; /** - * EAI 시스템이 요구하는 메시지 내용 파라미터명 + * EAI ?쒖뒪?쒖씠 ?붽뎄?섎뒗 硫붿떆吏€ ?댁슜 ?뚮씪誘명꽣紐? */ private String content; } @@ -504,14 +533,14 @@ public class ToolScaffolder { /** * @package %s.%s.io * @className %s_O - * @description AX HUB 시스템 처리 클래스 + * @description AX HUB ?쒖뒪??泥섎━ ?대옒?? * @author %s * @create %s *
-                 * ---------- 개정이력 ----------
-                 * 수정일      수정자    수정내용
+                 * ---------- 媛쒖젙?대젰 ----------
+                 * ?섏젙??     ?섏젙??   ?섏젙?댁슜
                  * ---------- -------- ---------------------------
-                 * %s  %s    최초생성
+                 * %s  %s    理쒖큹?앹꽦
                  * 
                  * 
*/ @@ -537,14 +566,14 @@ public class ToolScaffolder { /** * @package %s.converter * @className %sConverter - * @description AX HUB 시스템 처리 클래스 + * @description AX HUB ?쒖뒪??泥섎━ ?대옒?? * @author %s * @create %s *
-                 * ---------- 개정이력 ----------
-                 * 수정일      수정자    수정내용
+                 * ---------- 媛쒖젙?대젰 ----------
+                 * ?섏젙??     ?섏젙??   ?섏젙?댁슜
                  * ---------- -------- ---------------------------
-                 * %s  %s    최초생성
+                 * %s  %s    理쒖큹?앹꽦
                  * 
                  * 
*/ @@ -598,14 +627,14 @@ public class ToolScaffolder { /** * @package %s.%s * @className Mci%sClient - * @description AX HUB 시스템 처리 클래스 + * @description AX HUB ?쒖뒪??泥섎━ ?대옒?? * @author %s * @create %s *
-                     * ---------- 개정이력 ----------
-                     * 수정일      수정자    수정내용
+                     * ---------- 媛쒖젙?대젰 ----------
+                     * ?섏젙??     ?섏젙??   ?섏젙?댁슜
                      * ---------- -------- ---------------------------
-                     * %s  %s    최초생성
+                     * %s  %s    理쒖큹?앹꽦
                      * 
                      * 
*/ @@ -626,6 +655,32 @@ public class ToolScaffolder { log.append("[MCI Client] ").append(mciClientDir.resolve("Mci" + clientPrefixCap + "Client.java")).append("\n"); } + } else if (isHttp) { + String httpPackage = BASE_PACKAGE + "." + httpGroupPath.replace("/", "."); + String httpRequestClass = baseName + "HttpRequest"; + String httpResponseClass = baseName + "HttpResponse"; + String httpClientClass = httpApiClass + "Client"; + + Files.writeString(httpIoDir.resolve(httpRequestClass + ".java"), + dtoContent(httpPackage + ".io", httpRequestClass, inputFields, author, createDate, true)); + Files.writeString(httpIoDir.resolve(httpResponseClass + ".java"), + dtoContent(httpPackage + ".io", httpResponseClass, outputFields, author, createDate, false)); + Files.writeString(httpClientDir.resolve(httpClientClass + ".java"), + httpClientContent(httpPackage, httpClientClass, httpApiName)); + Files.writeString(converterDir.resolve(baseName + "Converter.java"), + httpConverterContent(bizPackage, baseName, httpPackage)); + + log.append("\n=========================================\n"); + log.append(" Scaffolding Complete! (Routing: HTTP)\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("[HTTP Request IO] ").append(httpIoDir.resolve(httpRequestClass + ".java")).append("\n"); + log.append("[HTTP Response IO] ").append(httpIoDir.resolve(httpResponseClass + ".java")).append("\n"); + log.append("[HTTP Client] ").append(httpClientDir.resolve(httpClientClass + ".java")).append("\n"); + log.append("[HTTP Converter] ").append(converterDir.resolve(baseName + "Converter.java")).append("\n"); } else { String legacyReqContent = """ package %s.legacy; @@ -635,26 +690,26 @@ public class ToolScaffolder { /** * @package %s.legacy * @className %sLegacyRequest - * @description AX HUB 시스템 처리 클래스 + * @description AX HUB ?쒖뒪??泥섎━ ?대옒?? * @author %s * @create %s *
-                 * ---------- 개정이력 ----------
-                 * 수정일      수정자    수정내용
+                 * ---------- 媛쒖젙?대젰 ----------
+                 * ?섏젙??     ?섏젙??   ?섏젙?댁슜
                  * ---------- -------- ---------------------------
-                 * %s  %s    최초생성
+                 * %s  %s    理쒖큹?앹꽦
                  * 
                  * 
*/ @Data public class %sLegacyRequest { /** - * EAI 시스템이 요구하는 수신자 번호 파라미터명 + * EAI ?쒖뒪?쒖씠 ?붽뎄?섎뒗 ?섏떊??踰덊샇 ?뚮씪誘명꽣紐? */ private String phone; /** - * EAI 시스템이 요구하는 메시지 내용 파라미터명 + * EAI ?쒖뒪?쒖씠 ?붽뎄?섎뒗 硫붿떆吏€ ?댁슜 ?뚮씪誘명꽣紐? */ private String content; } @@ -670,14 +725,14 @@ public class ToolScaffolder { /** * @package %s.legacy * @className %sLegacyResponse - * @description AX HUB 시스템 처리 클래스 + * @description AX HUB ?쒖뒪??泥섎━ ?대옒?? * @author %s * @create %s *
-                 * ---------- 개정이력 ----------
-                 * 수정일      수정자    수정내용
+                 * ---------- 媛쒖젙?대젰 ----------
+                 * ?섏젙??     ?섏젙??   ?섏젙?댁슜
                  * ---------- -------- ---------------------------
-                 * %s  %s    최초생성
+                 * %s  %s    理쒖큹?앹꽦
                  * 
                  * 
*/ @@ -703,14 +758,14 @@ public class ToolScaffolder { /** * @package %s.converter * @className %sConverter - * @description AX HUB 시스템 처리 클래스 + * @description AX HUB ?쒖뒪??泥섎━ ?대옒?? * @author %s * @create %s *
-                 * ---------- 개정이력 ----------
-                 * 수정일      수정자    수정내용
+                 * ---------- 媛쒖젙?대젰 ----------
+                 * ?섏젙??     ?섏젙??   ?섏젙?댁슜
                  * ---------- -------- ---------------------------
-                 * %s  %s    최초생성
+                 * %s  %s    理쒖큹?앹꽦
                  * 
                  * 
*/ @@ -750,7 +805,7 @@ public class ToolScaffolder { 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 일 때) + // schema resource ?뚯씪 ?앹꽦 (useSchemaResource=true ???? if (useSchemaResource) { Files.createDirectories(schemaDir); String inputSchema = """ @@ -760,7 +815,7 @@ public class ToolScaffolder { "properties": { "TODO_FIELD": { "type": "string", - "description": "TODO: 파라미터 설명을 입력하세요." + "description": "TODO: ?뚮씪誘명꽣 ?ㅻ챸???낅젰?섏꽭??" } }, "required": [] @@ -773,12 +828,12 @@ public class ToolScaffolder { "properties": { "status": { "type": "string", - "description": "처리 결과 상태 (SUCCESS / FAILURE)", + "description": "泥섎━ 寃곌낵 ?곹깭 (SUCCESS / FAILURE)", "enum": ["SUCCESS", "FAILURE"] }, "message": { "type": "string", - "description": "처리 결과 메시지" + "description": "泥섎━ 寃곌낵 硫붿떆吏€" } }, "required": ["status"] @@ -790,18 +845,32 @@ public class ToolScaffolder { log.append("[Output Schema] ").append(schemaDir.resolve(outputSchemaFileName)).append("\n"); } - Path mockResponsePath = rootDir.resolve(Paths.get(moduleName, "src/main/resources/mock-responses", toolName + ".json")); - Files.createDirectories(mockResponsePath.getParent()); - Files.writeString(mockResponsePath, mockResponseContent(outputFields)); + String mockResponse = mockResponseContent(outputFields); + if (isHttp) { + Path moduleRoot = rootDir.resolve(moduleName).toAbsolutePath().normalize(); + Path projectRoot = moduleRoot.getParent(); + Path wireMockBodyPath = projectRoot.resolve(Paths.get("mci-mock", "__files", toolName + ".json")); + Path wireMockMappingPath = projectRoot.resolve(Paths.get("mci-mock", "mappings", toolName + ".json")); + Files.createDirectories(wireMockBodyPath.getParent()); + Files.createDirectories(wireMockMappingPath.getParent()); + Files.writeString(wireMockBodyPath, mockResponse); + Files.writeString(wireMockMappingPath, wireMockMappingContent(interfaceId, wireMockBodyPath.getFileName().toString())); + log.append("[WireMock Response] ").append(wireMockBodyPath).append("\n"); + log.append("[WireMock Mapping] ").append(wireMockMappingPath).append("\n"); + } else { + Path mockResponsePath = rootDir.resolve(Paths.get(moduleName, "src/main/resources/mock-responses", toolName + ".json")); + Files.createDirectories(mockResponsePath.getParent()); + Files.writeString(mockResponsePath, mockResponse); + log.append("[Mock Response] ").append(mockResponsePath).append("\n"); + } Path generatedTestDir = rootDir.resolve(Paths.get(moduleName, "src/test/java/io/shinhanlife/dap/mcc/biz", group.toLowerCase(), "usecase")); Files.createDirectories(generatedTestDir); Path generatedTestPath = generatedTestDir.resolve(baseName + "UseCaseTest.java"); Files.writeString(generatedTestPath, useCaseTestContent(bizPackage, baseName)); - log.append("[Mock Response] ").append(mockResponsePath).append("\\n"); log.append("[Unit Test] ").append(generatedTestPath).append("\\n"); log.append("[Test Command] .\\gradlew.bat :").append(moduleName.substring(moduleName.lastIndexOf(java.io.File.separator) + 1)).append(":test --tests \"*").append(baseName).append("UseCaseTest\"\\n"); - log.append("\n Tip: ").append(interfaceId).append(" 목업 데이터를 mock-responses.json에 추가하세요.\n"); + log.append("\n Tip: HTTP Tool?€ WireMock???ㅽ뻾?????앹꽦??mapping URL濡??몄텧???뺤씤?섏꽭??\n"); return log.toString(); } @@ -847,56 +916,110 @@ public class ToolScaffolder { """.formatted(BASE_PACKAGE, packageSuffix, className, fieldLines(fields)); } - private static String httpUseCaseImplContent(String bizPackage, String baseName, String interfaceId, String author, String createDate) { + private static String httpUseCaseImplContent(String bizPackage, String baseName, String httpPackage, + String httpApiClass, String author, String createDate) { + String httpRequestClass = baseName + "HttpRequest"; + String httpResponseClass = baseName + "HttpResponse"; + String httpClientClass = httpApiClass + "Client"; + String clientVariable = Character.toLowerCase(httpClientClass.charAt(0)) + httpClientClass.substring(1); return """ package %s.usecase.impl; import %s.converter.%sConverter; import %s.dto.%sRequest; import %s.dto.%sResponse; - import %s.legacy.%sLegacyRequest; - import %s.legacy.%sLegacyResponse; + import %s.%s.%s; + import %s.%s.io.%s; + import %s.%s.io.%s; import %s.usecase.%sUseCase; - import io.shinhanlife.dap.lib.integration.http.component.AxhubHttpComponent; - import io.shinhanlife.dap.lib.integration.http.component.AxhubHttpDomain; - import io.shinhanlife.dap.mcc.usecase.AbstractMcpToolUseCase; import lombok.RequiredArgsConstructor; - import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; - /** - * HTTP Tool implementation. Calls the Glow HTTP adapter through AxhubHttpComponent. - * Configure the target domain in AxhubHttpDomain and glow.communication.http.api-list before use. - */ - @Slf4j @Service @RequiredArgsConstructor - public class %sUseCaseImpl extends AbstractMcpToolUseCase implements %sUseCase { + public class %sUseCaseImpl implements %sUseCase { private final %sConverter converter; - private final AxhubHttpComponent http; + private final %s %s; @Override public %sResponse execute(%sRequest req) { - %sLegacyRequest legacyRequest = converter.toLegacyRequest(req); - %sLegacyResponse legacyResponse = http.call( - AxhubHttpDomain.SAMPLE, - "/%s", - legacyRequest, - %sLegacyResponse.class - ); + %s httpRequest = converter.toHttpRequest(req); + %s httpResponse = %s.call(httpRequest, %s.class); - %sResponse response = converter.toResponse(legacyResponse); + %sResponse response = converter.toResponse(httpResponse); response.setResultCode("SUCCESS"); response.setResultMessage("HTTP API call completed."); return response; } } """.formatted( - bizPackage, bizPackage, baseName, bizPackage, baseName, bizPackage, baseName, - bizPackage, baseName, bizPackage, baseName, bizPackage, baseName, - baseName, baseName, baseName, baseName, baseName, baseName, baseName, - interfaceId, baseName, baseName); + bizPackage, + bizPackage, baseName, + bizPackage, baseName, + bizPackage, baseName, + BASE_PACKAGE, httpPackage, httpClientClass, + BASE_PACKAGE, httpPackage, httpRequestClass, + BASE_PACKAGE, httpPackage, httpResponseClass, + bizPackage, baseName, + baseName, baseName, + baseName, httpClientClass, clientVariable, + baseName, baseName, + httpRequestClass, httpResponseClass, clientVariable, httpResponseClass, + baseName); + } + + private static String httpClientContent(String httpPackage, String clientClass, String apiName) { + return """ + package %s; + + import io.shinhanlife.dap.lib.integration.http.component.AxhubHttpComponent; + import lombok.RequiredArgsConstructor; + import org.springframework.stereotype.Component; + + @Component + @RequiredArgsConstructor + public class %s { + private static final String API_NAME = "%s"; + + private final AxhubHttpComponent http; + + public O call(I request, Class responseType) { + return http.call(API_NAME, request, responseType); + } + } + """.formatted(httpPackage, clientClass, apiName); + } + + private static String httpConverterContent(String bizPackage, String baseName, String httpPackage) { + return """ + package %s.converter; + + import %s.dto.%sRequest; + import %s.dto.%sResponse; + import %s.%s.io.%sHttpRequest; + import %s.%s.io.%sHttpResponse; + import org.mapstruct.Mapper; + import org.mapstruct.ReportingPolicy; + + @Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE) + public interface %sConverter { + %sHttpRequest toHttpRequest(%sRequest request); + %sResponse toResponse(%sHttpResponse httpResponse); + } + """.formatted(bizPackage, + bizPackage, baseName, + bizPackage, baseName, + BASE_PACKAGE, httpPackage, baseName, + BASE_PACKAGE, httpPackage, baseName, + baseName, baseName, baseName, baseName, baseName); + } + + private static String toPackageSegment(String value) { + String normalized = value == null ? "" : value.toLowerCase(Locale.ROOT) + .replaceAll("[^a-z0-9]+", "_") + .replaceAll("^_+|_+$", ""); + return normalized.isBlank() ? "sample" : normalized; } private static String mciConverterContent(String bizPackage, String baseName, String mciPackage, String interfaceId) { return """ @@ -976,6 +1099,23 @@ public class ToolScaffolder { } + static String wireMockMappingContent(String interfaceId, String bodyFileName) { + return """ + { + "request" : { + "method" : "POST", + "urlPath" : "/%s" + }, + "response" : { + "status" : 200, + "headers" : { + "Content-Type" : "application/json;charset=UTF-8" + }, + "bodyFileName" : "%s" + } + } + """.formatted(jsonEscape(interfaceId), jsonEscape(bodyFileName)); + } private static String mockResponseContent(List outputFields) { StringBuilder json = new StringBuilder("{\n"); List fields = outputFields == null ? List.of() : outputFields; @@ -1031,10 +1171,6 @@ public class ToolScaffolder { baseName, baseName, baseName); } 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]+", " ") @@ -1042,8 +1178,7 @@ public class ToolScaffolder { 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), + return "%s_%s_%s".formatted( group.toLowerCase(Locale.ROOT), service, action); diff --git a/dap-was-lib/src/main/resources/glow/application-glow-local.yml b/dap-was-lib/src/main/resources/glow/application-glow-local.yml index 639fdd02..1797da4e 100644 --- a/dap-was-lib/src/main/resources/glow/application-glow-local.yml +++ b/dap-was-lib/src/main/resources/glow/application-glow-local.yml @@ -8,6 +8,17 @@ glow: communication: common: env-type: D + # Direct local process uses WireMock host port mapped by docker-compose. + http: + connection-timeout: 5 + read-timeout: 5 + api-list: + - name: sample + domain: ${AXHUB_SAMPLE_HTTP_DOMAIN:http://localhost:8089} + url: ${AXHUB_SAMPLE_HTTP_URL:/CLCNNB00001} + method: POST + content-type: application/json;charset=UTF-8 + biz-pod: false mci: host: ${GLOW_COMMUNICATION_MCI_HOST:http://localhost} port: ${GLOW_COMMUNICATION_MCI_PORT:8080} diff --git a/dap-was-lib/src/main/resources/glow/application-glow.yml b/dap-was-lib/src/main/resources/glow/application-glow.yml index e5925405..f2ee98f4 100644 --- a/dap-was-lib/src/main/resources/glow/application-glow.yml +++ b/dap-was-lib/src/main/resources/glow/application-glow.yml @@ -23,10 +23,15 @@ glow: read-timeout: 5 # HTTP Tool target catalog. Replace or add entries after the business endpoint is agreed. api-list: + # WireMock/개발환경 샘플입니다. 컨테이너 내부에서는 localhost가 Tool Pod 자신을 뜻하므로 + # Docker 서비스명(mci-mock)과 컨테이너 포트(8080)를 기본값으로 사용합니다. + # 로컬 PC에서 실행할 때는 AXHUB_SAMPLE_HTTP_DOMAIN=http://localhost:8089 로 덮어씁니다. - name: sample - domain: ${AXHUB_SAMPLE_HTTP_DOMAIN:http://localhost:8099} - path: "" - method: GET + domain: ${AXHUB_SAMPLE_HTTP_DOMAIN:http://mci-mock:8080} + # mci-mock/mappings/smp_employee_search.json의 urlPath와 동일해야 합니다. + url: ${AXHUB_SAMPLE_HTTP_URL:/CLCNNB00001} + method: POST + content-type: application/json;charset=UTF-8 biz-pod: false mci: uri: /ntl_mci/dap_rcv diff --git a/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/integration/http/component/AxhubHttpComponentTest.java b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/integration/http/component/AxhubHttpComponentTest.java index edb6c79f..adb5e21d 100644 --- a/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/integration/http/component/AxhubHttpComponentTest.java +++ b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/integration/http/component/AxhubHttpComponentTest.java @@ -3,6 +3,7 @@ package io.shinhanlife.dap.lib.integration.http.component; import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.http.MediaType.APPLICATION_JSON; import static org.springframework.test.web.client.match.MockRestRequestMatchers.header; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.method; import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; @@ -24,7 +25,7 @@ class AxhubHttpComponentTest { GlowHttpComponent glowHttpComponent = new GlowHttpComponent(builder); AxhubHttpProperties properties = new AxhubHttpProperties(); properties.setApiList(List.of(new AxhubHttpProperties.ApiDefinition( - "sample", "https://api.example.test", "/v1", HttpMethod.GET, false))); + "sample", "https://api.example.test", "/v1", HttpMethod.GET, "application/json", false))); AxhubHttpComponent component = new AxhubHttpComponent( glowHttpComponent, new ObjectMapper(), new GlowCommunicationProperties(), properties); @@ -38,6 +39,29 @@ class AxhubHttpComponentTest { server.verify(); } + @Test + void callByApiNameUsesConfiguredUrlMethodContentTypeAndBizPodHeader() { + RestClient.Builder builder = RestClient.builder(); + MockRestServiceServer server = MockRestServiceServer.bindTo(builder).build(); + GlowHttpComponent glowHttpComponent = new GlowHttpComponent(builder); + AxhubHttpProperties properties = new AxhubHttpProperties(); + properties.setApiList(List.of(new AxhubHttpProperties.ApiDefinition( + "employee", "https://employee.example.test", "/itrf/employee", HttpMethod.POST, + "application/json;charset=UTF-8", true))); + AxhubHttpComponent component = new AxhubHttpComponent( + glowHttpComponent, new ObjectMapper(), new GlowCommunicationProperties(), properties); + + server.expect(requestTo("https://employee.example.test/itrf/employee")) + .andExpect(method(HttpMethod.POST)) + .andExpect(header("Content-Type", "application/json;charset=UTF-8")) + .andExpect(header("X-POD-TO-POD", "true")) + .andRespond(withSuccess("{\"status\":\"OK\"}", APPLICATION_JSON)); + + SampleResponse response = component.call("employee", "{\"employeeId\":\"EMP10001\"}", SampleResponse.class); + + assertThat(response.status()).isEqualTo("OK"); + server.verify(); + } record SampleResponse(String status) { } } \ No newline at end of file diff --git a/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/mcp/McpToolMethodRegistryTest.java b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/mcp/McpToolMethodRegistryTest.java index 37b321be..647d4229 100644 --- a/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/mcp/McpToolMethodRegistryTest.java +++ b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/mcp/McpToolMethodRegistryTest.java @@ -18,10 +18,10 @@ class McpToolMethodRegistryTest { registry.initialize(); - McpToolMethodRegistry.RegisteredTool tool = registry.find("oth_cmm_echo_search"); + McpToolMethodRegistry.RegisteredTool tool = registry.find("cmm_echo_search"); assertNotNull(tool); assertEquals("execute", tool.method().getName()); - assertEquals("oth_cmm_echo_search", tool.annotation().name()); + assertEquals("cmm_echo_search", tool.annotation().name()); } @Test @@ -42,14 +42,14 @@ class McpToolMethodRegistryTest { } static class EchoTool { - @McpTool(name = "oth_cmm_echo_search") + @McpTool(name = "cmm_echo_search") public String execute(String request) { return request; } } static class DuplicateEchoTool { - @McpTool(name = "oth_cmm_echo_search") + @McpTool(name = "cmm_echo_search") public String execute(String request) { return request; } diff --git a/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/ToolScaffolderTest.java b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/ToolScaffolderTest.java index 5989594a..8cd3b178 100644 --- a/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/ToolScaffolderTest.java +++ b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/ToolScaffolderTest.java @@ -25,7 +25,7 @@ class ToolScaffolderTest { 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("name = \"cmm_claim_search\"")); assertTrue(useCase.contains("@ToolHint(register = true, categoryKey = \"cmm\", mappingId = \"CLM0001\")")); assertTrue(response.contains("private String resultCode;")); assertTrue(response.contains("private String resultMessage;")); @@ -41,7 +41,7 @@ class ToolScaffolderTest { "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\"")); + assertTrue(useCase.contains("name = \"cmm_notification_send\"")); } @Test @@ -121,7 +121,7 @@ class ToolScaffolderTest { ToolScaffolder.scaffold("claim search", "CLM0001", "Claim search", "cmm", "MCI", moduleName, "tester", "2026.08.10", true, null, null, null, List.of(), outputFields); - Path mockResponse = root.resolve("dap-was-oth/src/main/resources/mock-responses/oth_cmm_claim_search.json"); + Path mockResponse = root.resolve("dap-was-oth/src/main/resources/mock-responses/cmm_claim_search.json"); Path useCaseTest = root.resolve("dap-was-oth/src/test/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/ClaimSearchUseCaseTest.java"); assertTrue(Files.exists(mockResponse)); @@ -129,6 +129,15 @@ class ToolScaffolderTest { assertTrue(Files.exists(useCaseTest)); assertTrue(Files.readString(useCaseTest).contains("class ClaimSearchUseCaseTest")); } + @Test + void createsWireMockMappingForHttpTool() { + String mapping = ToolScaffolder.wireMockMappingContent("HR_EMPLOYEE_SEARCH", "smp_employee_search.json"); + + assertTrue(mapping.contains("\"method\" : \"POST\""), mapping); + assertTrue(mapping.contains("\"urlPath\" : \"/HR_EMPLOYEE_SEARCH\""), mapping); + assertTrue(mapping.contains("\"bodyFileName\" : \"smp_employee_search.json\""), mapping); + } + @Test void generatesDtoPackageAndRemovesDuplicateResponseFields() throws Exception { @@ -152,16 +161,56 @@ class ToolScaffolderTest { assertTrue(response.indexOf("private String resultCode;") == response.lastIndexOf("private String resultCode;"), response); assertTrue(response.indexOf("private String employeeName;") == response.lastIndexOf("private String employeeName;"), response); String converter = Files.readString(root.resolve("dap-was-http/src/main/java/io/shinhanlife/dap/mcc/biz/smp/converter/EmployeeSearchConverter.java")); - String legacyRequest = Files.readString(root.resolve("dap-was-http/src/main/java/io/shinhanlife/dap/mcc/biz/smp/legacy/EmployeeSearchLegacyRequest.java")); + String httpRequest = Files.readString(root.resolve("dap-was-http/src/main/java/io/shinhanlife/dap/mcc/infra/itrf/http/sample/io/EmployeeSearchHttpRequest.java")); + String httpResponse = Files.readString(root.resolve("dap-was-http/src/main/java/io/shinhanlife/dap/mcc/infra/itrf/http/sample/io/EmployeeSearchHttpResponse.java")); + String httpClient = Files.readString(root.resolve("dap-was-http/src/main/java/io/shinhanlife/dap/mcc/infra/itrf/http/sample/SampleClient.java")); assertFalse(converter.contains("phoneNumber"), converter); - assertTrue(converter.contains("unmappedTargetPolicy = ReportingPolicy.IGNORE"), converter); - assertTrue(legacyRequest.contains("private String employeeId;"), legacyRequest); + assertTrue(converter.contains("infra.itrf.http.sample.io.EmployeeSearchHttpRequest"), converter); + assertTrue(httpRequest.contains("private String employeeId;"), httpRequest); + assertTrue(httpResponse.contains("private String employeeName;"), httpResponse); + assertTrue(httpClient.contains("http.call(API_NAME, request, responseType)"), httpClient); + assertFalse(Files.exists(root.resolve("dap-was-http/src/main/java/io/shinhanlife/dap/mcc/biz/smp/legacy"))); String implementation = Files.readString(root.resolve("dap-was-http/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/impl/EmployeeSearchUseCaseImpl.java")); assertTrue(implementation.contains("public EmployeeSearchResponse execute(EmployeeSearchRequest req)"), implementation); - assertTrue(implementation.contains("import io.shinhanlife.dap.lib.integration.http.component.AxhubHttpComponent;"), implementation); - assertTrue(implementation.contains("private final AxhubHttpComponent http;"), implementation); - assertTrue(implementation.contains("AxhubHttpDomain.SAMPLE,"), implementation); - assertTrue(implementation.contains("\"/HR_EMPLOYEE_SEARCH\""), implementation); + assertTrue(implementation.contains("import io.shinhanlife.dap.mcc.infra.itrf.http.sample.SampleClient;"), implementation); + assertTrue(implementation.contains("private final SampleClient sampleClient;"), implementation); + assertTrue(implementation.contains("sampleClient.call(httpRequest, EmployeeSearchHttpResponse.class)"), implementation); + assertFalse(implementation.contains("AxhubHttpComponent"), implementation); assertFalse(implementation.contains("executeLegacy(\"HTTP\""), implementation); + Path wireMockResponse = root.resolve("mci-mock/__files/smp_employee_search.json"); + Path wireMockMapping = root.resolve("mci-mock/mappings/smp_employee_search.json"); + assertTrue(Files.exists(wireMockResponse), wireMockResponse.toString()); + assertTrue(Files.exists(wireMockMapping), wireMockMapping.toString()); + assertTrue(Files.readString(wireMockMapping).contains("\"urlPath\" : \"/HR_EMPLOYEE_SEARCH\"")); + } + @Test + void generatesSeparateToolTitleAndDescription() throws Exception { + String moduleName = root.resolve("dap-was-title").toString(); + + ToolScaffolder.scaffold("employee search", "HR_EMPLOYEE_SEARCH", "직원 정보 조회", + "사번을 입력받아 재직 중인 직원의 기본 정보를 조회한다.", "smp", "HTTP", moduleName, + "tester", "2026.08.11", false, null, null, null, List.of(), List.of()); + + Path useCasePath = root.resolve("dap-was-title/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/EmployeeSearchUseCase.java"); + String useCase = Files.readString(useCasePath); + + assertTrue(useCase.contains("title = \"직원 정보 조회\""), useCase); + assertTrue(useCase.contains("description = \"사번을 입력받아 재직 중인 직원의 기본 정보를 조회한다.\""), useCase); + } + @Test + void generatesHttpToolUsingConfiguredApiNameAndConfiguredUrlOnly() throws Exception { + String moduleName = root.resolve("dap-was-http-api-name").toString(); + + ToolScaffolder.scaffold("employee search", "HR_EMPLOYEE_SEARCH", "직원 정보 조회", + "사번으로 직원을 조회한다.", "smp", "HTTP", moduleName, "tester", "2026.08.11", + false, null, null, null, List.of(), List.of(), "employee"); + + Path implementationPath = root.resolve("dap-was-http-api-name/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/impl/EmployeeSearchUseCaseImpl.java"); + String implementation = Files.readString(implementationPath); + + assertTrue(implementation.contains("import io.shinhanlife.dap.mcc.infra.itrf.http.employee.EmployeeClient;"), implementation); + assertTrue(implementation.contains("employeeClient.call(httpRequest, EmployeeSearchHttpResponse.class)"), implementation); + assertFalse(implementation.contains("AxhubHttpDomain"), implementation); + assertFalse(implementation.contains("\"/HR_EMPLOYEE_SEARCH\""), implementation); } } diff --git a/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/ToolSourceUpdaterTest.java b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/ToolSourceUpdaterTest.java index 7c89445b..ca4c9720 100644 --- a/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/ToolSourceUpdaterTest.java +++ b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/ToolSourceUpdaterTest.java @@ -21,16 +21,16 @@ class ToolSourceUpdaterTest { import org.springaicommunity.mcp.annotation.McpTool; import io.shinhanlife.dap.lib.annotation.ToolHint; interface SampleUseCase { - @McpTool(name = "oth_cmm_sample_search", description = "old") + @McpTool(name = "cmm_sample_search", description = "old") @ToolHint(register = false, requiresApproval = false) void search(); } """); - ToolSourceUpdater.updateToolSource(temporaryRoot, "oth_cmm_sample_search", "customer", "new", true, true); + ToolSourceUpdater.updateToolSource(temporaryRoot, "cmm_sample_search", "customer", "new", true, true); String updated = Files.readString(source); - assertTrue(updated.contains("@McpTool(name = \"oth_cmm_sample_search\", description = \"new\")")); + assertTrue(updated.contains("@McpTool(name = \"cmm_sample_search\", description = \"new\")")); assertTrue(updated.contains("@ToolHint(register = true, requiresApproval = true")); assertTrue(updated.contains("categoryKey = \"customer\""), updated); } diff --git a/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/validation/McpToolNameValidatorTest.java b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/validation/McpToolNameValidatorTest.java index 2bb4b6ba..64c15708 100644 --- a/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/validation/McpToolNameValidatorTest.java +++ b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/validation/McpToolNameValidatorTest.java @@ -89,11 +89,11 @@ class McpToolNameValidatorTest { Path root = findProjectRoot(); assertToolName(root, "dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/CustomerInfoUseCase.java", - "oth_cmm_customer_detail", "detail"); + "cmm_customer_detail", "detail"); assertToolName(root, "dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/BillingProcessUseCase.java", - "oth_cmm_billing_process", "process"); + "cmm_billing_process", "process"); assertToolName(root, "dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/BondIssueUseCase.java", - "oth_cmm_bond_issue", "issue"); + "cmm_bond_issue", "issue"); } private void assertToolName(Path root, String relativePath, String expectedName, String legacyName) throws IOException { diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/BalanceUseCase.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/BalanceUseCase.java index 85f00573..2121fc0b 100644 --- a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/BalanceUseCase.java +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/BalanceUseCase.java @@ -6,7 +6,7 @@ import io.shinhanlife.dap.lib.annotation.ToolHint; import io.shinhanlife.dap.mcc.biz.cmm.dto.*; public interface BalanceUseCase { - @McpTool(name = "oth_cmm_balance_inquiry", title = "잔고 조회 툴", description = "고객의 계좌 잔액을 조회합니다.") + @McpTool(name = "cmm_balance_inquiry", title = "잔고 조회 툴", description = "고객의 계좌 잔액을 조회합니다.") @ToolHint(register = false, categoryKey = "cmm", mappingId = "ACC_001") Object execute(BalanceRequest req); } diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/BillingProcessUseCase.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/BillingProcessUseCase.java index 96270e8e..878d4bbc 100644 --- a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/BillingProcessUseCase.java +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/BillingProcessUseCase.java @@ -7,7 +7,7 @@ import io.shinhanlife.dap.mcc.biz.cmm.dto.*; public interface BillingProcessUseCase { Object getStatus(BillingStatusRequest req); - @McpTool(name = "oth_cmm_billing_process", title = "청구 프로세스 툴", description = "청구 처리") + @McpTool(name = "cmm_billing_process", title = "청구 프로세스 툴", description = "청구 처리") @ToolHint(register = false, categoryKey = "cmm", mappingId = "BILL_002") Object processBilling(BillingProcessRequest data); } diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/BondIssueUseCase.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/BondIssueUseCase.java index eabfcfdc..767ddbb3 100644 --- a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/BondIssueUseCase.java +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/BondIssueUseCase.java @@ -8,7 +8,7 @@ import io.shinhanlife.dap.mcc.biz.cmm.dto.*; public interface BondIssueUseCase { Object check(BondCheckRequest req); - @McpTool(name = "oth_cmm_bond_issue", title = "채권 발행 툴", description = "증권 발행 테스트1") + @McpTool(name = "cmm_bond_issue", title = "채권 발행 툴", description = "증권 발행 테스트1") @ToolHint(register = false, categoryKey = "cmm", mappingId = "BOND_002") Object issue(BondIssueRequest data); } diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/ClaimSearchSchemaSampleUseCase.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/ClaimSearchSchemaSampleUseCase.java index 13c4a7de..765ae5ed 100644 --- a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/ClaimSearchSchemaSampleUseCase.java +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/ClaimSearchSchemaSampleUseCase.java @@ -8,7 +8,7 @@ import io.shinhanlife.dap.mcc.biz.cmm.dto.ClaimSearchResponse; public interface ClaimSearchSchemaSampleUseCase { - @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)) + @McpTool(name = "cmm_claim_schema_search", title = "청구 조회 스키마 샘플", description = "Claim search Tool sample using input and output JSON Schema resources.", annotations = @McpTool.McpAnnotations(readOnlyHint = true, idempotentHint = true)) @ToolHint(register = false, categoryKey = "cmm", inputSchemaResource = "classpath:tool-schemas/cmm/claim-search-resource-input-schema.json", outputSchemaResource = "classpath:tool-schemas/cmm/claim-search-resource-output-schema.json") diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/CommonUtilityUseCase.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/CommonUtilityUseCase.java index 47c0a56f..e480bc63 100644 --- a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/CommonUtilityUseCase.java +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/CommonUtilityUseCase.java @@ -8,11 +8,11 @@ import io.shinhanlife.dap.mcc.biz.cmm.dto.*; public interface CommonUtilityUseCase { Object registerVacation(VacationRegisterRequest req); - @McpTool(name = "oth_cmm_leave_count", title = "공통 유틸리티 툴", description = "연차 갯수 조회") + @McpTool(name = "cmm_leave_count", title = "공통 유틸리티 툴", description = "연차 갯수 조회") @ToolHint(register = false, categoryKey = "cmm", mappingId = "HR_VAC_02") Object getLeaveCount(LeaveCountRequest data); - @McpTool(name = "oth_cmm_secret_execute", title = "공통 유틸리티 툴", description = "비공개 툴 테스트") + @McpTool(name = "cmm_secret_execute", title = "공통 유틸리티 툴", description = "비공개 툴 테스트") @ToolHint(register = false, categoryKey = "cmm", mappingId = "SECRET_001") Object secretTool(LeaveCountRequest data); } diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/ContractInquiryUseCase.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/ContractInquiryUseCase.java index 3031622e..66d1598c 100644 --- a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/ContractInquiryUseCase.java +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/ContractInquiryUseCase.java @@ -8,7 +8,7 @@ import io.shinhanlife.dap.mcc.biz.cmm.dto.*; public interface ContractInquiryUseCase { Object getStatus(ContractStatusRequest req); - @McpTool(name = "oth_cmm_contract_detail", title = "계약 상세조회 툴", description = "계약상세 조회") + @McpTool(name = "cmm_contract_detail", title = "계약 상세조회 툴", description = "계약상세 조회") @ToolHint(register = false, categoryKey = "cmm", mappingId = "CNTR_002") Object getDetail(ContractDetailRequest data); } diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/CustomerInfoUseCase.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/CustomerInfoUseCase.java index 30409a3b..9b0c2280 100644 --- a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/CustomerInfoUseCase.java +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/CustomerInfoUseCase.java @@ -8,7 +8,7 @@ import io.shinhanlife.dap.mcc.biz.cmm.dto.*; public interface CustomerInfoUseCase { Object getGrade(CustomerGradeRequest req); - @McpTool(name = "oth_cmm_customer_detail", title = "고객 상세조회 툴", description = "고객상세 정보 조회") + @McpTool(name = "cmm_customer_detail", title = "고객 상세조회 툴", description = "고객상세 정보 조회") @ToolHint(register = false, categoryKey = "cmm", mappingId = "CRM_002") Object getDetail(CustomerDetailRequest data); } diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/MetaCommonCodeUseCase.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/MetaCommonCodeUseCase.java index a7b2ae08..2dc43e5d 100644 --- a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/MetaCommonCodeUseCase.java +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/MetaCommonCodeUseCase.java @@ -20,7 +20,7 @@ import io.shinhanlife.dap.mcc.biz.cmm.dto.MetaCommonCodeRequest; * */ public interface MetaCommonCodeUseCase { - @McpTool(name = "oth_cmm_commonCode_lookup", title = "메타 공통코드 조회 툴", description = "메타 통합코드 목록을 조회해줘", annotations = @McpTool.McpAnnotations(openWorldHint = true)) + @McpTool(name = "cmm_commonCode_lookup", title = "메타 공통코드 조회 툴", description = "메타 통합코드 목록을 조회해줘", annotations = @McpTool.McpAnnotations(openWorldHint = true)) @ToolHint(register = false, requiresApproval = false, categoryKey = "cmm", mappingId = "CLCNNB00001") Object execute(MetaCommonCodeRequest req); } diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/MetaTableUseCase.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/MetaTableUseCase.java index 9c809945..6fe6c90f 100644 --- a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/MetaTableUseCase.java +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/MetaTableUseCase.java @@ -20,7 +20,7 @@ import io.shinhanlife.dap.mcc.biz.cmm.dto.MetaTableRequest; * */ public interface MetaTableUseCase { - @McpTool(name = "oth_cmm_meta_table", title = "메타 테이블 조회 툴", description = "메타 테이블 정보 목록을 조회해줘", annotations = @McpTool.McpAnnotations(openWorldHint = true)) + @McpTool(name = "cmm_meta_table", title = "메타 테이블 조회 툴", description = "메타 테이블 정보 목록을 조회해줘", annotations = @McpTool.McpAnnotations(openWorldHint = true)) @ToolHint(register = false, requiresApproval = false, categoryKey = "cmm", mappingId = "CLCNNB00001") Object execute(MetaTableRequest req); } diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/TemplateUtilityUseCase.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/TemplateUtilityUseCase.java index 64e0b7e1..c596e765 100644 --- a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/TemplateUtilityUseCase.java +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/TemplateUtilityUseCase.java @@ -8,7 +8,7 @@ import io.shinhanlife.dap.mcc.biz.cmm.dto.*; import java.util.Map; public interface TemplateUtilityUseCase { - @McpTool(name = "oth_cmm_template_url", title = "템플릿 유틸리티 툴", description = "요청한 템플릿(엑셀, 워드 등) 파일(양식)을 다운로드 받을 수 있는 시스템 URL을 반환합니다. AI는 이 URL을 사용자에게 마크다운 링크 형태로 제공해야 합니다.") + @McpTool(name = "cmm_template_url", title = "템플릿 유틸리티 툴", description = "요청한 템플릿(엑셀, 워드 등) 파일(양식)을 다운로드 받을 수 있는 시스템 URL을 반환합니다. AI는 이 URL을 사용자에게 마크다운 링크 형태로 제공해야 합니다.") @ToolHint(categoryKey = "cmm") Map getTemplateFileUrl(TemplateDownloadRequest req); } diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/oth/usecase/Onnba3011UseCase.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/oth/usecase/Onnba3011UseCase.java index be27dcaf..60a48c85 100644 --- a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/oth/usecase/Onnba3011UseCase.java +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/oth/usecase/Onnba3011UseCase.java @@ -5,7 +5,7 @@ import io.shinhanlife.dap.lib.annotation.ToolHint; import io.shinhanlife.dap.mcc.biz.oth.dto.*; public interface Onnba3011UseCase { - @McpTool(name = "oth_oth_onnba3011_call", description = "Onnba3011 호출 툴") + @McpTool(name = "onnba3011_call", description = "Onnba3011 호출 툴") @ToolHint(categoryKey = "oth", register = false) Object execute(Onnba3011Request req); } diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/DailyQuoteToolUseCase.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/DailyQuoteToolUseCase.java index 9ecc6c1a..6c5fe77a 100644 --- a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/DailyQuoteToolUseCase.java +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/DailyQuoteToolUseCase.java @@ -7,7 +7,7 @@ import io.shinhanlife.dap.mcc.biz.smp.dto.DailyQuoteRequest; import io.shinhanlife.dap.mcc.biz.smp.dto.DailyQuoteResponse; public interface DailyQuoteToolUseCase { - @McpTool(name = "oth_smp_quote_daily", title = "오늘의 명언 툴", description = "무작위로 영감을 주는 명언을 하나 가져옵니다.") + @McpTool(name = "smp_quote_daily", title = "오늘의 명언 툴", description = "무작위로 영감을 주는 명언을 하나 가져옵니다.") @ToolHint(register = false, categoryKey = "smp", mappingId = "QUOTE_001") DailyQuoteResponse execute(DailyQuoteRequest req); } diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/ExchangeRateToolUseCase.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/ExchangeRateToolUseCase.java index bf7c1972..aa4f3cb4 100644 --- a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/ExchangeRateToolUseCase.java +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/ExchangeRateToolUseCase.java @@ -9,7 +9,7 @@ import io.shinhanlife.dap.mcc.biz.smp.dto.ExchangeRateRequest; import io.shinhanlife.dap.mcc.biz.smp.dto.ExchangeRateResponse; public interface ExchangeRateToolUseCase { - @McpTool(name = "oth_smp_exchangeRate_inquiry", title = "실시간 환율 조회 툴", description = "원하는 통화의 실시간 환율을 조회합니다. (예: USD, EUR, JPY)") + @McpTool(name = "smp_exchangeRate_inquiry", title = "실시간 환율 조회 툴", description = "원하는 통화의 실시간 환율을 조회합니다. (예: USD, EUR, JPY)") @ToolHint(register = false, categoryKey = "smp", mappingId = "EXCHANGE_001") ExchangeRateResponse execute(ExchangeRateRequest req); } diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/SampleHttpStatusUseCase.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/SampleHttpStatusUseCase.java index 1eb10081..f75ccfea 100644 --- a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/SampleHttpStatusUseCase.java +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/SampleHttpStatusUseCase.java @@ -7,7 +7,7 @@ import org.springaicommunity.mcp.annotation.McpTool; /** Sample Tool that demonstrates a configured HTTP API integration. */ public interface SampleHttpStatusUseCase { - @McpTool(name = "oth_smp_sample_status", + @McpTool(name = "smp_sample_status", title = "Sample external HTTP API status", description = "Calls the configured sample HTTP API and returns its status.") @ToolHint(register = false, categoryKey = "smp", mappingId = "HTTP_SAMPLE_001") diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/TeamMemberUseCase.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/TeamMemberUseCase.java index dd801934..b947f7e7 100644 --- a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/TeamMemberUseCase.java +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/TeamMemberUseCase.java @@ -6,7 +6,7 @@ import io.shinhanlife.dap.lib.annotation.ToolHint; import io.shinhanlife.dap.mcc.biz.smp.dto.TeamMemberRequest; public interface TeamMemberUseCase { - @McpTool(name = "oth_smp_team_list", title = "신한라이프 MCP, TOOL 파트 구성원 조회", description = "신한라이프 MCP, TOOL 파트 구성원을 조회합니다.", annotations = @McpTool.McpAnnotations(openWorldHint = true)) + @McpTool(name = "smp_team_list", title = "신한라이프 MCP, TOOL 파트 구성원 조회", description = "신한라이프 MCP, TOOL 파트 구성원을 조회합니다.", annotations = @McpTool.McpAnnotations(openWorldHint = true)) @ToolHint(register = false, requiresApproval = false, categoryKey = "smp", mappingId = "DIRECT0001") Object execute(TeamMemberRequest req); } diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/WeatherToolUseCase.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/WeatherToolUseCase.java index d60b1e10..b960ade9 100644 --- a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/WeatherToolUseCase.java +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/WeatherToolUseCase.java @@ -6,7 +6,7 @@ import io.shinhanlife.dap.lib.annotation.ToolHint; import io.shinhanlife.dap.mcc.biz.smp.dto.*; public interface WeatherToolUseCase { - @McpTool(name = "oth_smp_weather_inquiry", title = "날씨 조회 툴", description = "특정 도시의 현재 날씨, 온도, 풍속 정보를 조회합니다.") + @McpTool(name = "smp_weather_inquiry", title = "날씨 조회 툴", description = "특정 도시의 현재 날씨, 온도, 풍속 정보를 조회합니다.") @ToolHint(register = false, categoryKey = "smp", mappingId = "WEATHER_001") WeatherResponse execute(WeatherRequest req); } diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/sol/usecase/SolReqDetailUseCase.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/sol/usecase/SolReqDetailUseCase.java index 853fff5f..ef8521ea 100644 --- a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/sol/usecase/SolReqDetailUseCase.java +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/sol/usecase/SolReqDetailUseCase.java @@ -21,7 +21,7 @@ import io.shinhanlife.dap.mcc.biz.sol.dto.SolReqDetailRequest; */ public interface SolReqDetailUseCase { - @McpTool(name = "oth_sol_request_detail", title = "SolReqDetail 툴", description = "SOL 의뢰서 상세 조회", annotations = @McpTool.McpAnnotations(openWorldHint = true, readOnlyHint = true)) + @McpTool(name = "sol_request_detail", title = "SolReqDetail 툴", description = "SOL 의뢰서 상세 조회", annotations = @McpTool.McpAnnotations(openWorldHint = true, readOnlyHint = true)) @ToolHint(register = false, requiresApproval = false, categoryKey = "sol", mappingId = "SOLG00000002") Object execute(SolReqDetailRequest req); } diff --git a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/sol/usecase/SolReqListUseCase.java b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/sol/usecase/SolReqListUseCase.java index a3c6e2a0..f82ebe8d 100644 --- a/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/sol/usecase/SolReqListUseCase.java +++ b/dap-was-oth/src/main/java/io/shinhanlife/dap/mcc/biz/sol/usecase/SolReqListUseCase.java @@ -6,7 +6,7 @@ import io.shinhanlife.dap.lib.annotation.ToolHint; import io.shinhanlife.dap.mcc.biz.sol.dto.SolReqListRequest; public interface SolReqListUseCase { - @McpTool(name = "oth_sol_request_list", title = "SolReqList 툴", description = "SOL 의뢰서 목록 조회해줘", annotations = @McpTool.McpAnnotations(openWorldHint = true)) + @McpTool(name = "sol_request_list", title = "SolReqList 툴", description = "SOL 의뢰서 목록 조회해줘", annotations = @McpTool.McpAnnotations(openWorldHint = true)) @ToolHint(register = false, requiresApproval = false, categoryKey = "sol", mappingId = "SOLG00000001") Object execute(SolReqListRequest req); } diff --git a/dap-was-oth/src/test/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ClaimSearchRequestSchemaTest.java b/dap-was-oth/src/test/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ClaimSearchRequestSchemaTest.java index 5da5877f..574310c8 100644 --- a/dap-was-oth/src/test/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ClaimSearchRequestSchemaTest.java +++ b/dap-was-oth/src/test/java/io/shinhanlife/dap/mcc/biz/cmm/dto/ClaimSearchRequestSchemaTest.java @@ -1,6 +1,7 @@ package io.shinhanlife.dap.mcc.biz.cmm.dto; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import com.fasterxml.jackson.databind.ObjectMapper; @@ -24,11 +25,9 @@ class ClaimSearchRequestSchemaTest { assertEquals(false, schema.get("additionalProperties")); assertEquals("^CLM[0-9]{13}$", properties.get("claimNo").get("pattern")); - assertEquals(50L, properties.get("size").get("maximum")); - assertEquals(20L, properties.get("size").get("default")); - assertEquals(List.of( - Map.of("required", List.of("claimNo")), - Map.of("required", List.of("contractNo"))), schema.get("anyOf")); + assertEquals("^[1-9][0-9]?$|^50$", properties.get("size").get("pattern")); + assertEquals("20", properties.get("size").get("default")); + assertFalse(schema.containsKey("anyOf")); } @Test diff --git a/dap-was-oth/src/test/java/io/shinhanlife/dap/mcc/biz/oth/usecase/impl/Onnba3011UseCaseImplTest.java b/dap-was-oth/src/test/java/io/shinhanlife/dap/mcc/biz/oth/usecase/impl/Onnba3011UseCaseImplTest.java index cc909c81..5fbb51df 100644 --- a/dap-was-oth/src/test/java/io/shinhanlife/dap/mcc/biz/oth/usecase/impl/Onnba3011UseCaseImplTest.java +++ b/dap-was-oth/src/test/java/io/shinhanlife/dap/mcc/biz/oth/usecase/impl/Onnba3011UseCaseImplTest.java @@ -37,7 +37,7 @@ class Onnba3011UseCaseImplTest { when(converter.toMciRequest(request)).thenReturn(mciRequest); when(mciCfpaClient.callCfpa0001(mciRequest)).thenReturn("success"); - Object result = useCase.callOnnba3011(request); + Object result = useCase.execute(request); assertEquals("success", result); verify(converter).toMciRequest(request); diff --git a/dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/ClaimSearchUseCase.java b/dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/ClaimSearchUseCase.java index 500cc752..e9e02557 100644 --- a/dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/ClaimSearchUseCase.java +++ b/dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/ClaimSearchUseCase.java @@ -21,7 +21,7 @@ import io.shinhanlife.dap.mcc.biz.cmm.dto.ClaimSearchResponse; */ public interface ClaimSearchUseCase { - @McpTool(name = "sms_cmm_claim_search", title = "청구번호 또는 계약번호로 보험금 청구 상태를 조회한다.", description = "청구번호 또는 계약번호로 보험금 청구 상태를 조회한다.") + @McpTool(name = "cmm_claim_search", title = "청구번호 또는 계약번호로 보험금 청구 상태를 조회한다.", description = "청구번호 또는 계약번호로 보험금 청구 상태를 조회한다.") @ToolHint(register = false, categoryKey = "cmm", mappingId = "CLCNNB00001", inputSchemaResource = "classpath:tool-schemas/cmm/claim-search-resource-input-schema.json", outputSchemaResource = "classpath:tool-schemas/cmm/claim-search-resource-output-schema.json") diff --git a/dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/impl/ClaimSearchUseCaseImpl.java b/dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/impl/ClaimSearchUseCaseImpl.java index 8ce78e7b..b6dfd97f 100644 --- a/dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/impl/ClaimSearchUseCaseImpl.java +++ b/dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/impl/ClaimSearchUseCaseImpl.java @@ -37,7 +37,7 @@ public class ClaimSearchUseCaseImpl implements ClaimSearchUseCase { @Override public ClaimSearchResponse execute(ClaimSearchRequest req) { - log.info("[MCI Tool] {} 요청 수신.", "sms_cmm_claim_search"); + log.info("[MCI Tool] {} 요청 수신.", "cmm_claim_search"); try { // MapStruct를 이용한 자동 매핑 (AI DTO -> MCI DTO) CLCNNB00001_I mciReq = converter.toLegacyRequest(req); diff --git a/dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/biz/smp/converter/EmployeeSearchConverter.java b/dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/biz/smp/converter/EmployeeSearchConverter.java new file mode 100644 index 00000000..f74dd9f1 --- /dev/null +++ b/dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/biz/smp/converter/EmployeeSearchConverter.java @@ -0,0 +1,14 @@ +package io.shinhanlife.dap.mcc.biz.smp.converter; + +import io.shinhanlife.dap.mcc.biz.smp.dto.EmployeeSearchRequest; +import io.shinhanlife.dap.mcc.biz.smp.dto.EmployeeSearchResponse; +import io.shinhanlife.dap.mcc.infra.itrf.http.sample.io.EmployeeSearchHttpRequest; +import io.shinhanlife.dap.mcc.infra.itrf.http.sample.io.EmployeeSearchHttpResponse; +import org.mapstruct.Mapper; +import org.mapstruct.ReportingPolicy; + +@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE) +public interface EmployeeSearchConverter { + EmployeeSearchHttpRequest toHttpRequest(EmployeeSearchRequest request); + EmployeeSearchResponse toResponse(EmployeeSearchHttpResponse httpResponse); +} \ No newline at end of file diff --git a/dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/biz/smp/dto/EmployeeSearchRequest.java b/dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/biz/smp/dto/EmployeeSearchRequest.java new file mode 100644 index 00000000..4d7cb10f --- /dev/null +++ b/dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/biz/smp/dto/EmployeeSearchRequest.java @@ -0,0 +1,16 @@ +package io.shinhanlife.dap.mcc.biz.smp.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +public class EmployeeSearchRequest { + @Schema(description = "조회할 사번", example = "EMP10001", requiredMode = Schema.RequiredMode.REQUIRED) + private String employeeId; + + @Schema(description = "직원명. 사번 없이 이름으로 조회할 때 사용", example = "홍길동") + private String employeeName; + +} diff --git a/dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/biz/smp/dto/EmployeeSearchResponse.java b/dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/biz/smp/dto/EmployeeSearchResponse.java new file mode 100644 index 00000000..62813a7e --- /dev/null +++ b/dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/biz/smp/dto/EmployeeSearchResponse.java @@ -0,0 +1,19 @@ +package io.shinhanlife.dap.mcc.biz.smp.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +public class EmployeeSearchResponse { + private String resultCode; + + private String resultMessage; + @Schema(description = "직원명", example = "홍길동") + private String employeeName; + + @Schema(description = "소속 부서명", example = "AX추진팀") + private String departmentName; + +} diff --git a/dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/EmployeeSearchUseCase.java b/dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/EmployeeSearchUseCase.java new file mode 100644 index 00000000..54fb4f17 --- /dev/null +++ b/dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/EmployeeSearchUseCase.java @@ -0,0 +1,27 @@ +package io.shinhanlife.dap.mcc.biz.smp.usecase; + +import org.springaicommunity.mcp.annotation.McpTool; +import io.shinhanlife.dap.lib.annotation.ToolHint; +import io.shinhanlife.dap.mcc.biz.smp.dto.EmployeeSearchRequest; +import io.shinhanlife.dap.mcc.biz.smp.dto.EmployeeSearchResponse; + +/** + * @package io.shinhanlife.dap.mcc.biz.smp.usecase + * @className EmployeeSearchUseCase + * @description AX HUB ?쒖뒪??泥섎━ ?대옒?? + * @author jade + * @create 2026.08.11 + *
+ * ---------- 媛쒖젙?대젰 ----------
+ * ?섏젙??     ?섏젙??   ?섏젙?댁슜
+ * ---------- -------- ---------------------------
+ * 2026.08.11  jade    理쒖큹?앹꽦
+ *
+ * 
+ */ +public interface EmployeeSearchUseCase { + + @McpTool(name = "smp_employee_search", title = "직원 정보 조회", description = "사번 또는 직원명을 기준으로 직원 정보를 조회합니다.") + @ToolHint(register = false, categoryKey = "smp", mappingId = "CLCNNB00001") + EmployeeSearchResponse execute(EmployeeSearchRequest req); +} diff --git a/dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/impl/EmployeeSearchUseCaseImpl.java b/dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/impl/EmployeeSearchUseCaseImpl.java new file mode 100644 index 00000000..0ea24489 --- /dev/null +++ b/dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase/impl/EmployeeSearchUseCaseImpl.java @@ -0,0 +1,30 @@ +package io.shinhanlife.dap.mcc.biz.smp.usecase.impl; + +import io.shinhanlife.dap.mcc.biz.smp.converter.EmployeeSearchConverter; +import io.shinhanlife.dap.mcc.biz.smp.dto.EmployeeSearchRequest; +import io.shinhanlife.dap.mcc.biz.smp.dto.EmployeeSearchResponse; +import io.shinhanlife.dap.mcc.biz.smp.usecase.EmployeeSearchUseCase; +import io.shinhanlife.dap.mcc.infra.itrf.http.sample.SampleClient; +import io.shinhanlife.dap.mcc.infra.itrf.http.sample.io.EmployeeSearchHttpRequest; +import io.shinhanlife.dap.mcc.infra.itrf.http.sample.io.EmployeeSearchHttpResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class EmployeeSearchUseCaseImpl implements EmployeeSearchUseCase { + + private final EmployeeSearchConverter converter; + private final SampleClient sampleClient; + + @Override + public EmployeeSearchResponse execute(EmployeeSearchRequest request) { + EmployeeSearchHttpRequest httpRequest = converter.toHttpRequest(request); + EmployeeSearchHttpResponse httpResponse = sampleClient.call(httpRequest, EmployeeSearchHttpResponse.class); + + EmployeeSearchResponse response = converter.toResponse(httpResponse); + response.setResultCode("SUCCESS"); + response.setResultMessage("HTTP API call completed."); + return response; + } +} \ No newline at end of file diff --git a/dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/biz/sms/usecase/SmsToolUseCase.java b/dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/biz/sms/usecase/SmsToolUseCase.java index ccfdf333..545528dc 100644 --- a/dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/biz/sms/usecase/SmsToolUseCase.java +++ b/dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/biz/sms/usecase/SmsToolUseCase.java @@ -5,7 +5,7 @@ import io.shinhanlife.dap.lib.annotation.ToolHint; import io.shinhanlife.dap.mcc.biz.sms.dto.*; public interface SmsToolUseCase { - @McpTool(name = "sms_sms_msg_send", title = "SMS 발송 툴", description = "SMS 발송 기능을 제공합니다.") + @McpTool(name = "sms_msg_send", title = "SMS 발송 툴", description = "SMS 발송 기능을 제공합니다.") @ToolHint(register = false, categoryKey = "sms", mappingId = "SMS_SEND") Object sendSms(SmsSendRequest req); } diff --git a/dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/infra/itrf/http/sample/SampleClient.java b/dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/infra/itrf/http/sample/SampleClient.java new file mode 100644 index 00000000..8f237d21 --- /dev/null +++ b/dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/infra/itrf/http/sample/SampleClient.java @@ -0,0 +1,17 @@ +package io.shinhanlife.dap.mcc.infra.itrf.http.sample; + +import io.shinhanlife.dap.lib.integration.http.component.AxhubHttpComponent; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class SampleClient { + private static final String API_NAME = "sample"; + + private final AxhubHttpComponent http; + + public O call(I request, Class responseType) { + return http.call(API_NAME, request, responseType); + } +} \ No newline at end of file diff --git a/dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/infra/itrf/http/sample/io/EmployeeSearchHttpRequest.java b/dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/infra/itrf/http/sample/io/EmployeeSearchHttpRequest.java new file mode 100644 index 00000000..88776b7c --- /dev/null +++ b/dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/infra/itrf/http/sample/io/EmployeeSearchHttpRequest.java @@ -0,0 +1,11 @@ +package io.shinhanlife.dap.mcc.infra.itrf.http.sample.io; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Data; + +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +public class EmployeeSearchHttpRequest { + private String employeeId; + private String employeeName; +} \ No newline at end of file diff --git a/dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/infra/itrf/http/sample/io/EmployeeSearchHttpResponse.java b/dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/infra/itrf/http/sample/io/EmployeeSearchHttpResponse.java new file mode 100644 index 00000000..5349af4a --- /dev/null +++ b/dap-was-sms/src/main/java/io/shinhanlife/dap/mcc/infra/itrf/http/sample/io/EmployeeSearchHttpResponse.java @@ -0,0 +1,12 @@ +package io.shinhanlife.dap.mcc.infra.itrf.http.sample.io; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Data; + +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +public class EmployeeSearchHttpResponse { + private String resultCode; + private String employeeName; + private String departmentName; +} \ No newline at end of file diff --git a/dap-was-sms/src/test/java/io/shinhanlife/dap/mcc/biz/smp/usecase/EmployeeSearchUseCaseTest.java b/dap-was-sms/src/test/java/io/shinhanlife/dap/mcc/biz/smp/usecase/EmployeeSearchUseCaseTest.java new file mode 100644 index 00000000..0e5cdd87 --- /dev/null +++ b/dap-was-sms/src/test/java/io/shinhanlife/dap/mcc/biz/smp/usecase/EmployeeSearchUseCaseTest.java @@ -0,0 +1,34 @@ +package io.shinhanlife.dap.mcc.biz.smp.usecase; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import io.shinhanlife.dap.mcc.infra.itrf.http.sample.SampleClient; + +import io.shinhanlife.dap.mcc.biz.smp.dto.EmployeeSearchRequest; +import io.shinhanlife.dap.mcc.biz.smp.dto.EmployeeSearchResponse; +import io.shinhanlife.dap.mcc.biz.smp.usecase.impl.EmployeeSearchUseCaseImpl; +import io.shinhanlife.dap.mcc.usecase.AbstractMcpToolUseCase; +import org.junit.jupiter.api.Test; + +class EmployeeSearchUseCaseTest { + + @Test + void createsToolRequestAndResponseDtos() { + assertNotNull(new EmployeeSearchRequest()); + assertNotNull(new EmployeeSearchResponse()); + } + + @Test + void doesNotDependOnAbstractMcpToolUseCase() { + assertFalse(AbstractMcpToolUseCase.class.isAssignableFrom(EmployeeSearchUseCaseImpl.class)); + } + + @Test + void delegatesHttpCallThroughApiSpecificClient() { + assertNotNull(EmployeeSearchUseCaseImpl.class.getDeclaredFields()); + assertTrue(java.util.Arrays.stream(EmployeeSearchUseCaseImpl.class.getDeclaredFields()) + .anyMatch(field -> field.getType().equals(SampleClient.class))); + } +} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 73e48c60..e8335757 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -61,6 +61,10 @@ services: - AXHUB_GATEWAY_URL=http://gateway:8081 - AXHUB_SOURCE_DIR=/src - AXHUB_TOOL_URL=http://was-sms:8082 + # HTTP Tool sample target inside the Docker network. + # WireMock 서비스명과 컨테이너 포트(8080)를 사용합니다. + - AXHUB_SAMPLE_HTTP_DOMAIN=http://mci-mock:8080 + - AXHUB_SAMPLE_HTTP_URL=/CLCNNB00001 - GLOW_COMMUNICATION_MCI_HOMT=http://mci-mock - GLOW_COMMUNICATION_MCI_PORT=8080 - GLOW_COMMUNICATION_EXTMCI_HOMT=http://mci-mock @@ -84,6 +88,10 @@ services: - SPRING_DATA_REDIS_PORT=6379 - AXHUB_GATEWAY_URL=http://gateway:8081 - AXHUB_TOOL_URL=http://was-oth:8084 + # HTTP Tool sample target inside the Docker network. + # WireMock 서비스명과 컨테이너 포트(8080)를 사용합니다. + - AXHUB_SAMPLE_HTTP_DOMAIN=http://mci-mock:8080 + - AXHUB_SAMPLE_HTTP_URL=/CLCNNB00001 - GLOW_COMMUNICATION_MCI_HOMT=http://mci-mock - GLOW_COMMUNICATION_MCI_PORT=8080 - GLOW_COMMUNICATION_EXTMCI_HOMT=http://mci-mock diff --git a/mci-mock/__files/smp_employee_search.json b/mci-mock/__files/smp_employee_search.json new file mode 100644 index 00000000..7f8df15d --- /dev/null +++ b/mci-mock/__files/smp_employee_search.json @@ -0,0 +1,5 @@ +{ + "resultCode" : "SUCCESS", + "employeeName" : "홍길동", + "departmentName" : "AX추진팀" +} diff --git a/mci-mock/mappings/smp_employee_search.json b/mci-mock/mappings/smp_employee_search.json new file mode 100644 index 00000000..92706bcf --- /dev/null +++ b/mci-mock/mappings/smp_employee_search.json @@ -0,0 +1,13 @@ +{ + "request" : { + "method" : "POST", + "urlPath" : "/CLCNNB00001" + }, + "response" : { + "status" : 200, + "headers" : { + "Content-Type" : "application/json;charset=UTF-8" + }, + "bodyFileName" : "smp_employee_search.json" + } +}