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 f04348194..c70b37099 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 @@ -13,15 +13,16 @@ import java.util.List; import java.util.Locale; import java.util.Set; import java.util.Scanner; +import java.util.stream.Collectors; /** * MCP Tool 코드를 자동 생성(Scaffolding)하는 유틸리티 클래스입니다. - * + * * [실행 방법] * 방법 1. IDE(IntelliJ 등)에서 직접 실행 * - ToolScaffolder.java의 main 메서드를 실행합니다. * - 콘솔 질문에 차례대로 값을 입력하면 파일이 생성됩니다. - * + * * 방법 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] "[설명]" "[그룹]" "[통신방식]" "[모듈명]" @@ -37,7 +38,7 @@ import java.util.Scanner; * 수정일 수정자 수정내용 * ---------- -------- --------------------------- * 2026.09.01 0986406 최초생성 - * + * * */ public class ToolScaffolder { @@ -79,8 +80,11 @@ public class ToolScaffolder { String group = tools.getFirst().group().toLowerCase(Locale.ROOT); validateToolMethods(tools, group); - String sourceDir = System.getenv("AXHUB_SOURCE_DIR"); - Path rootDir = sourceDir == null ? Paths.get(".") : Paths.get(sourceDir); + String sourceDir = System.getProperty("AXHUB_SOURCE_DIR"); + if (sourceDir == null) { + sourceDir = System.getenv("AXHUB_SOURCE_DIR"); + } + Path rootDir = sourceDir != null ? Paths.get(sourceDir) : Paths.get("."); Path configuredModule = Paths.get(moduleName); Path moduleRoot = configuredModule.isAbsolute() ? configuredModule : rootDir.resolve(configuredModule); Path sourceRoot = moduleRoot.resolve(BASE_PACKAGE_PATH); @@ -199,9 +203,6 @@ public class ToolScaffolder { } String toolName = toToolName(moduleName, tool.group(), baseName); - writeUtf8(definitionDir.resolve(toolName + ".yml"), toolDefinitionContentV17(toolName, - option(tool.title(), baseName), tool.description(), tool.group(), tool.interfaceId(), - tool.inputFields(), isMutationTool(baseName), tool.definitionOptions())); writeUtf8(mockDir.resolve(toolName + ".json"), mockResponseContent(tool.outputFields())); log.append("[Tool] ").append(toolName).append(" -> ").append(clientDir.resolve(baseName + "Client.java")).append("\n"); } @@ -214,12 +215,43 @@ public class ToolScaffolder { String baseName = toPascalCase(tool.baseName()); imports.append("import ").append(bizPackage).append(".dto.").append(baseName).append("Request;\n") .append("import ").append(bizPackage).append(".dto.").append(baseName).append("Response;\n"); + boolean isMutation = isMutationTool(baseName); + ToolDefinitionOptions opts = tool.definitionOptions() == null ? new ToolDefinitionOptions(null, null, null, null, null, null, null, null) : tool.definitionOptions(); + methods.append(" @McpTool(name = \"").append(toToolName(moduleName, tool.group(), baseName)) .append("\", title = \"").append(javaText(option(tool.title(), baseName))) .append("\", description = \"").append(javaText(option(tool.description(), ""))).append("\")\n") - .append(" @GrowToolHint(register = ").append(tool.register()).append(", categoryKey = \"") - .append(tool.group().toLowerCase(Locale.ROOT)).append("\", mappingId = \"") - .append(javaText(tool.interfaceId())).append("\")\n") + .append(" @GrowToolHint(\n") + .append(" requiresApproval = ").append(isMutation).append(",\n") + .append(" categoryKey = \"").append(tool.group().toLowerCase(Locale.ROOT)).append("\",\n"); + if (tool.interfaceId() != null && !tool.interfaceId().isBlank()) { + methods.append(" mappingId = \"").append(javaText(tool.interfaceId())).append("\",\n"); + } + methods.append(" functionDescription = \"").append(javaText(opts.functionDescription() != null && !opts.functionDescription().isBlank() ? opts.functionDescription() : option(tool.title(), baseName) + " 기능을 수행합니다.")).append("\",\n") + .append(" whenToUse = \"").append(javaText(opts.whenToUse() != null && !opts.whenToUse().isBlank() ? opts.whenToUse() : "사용자가 이 업무 기능의 실행 또는 조회를 요청할 때 사용합니다.")).append("\",\n") + .append(" whenNotToUse = \"").append(javaText(opts.whenNotToUse() != null && !opts.whenNotToUse().isBlank() ? opts.whenNotToUse() : "정보 변경이나 실행 작업에는 사용하지 않습니다.")).append("\",\n") + .append(" ioLimits = \"").append(javaText(opts.ioLimits() != null && !opts.ioLimits().isBlank() ? opts.ioLimits() : "정의된 입력 항목만 허용하며 업무 결과만 반환합니다.")).append("\",\n") + .append(" displayDescription = \"").append(javaText(opts.displayDescription() != null && !opts.displayDescription().isBlank() ? opts.displayDescription() : option(tool.title(), baseName) + " 정보를 처리합니다.")).append("\",\n"); + + List examples = opts.exampleQueries(); + if (examples == null || examples.isEmpty()) { + examples = List.of(option(tool.title(), baseName) + " 정보를 보여줘", option(tool.title(), baseName) + " 확인해줘", "현재 " + option(tool.title(), baseName) + " 알려줘"); + } + methods.append(" exampleQueries = {") + .append(examples.stream().map(q -> "\"" + javaText(q) + "\"").collect(Collectors.joining(", "))) + .append("},\n") + .append(" destructive = ").append(isMutation).append(",\n") + .append(" idempotent = ").append(!isMutation).append(",\n"); + + List tags = opts.tags(); + if (tags == null || tags.isEmpty()) { + tags = List.of(tool.group().toLowerCase(Locale.ROOT), isMutation ? "처리" : "조회"); + } + methods.append(" tags = {") + .append(tags.stream().map(t -> "\"" + javaText(t) + "\"").collect(Collectors.joining(", "))) + .append("},\n") + .append(" ownerOrg = \"").append(javaText(opts.ownerOrg() != null && !opts.ownerOrg().isBlank() ? opts.ownerOrg() : "MCP_TOOL")).append("\"\n") + .append(" )\n") .append(" ").append(baseName).append("Response ").append(tool.methodName()).append("(") .append(baseName).append("Request req);\n\n"); } @@ -327,11 +359,47 @@ public class ToolScaffolder { useCase = addImport(useCase, "import " + bizPackage + ".dto." + requestType + ";") ; useCase = addImport(useCase, "import " + bizPackage + ".dto." + responseType + ";") ; - String declaration = "\n @McpTool(name = \"" + toolName + "\", title = \"" + javaText(option(tool.title(), baseName)) - + "\", description = \"" + javaText(option(tool.description(), "")) + "\")\n" - + " @GrowToolHint(register = " + tool.register() + ", categoryKey = \"" + tool.group().toLowerCase(Locale.ROOT) - + "\", mappingId = \"" + javaText(option(tool.interfaceId(), tool.httpApiName())) + "\")\n" - + " " + responseType + " " + methodName + "(" + requestType + " req);\n"; + boolean isMutation = isMutationTool(baseName); + ToolDefinitionOptions opts = tool.definitionOptions() == null ? new ToolDefinitionOptions(null, null, null, null, null, null, null, null) : tool.definitionOptions(); + + StringBuilder declBuilder = new StringBuilder("\n @McpTool(name = \"").append(toolName) + .append("\", title = \"").append(javaText(option(tool.title(), baseName))) + .append("\", description = \"").append(javaText(option(tool.description(), ""))).append("\")\n") + .append(" @GrowToolHint(\n") + .append(" requiresApproval = ").append(isMutation).append(",\n") + .append(" categoryKey = \"").append(tool.group().toLowerCase(Locale.ROOT)).append("\",\n"); + String mappingId = option(tool.interfaceId(), tool.httpApiName()); + if (mappingId != null && !mappingId.isBlank()) { + declBuilder.append(" mappingId = \"").append(javaText(mappingId)).append("\",\n"); + } + declBuilder.append(" functionDescription = \"").append(javaText(opts.functionDescription() != null && !opts.functionDescription().isBlank() ? opts.functionDescription() : option(tool.title(), baseName) + " 기능을 수행합니다.")).append("\",\n") + .append(" whenToUse = \"").append(javaText(opts.whenToUse() != null && !opts.whenToUse().isBlank() ? opts.whenToUse() : "사용자가 이 업무 기능의 실행 또는 조회를 요청할 때 사용합니다.")).append("\",\n") + .append(" whenNotToUse = \"").append(javaText(opts.whenNotToUse() != null && !opts.whenNotToUse().isBlank() ? opts.whenNotToUse() : "정보 변경이나 실행 작업에는 사용하지 않습니다.")).append("\",\n") + .append(" ioLimits = \"").append(javaText(opts.ioLimits() != null && !opts.ioLimits().isBlank() ? opts.ioLimits() : "정의된 입력 항목만 허용하며 업무 결과만 반환합니다.")).append("\",\n") + .append(" displayDescription = \"").append(javaText(opts.displayDescription() != null && !opts.displayDescription().isBlank() ? opts.displayDescription() : option(tool.title(), baseName) + " 정보를 처리합니다.")).append("\",\n"); + + List examples = opts.exampleQueries(); + if (examples == null || examples.isEmpty()) { + examples = List.of(option(tool.title(), baseName) + " 정보를 보여줘", option(tool.title(), baseName) + " 확인해줘", "현재 " + option(tool.title(), baseName) + " 알려줘"); + } + declBuilder.append(" exampleQueries = {") + .append(examples.stream().map(q -> "\"" + javaText(q) + "\"").collect(Collectors.joining(", "))) + .append("},\n") + .append(" destructive = ").append(isMutation).append(",\n") + .append(" idempotent = ").append(!isMutation).append(",\n"); + + List tags = opts.tags(); + if (tags == null || tags.isEmpty()) { + tags = List.of(tool.group().toLowerCase(Locale.ROOT), isMutation ? "처리" : "조회"); + } + declBuilder.append(" tags = {") + .append(tags.stream().map(t -> "\"" + javaText(t) + "\"").collect(Collectors.joining(", "))) + .append("},\n") + .append(" ownerOrg = \"").append(javaText(opts.ownerOrg() != null && !opts.ownerOrg().isBlank() ? opts.ownerOrg() : "MCP_TOOL")).append("\"\n") + .append(" )\n") + .append(" ").append(responseType).append(" ").append(methodName).append("(").append(requestType).append(" req);\n"); + + String declaration = declBuilder.toString(); useCase = insertBeforeLastBrace(useCase, declaration); implementation = addImport(implementation, "import " + bizPackage + ".dto." + requestType + ";"); @@ -456,6 +524,12 @@ public class ToolScaffolder { 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 defaultWorkspace = "c:\\eGovFrameDev-4.3.1-64bit\\workspace-egov\\dap-was-dapmt"; + String workspace = getOrAsk(args, 10, scanner, "11. 대상 프로젝트 워크스페이스 경로 (default: " + defaultWorkspace + "): "); + if (workspace.trim().isEmpty()) { + workspace = defaultWorkspace; + } + System.setProperty("AXHUB_SOURCE_DIR", workspace); String result = scaffold(baseName, interfaceId, title, description, group, routingType, moduleName, author, createDate, false, null, inputSchemaResource, outputSchemaResource, List.of(new FieldDefinition("query", "String", "Search query", List.of("example"), "", false)), List.of()); System.out.println(result); @@ -513,9 +587,12 @@ public class ToolScaffolder { title = title == null || title.isBlank() ? baseName : title.trim(); description = description == null ? "" : description.trim(); httpApiName = httpApiName == null || httpApiName.isBlank() ? toKebabCase(baseName) : httpApiName.trim(); - String envSourceDir = System.getenv("AXHUB_SOURCE_DIR"); + String envSourceDir = System.getProperty("AXHUB_SOURCE_DIR"); + if (envSourceDir == null) { + envSourceDir = System.getenv("AXHUB_SOURCE_DIR"); + } Path rootDir = envSourceDir != null ? Paths.get(envSourceDir) : Paths.get("."); - + Path usecaseDir = rootDir.resolve(Paths.get(moduleName, BASE_PACKAGE_PATH, "biz", group.toLowerCase(), "usecase")); Path usecaseImplDir = usecaseDir.resolve("impl"); Path dtoDir = rootDir.resolve(Paths.get(moduleName, BASE_PACKAGE_PATH, "biz", group.toLowerCase(), "dto")); @@ -654,14 +731,45 @@ public class ToolScaffolder { String toolName = toToolName(moduleName, group, baseName); - String toolHintLine; + boolean isMutation = isMutationTool(baseName); + StringBuilder sb = new StringBuilder(" @GrowToolHint(\n"); if (useSchemaResource) { - toolHintLine = (" @GrowToolHint(register = %s, categoryKey = \"%s\", mappingId = \"%s\",\n" + - " inputSchemaResource = \"%s\",\n" + - " outputSchemaResource = \"%s\")").formatted(register, group.toLowerCase(Locale.ROOT), interfaceId, inputSchemaClasspath, outputSchemaClasspath); - } else { - toolHintLine = " @GrowToolHint(register = %s, categoryKey = \"%s\", mappingId = \"%s\")".formatted(register, group.toLowerCase(Locale.ROOT), interfaceId); + sb.append(" inputSchemaResource = \"").append(inputSchemaClasspath).append("\",\n"); + sb.append(" outputSchemaResource = \"").append(outputSchemaClasspath).append("\",\n"); } + sb.append(" requiresApproval = ").append(isMutation).append(",\n"); + sb.append(" categoryKey = \"").append(group.toLowerCase(Locale.ROOT)).append("\",\n"); + if (interfaceId != null && !interfaceId.isBlank()) { + sb.append(" mappingId = \"").append(interfaceId).append("\",\n"); + } + sb.append(" functionDescription = \"").append(javaText(definitionOptions.functionDescription() != null && !definitionOptions.functionDescription().isBlank() ? definitionOptions.functionDescription() : title + " 기능을 수행합니다.")).append("\",\n"); + sb.append(" whenToUse = \"").append(javaText(definitionOptions.whenToUse() != null && !definitionOptions.whenToUse().isBlank() ? definitionOptions.whenToUse() : "사용자가 이 업무 기능의 실행 또는 조회를 요청할 때 사용합니다.")).append("\",\n"); + sb.append(" whenNotToUse = \"").append(javaText(definitionOptions.whenNotToUse() != null && !definitionOptions.whenNotToUse().isBlank() ? definitionOptions.whenNotToUse() : "정보 변경이나 실행 작업에는 사용하지 않습니다.")).append("\",\n"); + sb.append(" ioLimits = \"").append(javaText(definitionOptions.ioLimits() != null && !definitionOptions.ioLimits().isBlank() ? definitionOptions.ioLimits() : "정의된 입력 항목만 허용하며 업무 결과만 반환합니다.")).append("\",\n"); + sb.append(" displayDescription = \"").append(javaText(definitionOptions.displayDescription() != null && !definitionOptions.displayDescription().isBlank() ? definitionOptions.displayDescription() : title + " 정보를 처리합니다.")).append("\",\n"); + + List examples = definitionOptions.exampleQueries(); + if (examples == null || examples.isEmpty()) { + examples = List.of(title + " 정보를 보여줘", title + " 확인해줘", "현재 " + title + " 알려줘"); + } + sb.append(" exampleQueries = {") + .append(examples.stream().map(q -> "\"" + javaText(q) + "\"").collect(Collectors.joining(", "))) + .append("},\n"); + + sb.append(" destructive = ").append(isMutation).append(",\n"); + sb.append(" idempotent = ").append(!isMutation).append(",\n"); + + List tags = definitionOptions.tags(); + if (tags == null || tags.isEmpty()) { + tags = List.of(group.toLowerCase(Locale.ROOT), isMutation ? "처리" : "조회"); + } + sb.append(" tags = {") + .append(tags.stream().map(t -> "\"" + javaText(t) + "\"").collect(Collectors.joining(", "))) + .append("},\n"); + + sb.append(" ownerOrg = \"").append(javaText(definitionOptions.ownerOrg() != null && !definitionOptions.ownerOrg().isBlank() ? definitionOptions.ownerOrg() : "MCP_TOOL")).append("\"\n"); + sb.append(" )"); + String toolHintLine = sb.toString(); String serviceInterfaceContent = """ package %s.usecase; @@ -700,7 +808,7 @@ public class ToolScaffolder { toolName, title, description, toolHintLine, baseName, baseName - ); + ); writeUtf8(usecaseDir.resolve(baseName + "UseCase.java"), serviceInterfaceContent); @@ -798,7 +906,7 @@ public class ToolScaffolder { baseName, baseName, baseName - ); + ); String mciIoPackage = BASE_PACKAGE + "." + mciGroupPath.replace("/", "."); serviceImplContent = serviceImplContent .replace("import " + mciIoPackage + ".io." + interfaceId + "_I;", @@ -874,9 +982,9 @@ public class ToolScaffolder { baseName, baseName, routingType, interfaceId - ); + ); } - + writeUtf8(usecaseImplDir.resolve(baseName + "UseCaseImpl.java"), serviceImplContent); if (isMci) { @@ -992,10 +1100,10 @@ public class ToolScaffolder { baseName, interfaceId, baseName, baseName, interfaceId, baseName, interfaceId - ); + ); converterContent = mciConverterContent(bizPackage, baseName, mciGroupPath.replace("/", "."), interfaceId); writeUtf8(converterDir.resolve(baseName + "Converter.java"), converterContent); - + log.append("\n=========================================\n"); log.append(" Scaffolding Complete! (Routing: " + routingType + ")\n"); log.append("=========================================\n"); @@ -1040,9 +1148,9 @@ public class ToolScaffolder { } } """.formatted( - BASE_PACKAGE, mciGroupPath.replace("/", "."), + BASE_PACKAGE, mciGroupPath.replace("/", "."), BASE_PACKAGE, mciGroupPath.replace("/", "."), clientPrefixCap, author, createDate, createDate, author, clientPrefixCap - ); + ); writeUtf8(mciClientDir.resolve("Mci" + clientPrefixCap + "Client.java"), mciClientContent); log.append("[MCI Client] ").append(mciClientDir.resolve("Mci" + clientPrefixCap + "Client.java")).append("\n"); } @@ -1184,7 +1292,7 @@ public class ToolScaffolder { bizPackage, baseName, bizPackage, baseName, author, createDate, createDate, author, baseName, baseName, baseName, baseName, baseName, baseName, baseName - ); + ); converterContent = legacyConverterContent(bizPackage, baseName); writeUtf8(converterDir.resolve(baseName + "Converter.java"), converterContent); @@ -1269,11 +1377,8 @@ public class ToolScaffolder { 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"); - writeUtf8(definitionPath, toolDefinitionContentV17(toolName, title, description, group, - interfaceId, inputFields, isMutationTool(baseName), definitionOptions)); - log.append("[V17 Tool Definition] ").append(definitionPath).append("\n"); + // Tool Definition YML is no longer generated. We use @GrowToolHint instead. + // log.append("[V17 Tool Definition] ...\n"); log.append("\n Tip: HTTP Tool은 WireMock 실행 후 생성된 mapping URL로 호출을 확인하세요.\n"); return log.toString(); @@ -1442,9 +1547,6 @@ public class ToolScaffolder { properties.append(" ").append(field.name().trim()).append(":\n") .append(" type: ").append(jsonSchemaType(field.type())).append("\n") .append(" description: ").append(yamlText(richDescription(field))).append("\n"); - if (field.pattern() != null && !field.pattern().isBlank()) { - properties.append(" pattern: ").append(yamlText(field.pattern())).append("\n"); - } if ("Enum".equals(field.type()) && field.enumValues() != null && !field.enumValues().isEmpty()) { properties.append(" enum: [").append(field.enumValues().stream() .filter(value -> value != null && !value.isBlank()).map(String::trim) @@ -1459,9 +1561,6 @@ public class ToolScaffolder { properties.append(" ").append(itemField.name()).append(":\n") .append(" type: ").append(jsonSchemaType(itemField.type())).append("\n") .append(" description: ").append(yamlText(richDescription(itemField))).append("\n"); - if (itemField.pattern() != null && !itemField.pattern().isBlank()) { - properties.append(" pattern: ").append(yamlText(itemField.pattern())).append("\n"); - } } } } @@ -1675,7 +1774,7 @@ public class ToolScaffolder { } private static String httpUseCaseImplContent(String bizPackage, String baseName, String httpPackage, - String httpApiClass, String author, String createDate) { + String httpApiClass, String author, String createDate) { String httpRequestClass = baseName + "HttpRequest"; String httpResponseClass = baseName + "HttpResponse"; String httpClientClass = httpApiClass + "Client"; @@ -1900,10 +1999,8 @@ public class ToolScaffolder { return supportedType(itemType); } private static String supportedType(String type) { - return switch (type == null ? "String" : type) { - case "String", "Integer", "Long", "Double", "Boolean", "BigDecimal" -> type; - default -> throw new IllegalArgumentException("Unsupported field type: " + type); - }; + if (type == null || type.isBlank()) return "String"; + return type; } @@ -1957,7 +2054,7 @@ public class ToolScaffolder { FieldDefinition item = new FieldDefinition("item", field.itemType(), "", field.examples(), field.pattern(), false); return "[" + mockValue(item) + "]"; } - + String exampleStr = (field.examples() != null && !field.examples().isEmpty()) ? field.examples().get(0) : null; if (exampleStr == null || exampleStr.isBlank()) { return "null";