fix: preserve Korean text in scaffold output
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 2m28s

This commit is contained in:
jade
2026-08-12 17:29:10 +09:00
parent 017812cd29
commit 6aa62a5499
8 changed files with 183 additions and 131 deletions

View File

@@ -4,6 +4,7 @@ import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.charset.StandardCharsets;
import java.nio.file.StandardOpenOption;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
@@ -72,7 +73,7 @@ public class PodScaffolder {
implementation project(':dap-was-lib')
}
""";
Files.writeString(modulePath.resolve("build.gradle"), buildGradle);
writeUtf8(modulePath.resolve("build.gradle"), buildGradle);
log.append("[3/6] Dockerfile 생성 중...\n");
String dockerfile = """
@@ -84,7 +85,7 @@ public class PodScaffolder {
EXPOSE %s
ENTRYPOINT ["java", "-jar", "app.jar"]
""".formatted(moduleName, portStr);
Files.writeString(modulePath.resolve("Dockerfile"), dockerfile);
writeUtf8(modulePath.resolve("Dockerfile"), dockerfile);
log.append("[4/6] Application 클래스 및 설정 파일 생성 중...\n");
Path srcPath = modulePath.resolve("src/main/java/io/shinhanlife/dap/mcc/" + shortName);
@@ -121,7 +122,7 @@ public class PodScaffolder {
}
}
""".formatted(shortName, shortName, capitalize(shortName), author, createDate, createDate, author, capitalize(shortName), capitalize(shortName));
Files.writeString(srcPath.resolve("DapWas" + capitalize(shortName) + "Application.java"), appClass);
writeUtf8(srcPath.resolve("DapWas" + capitalize(shortName) + "Application.java"), appClass);
Path resPath = modulePath.resolve("src/main/resources");
Files.createDirectories(resPath);
@@ -145,7 +146,7 @@ public class PodScaffolder {
tenant-domains:
TESTER-DEV: ALL
""".formatted(portStr, moduleName, moduleName.replace("dap-", ""));
Files.writeString(resPath.resolve("application.yml"), applicationYml);
writeUtf8(resPath.resolve("application.yml"), applicationYml);
String applicationLocalYml = """
# Local 환경 전용 설정 (H2 메모리 DB 등)
@@ -194,7 +195,7 @@ public class PodScaffolder {
tool:
url: ${AXHUB_TOOL_URL:http://localhost:${server.port}}
""";
Files.writeString(resPath.resolve("application-local.yml"), applicationLocalYml);
writeUtf8(resPath.resolve("application-local.yml"), applicationLocalYml);
String applicationDevYml = """
# OCI 클라우드 환경 전용 설정
@@ -244,7 +245,7 @@ public class PodScaffolder {
externalMci:
url: http://10.176.32.176
""".formatted(portStr, portStr);
Files.writeString(resPath.resolve("application-dev.yml"), applicationDevYml);
writeUtf8(resPath.resolve("application-dev.yml"), applicationDevYml);
String applicationTestYml = """
server:
@@ -264,7 +265,7 @@ public class PodScaffolder {
tool:
url: ${AXHUB_TOOL_URL}
""".formatted(portStr);
Files.writeString(resPath.resolve("application-test.yml"), applicationTestYml);
writeUtf8(resPath.resolve("application-test.yml"), applicationTestYml);
String applicationProdYml = """
server:
@@ -284,7 +285,7 @@ public class PodScaffolder {
tool:
url: ${AXHUB_TOOL_URL}
""".formatted(portStr);
Files.writeString(resPath.resolve("application-prod.yml"), applicationProdYml);
writeUtf8(resPath.resolve("application-prod.yml"), applicationProdYml);
String logbackXml = """
<?xml version="1.0" encoding="UTF-8"?>
@@ -312,21 +313,21 @@ public class PodScaffolder {
<logger name="io.shinhanlife" level="DEBUG" />
</configuration>
""".formatted(moduleName, moduleName);
Files.writeString(resPath.resolve("logback-spring.xml"), logbackXml);
writeUtf8(resPath.resolve("logback-spring.xml"), logbackXml);
log.append("[5/6] settings.gradle 에 모듈 등록 중...\n");
Path settingsPath = rootDir.resolve(Paths.get("settings.gradle"));
if (Files.exists(settingsPath)) {
String settings = Files.readString(settingsPath);
String settings = Files.readString(settingsPath, StandardCharsets.UTF_8);
if (!settings.contains("include '" + moduleName + "'")) {
Files.writeString(settingsPath, System.lineSeparator() + "include '" + moduleName + "'" + System.lineSeparator(), StandardOpenOption.APPEND);
Files.writeString(settingsPath, System.lineSeparator() + "include '" + moduleName + "'" + System.lineSeparator(), StandardCharsets.UTF_8, StandardOpenOption.APPEND);
}
}
log.append("[6/6] docker-compose.yml 에 서비스 추가 중...\n");
Path dockerComposePath = rootDir.resolve(Paths.get("docker-compose.yml"));
if (Files.exists(dockerComposePath)) {
String compose = Files.readString(dockerComposePath);
String compose = Files.readString(dockerComposePath, StandardCharsets.UTF_8);
String serviceName = moduleName.replace("dap-", ""); // e.g. tool-payment
if (!compose.contains(" " + serviceName + ":")) {
String newService = """
@@ -352,7 +353,7 @@ public class PodScaffolder {
- GLOW_COMMUNICATION_EAI_HOST=http://mci-mock
- GLOW_COMMUNICATION_EAI_PORT=8080
""".formatted(serviceName, moduleName, portStr, portStr, serviceName, portStr);
Files.writeString(dockerComposePath, System.lineSeparator() + newService, StandardOpenOption.APPEND);
Files.writeString(dockerComposePath, System.lineSeparator() + newService, StandardCharsets.UTF_8, StandardOpenOption.APPEND);
}
}
@@ -365,6 +366,10 @@ public class PodScaffolder {
return log.toString();
}
private static void writeUtf8(Path path, String content) throws IOException {
Files.writeString(path, content, StandardCharsets.UTF_8);
}
private static String capitalize(String str) {
if (str == null || str.isEmpty()) return str;
return str.substring(0, 1).toUpperCase() + str.substring(1);

View File

@@ -5,6 +5,7 @@ import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.charset.StandardCharsets;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.LinkedHashSet;
@@ -14,28 +15,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 메서드를 실행합니다.
* - 콘솔 질문에 차례대로 값을 입력하면 파일이 생성됩니다.
*
* 諛⑸쾿 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]" "[?듭떊諛⑹떇]" "[紐⑤뱢紐?"
* 방법 2. 명령줄에서 실행
* - 컴파일: javac -encoding UTF-8 dap-was-lib/src/main/java/io/shinhanlife/dap/lib/util/ToolScaffolder.java
* - 실행: java -cp dap-was-lib/src/main/java io.shinhanlife.dap.lib.util.ToolScaffolder [이름] [ID] "[설명]" "[그룹]" "[통신방식]" "[모듈명]"
*/
/**
* @package io.shinhanlife.dap.lib.util
* @className ToolScaffolder
* @description AX HUB ?쒖뒪??泥섎━ ?대옒??
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 媛쒖젙?대젰 ----------
* ?섏젙?? ?섏젙?? ?섏젙?댁슜
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 理쒖큹?앹꽦
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
@@ -65,8 +66,8 @@ 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 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: ");
@@ -83,12 +84,12 @@ public class ToolScaffolder {
String defaultAuthor = System.getProperty("user.name");
String defaultDate = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy.MM.dd"));
String author = getOrAsk(args, 7, scanner, "7. ?묒꽦??(?뷀꽣 ?낅젰 ??'" + defaultAuthor + "'): ");
String author = getOrAsk(args, 7, scanner, "8. 작성자(Enter 입력 시 '" + defaultAuthor + "'): ");
if (author.trim().isEmpty()) author = defaultAuthor;
String createDate = getOrAsk(args, 8, scanner, "8. ?묒꽦??(?뷀꽣 ?낅젰 ??'" + defaultDate + "'): ");
String createDate = getOrAsk(args, 8, scanner, "9. 작성일(Enter 입력 시 '" + defaultDate + "'): ");
if (createDate.trim().isEmpty()) createDate = defaultDate;
String useSchemaResourceStr = getOrAsk(args, 9, scanner, "9. input/output JSON Schema ?뚯씪 ?먮룞 ?앹꽦 ?щ? (y/N): ");
String useSchemaResourceStr = getOrAsk(args, 9, scanner, "10. input/output JSON Schema 파일 자동 생성 여부 (y/N): ");
boolean useSchemaResource = "y".equalsIgnoreCase(useSchemaResourceStr.trim());
String schemaResourceDirectory = "classpath:tool-schemas/" + group.toLowerCase() + "/";
@@ -161,7 +162,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";
@@ -222,25 +223,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);
@@ -250,9 +251,9 @@ 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);
writeUtf8(dtoDir.resolve(baseName + "Request.java"), reqContent);
// Generate Response DTO
String resContent = """
@@ -264,14 +265,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>
*/
@@ -286,7 +287,7 @@ public class ToolScaffolder {
}
""".formatted(bizPackage, bizPackage, baseName, author, createDate, createDate, author, baseName);
resContent = dtoContent(bizPackage + ".dto", baseName + "Response", outputFields, author, createDate, false);
Files.writeString(dtoDir.resolve(baseName + "Response.java"), resContent);
writeUtf8(dtoDir.resolve(baseName + "Response.java"), resContent);
String toolName = toToolName(moduleName, group, baseName);
@@ -310,14 +311,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>
*/
@@ -338,7 +339,7 @@ public class ToolScaffolder {
baseName, baseName
);
Files.writeString(usecaseDir.resolve(baseName + "UseCase.java"), serviceInterfaceContent);
writeUtf8(usecaseDir.resolve(baseName + "UseCase.java"), serviceInterfaceContent);
String serviceImplContent;
@@ -361,14 +362,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>
*/
@@ -382,9 +383,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(
@@ -400,7 +401,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");
@@ -460,14 +461,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>
*/
@@ -513,7 +514,7 @@ public class ToolScaffolder {
);
}
Files.writeString(usecaseImplDir.resolve(baseName + "UseCaseImpl.java"), serviceImplContent);
writeUtf8(usecaseImplDir.resolve(baseName + "UseCaseImpl.java"), serviceImplContent);
if (isMci) {
String mciReqContent = """
@@ -524,32 +525,32 @@ 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;
}
""".formatted(BASE_PACKAGE, mciGroupPath.replace("/", "."), BASE_PACKAGE, mciGroupPath.replace("/", "."), interfaceId, author, createDate, createDate, author, interfaceId);
mciReqContent = mciIoContent(mciGroupPath.replace("/", "."), interfaceId + "_I", inputFields, author, createDate);
Files.writeString(mciIoDir.resolve(interfaceId + "_I.java"), mciReqContent);
writeUtf8(mciIoDir.resolve(interfaceId + "_I.java"), mciReqContent);
String mciResContent = """
package %s.%s.io;
@@ -559,14 +560,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>
*/
@@ -576,7 +577,7 @@ public class ToolScaffolder {
}
""".formatted(BASE_PACKAGE, mciGroupPath.replace("/", "."), BASE_PACKAGE, mciGroupPath.replace("/", "."), interfaceId, author, createDate, createDate, author, interfaceId);
mciResContent = mciIoContent(mciGroupPath.replace("/", "."), interfaceId + "_O", outputFields, author, createDate);
Files.writeString(mciIoDir.resolve(interfaceId + "_O.java"), mciResContent);
writeUtf8(mciIoDir.resolve(interfaceId + "_O.java"), mciResContent);
String converterContent = """
package %s.converter;
@@ -592,14 +593,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>
*/
@@ -628,7 +629,7 @@ public class ToolScaffolder {
baseName, interfaceId
);
converterContent = mciConverterContent(bizPackage, baseName, mciGroupPath.replace("/", "."), interfaceId);
Files.writeString(converterDir.resolve(baseName + "Converter.java"), converterContent);
writeUtf8(converterDir.resolve(baseName + "Converter.java"), converterContent);
log.append("\n=========================================\n");
log.append(" Scaffolding Complete! (Routing: " + routingType + ")\n");
@@ -653,14 +654,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>
*/
@@ -677,7 +678,7 @@ public class ToolScaffolder {
BASE_PACKAGE, mciGroupPath.replace("/", "."),
BASE_PACKAGE, mciGroupPath.replace("/", "."), clientPrefixCap, author, createDate, createDate, author, clientPrefixCap
);
Files.writeString(mciClientDir.resolve("Mci" + clientPrefixCap + "Client.java"), mciClientContent);
writeUtf8(mciClientDir.resolve("Mci" + clientPrefixCap + "Client.java"), mciClientContent);
log.append("[MCI Client] ").append(mciClientDir.resolve("Mci" + clientPrefixCap + "Client.java")).append("\n");
}
@@ -687,13 +688,13 @@ public class ToolScaffolder {
String httpResponseClass = baseName + "HttpResponse";
String httpClientClass = httpApiClass + "Client";
Files.writeString(httpIoDir.resolve(httpRequestClass + ".java"),
writeUtf8(httpIoDir.resolve(httpRequestClass + ".java"),
dtoContent(httpPackage + ".io", httpRequestClass, inputFields, author, createDate, true));
Files.writeString(httpIoDir.resolve(httpResponseClass + ".java"),
writeUtf8(httpIoDir.resolve(httpResponseClass + ".java"),
dtoContent(httpPackage + ".io", httpResponseClass, outputFields, author, createDate, false));
Files.writeString(httpClientDir.resolve(httpClientClass + ".java"),
writeUtf8(httpClientDir.resolve(httpClientClass + ".java"),
httpClientContent(httpPackage, httpClientClass, httpApiName));
Files.writeString(converterDir.resolve(baseName + "Converter.java"),
writeUtf8(converterDir.resolve(baseName + "Converter.java"),
httpConverterContent(bizPackage, baseName, httpPackage));
log.append("\n=========================================\n");
@@ -716,32 +717,32 @@ 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;
}
""".formatted(bizPackage, bizPackage, baseName, author, createDate, createDate, author, baseName);
legacyReqContent = dtoContent(bizPackage + ".legacy", baseName + "LegacyRequest", inputFields, author, createDate, true);
Files.writeString(legacyDtoDir.resolve(baseName + "LegacyRequest.java"), legacyReqContent);
writeUtf8(legacyDtoDir.resolve(baseName + "LegacyRequest.java"), legacyReqContent);
String legacyResContent = """
package %s.legacy;
@@ -751,14 +752,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>
*/
@@ -768,7 +769,7 @@ public class ToolScaffolder {
}
""".formatted(bizPackage, bizPackage, baseName, author, createDate, createDate, author, baseName);
legacyResContent = dtoContent(bizPackage + ".legacy", baseName + "LegacyResponse", outputFields, author, createDate, true);
Files.writeString(legacyDtoDir.resolve(baseName + "LegacyResponse.java"), legacyResContent);
writeUtf8(legacyDtoDir.resolve(baseName + "LegacyResponse.java"), legacyResContent);
String converterContent = """
package %s.converter;
@@ -784,14 +785,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>
*/
@@ -818,7 +819,7 @@ public class ToolScaffolder {
baseName, baseName, baseName, baseName, baseName, baseName, baseName
);
converterContent = legacyConverterContent(bizPackage, baseName);
Files.writeString(converterDir.resolve(baseName + "Converter.java"), converterContent);
writeUtf8(converterDir.resolve(baseName + "Converter.java"), converterContent);
log.append("\n=========================================\n");
log.append(" Scaffolding Complete! (Routing: " + routingType + ")\n");
@@ -831,7 +832,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 = """
@@ -841,7 +842,7 @@ public class ToolScaffolder {
"properties": {
"TODO_FIELD": {
"type": "string",
"description": "TODO: ?뚮씪誘명꽣 ?ㅻ챸???낅젰?섏꽭??"
"description": "TODO: 파라미터 설명을 입력하세요."
}
},
"required": []
@@ -854,19 +855,19 @@ public class ToolScaffolder {
"properties": {
"status": {
"type": "string",
"description": "泥섎━ 寃곌낵 ?곹깭 (SUCCESS / FAILURE)",
"description": "처리 결과 상태 (SUCCESS / FAILURE)",
"enum": ["SUCCESS", "FAILURE"]
},
"message": {
"type": "string",
"description": "泥섎━ 寃곌낵 硫붿떆吏€"
"description": "처리 결과 메시지"
}
},
"required": ["status"]
}
""";
Files.writeString(schemaDir.resolve(inputSchemaFileName), inputSchema);
Files.writeString(schemaDir.resolve(outputSchemaFileName), outputSchema);
writeUtf8(schemaDir.resolve(inputSchemaFileName), inputSchema);
writeUtf8(schemaDir.resolve(outputSchemaFileName), outputSchema);
log.append("[Input Schema] ").append(schemaDir.resolve(inputSchemaFileName)).append("\n");
log.append("[Output Schema] ").append(schemaDir.resolve(outputSchemaFileName)).append("\n");
}
@@ -881,9 +882,9 @@ public class ToolScaffolder {
Files.createDirectories(wireMockBodyPath.getParent());
Files.createDirectories(wireMockMappingPath.getParent());
Files.createDirectories(podMockResponsePath.getParent());
Files.writeString(wireMockBodyPath, mockResponse);
Files.writeString(wireMockMappingPath, wireMockMappingContent(interfaceId, wireMockBodyPath.getFileName().toString()));
Files.writeString(podMockResponsePath, mockResponse);
writeUtf8(wireMockBodyPath, mockResponse);
writeUtf8(wireMockMappingPath, wireMockMappingContent(interfaceId, wireMockBodyPath.getFileName().toString()));
writeUtf8(podMockResponsePath, mockResponse);
ensureLocalHttpApiConfiguration(projectRoot, httpApiName, toolName);
log.append("[WireMock Response] ").append(wireMockBodyPath).append("\n");
log.append("[WireMock Mapping] ").append(wireMockMappingPath).append("\n");
@@ -891,19 +892,19 @@ public class ToolScaffolder {
} else {
Path mockResponsePath = rootDir.resolve(Paths.get(moduleName, "src/main/resources/mock-responses", toolName + ".json"));
Files.createDirectories(mockResponsePath.getParent());
Files.writeString(mockResponsePath, mockResponse);
writeUtf8(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));
writeUtf8(generatedTestPath, useCaseTestContent(bizPackage, baseName));
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");
Files.createDirectories(definitionDir);
Path definitionPath = definitionDir.resolve(toolName + ".yml");
Files.writeString(definitionPath, toolDefinitionContentV17(toolName, title, description, group,
writeUtf8(definitionPath, toolDefinitionContentV17(toolName, title, description, group,
interfaceId, inputFields, isMutationTool(baseName), definitionOptions));
log.append("[V17 Tool Definition] ").append(definitionPath).append("\n");
log.append("\n Tip: HTTP Tool은 WireMock 실행 후 생성된 mapping URL로 호출을 확인하세요.\n");
@@ -1089,7 +1090,7 @@ public class ToolScaffolder {
private static void ensureLocalHttpApiConfiguration(Path projectRoot, String httpApiName, String toolName) throws IOException {
Path localConfigPath = projectRoot.resolve("dap-was-lib/src/main/resources/glow/application-glow-local.yml");
Files.createDirectories(localConfigPath.getParent());
String existing = Files.exists(localConfigPath) ? Files.readString(localConfigPath) : "";
String existing = Files.exists(localConfigPath) ? Files.readString(localConfigPath, StandardCharsets.UTF_8) : "";
if (java.util.regex.Pattern.compile("(?m)^\\s*-\\s+name:\\s*"
+ java.util.regex.Pattern.quote(httpApiName) + "\\s*$").matcher(existing).find()) {
return;
@@ -1133,7 +1134,11 @@ public class ToolScaffolder {
enabled: true
""";
}
Files.writeString(localConfigPath, existing);
writeUtf8(localConfigPath, existing);
}
private static void writeUtf8(Path path, String content) throws IOException {
Files.writeString(path, content, StandardCharsets.UTF_8);
}
private static String dtoContent(String packageName, String className, List<FieldDefinition> fields,

View File

@@ -4,6 +4,7 @@ import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.charset.StandardCharsets;
import java.util.Comparator;
/** Updates MCP SDK and project-owned metadata in a generated tool source file. */
@@ -27,10 +28,10 @@ public final class ToolSourceUpdater {
throw new IllegalArgumentException("Tool source not found: " + toolName);
}
String content = Files.readString(targetFile);
String content = Files.readString(targetFile, StandardCharsets.UTF_8);
content = updateMcpTool(content, toolName, description);
content = updateToolHint(content, categoryKey, register, requiresApproval);
Files.writeString(targetFile, content);
Files.writeString(targetFile, content, StandardCharsets.UTF_8);
}
private static Path findToolSource(Path rootDirectory, String toolName) throws IOException {
@@ -47,7 +48,7 @@ public final class ToolSourceUpdater {
private static boolean containsMcpTool(Path path, String toolName) {
try {
return annotationArguments(Files.readString(path), "McpTool", toolName) != null;
return annotationArguments(Files.readString(path, StandardCharsets.UTF_8), "McpTool", toolName) != null;
} catch (IOException exception) {
throw new IllegalStateException("Failed to read tool source: " + path, exception);
}

View File

@@ -10,12 +10,53 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.charset.StandardCharsets;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
class ToolScaffolderTest {
@Test
void generatesEveryToolSourceAsUtf8WithoutBrokenKoreanOrBom() throws Exception {
String moduleName = root.resolve("dap-was-korean").toString();
ToolScaffolder.scaffold("analysis data query", "CLYMCI00001", "분석 데이터 조회",
"분석 데이터를 조회하기 위한 도구로 다양한 분석 결과를 제공합니다.",
"cmm", "MCI", moduleName, "테스터", "2026.08.12",
false, null, null, null,
List.of(new ToolScaffolder.FieldDefinition("query", "String", "조회 조건", "계약 분석", true)),
List.of(new ToolScaffolder.FieldDefinition("analysisResult", "String", "분석 결과", "정상", false)));
Path sourceRoot = root.resolve("dap-was-korean/src/main/java/io/shinhanlife/dap/mcc");
Path useCase = sourceRoot.resolve("biz/cmm/usecase/AnalysisDataQueryUseCase.java");
Path implementation = sourceRoot.resolve("biz/cmm/usecase/impl/AnalysisDataQueryUseCaseImpl.java");
String useCaseSource = Files.readString(useCase, StandardCharsets.UTF_8);
String implementationSource = Files.readString(implementation, StandardCharsets.UTF_8);
assertTrue(useCaseSource.contains("title = \"분석 데이터 조회\""), useCaseSource);
assertTrue(useCaseSource.contains("description = \"분석 데이터를 조회하기 위한 도구로 다양한 분석 결과를 제공합니다.\""), useCaseSource);
assertTrue(useCaseSource.contains("AX HUB 시스템 처리 클래스"), useCaseSource);
assertTrue(useCaseSource.contains("개정이력"), useCaseSource);
assertTrue(useCaseSource.contains("최초생성"), useCaseSource);
assertTrue(implementationSource.contains("요청 수신"), implementationSource);
assertTrue(implementationSource.contains("MapStruct를 이용한 자동 매핑"), implementationSource);
assertTrue(implementationSource.contains("연동 중 오류 발생"), implementationSource);
try (var files = Files.walk(root.resolve("dap-was-korean"))) {
for (Path file : files.filter(Files::isRegularFile).toList()) {
byte[] bytes = Files.readAllBytes(file);
assertFalse(bytes.length >= 3
&& (bytes[0] & 0xff) == 0xef
&& (bytes[1] & 0xff) == 0xbb
&& (bytes[2] & 0xff) == 0xbf,
"UTF-8 BOM must not be generated: " + file);
String content = Files.readString(file, StandardCharsets.UTF_8);
assertFalse(content.contains("<EFBFBD>"), "Invalid replacement character: " + file);
}
}
}
@Test
void generatesV17ToolDefinitionTogetherWithToolSources() throws Exception {
String moduleName = root.resolve("dap-was-v17-definition").toString();

View File

@@ -8,14 +8,14 @@ import io.shinhanlife.dap.mcc.biz.ins.dto.InsuranceClaimProcessorResponse;
/**
* @package io.shinhanlife.dap.mcc.biz.ins.usecase
* @className InsuranceClaimProcessorUseCase
* @description AX HUB ?쒖뒪??泥섎━ ?대옒??
* @description AX HUB 시스템 처리 클래스
* @author Admin
* @create 2026.08.11
* <pre>
* ---------- 媛쒖젙?대젰 ----------
* ?섏젙?? ?섏젙?? ?섏젙?댁슜
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.08.11 Admin 理쒖큹?앹꽦
* 2026.08.11 Admin 최초생성
*
* </pre>
*/

View File

@@ -1,4 +1,4 @@
# OCI ?대씪?곕뱶 ?섍꼍 ?꾩슜 ?ㅼ젙
# OCI 클라우드 개발 환경 전용 설정
server:
port: ${PORT:8084}

View File

@@ -8,14 +8,14 @@ import io.shinhanlife.dap.mcc.biz.cmm.dto.MemoListRetrieverResponse;
/**
* @package io.shinhanlife.dap.mcc.biz.cmm.usecase
* @className MemoListRetrieverUseCase
* @description AX HUB ?쒖뒪??泥섎━ ?대옒??
* @description AX HUB 시스템 처리 클래스
* @author Admin
* @create 2026.08.11
* <pre>
* ---------- 媛쒖젙?대젰 ----------
* ?섏젙?? ?섏젙?? ?섏젙?댁슜
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.08.11 Admin 理쒖큹?앹꽦
* 2026.08.11 Admin 최초생성
*
* </pre>
*/

View File

@@ -1,4 +1,4 @@
# OCI ?대씪?곕뱶 ?섍꼍 ?꾩슜 ?ㅼ젙
# OCI 클라우드 개발 환경 전용 설정
server:
port: ${PORT:8082}