fix: resolve workspace path resolution and enhance GrowToolHint generation in ToolScaffolder
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 1m8s

This commit is contained in:
jade
2026-08-19 13:50:49 +09:00
parent d5a07cd846
commit 870412f0c7
3 changed files with 192 additions and 51 deletions

View File

@@ -108,6 +108,13 @@ public class ScaffoldingController {
parseDelimited(req.get("tags")), parseDelimited(req.get("tags")),
req.get("ownerOrg")); req.get("ownerOrg"));
String defaultWorkspace = "c:\\eGovFrameDev-4.3.1-64bit\\workspace-egov\\dap-was-dapmt";
String workspacePath = req.getOrDefault("workspacePath", defaultWorkspace);
if (workspacePath.trim().isEmpty()) {
workspacePath = defaultWorkspace;
}
System.setProperty("AXHUB_SOURCE_DIR", workspacePath);
return ToolScaffolder.scaffold(baseName, interfaceId, title, description, group, routingType, return ToolScaffolder.scaffold(baseName, interfaceId, title, description, group, routingType,
moduleName, author, date, register, clientSystemCode, inputSchemaResource, moduleName, author, date, register, clientSystemCode, inputSchemaResource,
outputSchemaResource, inputFields, outputFields, httpApiName, definitionOptions); outputSchemaResource, inputFields, outputFields, httpApiName, definitionOptions);
@@ -129,6 +136,9 @@ public class ScaffoldingController {
? System.getProperty("user.name") : request.author().trim(); ? System.getProperty("user.name") : request.author().trim();
String date = request.date() == null || request.date().isBlank() String date = request.date() == null || request.date().isBlank()
? LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy.MM.dd")) : request.date().trim(); ? LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy.MM.dd")) : request.date().trim();
String workspacePath = request.workspacePath() != null && !request.workspacePath().isBlank()
? request.workspacePath().trim() : "c:\\eGovFrameDev-4.3.1-64bit\\workspace-egov\\dap-was-dapmt";
System.setProperty("AXHUB_SOURCE_DIR", workspacePath);
return ToolScaffolder.scaffoldUseCase(request.useCaseName().trim(), moduleName, author, date, return ToolScaffolder.scaffoldUseCase(request.useCaseName().trim(), moduleName, author, date,
request.tools() == null ? List.of() : request.tools()); request.tools() == null ? List.of() : request.tools());
} catch (Exception e) { } catch (Exception e) {
@@ -137,14 +147,15 @@ public class ScaffoldingController {
} }
@GetMapping("/usecases") @GetMapping("/usecases")
public List<String> listUseCases(@RequestParam String moduleName, @RequestParam String categoryKey) { public List<String> listUseCases(@RequestParam String moduleName, @RequestParam String categoryKey, @RequestParam(required = false) String workspacePath) {
if (moduleName == null || !moduleName.matches("^dap-was-[a-z0-9-]+$")) { if (moduleName == null || !moduleName.matches("^dap-was-[a-z0-9-]+$")) {
throw new IllegalArgumentException("Invalid target module."); throw new IllegalArgumentException("Invalid target module.");
} }
if (categoryKey == null || !categoryKey.matches("^[a-z0-9]{3}$")) { if (categoryKey == null || !categoryKey.matches("^[a-z0-9]{3}$")) {
throw new IllegalArgumentException("Invalid domain category."); throw new IllegalArgumentException("Invalid domain category.");
} }
String sourceDir = System.getenv("AXHUB_SOURCE_DIR"); String sourceDir = (workspacePath != null && !workspacePath.isBlank()) ? workspacePath : System.getProperty("AXHUB_SOURCE_DIR");
if (sourceDir == null || sourceDir.isBlank()) sourceDir = System.getenv("AXHUB_SOURCE_DIR");
if (sourceDir == null || sourceDir.isBlank()) sourceDir = System.getProperty("user.dir"); if (sourceDir == null || sourceDir.isBlank()) sourceDir = System.getProperty("user.dir");
File useCaseDir = new File(sourceDir, moduleName + "/src/main/java/io/shinhanlife/dap/mcc/biz/" File useCaseDir = new File(sourceDir, moduleName + "/src/main/java/io/shinhanlife/dap/mcc/biz/"
+ categoryKey + "/usecase"); + categoryKey + "/usecase");
@@ -156,11 +167,12 @@ public class ScaffoldingController {
} }
@GetMapping("/categories") @GetMapping("/categories")
public List<String> listCategories(@RequestParam String moduleName) { public List<String> listCategories(@RequestParam String moduleName, @RequestParam(required = false) String workspacePath) {
if (moduleName == null || !moduleName.matches("^dap-was-[a-z0-9-]+$")) { if (moduleName == null || !moduleName.matches("^dap-was-[a-z0-9-]+$")) {
throw new IllegalArgumentException("Invalid target module."); throw new IllegalArgumentException("Invalid target module.");
} }
String sourceDir = System.getenv("AXHUB_SOURCE_DIR"); String sourceDir = (workspacePath != null && !workspacePath.isBlank()) ? workspacePath : System.getProperty("AXHUB_SOURCE_DIR");
if (sourceDir == null || sourceDir.isBlank()) sourceDir = System.getenv("AXHUB_SOURCE_DIR");
if (sourceDir == null || sourceDir.isBlank()) sourceDir = System.getProperty("user.dir"); if (sourceDir == null || sourceDir.isBlank()) sourceDir = System.getProperty("user.dir");
File bizDir = new File(sourceDir, moduleName + "/src/main/java/io/shinhanlife/dap/mcc/biz"); File bizDir = new File(sourceDir, moduleName + "/src/main/java/io/shinhanlife/dap/mcc/biz");
File[] dirs = bizDir.listFiles(File::isDirectory); File[] dirs = bizDir.listFiles(File::isDirectory);
@@ -256,9 +268,10 @@ public class ScaffoldingController {
} }
@GetMapping("/modules") @GetMapping("/modules")
public List<String> listModules() { public List<String> listModules(@RequestParam(required = false) String workspacePath) {
try { try {
String sourceDir = System.getenv("AXHUB_SOURCE_DIR"); String sourceDir = (workspacePath != null && !workspacePath.isBlank()) ? workspacePath : System.getProperty("AXHUB_SOURCE_DIR");
if (sourceDir == null) sourceDir = System.getenv("AXHUB_SOURCE_DIR");
if (sourceDir == null) sourceDir = System.getProperty("user.dir"); if (sourceDir == null) sourceDir = System.getProperty("user.dir");
File dir = new File(sourceDir); File dir = new File(sourceDir);
@@ -426,7 +439,7 @@ public class ScaffoldingController {
List<ToolScaffolder.FieldDefinition> outputFields) { List<ToolScaffolder.FieldDefinition> outputFields) {
} }
private record ToolGroupRequest(String useCaseName, String moduleName, String author, String date, private record ToolGroupRequest(String useCaseName, String moduleName, String author, String date, String workspacePath,
List<ToolScaffolder.ToolMethodDefinition> tools) { List<ToolScaffolder.ToolMethodDefinition> tools) {
} }
} }

View File

@@ -825,6 +825,20 @@
<!-- Tool Creation Form --> <!-- Tool Creation Form -->
<div class="tab-pane fade" id="tool" role="tabpanel"> <div class="tab-pane fade" id="tool" role="tabpanel">
<form id="toolForm"> <form id="toolForm">
<div class="row mb-4">
<div class="col-md-12">
<label class="form-label">Workspace Path</label>
<div class="input-group">
<input type="text" class="form-control" name="workspacePath" id="workspacePathInput" placeholder="직접 폴더 경로를 입력(붙여넣기) 하거나 선택하세요" value="c:\eGovFrameDev-4.3.1-64bit\workspace-egov\dap-was-dapmt">
<button class="btn btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false" style="border-color: #3f3f46; color: #a1a1aa;">폴더 선택</button>
<ul class="dropdown-menu dropdown-menu-end dropdown-menu-dark" style="background-color: #18181b; border-color: #3f3f46;">
<li><a class="dropdown-item" href="#" onclick="const input = document.getElementById('workspacePathInput'); input.value='c:\\eGovFrameDev-4.3.1-64bit\\workspace-egov\\dap-was-dapmt'; input.dispatchEvent(new Event('change')); return false;" style="color: #f4f4f5;">dap-was-dapmt (기본)</a></li>
<li><a class="dropdown-item" href="#" onclick="const input = document.getElementById('workspacePathInput'); input.value='c:\\eGovFrameDev-4.3.1-64bit\\workspace-egov\\axhub-backend-main'; input.dispatchEvent(new Event('change')); return false;" style="color: #f4f4f5;">axhub-backend-main</a></li>
</ul>
</div>
<div class="input-hint">직접 경로를 붙여넣거나 우측 <b>[폴더 선택]</b> 버튼을 눌러 선택하세요.</div>
</div>
</div>
<div class="row mb-4"> <div class="row mb-4">
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label">Target Module</label> <label class="form-label">Target Module</label>
@@ -1521,18 +1535,27 @@
} }
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
fetch('/api/v1/scaffold/modules') const loadModules = () => {
.then(res => res.json()) const workspacePath = document.querySelector('input[name="workspacePath"]')?.value || '';
.then(modules => { fetch(`/api/v1/scaffold/modules?workspacePath=${encodeURIComponent(workspacePath)}`)
const select = document.getElementById('targetModuleSelect'); .then(res => res.json())
if (modules && modules.length > 0) { .then(modules => {
select.innerHTML = modules.map(m => `<option value="${m}" ${m === 'dap-was-cus' ? 'selected' : ''}>${m}</option>`).join(''); const select = document.getElementById('targetModuleSelect');
} if (modules && modules.length > 0) {
if (typeof loadCategoriesForSelection === 'function') { select.innerHTML = modules.map(m => `<option value="${m}" ${m === 'dap-was-cus' ? 'selected' : ''}>${m}</option>`).join('');
loadCategoriesForSelection(); }
} if (typeof loadCategoriesForSelection === 'function') {
}) loadCategoriesForSelection();
.catch(err => console.error('Failed to load modules:', err)); }
})
.catch(err => console.error('Failed to load modules:', err));
};
loadModules();
const wsInput = document.querySelector('input[name="workspacePath"]');
if (wsInput) {
wsInput.addEventListener('change', loadModules);
wsInput.addEventListener('blur', loadModules); // Typing custom path and clicking away
}
}); });
handleFormSubmit('podForm', '/api/v1/scaffold/pod'); handleFormSubmit('podForm', '/api/v1/scaffold/pod');
@@ -1603,12 +1626,13 @@
async function loadUseCasesForSelection() { async function loadUseCasesForSelection() {
const moduleName = document.getElementById('targetModuleSelect').value; const moduleName = document.getElementById('targetModuleSelect').value;
const workspacePath = document.querySelector('input[name="workspacePath"]')?.value || '';
const categoryKey = document.querySelector('#toolForm [name="categoryKey"]').value.trim(); const categoryKey = document.querySelector('#toolForm [name="categoryKey"]').value.trim();
const select = document.getElementById('toolGroupUseCaseSelect'); const select = document.getElementById('toolGroupUseCaseSelect');
select.innerHTML = '<option value="">새 UseCase 생성</option>'; select.innerHTML = '<option value="">새 UseCase 생성</option>';
if (!/^[a-z0-9]{3}$/.test(categoryKey)) return; if (!/^[a-z0-9]{3}$/.test(categoryKey)) return;
try { try {
const response = await fetch(`/api/v1/scaffold/usecases?moduleName=${encodeURIComponent(moduleName)}&categoryKey=${encodeURIComponent(categoryKey)}`); const response = await fetch(`/api/v1/scaffold/usecases?moduleName=${encodeURIComponent(moduleName)}&categoryKey=${encodeURIComponent(categoryKey)}&workspacePath=${encodeURIComponent(workspacePath)}`);
if (!response.ok) throw new Error('UseCase 목록 조회 실패'); if (!response.ok) throw new Error('UseCase 목록 조회 실패');
const useCases = await response.json(); const useCases = await response.json();
useCases.forEach(useCaseName => select.add(new Option(`기존 ${useCaseName}에 함수 추가`, useCaseName))); useCases.forEach(useCaseName => select.add(new Option(`기존 ${useCaseName}에 함수 추가`, useCaseName)));
@@ -1619,11 +1643,12 @@
async function loadCategoriesForSelection() { async function loadCategoriesForSelection() {
const moduleName = document.getElementById('targetModuleSelect').value; const moduleName = document.getElementById('targetModuleSelect').value;
const workspacePath = document.querySelector('input[name="workspacePath"]')?.value || '';
const select = document.getElementById('domainCategorySelect'); const select = document.getElementById('domainCategorySelect');
const nameInput = document.getElementById('domainCategoryName'); const nameInput = document.getElementById('domainCategoryName');
select.innerHTML = '<option value="">새 도메인 생성</option>'; select.innerHTML = '<option value="">새 도메인 생성</option>';
try { try {
const response = await fetch(`/api/v1/scaffold/categories?moduleName=${encodeURIComponent(moduleName)}`); const response = await fetch(`/api/v1/scaffold/categories?moduleName=${encodeURIComponent(moduleName)}&workspacePath=${encodeURIComponent(workspacePath)}`);
if (!response.ok) throw new Error('Category 목록 조회 실패'); if (!response.ok) throw new Error('Category 목록 조회 실패');
const categories = await response.json(); const categories = await response.json();
categories.forEach(cat => select.add(new Option(`기존 ${cat} 도메인`, cat))); categories.forEach(cat => select.add(new Option(`기존 ${cat} 도메인`, cat)));
@@ -1687,7 +1712,7 @@
fetch('/api/v1/scaffold/tool-group', { fetch('/api/v1/scaffold/tool-group', {
method: 'POST', headers: {'Content-Type': 'application/json'}, method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({useCaseName, moduleName: data.moduleName, author: data.author, date: data.date, tools}) body: JSON.stringify({useCaseName, moduleName: data.moduleName, author: data.author, date: data.date, workspacePath: data.workspacePath, tools})
}).then(response => response.text()).then(result => { }).then(response => response.text()).then(result => {
const resultBox = document.getElementById('resultBox'); const resultBox = document.getElementById('resultBox');
resultBox.style.display = 'block'; resultBox.className = result.startsWith('Error:') ? 'error' : 'success'; resultBox.style.display = 'block'; resultBox.className = result.startsWith('Error:') ? 'error' : 'success';

View File

@@ -13,6 +13,7 @@ import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Set; import java.util.Set;
import java.util.Scanner; import java.util.Scanner;
import java.util.stream.Collectors;
/** /**
* MCP Tool 코드를 자동 생성(Scaffolding)하는 유틸리티 클래스입니다. * MCP Tool 코드를 자동 생성(Scaffolding)하는 유틸리티 클래스입니다.
@@ -79,8 +80,11 @@ public class ToolScaffolder {
String group = tools.getFirst().group().toLowerCase(Locale.ROOT); String group = tools.getFirst().group().toLowerCase(Locale.ROOT);
validateToolMethods(tools, group); validateToolMethods(tools, group);
String sourceDir = System.getenv("AXHUB_SOURCE_DIR"); String sourceDir = System.getProperty("AXHUB_SOURCE_DIR");
Path rootDir = sourceDir == null ? Paths.get(".") : Paths.get(sourceDir); if (sourceDir == null) {
sourceDir = System.getenv("AXHUB_SOURCE_DIR");
}
Path rootDir = sourceDir != null ? Paths.get(sourceDir) : Paths.get(".");
Path configuredModule = Paths.get(moduleName); Path configuredModule = Paths.get(moduleName);
Path moduleRoot = configuredModule.isAbsolute() ? configuredModule : rootDir.resolve(configuredModule); Path moduleRoot = configuredModule.isAbsolute() ? configuredModule : rootDir.resolve(configuredModule);
Path sourceRoot = moduleRoot.resolve(BASE_PACKAGE_PATH); Path sourceRoot = moduleRoot.resolve(BASE_PACKAGE_PATH);
@@ -199,9 +203,6 @@ public class ToolScaffolder {
} }
String toolName = toToolName(moduleName, tool.group(), baseName); 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())); writeUtf8(mockDir.resolve(toolName + ".json"), mockResponseContent(tool.outputFields()));
log.append("[Tool] ").append(toolName).append(" -> ").append(clientDir.resolve(baseName + "Client.java")).append("\n"); 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()); String baseName = toPascalCase(tool.baseName());
imports.append("import ").append(bizPackage).append(".dto.").append(baseName).append("Request;\n") imports.append("import ").append(bizPackage).append(".dto.").append(baseName).append("Request;\n")
.append("import ").append(bizPackage).append(".dto.").append(baseName).append("Response;\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)) methods.append(" @McpTool(name = \"").append(toToolName(moduleName, tool.group(), baseName))
.append("\", title = \"").append(javaText(option(tool.title(), baseName))) .append("\", title = \"").append(javaText(option(tool.title(), baseName)))
.append("\", description = \"").append(javaText(option(tool.description(), ""))).append("\")\n") .append("\", description = \"").append(javaText(option(tool.description(), ""))).append("\")\n")
.append(" @GrowToolHint(register = ").append(tool.register()).append(", categoryKey = \"") .append(" @GrowToolHint(\n")
.append(tool.group().toLowerCase(Locale.ROOT)).append("\", mappingId = \"") .append(" requiresApproval = ").append(isMutation).append(",\n")
.append(javaText(tool.interfaceId())).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<String> 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<String> 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(" ").append(baseName).append("Response ").append(tool.methodName()).append("(")
.append(baseName).append("Request req);\n\n"); .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." + requestType + ";") ;
useCase = addImport(useCase, "import " + bizPackage + ".dto." + responseType + ";") ; useCase = addImport(useCase, "import " + bizPackage + ".dto." + responseType + ";") ;
String declaration = "\n @McpTool(name = \"" + toolName + "\", title = \"" + javaText(option(tool.title(), baseName)) boolean isMutation = isMutationTool(baseName);
+ "\", description = \"" + javaText(option(tool.description(), "")) + "\")\n" ToolDefinitionOptions opts = tool.definitionOptions() == null ? new ToolDefinitionOptions(null, null, null, null, null, null, null, null) : tool.definitionOptions();
+ " @GrowToolHint(register = " + tool.register() + ", categoryKey = \"" + tool.group().toLowerCase(Locale.ROOT)
+ "\", mappingId = \"" + javaText(option(tool.interfaceId(), tool.httpApiName())) + "\")\n" StringBuilder declBuilder = new StringBuilder("\n @McpTool(name = \"").append(toolName)
+ " " + responseType + " " + methodName + "(" + requestType + " req);\n"; .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<String> 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<String> 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); useCase = insertBeforeLastBrace(useCase, declaration);
implementation = addImport(implementation, "import " + bizPackage + ".dto." + requestType + ";"); implementation = addImport(implementation, "import " + bizPackage + ".dto." + requestType + ";");
@@ -456,6 +524,12 @@ public class ToolScaffolder {
String schemaResourceDirectory = "classpath:tool-schemas/" + group.toLowerCase() + "/"; String schemaResourceDirectory = "classpath:tool-schemas/" + group.toLowerCase() + "/";
String inputSchemaResource = useSchemaResource ? schemaResourceDirectory + toKebabCase(baseName) + "-resource-input-schema.json" : null; String inputSchemaResource = useSchemaResource ? schemaResourceDirectory + toKebabCase(baseName) + "-resource-input-schema.json" : null;
String outputSchemaResource = useSchemaResource ? schemaResourceDirectory + toKebabCase(baseName) + "-resource-output-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()); 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); System.out.println(result);
@@ -513,7 +587,10 @@ public class ToolScaffolder {
title = title == null || title.isBlank() ? baseName : title.trim(); title = title == null || title.isBlank() ? baseName : title.trim();
description = description == null ? "" : description.trim(); description = description == null ? "" : description.trim();
httpApiName = httpApiName == null || httpApiName.isBlank() ? toKebabCase(baseName) : httpApiName.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 rootDir = envSourceDir != null ? Paths.get(envSourceDir) : Paths.get(".");
Path usecaseDir = rootDir.resolve(Paths.get(moduleName, BASE_PACKAGE_PATH, "biz", group.toLowerCase(), "usecase")); Path usecaseDir = rootDir.resolve(Paths.get(moduleName, BASE_PACKAGE_PATH, "biz", group.toLowerCase(), "usecase"));
@@ -654,14 +731,45 @@ public class ToolScaffolder {
String toolName = toToolName(moduleName, group, baseName); String toolName = toToolName(moduleName, group, baseName);
String toolHintLine; boolean isMutation = isMutationTool(baseName);
StringBuilder sb = new StringBuilder(" @GrowToolHint(\n");
if (useSchemaResource) { if (useSchemaResource) {
toolHintLine = (" @GrowToolHint(register = %s, categoryKey = \"%s\", mappingId = \"%s\",\n" + sb.append(" inputSchemaResource = \"").append(inputSchemaClasspath).append("\",\n");
" inputSchemaResource = \"%s\",\n" + sb.append(" outputSchemaResource = \"").append(outputSchemaClasspath).append("\",\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(" 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<String> 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<String> 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 = """ String serviceInterfaceContent = """
package %s.usecase; package %s.usecase;
@@ -1269,11 +1377,8 @@ public class ToolScaffolder {
writeUtf8(generatedTestPath, useCaseTestContent(bizPackage, baseName)); writeUtf8(generatedTestPath, useCaseTestContent(bizPackage, baseName));
log.append("[Unit Test] ").append(generatedTestPath).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("[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); // Tool Definition YML is no longer generated. We use @GrowToolHint instead.
Path definitionPath = definitionDir.resolve(toolName + ".yml"); // log.append("[V17 Tool Definition] ...\n");
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"); log.append("\n Tip: HTTP Tool은 WireMock 실행 후 생성된 mapping URL로 호출을 확인하세요.\n");
return log.toString(); return log.toString();
@@ -1894,10 +1999,8 @@ public class ToolScaffolder {
return supportedType(itemType); return supportedType(itemType);
} }
private static String supportedType(String type) { private static String supportedType(String type) {
return switch (type == null ? "String" : type) { if (type == null || type.isBlank()) return "String";
case "String", "Integer", "Long", "Double", "Boolean", "BigDecimal" -> type; return type;
default -> throw new IllegalArgumentException("Unsupported field type: " + type);
};
} }