feat: standardize tool names and http clients
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled

This commit is contained in:
jade
2026-08-11 17:27:16 +09:00
parent 95a8ae3a1b
commit 9c420dc5ab
50 changed files with 726 additions and 221 deletions

View File

@@ -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));
}
}

View File

@@ -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 <T, R> R call(AxhubHttpDomain domain, String uri, T inputDto, Class<R> responseBodyClass) {
return call(domain, uri, inputDto, responseBodyClass, 0);
/** Calls the exact URL configured for the API name. */
public <T, R> R call(String apiName, T inputDto, Class<R> responseBodyClass) {
return call(apiName, "", inputDto, responseBodyClass, 0);
}
/** Calls the configured URL with an optional resource suffix. */
public <T, R> R call(String apiName, String uri, T inputDto, Class<R> responseBodyClass) {
return call(apiName, uri, inputDto, responseBodyClass, 0);
}
public <T, R> R call(String apiName, String uri, T inputDto, Class<R> responseBodyClass, int timeout) {
AxhubHttpProperties.ApiDefinition api = resolveApi(apiName);
return execute(api, uri, inputDto, responseBodyClass, timeout);
}
/** Backward-compatible enum overload. */
public <T, R> R call(AxhubHttpDomain domain, String uri, T inputDto, Class<R> responseBodyClass) {
return call(domain.getCode(), uri, inputDto, responseBodyClass, 0);
}
/** Backward-compatible enum overload. */
public <T, R> R call(AxhubHttpDomain domain, String uri, T inputDto, Class<R> 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 <T, R> R callBizPod(String apiName, T inputDto, Class<R> 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 <T, R> R callBizPod(AxhubHttpDomain domain, String uri, T inputDto, Class<R> 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 <T, R> R execute(AxhubHttpProperties.ApiDefinition api, String uri, T inputDto,
Class<R> responseBodyClass, int timeout) {
HttpHeader header = createHeader(api, timeout);
String requestUri = joinPath(api.path(), uri);
String requestUri = joinPath(api.url(), uri);
HttpTransfer<T> request = HttpTransfer.<T>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<HttpBody> response = http.sync(request);
return convertResponse(response.getBody(), responseBodyClass);
}
/** Convenience method for APIs configured as internal business Pods. */
public <T, R> R callBizPod(AxhubHttpDomain domain, String uri, T inputDto, Class<R> 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> R convertResponse(HttpBody responseBody, Class<R> 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;
}
}

View File

@@ -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.
*
* <p>Each target follows the ShinhanLife standard: name, domain, url, method,
* content-type, and biz-pod. Target-specific values belong in application-glow*.yml.</p>
*/
@Getter
@Setter
@Component
@@ -17,6 +22,13 @@ public class AxhubHttpProperties {
private List<ApiDefinition> 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
) {
}
}

View File

@@ -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
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- 媛쒖젙?대젰 ----------
* ?섏젙?? ?섏젙?? ?섏젙?댁슜
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
* 2026.09.01 0986406 理쒖큹?앹꽦
*
* </pre>
*/
@@ -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<FieldDefinition> inputFields, List<FieldDefinition> 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<FieldDefinition> inputFields, List<FieldDefinition> 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<FieldDefinition> inputFields, List<FieldDefinition> 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
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- 媛쒖젙?대젰 ----------
* ?섏젙?? ?섏젙?? ?섏젙?댁슜
* ---------- -------- ---------------------------
* %s %s 최초생성
* %s %s 理쒖큹?앹꽦
*
* </pre>
*/
@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
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- 媛쒖젙?대젰 ----------
* ?섏젙?? ?섏젙?? ?섏젙?댁슜
* ---------- -------- ---------------------------
* %s %s 최초생성
* %s %s 理쒖큹?앹꽦
*
* </pre>
*/
@@ -254,14 +283,14 @@ public class ToolScaffolder {
/**
* @package %s.usecase
* @className %sUseCase
* @description AX HUB 시스템 처리 클래스
* @description AX HUB ?쒖뒪??泥섎━ ?대옒??
* @author %s
* @create %s
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- 媛쒖젙?대젰 ----------
* ?섏젙?? ?섏젙?? ?섏젙?댁슜
* ---------- -------- ---------------------------
* %s %s 최초생성
* %s %s 理쒖큹?앹꽦
*
* </pre>
*/
@@ -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
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- 媛쒖젙?대젰 ----------
* ?섏젙?? ?섏젙?? ?섏젙?댁슜
* ---------- -------- ---------------------------
* %s %s 최초생성
* %s %s 理쒖큹?앹꽦
*
* </pre>
*/
@@ -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<Object> 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
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- 媛쒖젙?대젰 ----------
* ?섏젙?? ?섏젙?? ?섏젙?댁슜
* ---------- -------- ---------------------------
* %s %s 최초생성
* %s %s 理쒖큹?앹꽦
*
* </pre>
*/
@@ -469,26 +498,26 @@ public class ToolScaffolder {
/**
* @package %s.%s.io
* @className %s_I
* @description AX HUB 시스템 처리 클래스
* @description AX HUB ?쒖뒪??泥섎━ ?대옒??
* @author %s
* @create %s
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- 媛쒖젙?대젰 ----------
* ?섏젙?? ?섏젙?? ?섏젙?댁슜
* ---------- -------- ---------------------------
* %s %s 최초생성
* %s %s 理쒖큹?앹꽦
*
* </pre>
*/
@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
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- 媛쒖젙?대젰 ----------
* ?섏젙?? ?섏젙?? ?섏젙?댁슜
* ---------- -------- ---------------------------
* %s %s 최초생성
* %s %s 理쒖큹?앹꽦
*
* </pre>
*/
@@ -537,14 +566,14 @@ public class ToolScaffolder {
/**
* @package %s.converter
* @className %sConverter
* @description AX HUB 시스템 처리 클래스
* @description AX HUB ?쒖뒪??泥섎━ ?대옒??
* @author %s
* @create %s
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- 媛쒖젙?대젰 ----------
* ?섏젙?? ?섏젙?? ?섏젙?댁슜
* ---------- -------- ---------------------------
* %s %s 최초생성
* %s %s 理쒖큹?앹꽦
*
* </pre>
*/
@@ -598,14 +627,14 @@ public class ToolScaffolder {
/**
* @package %s.%s
* @className Mci%sClient
* @description AX HUB 시스템 처리 클래스
* @description AX HUB ?쒖뒪??泥섎━ ?대옒??
* @author %s
* @create %s
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- 媛쒖젙?대젰 ----------
* ?섏젙?? ?섏젙?? ?섏젙?댁슜
* ---------- -------- ---------------------------
* %s %s 최초생성
* %s %s 理쒖큹?앹꽦
*
* </pre>
*/
@@ -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
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- 媛쒖젙?대젰 ----------
* ?섏젙?? ?섏젙?? ?섏젙?댁슜
* ---------- -------- ---------------------------
* %s %s 최초생성
* %s %s 理쒖큹?앹꽦
*
* </pre>
*/
@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
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- 媛쒖젙?대젰 ----------
* ?섏젙?? ?섏젙?? ?섏젙?댁슜
* ---------- -------- ---------------------------
* %s %s 최초생성
* %s %s 理쒖큹?앹꽦
*
* </pre>
*/
@@ -703,14 +758,14 @@ public class ToolScaffolder {
/**
* @package %s.converter
* @className %sConverter
* @description AX HUB 시스템 처리 클래스
* @description AX HUB ?쒖뒪??泥섎━ ?대옒??
* @author %s
* @create %s
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- 媛쒖젙?대젰 ----------
* ?섏젙?? ?섏젙?? ?섏젙?댁슜
* ---------- -------- ---------------------------
* %s %s 최초생성
* %s %s 理쒖큹?앹꽦
*
* </pre>
*/
@@ -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 <I, O> O call(I request, Class<O> 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<FieldDefinition> outputFields) {
StringBuilder json = new StringBuilder("{\n");
List<FieldDefinition> 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);

View File

@@ -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}

View File

@@ -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

View File

@@ -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) {
}
}

View File

@@ -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;
}

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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 {