feat: enhance tool scaffold and usecase naming
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 0s

This commit is contained in:
jade
2026-08-12 23:42:14 +09:00
parent bd4d2a7624
commit 97d380afe0
35 changed files with 657 additions and 60 deletions

View File

@@ -39,7 +39,7 @@ import org.springframework.web.bind.annotation.*;
public class ScaffoldingController {
private static final Set<String> SUPPORTED_FIELD_TYPES = Set.of(
"String", "Integer", "Long", "Double", "Boolean", "BigDecimal");
"String", "Integer", "Long", "Double", "Boolean", "BigDecimal", "Enum", "List");
private static final Set<String> SUPPORTED_AI_MODELS = Set.of(
"inclusionai/ling-3.0-flash:free",
"openai/gpt-oss-20b:free",
@@ -116,6 +116,26 @@ public class ScaffoldingController {
}
}
@PostMapping("/tool-group")
public String scaffoldToolGroup(@RequestBody ToolGroupRequest request) {
try {
if (request == null || request.useCaseName() == null
|| !request.useCaseName().trim().matches("^[A-Z][A-Za-z0-9]*$")) {
throw new IllegalArgumentException("UseCase name must be PascalCase.");
}
String moduleName = request.moduleName() == null || request.moduleName().isBlank()
? "dap-was-oth" : request.moduleName().trim();
String author = request.author() == null || request.author().isBlank()
? System.getProperty("user.name") : request.author().trim();
String date = request.date() == null || request.date().isBlank()
? LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy.MM.dd")) : request.date().trim();
return ToolScaffolder.scaffoldUseCase(request.useCaseName().trim(), moduleName, author, date,
request.tools() == null ? List.of() : request.tools());
} catch (Exception e) {
return "Error: " + safeMessage(e);
}
}
@PostMapping("/field-draft")
public ResponseEntity<?> generateFieldDraft(@RequestBody Map<String, String> req) {
String description = req.getOrDefault("description", "").trim();
@@ -133,8 +153,9 @@ public class ScaffoldingController {
Generate Java DTO fields for an MCP tool.
Return JSON only. Do not add Markdown, explanations, or code fences.
The response must have this exact shape:
{"fields":[{"name":"camelCaseName","type":"String","description":"short description","example":"example","required":true}]}
Allowed type values: String, Integer, Long, Double, Boolean, BigDecimal.
{"fields":[{"name":"camelCaseName","type":"String","description":"short description","example":"example","required":true,"enumValues":[],"itemType":null,"itemFields":[]}]}
Allowed type values: String, Integer, Long, Double, Boolean, BigDecimal, Enum, List.
Enum fields must include enumValues. List fields must include itemType; use Object plus itemFields for object lists.
Generate fields only for the requested target: %s.
For OUTPUT fields, include resultCode and resultMessage when appropriate.
Keep field names valid Java camelCase identifiers. Generate at most 10 fields.
@@ -162,12 +183,12 @@ public class ScaffoldingController {
Generate an MCP Tool scaffold from the user request.
Return JSON only. Do not add Markdown, explanations, or code fences.
The response must have this exact shape:
{"baseName":"PascalCaseName","title":"short Korean title","description":"clear Korean LLM tool guidance","categoryKey":"cmm","routingType":"HTTP","httpApiName":"simple-api-name","functionDescription":"core business function","displayDescription":"short portal description","whenToUse":"specific user requests that should select this tool","whenNotToUse":"requests or conditions that must not select this tool","ioLimits":"allowed input and output scope and limits","exampleQueries":["query 1","query 2","query 3"],"tags":["domain","action"],"ownerOrg":"MCP_TOOL","inputFields":[{"name":"camelCaseName","type":"String","description":"short description","example":"example","required":true}],"outputFields":[{"name":"resultCode","type":"String","description":"result code","example":"SUCCESS","required":true}]}
{"baseName":"PascalCaseName","title":"short Korean title","description":"clear Korean LLM tool guidance","categoryKey":"cmm","routingType":"HTTP","httpApiName":"simple-api-name","functionDescription":"core business function","displayDescription":"short portal description","whenToUse":"specific user requests that should select this tool","whenNotToUse":"requests or conditions that must not select this tool","ioLimits":"allowed input and output scope and limits","exampleQueries":["query 1","query 2","query 3"],"tags":["domain","action"],"ownerOrg":"MCP_TOOL","inputFields":[{"name":"camelCaseName","type":"String","description":"short description","example":"example","required":true,"enumValues":[],"itemType":null,"itemFields":[]}],"outputFields":[{"name":"resultCode","type":"String","description":"result code","example":"SUCCESS","required":true,"enumValues":[],"itemType":null,"itemFields":[]}]}
categoryKey must be exactly three lowercase letters or digits.
routingType must be either HTTP or MCI. httpApiName can contain only letters, digits, hyphens, and underscores.
Write every V17 metadata field for its distinct purpose; do not copy the same sentence into all fields.
Generate 3 to 10 realistic exampleQueries and concise search tags. Use MCP_TOOL for ownerOrg unless the user names an owner.
Allowed field type values: String, Integer, Long, Double, Boolean, BigDecimal.
Allowed field type values: String, Integer, Long, Double, Boolean, BigDecimal, Enum, List. Enum must include enumValues; List must include itemType and object lists include itemFields.
Keep all field names valid Java camelCase identifiers. Generate at most 10 fields per list.
Do not generate interfaceId or clientSystemCode; those must come from a real integration contract.
User request: %s
@@ -248,7 +269,10 @@ public class ScaffoldingController {
field.type() == null ? "String" : field.type().trim(),
field.description() == null ? "" : field.description().trim(),
field.example() == null ? "" : field.example().trim(),
field.required()))
field.required(),
field.enumValues() == null ? List.of() : field.enumValues(),
field.itemType(),
field.itemFields() == null ? List.of() : field.itemFields()))
.peek(field -> {
if (!field.name().matches("^[A-Za-z_$][A-Za-z0-9_$]*$")) {
throw new IllegalArgumentException("AI가 올바르지 않은 필드명을 생성했습니다: " + field.name());
@@ -256,6 +280,7 @@ public class ScaffoldingController {
if (!SUPPORTED_FIELD_TYPES.contains(field.type())) {
throw new IllegalArgumentException("AI가 지원하지 않는 Type을 생성했습니다: " + field.type());
}
validateStructuredField(field);
if (!names.add(field.name())) {
throw new IllegalArgumentException("AI가 중복 필드명을 생성했습니다: " + field.name());
}
@@ -263,6 +288,18 @@ public class ScaffoldingController {
.toList();
}
private void validateStructuredField(ToolScaffolder.FieldDefinition field) {
if ("Enum".equals(field.type()) && field.enumValues().isEmpty()) {
throw new IllegalArgumentException("Enum field needs enumValues: " + field.name());
}
if ("List".equals(field.type()) && (field.itemType() == null || field.itemType().isBlank())) {
throw new IllegalArgumentException("List field needs itemType: " + field.name());
}
if ("List".equals(field.type()) && "Object".equals(field.itemType()) && field.itemFields().isEmpty()) {
throw new IllegalArgumentException("Object List field needs itemFields: " + field.name());
}
}
private ToolDraft validateToolDraft(ToolDraft draft) {
if (draft == null || draft.baseName() == null || !draft.baseName().trim().matches("^[A-Z][A-Za-z0-9]*$")) {
throw new IllegalArgumentException("AI가 올바르지 않은 Base Name을 생성했습니다.");
@@ -355,4 +392,8 @@ public class ScaffoldingController {
List<ToolScaffolder.FieldDefinition> inputFields,
List<ToolScaffolder.FieldDefinition> outputFields) {
}
private record ToolGroupRequest(String useCaseName, String moduleName, String author, String date,
List<ToolScaffolder.ToolMethodDefinition> tools) {
}
}

View File

@@ -824,6 +824,19 @@
<!-- Tool Creation Form -->
<div class="tab-pane fade" id="tool" role="tabpanel">
<form id="toolForm">
<div class="mb-4 p-3 rounded" style="background:#18181b; border:1px solid #3f3f46;">
<div class="d-flex justify-content-between align-items-center gap-3">
<div>
<label class="form-label mb-1">Multi Tool UseCase (MCI)</label>
<div class="input-hint mt-0">현재 Tool을 같은 UseCase에 추가하면, Tool별 Client를 호출하는 여러 MCP Tool 메서드가 생성됩니다.</div>
</div>
<button type="button" class="btn-secondary-action" onclick="addCurrentToolToGroup()">현재 Tool 묶음에 추가</button>
</div>
<div class="row g-2 mt-1">
<div class="col-md-5"><input id="toolGroupUseCaseName" type="text" class="form-control" placeholder="UseCase 이름 e.g. Customer"></div>
<div class="col-md-7"><div id="toolGroupSummary" class="input-hint pt-2">묶음에 추가된 Tool 없음 — 일반 Generate Tool은 기존 단일 Tool 생성입니다.</div></div>
</div>
</div>
<div class="mb-4 p-3 rounded" style="background:#18181b; border:1px solid #3f3f46;">
<label class="form-label">AI Tool 초안 만들기</label>
<div class="row g-2">
@@ -1158,6 +1171,7 @@
<tr>
<th style="min-width: 150px;">Name</th>
<th style="min-width: 125px;">Type</th>
<th style="min-width: 170px;">Enum values / List item type</th>
<th style="min-width: 210px;">Description</th>
<th style="min-width: 180px;">Example</th>
<th style="min-width: 125px;">Required</th>
@@ -1469,6 +1483,95 @@
});
handleFormSubmit('podForm', '/api/v1/scaffold/pod');
const groupedTools = [];
function toMethodName(baseName) {
return baseName ? baseName.charAt(0).toLowerCase() + baseName.slice(1) : '';
}
function parseToolFields(id) {
const text = document.getElementById(id).value.trim();
if (!text) return [];
const fields = JSON.parse(text);
if (!Array.isArray(fields)) throw new Error(`${id} must be a JSON array.`);
return fields;
}
function currentToolDefinition() {
const form = document.getElementById('toolForm');
const data = Object.fromEntries(new FormData(form).entries());
if (!data.baseName || !data.interfaceId || !data.clientSystemCode) {
throw new Error('Base Name, Legacy Interface ID, Client System Code를 입력해주세요.');
}
if (data.routingType !== 'MCI') {
throw new Error('여러 Tool을 하나의 UseCase로 생성하는 기능은 현재 MCI 전용입니다.');
}
return {
baseName: data.baseName,
methodName: toMethodName(data.baseName),
interfaceId: data.interfaceId,
title: data.title || data.baseName,
description: data.description || '',
group: data.categoryKey,
routingType: data.routingType,
register: data.register === 'true',
clientSystemCode: data.clientSystemCode,
httpApiName: data.httpApiName || null,
inputFields: parseToolFields('inputFields'),
outputFields: parseToolFields('outputFields'),
definitionOptions: {
functionDescription: data.functionDescription || '', displayDescription: data.displayDescription || '',
whenToUse: data.whenToUse || '', whenNotToUse: data.whenNotToUse || '', ioLimits: data.ioLimits || '',
exampleQueries: (data.exampleQueries || '').split(/[\n,]+/).map(value => value.trim()).filter(Boolean),
tags: (data.tags || '').split(',').map(value => value.trim()).filter(Boolean), ownerOrg: data.ownerOrg || 'MCP_TOOL'
}
};
}
function addCurrentToolToGroup() {
try {
const tool = currentToolDefinition();
if (groupedTools.some(item => item.methodName === tool.methodName || item.baseName === tool.baseName)) {
throw new Error('같은 Base Name 또는 메서드명이 이미 묶음에 있습니다.');
}
groupedTools.push(tool);
renderToolGroupSummary();
} catch (error) {
alert(error.message);
}
}
function renderToolGroupSummary() {
const summary = document.getElementById('toolGroupSummary');
summary.textContent = groupedTools.length
? `${groupedTools.length}개 Tool: ${groupedTools.map(tool => `${tool.baseName}${tool.methodName}()`).join(', ')}`
: '묶음에 추가된 Tool 없음 — 일반 Generate Tool은 기존 단일 Tool 생성입니다.';
}
document.getElementById('toolForm').addEventListener('submit', function(e) {
if (!groupedTools.length) return;
e.preventDefault();
e.stopImmediatePropagation();
try {
const useCaseName = document.getElementById('toolGroupUseCaseName').value.trim();
if (!/^[A-Z][A-Za-z0-9]*$/.test(useCaseName)) throw new Error('UseCase 이름은 PascalCase로 입력해주세요. 예: Customer');
const current = currentToolDefinition();
const tools = groupedTools.some(item => item.methodName === current.methodName) ? groupedTools : [...groupedTools, current];
const data = Object.fromEntries(new FormData(this).entries());
fetch('/api/v1/scaffold/tool-group', {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({useCaseName, moduleName: data.moduleName, author: data.author, date: data.date, tools})
}).then(response => response.text()).then(result => {
const resultBox = document.getElementById('resultBox');
resultBox.style.display = 'block'; resultBox.className = result.startsWith('Error:') ? 'error' : 'success';
resultBox.textContent = result;
});
} catch (error) {
alert(error.message);
}
});
handleFormSubmit('toolForm', '/api/v1/scaffold/tool');
const fieldExamples = {
@@ -1498,14 +1601,16 @@
}
}
const fieldTypes = ['String', 'Integer', 'Long', 'Double', 'Boolean', 'BigDecimal'];
const fieldTypes = ['String', 'Integer', 'Long', 'Double', 'Boolean', 'BigDecimal', 'Enum', 'List'];
const typeExamples = {
String: 'example',
Integer: '1',
Long: '1',
Double: '1.0',
Boolean: 'true',
BigDecimal: '1000.00'
BigDecimal: '1000.00',
Enum: 'OPEN',
List: 'C001'
};
const fieldTemplates = {
customer: {
@@ -1608,18 +1713,23 @@
updateFieldEditorPreview();
});
const detailsInput = makeInput(
field.type === 'Enum' ? (field.enumValues || []).join(', ') : (field.type === 'List' ? (field.itemType || 'String') : ''),
'Enum: OPEN, CLOSED / List: String');
detailsInput.dataset.field = 'details';
const requiredSelect = document.createElement('select');
requiredSelect.className = 'form-select form-select-sm';
requiredSelect.dataset.field = 'required';
requiredSelect.add(new Option('Required', 'true', false, field.required === true || field.required === 'true'));
requiredSelect.add(new Option('Optional', 'false', false, !(field.required === true || field.required === 'true')));
[nameInput, descriptionInput, exampleInput, requiredSelect].forEach(control => {
[nameInput, descriptionInput, exampleInput, requiredSelect, detailsInput].forEach(control => {
control.addEventListener('input', updateFieldEditorPreview);
control.addEventListener('change', updateFieldEditorPreview);
});
[nameInput, typeSelect, descriptionInput, exampleInput, requiredSelect].forEach(control => {
[nameInput, typeSelect, detailsInput, descriptionInput, exampleInput, requiredSelect].forEach(control => {
const cell = document.createElement('td');
cell.appendChild(control);
row.appendChild(cell);
@@ -1641,13 +1751,19 @@
function currentEditorFields() {
return [...document.querySelectorAll('#fieldEditorBody tr')]
.map(row => ({
name: row.querySelector('[data-field="name"]').value.trim(),
type: row.querySelector('[data-field="type"]').value,
description: row.querySelector('[data-field="description"]').value.trim(),
example: row.querySelector('[data-field="example"]').value.trim(),
required: row.querySelector('[data-field="required"]').value === 'true'
}))
.map(row => {
const type = row.querySelector('[data-field="type"]').value;
const details = row.querySelector('[data-field="details"]').value.trim();
return {
name: row.querySelector('[data-field="name"]').value.trim(), type,
description: row.querySelector('[data-field="description"]').value.trim(),
example: row.querySelector('[data-field="example"]').value.trim(),
required: row.querySelector('[data-field="required"]').value === 'true',
enumValues: type === 'Enum' ? details.split(',').map(value => value.trim()).filter(Boolean) : [],
itemType: type === 'List' ? (details || 'String') : null,
itemFields: []
};
})
.filter(field => field.name);
}

View File

@@ -8,6 +8,9 @@ import org.springframework.http.MediaType;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.junit.jupiter.api.io.TempDir;
import java.nio.file.Path;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
@@ -16,9 +19,33 @@ import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
class ScaffoldingControllerToolDraftTest {
@TempDir
Path root;
@Test
void groupedToolRequestGeneratesOneUseCase() throws Exception {
ChatClient.Builder builder = mock(ChatClient.Builder.class);
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(
new ScaffoldingController(builder, new ObjectMapper()))
.setMessageConverters(new MappingJackson2HttpMessageConverter())
.build();
String moduleName = root.resolve("dap-was-customer").toString().replace("\\", "\\\\");
String request = """
{"useCaseName":"Customer","moduleName":"%s","author":"tester","date":"2026.08.12","tools":[
{"baseName":"CustomerGuidance","methodName":"searchGuidance","interfaceId":"CTMNILO00007","title":"Customer guidance","description":"Search guidance","group":"cmm","routingType":"MCI","register":false,"clientSystemCode":"NILD","inputFields":[{"name":"customerId","type":"String","description":"Customer ID","example":"C001","required":true}],"outputFields":[]}
]}
""".formatted(moduleName);
mockMvc.perform(post("/api/v1/scaffold/tool-group")
.contentType(MediaType.APPLICATION_JSON).content(request))
.andExpect(status().isOk())
.andExpect(content().string(org.hamcrest.Matchers.containsString("CustomerUseCase.java")));
}
@Test
void toolDraftEndpointIsAvailable() throws Exception {
ChatClient.Builder builder = mock(ChatClient.Builder.class);

View File

@@ -45,7 +45,18 @@ public class ToolScaffolder {
private static final String BASE_PACKAGE = "io.shinhanlife.dap.mcc";
private static final String BASE_PACKAGE_PATH = "src/main/java/io/shinhanlife/dap/mcc";
public record FieldDefinition(String name, String type, String description, String example, boolean required) {
public record FieldDefinition(String name, String type, String description, String example, boolean required,
List<String> enumValues, String itemType, List<FieldDefinition> itemFields) {
public FieldDefinition(String name, String type, String description, String example, boolean required) {
this(name, type, description, example, required, List.of(), null, List.of());
}
}
public record ToolMethodDefinition(String baseName, String methodName, String interfaceId,
String title, String description, String group, String routingType,
boolean register, String clientSystemCode, String httpApiName,
List<FieldDefinition> inputFields, List<FieldDefinition> outputFields,
ToolDefinitionOptions definitionOptions) {
}
public record ToolDefinitionOptions(
@@ -59,6 +70,206 @@ public class ToolScaffolder {
String ownerOrg) {
}
public static String scaffoldUseCase(String useCaseName, String moduleName, String author,
String createDate, List<ToolMethodDefinition> tools) throws IOException {
if (tools == null || tools.isEmpty()) {
throw new IllegalArgumentException("At least one Tool method is required.");
}
String useCaseBaseName = toPascalCase(useCaseName);
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);
Path configuredModule = Paths.get(moduleName);
Path moduleRoot = configuredModule.isAbsolute() ? configuredModule : rootDir.resolve(configuredModule);
Path sourceRoot = moduleRoot.resolve(BASE_PACKAGE_PATH);
Path useCaseDir = sourceRoot.resolve(Paths.get("biz", group, "usecase"));
Path implDir = useCaseDir.resolve("impl");
Path dtoDir = sourceRoot.resolve(Paths.get("biz", group, "dto"));
Path converterDir = sourceRoot.resolve(Paths.get("biz", group, "converter"));
Path definitionDir = moduleRoot.resolve(Paths.get("src", "main", "resources", "tool-definitions", group));
Path mockDir = moduleRoot.resolve(Paths.get("src", "main", "resources", "mock-responses"));
Files.createDirectories(useCaseDir);
Files.createDirectories(implDir);
Files.createDirectories(dtoDir);
Files.createDirectories(converterDir);
Files.createDirectories(definitionDir);
Files.createDirectories(mockDir);
String bizPackage = BASE_PACKAGE + ".biz." + group;
writeUtf8(useCaseDir.resolve(useCaseBaseName + "UseCase.java"),
groupedUseCaseContent(bizPackage, useCaseBaseName, moduleName, tools));
writeUtf8(implDir.resolve(useCaseBaseName + "UseCaseImpl.java"),
groupedUseCaseImplContent(bizPackage, useCaseBaseName, tools));
writeUtf8(converterDir.resolve(useCaseBaseName + "Converter.java"),
groupedConverterContent(bizPackage, useCaseBaseName, tools));
StringBuilder log = new StringBuilder("\n=========================================\n")
.append(" Multi Tool Scaffolding Complete\n")
.append("=========================================\n")
.append("[Usecase Interface] ").append(useCaseDir.resolve(useCaseBaseName + "UseCase.java")).append("\n")
.append("[Usecase Impl] ").append(implDir.resolve(useCaseBaseName + "UseCaseImpl.java")).append("\n");
for (ToolMethodDefinition tool : tools) {
writeGroupedToolFiles(moduleRoot, sourceRoot, dtoDir, definitionDir, mockDir, bizPackage, tool, moduleName, log);
}
log.append("[Converter] ").append(converterDir.resolve(useCaseBaseName + "Converter.java")).append("\n");
return log.toString();
}
private static void validateToolMethods(List<ToolMethodDefinition> tools, String expectedGroup) {
Set<String> methods = new LinkedHashSet<>();
Set<String> toolNames = new LinkedHashSet<>();
for (ToolMethodDefinition tool : tools) {
if (tool == null || tool.baseName() == null || tool.baseName().isBlank()
|| tool.methodName() == null || !tool.methodName().matches("^[a-zA-Z_$][a-zA-Z0-9_$]*$")) {
throw new IllegalArgumentException("Every Tool needs a valid base name and Java method name.");
}
if (!expectedGroup.equalsIgnoreCase(tool.group())) {
throw new IllegalArgumentException("All Tool methods in one UseCase must use the same category.");
}
if (!"MCI".equalsIgnoreCase(tool.routingType())) {
throw new IllegalArgumentException("Grouped Tool scaffolding currently supports MCI Tools only.");
}
if (tool.interfaceId() == null || tool.interfaceId().isBlank()
|| tool.clientSystemCode() == null || tool.clientSystemCode().isBlank()) {
throw new IllegalArgumentException("MCI Tool needs an interface ID and Client system code.");
}
String toolName = toToolName("", tool.group(), toPascalCase(tool.baseName()));
if (!methods.add(tool.methodName()) || !toolNames.add(toolName)) {
throw new IllegalArgumentException("Tool method names and MCP Tool names must be unique.");
}
}
}
private static void writeGroupedToolFiles(Path moduleRoot, Path sourceRoot, Path dtoDir, Path definitionDir,
Path mockDir, String bizPackage, ToolMethodDefinition tool,
String moduleName, StringBuilder log) throws IOException {
String baseName = toPascalCase(tool.baseName());
String code = tool.clientSystemCode().toLowerCase(Locale.ROOT);
String ioPackage = BASE_PACKAGE + ".infra.itrf.mci." + code;
Path clientDir = sourceRoot.resolve(Paths.get("infra", "itrf", "mci", code));
Path ioDir = clientDir.resolve("io");
Files.createDirectories(ioDir);
writeUtf8(dtoDir.resolve(baseName + "Request.java"),
dtoContent(bizPackage + ".dto", baseName + "Request", tool.inputFields(), "", "", true));
writeUtf8(dtoDir.resolve(baseName + "Response.java"),
dtoContent(bizPackage + ".dto", baseName + "Response", tool.outputFields(), "", "", false));
writeStructuredFieldTypes(dtoDir, bizPackage + ".dto", baseName + "Request", tool.inputFields());
writeStructuredFieldTypes(dtoDir, bizPackage + ".dto", baseName + "Response", tool.outputFields());
writeUtf8(ioDir.resolve(baseName + "_I.java"),
mciIoContent("infra.itrf.mci." + code, baseName + "_I", tool.inputFields(), "", ""));
writeUtf8(ioDir.resolve(baseName + "_O.java"),
mciIoContent("infra.itrf.mci." + code, baseName + "_O", tool.outputFields(), "", ""));
writeStructuredFieldTypes(ioDir, ioPackage + ".io", baseName + "_I", tool.inputFields());
writeStructuredFieldTypes(ioDir, ioPackage + ".io", baseName + "_O", tool.outputFields());
writeUtf8(clientDir.resolve(baseName + "Client.java"),
groupedMciClientContent(ioPackage, baseName, tool.interfaceId()));
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");
}
private static String groupedUseCaseContent(String bizPackage, String useCaseBaseName, String moduleName,
List<ToolMethodDefinition> tools) {
StringBuilder imports = new StringBuilder();
StringBuilder methods = new StringBuilder();
for (ToolMethodDefinition tool : tools) {
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");
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(" @ToolHint(register = ").append(tool.register()).append(", categoryKey = \"")
.append(tool.group().toLowerCase(Locale.ROOT)).append("\", mappingId = \"")
.append(javaText(tool.interfaceId())).append("\")\n")
.append(" ").append(baseName).append("Response ").append(tool.methodName()).append("(")
.append(baseName).append("Request req);\n\n");
}
return "package " + bizPackage + ".usecase;\n\n"
+ "import org.springaicommunity.mcp.annotation.McpTool;\n"
+ "import io.shinhanlife.dap.lib.annotation.ToolHint;\n"
+ imports + "\npublic interface " + useCaseBaseName + "UseCase {\n\n" + methods + "}\n";
}
private static String groupedUseCaseImplContent(String bizPackage, String useCaseBaseName,
List<ToolMethodDefinition> tools) {
StringBuilder imports = new StringBuilder();
StringBuilder fields = new StringBuilder();
StringBuilder methods = new StringBuilder();
for (ToolMethodDefinition tool : tools) {
String baseName = toPascalCase(tool.baseName());
String code = tool.clientSystemCode().toLowerCase(Locale.ROOT);
String clientVariable = Character.toLowerCase(baseName.charAt(0)) + baseName.substring(1) + "Client";
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(BASE_PACKAGE).append(".infra.itrf.mci.").append(code).append(".").append(baseName).append("Client;\n")
.append("import ").append(BASE_PACKAGE).append(".infra.itrf.mci.").append(code).append(".io.").append(baseName).append("_I;\n")
.append("import ").append(BASE_PACKAGE).append(".infra.itrf.mci.").append(code).append(".io.").append(baseName).append("_O;\n");
fields.append(" private final ").append(baseName).append("Client ").append(clientVariable).append(";\n");
methods.append(" @Override\n public ").append(baseName).append("Response ").append(tool.methodName())
.append("(").append(baseName).append("Request req) {\n")
.append(" ").append(baseName).append("_I request = converter.to").append(baseName).append("Request(req);\n")
.append(" ").append(baseName).append("_O response = ").append(clientVariable).append(".call").append(baseName).append("(request);\n")
.append(" ").append(baseName).append("Response toolResponse = converter.to").append(baseName).append("Response(response);\n")
.append(" if (toolResponse == null) toolResponse = new ").append(baseName).append("Response();\n")
.append(" toolResponse.setResultCode(\"SUCCESS\");\n")
.append(" return toolResponse;\n }\n\n");
}
return "package " + bizPackage + ".usecase.impl;\n\n"
+ "import " + bizPackage + ".converter." + useCaseBaseName + "Converter;\n"
+ "import " + bizPackage + ".usecase." + useCaseBaseName + "UseCase;\n"
+ "import lombok.RequiredArgsConstructor;\nimport org.springframework.stereotype.Service;\n" + imports
+ "\n@Service\n@RequiredArgsConstructor\npublic class " + useCaseBaseName + "UseCaseImpl implements " + useCaseBaseName + "UseCase {\n\n"
+ " private final " + useCaseBaseName + "Converter converter;\n" + fields + "\n" + methods + "}\n";
}
private static String groupedConverterContent(String bizPackage, String useCaseBaseName,
List<ToolMethodDefinition> tools) {
StringBuilder imports = new StringBuilder();
StringBuilder methods = new StringBuilder();
for (ToolMethodDefinition tool : tools) {
String baseName = toPascalCase(tool.baseName());
String code = tool.clientSystemCode().toLowerCase(Locale.ROOT);
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(BASE_PACKAGE).append(".infra.itrf.mci.").append(code).append(".io.").append(baseName).append("_I;\n")
.append("import ").append(BASE_PACKAGE).append(".infra.itrf.mci.").append(code).append(".io.").append(baseName).append("_O;\n");
methods.append(" ").append(baseName).append("_I to").append(baseName).append("Request(").append(baseName).append("Request request);\n")
.append(" ").append(baseName).append("Response to").append(baseName).append("Response(").append(baseName).append("_O response);\n\n");
}
return "package " + bizPackage + ".converter;\n\nimport org.mapstruct.Mapper;\nimport org.mapstruct.ReportingPolicy;\n"
+ imports + "\n@Mapper(componentModel = \"spring\", unmappedTargetPolicy = ReportingPolicy.IGNORE)\n"
+ "public interface " + useCaseBaseName + "Converter {\n\n" + methods + "}\n";
}
private static String groupedMciClientContent(String ioPackage, String baseName, String interfaceId) {
return "package " + ioPackage + ";\n\n"
+ "import io.shinhanlife.dap.lib.integration.mci.component.AxhubMciComponent;\n"
+ "import io.shinhanlife.glow.communication.dto.Transfer;\n"
+ "import lombok.RequiredArgsConstructor;\nimport org.springframework.stereotype.Component;\n"
+ "import " + ioPackage + ".io." + baseName + "_I;\n"
+ "import " + ioPackage + ".io." + baseName + "_O;\n\n"
+ "@Component\n@RequiredArgsConstructor\npublic class " + baseName + "Client {\n"
+ " private final AxhubMciComponent mci;\n\n"
+ " public " + baseName + "_O call" + baseName + "(" + baseName + "_I request) {\n"
+ " try {\n"
+ " Transfer<" + baseName + "_O> transfer = mci.callTo(\"" + javaText(interfaceId) + "\", null, request, " + baseName + "_O.class);\n"
+ " return transfer.getBody();\n"
+ " } catch (Exception e) {\n"
+ " throw new IllegalStateException(\"MCI call failed: " + javaText(interfaceId) + "\", e);\n"
+ " }\n }\n}\n";
}
private static String javaText(String value) {
return value == null ? "" : value.replace("\\", "\\\\").replace("\"", "\\\"").replace("\r", " ").replace("\n", " ");
}
public static void main(String[] args) throws IOException {
Scanner scanner = new Scanner(System.in);
@@ -254,6 +465,7 @@ public class ToolScaffolder {
.replace("private String message;", "@Schema(example = \"테스트 메시지입니다.\")\n private String message;");
reqContent = dtoContent(bizPackage + ".dto", baseName + "Request", inputFields, author, createDate, true);
writeUtf8(dtoDir.resolve(baseName + "Request.java"), reqContent);
writeStructuredFieldTypes(dtoDir, bizPackage + ".dto", baseName + "Request", inputFields);
// Generate Response DTO
String resContent = """
@@ -288,6 +500,7 @@ public class ToolScaffolder {
""".formatted(bizPackage, bizPackage, baseName, author, createDate, createDate, author, baseName);
resContent = dtoContent(bizPackage + ".dto", baseName + "Response", outputFields, author, createDate, false);
writeUtf8(dtoDir.resolve(baseName + "Response.java"), resContent);
writeStructuredFieldTypes(dtoDir, bizPackage + ".dto", baseName + "Response", outputFields);
String toolName = toToolName(moduleName, group, baseName);
@@ -551,6 +764,7 @@ public class ToolScaffolder {
""".formatted(BASE_PACKAGE, mciGroupPath.replace("/", "."), BASE_PACKAGE, mciGroupPath.replace("/", "."), interfaceId, author, createDate, createDate, author, interfaceId);
mciReqContent = mciIoContent(mciGroupPath.replace("/", "."), interfaceId + "_I", inputFields, author, createDate);
writeUtf8(mciIoDir.resolve(interfaceId + "_I.java"), mciReqContent);
writeStructuredFieldTypes(mciIoDir, BASE_PACKAGE + "." + mciGroupPath.replace("/", ".") + ".io", interfaceId + "_I", inputFields);
String mciResContent = """
package %s.%s.io;
@@ -578,6 +792,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);
writeUtf8(mciIoDir.resolve(interfaceId + "_O.java"), mciResContent);
writeStructuredFieldTypes(mciIoDir, BASE_PACKAGE + "." + mciGroupPath.replace("/", ".") + ".io", interfaceId + "_O", outputFields);
String converterContent = """
package %s.converter;
@@ -692,6 +907,8 @@ public class ToolScaffolder {
dtoContent(httpPackage + ".io", httpRequestClass, inputFields, author, createDate, true));
writeUtf8(httpIoDir.resolve(httpResponseClass + ".java"),
dtoContent(httpPackage + ".io", httpResponseClass, outputFields, author, createDate, false));
writeStructuredFieldTypes(httpIoDir, httpPackage + ".io", httpRequestClass, inputFields);
writeStructuredFieldTypes(httpIoDir, httpPackage + ".io", httpResponseClass, outputFields);
writeUtf8(httpClientDir.resolve(httpClientClass + ".java"),
httpClientContent(httpPackage, httpClientClass, httpApiName));
writeUtf8(converterDir.resolve(baseName + "Converter.java"),
@@ -925,9 +1142,7 @@ public class ToolScaffolder {
|| !generatedNames.add(field.name().trim())) {
continue;
}
properties.append(" ").append(field.name()).append(":\n")
.append(" type: ").append(jsonSchemaType(field.type())).append("\n")
.append(" description: ").append(yamlText(field.description())).append("\n");
appendSchemaProperty(properties, field);
if (field.required()) {
required.append(" - ").append(field.name()).append("\n");
}
@@ -996,9 +1211,7 @@ public class ToolScaffolder {
|| !generatedNames.add(field.name().trim())) {
continue;
}
properties.append(" ").append(field.name().trim()).append(":\n")
.append(" type: ").append(jsonSchemaType(field.type())).append("\n")
.append(" description: ").append(yamlText(field.description())).append("\n");
appendSchemaProperty(properties, field);
if (field.required()) {
required.append(" - ").append(field.name().trim()).append("\n");
}
@@ -1070,10 +1283,34 @@ public class ToolScaffolder {
case "Integer", "Long" -> "integer";
case "Double", "BigDecimal" -> "number";
case "Boolean" -> "boolean";
case "List" -> "array";
default -> "string";
};
}
private static void appendSchemaProperty(StringBuilder properties, FieldDefinition field) {
properties.append(" ").append(field.name().trim()).append(":\n")
.append(" type: ").append(jsonSchemaType(field.type())).append("\n")
.append(" description: ").append(yamlText(field.description())).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)
.collect(java.util.stream.Collectors.joining(", "))).append("]\n");
}
if ("List".equals(field.type())) {
properties.append(" items:\n")
.append(" type: ").append("Object".equals(field.itemType()) ? "object" : jsonSchemaType(field.itemType())).append("\n");
if ("Object".equals(field.itemType()) && field.itemFields() != null && !field.itemFields().isEmpty()) {
properties.append(" properties:\n");
for (FieldDefinition itemField : field.itemFields()) {
properties.append(" ").append(itemField.name()).append(":\n")
.append(" type: ").append(jsonSchemaType(itemField.type())).append("\n")
.append(" description: ").append(yamlText(itemField.description())).append("\n");
}
}
}
}
private static String yamlText(String value) {
String safe = value == null ? "" : value.replace("\\", "\\\\").replace("\"", "\\\"")
.replace("\r", " ").replace("\n", " ");
@@ -1143,36 +1380,110 @@ public class ToolScaffolder {
private static String dtoContent(String packageName, String className, List<FieldDefinition> fields,
String author, String createDate, boolean request) {
String body = fieldLines(fields, request ? Set.of() : Set.of("resultCode", "resultMessage"));
String body = fieldLines(fields, request ? Set.of() : Set.of("resultCode", "resultMessage"), className);
if (!request) {
body = " private String resultCode;\n\n private String resultMessage;\n" + body;
}
String listImport = hasListField(fields) ? "import java.util.List;\n" : "";
return """
package %s;
import com.fasterxml.jackson.annotation.JsonInclude;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
%s
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class %s {
%s}
""".formatted(packageName, className, body);
""".formatted(packageName, listImport, className, body);
}
private static String mciIoContent(String packageSuffix, String className, List<FieldDefinition> fields,
String author, String createDate) {
String listImport = hasListField(fields) ? "import java.util.List;\n" : "";
return """
package %s.%s.io;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
%s
@Data
public class %s {
%s}
""".formatted(BASE_PACKAGE, packageSuffix, className, fieldLines(fields));
""".formatted(BASE_PACKAGE, packageSuffix, listImport, className, fieldLines(fields, Set.of(), className));
}
private static boolean hasListField(List<FieldDefinition> fields) {
return fields != null && fields.stream().anyMatch(field -> field != null && "List".equals(field.type()));
}
private static void writeStructuredFieldTypes(Path directory, String packageName, String ownerClass,
List<FieldDefinition> fields) throws IOException {
for (FieldDefinition field : fields == null ? List.<FieldDefinition>of() : fields) {
if (field == null || field.name() == null || field.name().isBlank()) {
continue;
}
if ("Enum".equals(field.type())) {
String enumName = toPascalCase(field.name());
List<String> values = field.enumValues() == null ? List.of() : field.enumValues().stream()
.filter(value -> value != null && !value.isBlank()).map(String::trim).distinct().toList();
if (values.isEmpty()) {
throw new IllegalArgumentException("Enum field needs at least one allowed value: " + field.name());
}
String constants = values.stream().map(value -> " " + enumConstant(value) + "(\"" + javaText(value) + "\")")
.collect(java.util.stream.Collectors.joining(",\n"));
writeUtf8(directory.resolve(enumName + ".java"), """
package %s;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
public enum %s {
%s;
private final String value;
%s(String value) {
this.value = value;
}
@JsonValue
public String getValue() {
return value;
}
@JsonCreator
public static %s fromValue(String value) {
for (%s candidate : values()) {
if (candidate.value.equals(value)) return candidate;
}
throw new IllegalArgumentException("Unsupported value: " + value);
}
}
""".formatted(packageName, enumName, constants, enumName, enumName, enumName));
}
if ("List".equals(field.type()) && "Object".equals(field.itemType())) {
List<FieldDefinition> itemFields = field.itemFields() == null ? List.of() : field.itemFields();
if (itemFields.isEmpty()) {
throw new IllegalArgumentException("Object List field needs item fields: " + field.name());
}
String itemName = listItemClassName(ownerClass, field);
writeUtf8(directory.resolve(itemName + ".java"), dtoContent(packageName, itemName, itemFields, "", "", true));
writeStructuredFieldTypes(directory, packageName, itemName, itemFields);
}
}
}
private static String enumConstant(String value) {
String constant = value.toUpperCase(Locale.ROOT).replaceAll("[^A-Z0-9]+", "_").replaceAll("^_+|_+$", "");
return constant.isBlank() ? "VALUE" : (Character.isDigit(constant.charAt(0)) ? "VALUE_" + constant : constant);
}
private static String listItemClassName(String ownerClass, FieldDefinition field) {
return ownerClass + toPascalCase(field.name()) + "Item";
}
private static String httpUseCaseImplContent(String bizPackage, String baseName, String httpPackage,
@@ -1334,10 +1645,14 @@ public class ToolScaffolder {
baseName, baseName, baseName, baseName, baseName, baseName, baseName);
}
private static String fieldLines(List<FieldDefinition> fields) {
return fieldLines(fields, Set.of());
return fieldLines(fields, Set.of(), "");
}
private static String fieldLines(List<FieldDefinition> fields, Set<String> excludedNames) {
return fieldLines(fields, excludedNames, "");
}
private static String fieldLines(List<FieldDefinition> fields, Set<String> excludedNames, String ownerClass) {
StringBuilder source = new StringBuilder();
Set<String> generatedNames = new LinkedHashSet<>();
for (FieldDefinition field : fields == null ? List.<FieldDefinition>of() : fields) {
@@ -1348,7 +1663,7 @@ public class ToolScaffolder {
if (excludedNames.contains(fieldName) || !generatedNames.add(fieldName)) {
continue;
}
String type = supportedType(field.type());
String type = javaFieldType(field, ownerClass);
String description = field.description() == null ? "" : field.description().replace("\"", "\\\"");
String example = field.example() == null ? "" : field.example().replace("\"", "\\\"");
source.append(" @Schema(description = \"").append(description).append("\", example = \"")
@@ -1360,6 +1675,22 @@ public class ToolScaffolder {
}
return source.toString();
}
private static String javaFieldType(FieldDefinition field, String ownerClass) {
return switch (field.type() == null ? "String" : field.type()) {
case "Enum" -> toPascalCase(field.name());
case "List" -> "List<" + listItemJavaType(field, ownerClass) + ">";
default -> supportedType(field.type());
};
}
private static String listItemJavaType(FieldDefinition field, String ownerClass) {
String itemType = field.itemType() == null ? "" : field.itemType();
if ("Object".equals(itemType)) {
return listItemClassName(ownerClass, field);
}
return supportedType(itemType);
}
private static String supportedType(String type) {
return switch (type == null ? "String" : type) {
case "String", "Integer", "Long", "Double", "Boolean", "BigDecimal" -> type;
@@ -1404,9 +1735,26 @@ public class ToolScaffolder {
}
private static String mockValue(FieldDefinition field) {
if ("List".equals(field.type())) {
if ("Object".equals(field.itemType())) {
StringBuilder object = new StringBuilder("{");
boolean first = true;
for (FieldDefinition itemField : field.itemFields() == null ? List.<FieldDefinition>of() : field.itemFields()) {
if (!first) object.append(", ");
object.append("\"").append(jsonEscape(itemField.name())).append("\" : ").append(mockValue(itemField));
first = false;
}
return "[" + object + "]";
}
FieldDefinition item = new FieldDefinition("item", field.itemType(), "", field.example(), false);
return "[" + mockValue(item) + "]";
}
if (field.example() == null || field.example().isBlank()) {
return "null";
}
if ("Enum".equals(field.type())) {
return "\"" + jsonEscape(field.example()) + "\"";
}
return switch (supportedType(field.type())) {
case "Integer", "Long", "Double", "BigDecimal" -> field.example();
case "Boolean" -> Boolean.parseBoolean(field.example()) ? "true" : "false";

View File

@@ -7,6 +7,7 @@ import io.shinhanlife.dap.lib.metadata.ToolDefinitionValidator;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -17,6 +18,70 @@ import org.junit.jupiter.api.io.TempDir;
class ToolScaffolderTest {
@Test
void exposesGroupedUseCaseScaffoldApi() {
assertDoesNotThrow(() -> ToolScaffolder.class.getMethod(
"scaffoldUseCase", String.class, String.class, String.class, String.class, List.class));
}
@Test
void generatesOneUseCaseWithTwoMcpToolMethodsAndTypedClients() throws Exception {
String moduleName = root.resolve("dap-was-customer").toString();
ToolScaffolder.scaffoldUseCase("Customer", moduleName, "tester", "2026.08.12", List.of(
new ToolScaffolder.ToolMethodDefinition(
"CustomerGuidance", "searchGuidance", "CTMNILO00007", "Customer guidance", "Search guidance", "cmm", "MCI",
false, "NILD", null,
List.of(new ToolScaffolder.FieldDefinition("customerId", "String", "Customer ID", "C001", true)),
List.of(new ToolScaffolder.FieldDefinition("guidanceStatus", "String", "Guidance status", "OPEN", false)), null),
new ToolScaffolder.ToolMethodDefinition(
"CustomerContract", "searchContract", "CTMCNT00001", "Customer contract", "Search contract", "cmm", "MCI",
false, "CNTD", null,
List.of(new ToolScaffolder.FieldDefinition("customerId", "String", "Customer ID", "C001", true)),
List.of(new ToolScaffolder.FieldDefinition("contractStatus", "String", "Contract status", "ACTIVE", false)), null)));
Path sourceRoot = root.resolve("dap-was-customer/src/main/java/io/shinhanlife/dap/mcc");
String useCase = Files.readString(sourceRoot.resolve("biz/cmm/usecase/CustomerUseCase.java"));
String implementation = Files.readString(sourceRoot.resolve("biz/cmm/usecase/impl/CustomerUseCaseImpl.java"));
String guidanceClient = Files.readString(sourceRoot.resolve("infra/itrf/mci/nild/CustomerGuidanceClient.java"));
assertTrue(useCase.contains("CustomerGuidanceResponse searchGuidance(CustomerGuidanceRequest req)"), useCase);
assertTrue(useCase.contains("CustomerContractResponse searchContract(CustomerContractRequest req)"), useCase);
assertTrue(implementation.contains("private final CustomerGuidanceClient customerGuidanceClient;"), implementation);
assertTrue(implementation.contains("customerGuidanceClient.callCustomerGuidance(request)"), implementation);
assertTrue(guidanceClient.contains("CustomerGuidance_O callCustomerGuidance(CustomerGuidance_I request)"), guidanceClient);
}
@Test
void generatesEnumAndListFieldsInDtoSchemaAndMockResponse() throws Exception {
String moduleName = root.resolve("dap-was-claim").toString();
List<ToolScaffolder.FieldDefinition> inputFields = List.of(
new ToolScaffolder.FieldDefinition("claimStatus", "Enum", "Claim status", "OPEN", true,
List.of("OPEN", "CLOSED"), null, List.of()),
new ToolScaffolder.FieldDefinition("customerIds", "List", "Customer IDs", "C001", false,
List.of(), "String", List.of()));
List<ToolScaffolder.FieldDefinition> outputFields = List.of(
new ToolScaffolder.FieldDefinition("guidanceItems", "List", "Guidance items", "", false,
List.of(), "Object", List.of(new ToolScaffolder.FieldDefinition("status", "String", "Status", "OPEN", true))));
ToolScaffolder.scaffold("claim search", "CLM0001", "Claim search", "cmm", "MCI", moduleName,
"tester", "2026.08.12", false, "CLM1", null, null, inputFields, outputFields);
Path dtoRoot = root.resolve("dap-was-claim/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/dto");
String request = Files.readString(dtoRoot.resolve("ClaimSearchRequest.java"));
String response = Files.readString(dtoRoot.resolve("ClaimSearchResponse.java"));
String definition = Files.readString(root.resolve("dap-was-claim/src/main/resources/tool-definitions/cmm/cmm_claim_search.yml"));
String mock = Files.readString(root.resolve("dap-was-claim/src/main/resources/mock-responses/cmm_claim_search.json"));
assertTrue(request.contains("private ClaimStatus claimStatus;"), request);
assertTrue(request.contains("private List<String> customerIds;"), request);
assertTrue(Files.exists(dtoRoot.resolve("ClaimStatus.java")));
assertTrue(response.contains("private List<ClaimSearchResponseGuidanceItemsItem> guidanceItems;"), response);
assertTrue(Files.exists(dtoRoot.resolve("ClaimSearchResponseGuidanceItemsItem.java")));
assertTrue(definition.contains("enum: [OPEN, CLOSED]"), definition);
assertTrue(definition.contains("type: array"), definition);
assertTrue(mock.contains("\"guidanceItems\" : [{"), mock);
}
@Test
void generatesEveryToolSourceAsUtf8WithoutBrokenKoreanOrBom() throws Exception {
String moduleName = root.resolve("dap-was-korean").toString();

View File

@@ -26,5 +26,5 @@ public interface CustomerGuidanceToolUseCase {
@McpTool(name = "cmm_customer_tool", title = "고객 통합 안내이력 조회", description = "고객의 통합 안내 이력을 조회하는 도구입니다. 고객 ID와 조회 기간을 입력하면 해당 기간 동안의 안내 이력을 반환합니다.")
@ToolHint(register = false, categoryKey = "cmm", mappingId = "ONILD0320")
CustomerGuidanceToolResponse execute(CustomerGuidanceToolRequest req) throws Exception;
CustomerGuidanceToolResponse searchCustomerGuidance(CustomerGuidanceToolRequest req) throws Exception;
}

View File

@@ -22,5 +22,5 @@ import io.shinhanlife.dap.mcc.biz.cmm.dto.MetaCommonCodeRequest;
public interface MetaCommonCodeUseCase {
@McpTool(name = "cmm_comcode_lookup", title = "메타 공통코드 조회 툴", description = "메타 통합코드 목록을 조회해줘", annotations = @McpTool.McpAnnotations(openWorldHint = true))
@ToolHint(register = false, requiresApproval = false, categoryKey = "cmm", mappingId = "CLCNNB00001")
Object execute(MetaCommonCodeRequest req);
Object searchCommonCode(MetaCommonCodeRequest req);
}

View File

@@ -22,5 +22,5 @@ import io.shinhanlife.dap.mcc.biz.cmm.dto.MetaTableRequest;
public interface MetaTableUseCase {
@McpTool(name = "cmm_meta_table", title = "메타 테이블 조회 툴", description = "메타 테이블 정보 목록을 조회해줘", annotations = @McpTool.McpAnnotations(openWorldHint = true))
@ToolHint(register = false, requiresApproval = false, categoryKey = "cmm", mappingId = "CLCNNB00001")
Object execute(MetaTableRequest req);
Object searchMetaTable(MetaTableRequest req);
}

View File

@@ -43,7 +43,7 @@ public class CustomerGuidanceToolUseCaseImpl implements CustomerGuidanceToolUseC
private final CustomerGuidanceToolConverter converter;
@Override
public CustomerGuidanceToolResponse execute(CustomerGuidanceToolRequest req) throws Exception {
public CustomerGuidanceToolResponse searchCustomerGuidance(CustomerGuidanceToolRequest req) throws Exception {
ONILD0320_I onild0320_i = converter.toONILD0320_I(req);
ONILD0320_O onild0320_o = mci.callOnild0320(onild0320_i);

View File

@@ -38,7 +38,7 @@ public class MetaCommonCodeUseCaseImpl implements MetaCommonCodeUseCase {
private final MetaCommonCodeConverter converter;
@Override
public Object execute(MetaCommonCodeRequest req) {
public Object searchCommonCode(MetaCommonCodeRequest req) {
log.info("[MCI Tool] {} 요청 수신. 파라미터: {}", "metaCommonCode", req);
try {
CLCNNB00001_I mciRequest = converter.toLegacyRequest(req);

View File

@@ -38,7 +38,7 @@ public class MetaTableUseCaseImpl implements MetaTableUseCase {
private final MetaTableConverter converter;
@Override
public Object execute(MetaTableRequest req) {
public Object searchMetaTable(MetaTableRequest req) {
log.info("[MCI Tool] {} 요청 수신. 파라미터: {}", "metaTable", req);
try {
CLCNNB00001_I mciRequest = converter.toLegacyRequest(req);

View File

@@ -23,5 +23,5 @@ public interface InsuranceClaimProcessorUseCase {
@McpTool(name = "ins_insurance_processor", title = "보험금 청구", description = "보험금 청구 요청을 처리하고 결과를 반환하는 LLM 도구 가이드")
@ToolHint(register = false, categoryKey = "ins", mappingId = "CLAIM0000001")
InsuranceClaimProcessorResponse execute(InsuranceClaimProcessorRequest req);
InsuranceClaimProcessorResponse processInsuranceClaim(InsuranceClaimProcessorRequest req);
}

View File

@@ -18,7 +18,7 @@ public class InsuranceClaimProcessorUseCaseImpl implements InsuranceClaimProcess
private final InsuranceClient insuranceClient;
@Override
public InsuranceClaimProcessorResponse execute(InsuranceClaimProcessorRequest req) {
public InsuranceClaimProcessorResponse processInsuranceClaim(InsuranceClaimProcessorRequest req) {
InsuranceClaimProcessorHttpRequest httpRequest = converter.toHttpRequest(req);
InsuranceClaimProcessorHttpResponse httpResponse = insuranceClient.call(httpRequest, InsuranceClaimProcessorHttpResponse.class);

View File

@@ -7,5 +7,5 @@ import io.shinhanlife.dap.mcc.biz.oth.dto.*;
public interface Onnba3011UseCase {
@McpTool(name = "oth_onnba3011_call", description = "Onnba3011 호출 툴")
@ToolHint(categoryKey = "oth", register = false)
Object execute(Onnba3011Request req);
Object callOnnba3011(Onnba3011Request req);
}

View File

@@ -37,7 +37,7 @@ public class Onnba3011UseCaseImpl implements Onnba3011UseCase {
* AI Agent가 호출하게 될 메서드입니다.
*/
@Override
public Object execute(Onnba3011Request req) {
public Object callOnnba3011(Onnba3011Request req) {
log.info("[MCI Tool] 보종By가입설계한도계산조회 요청 수신.");
try {

View File

@@ -9,5 +9,5 @@ import io.shinhanlife.dap.mcc.biz.smp.dto.DailyQuoteResponse;
public interface DailyQuoteToolUseCase {
@McpTool(name = "smp_quote_daily", title = "오늘의 명언 툴", description = "무작위로 영감을 주는 명언을 하나 가져옵니다.")
@ToolHint(register = false, categoryKey = "smp", mappingId = "QUOTE_001")
DailyQuoteResponse execute(DailyQuoteRequest req);
DailyQuoteResponse getDailyQuote(DailyQuoteRequest req);
}

View File

@@ -11,5 +11,5 @@ import io.shinhanlife.dap.mcc.biz.smp.dto.ExchangeRateResponse;
public interface ExchangeRateToolUseCase {
@McpTool(name = "smp_exchange_inquiry", title = "실시간 환율 조회 툴", description = "원하는 통화의 실시간 환율을 조회합니다. (예: USD, EUR, JPY)")
@ToolHint(register = false, categoryKey = "smp", mappingId = "EXCHANGE_001")
ExchangeRateResponse execute(ExchangeRateRequest req);
ExchangeRateResponse getExchangeRate(ExchangeRateRequest req);
}

View File

@@ -8,5 +8,5 @@ import io.shinhanlife.dap.mcc.biz.smp.dto.TeamMemberRequest;
public interface TeamMemberUseCase {
@McpTool(name = "smp_team_list", title = "신한라이프 MCP, TOOL 파트 구성원 조회", description = "신한라이프 MCP, TOOL 파트 구성원을 조회합니다.", annotations = @McpTool.McpAnnotations(openWorldHint = true))
@ToolHint(register = false, requiresApproval = false, categoryKey = "smp", mappingId = "DIRECT0001")
Object execute(TeamMemberRequest req);
Object getTeamMember(TeamMemberRequest req);
}

View File

@@ -8,5 +8,5 @@ import io.shinhanlife.dap.mcc.biz.smp.dto.*;
public interface WeatherToolUseCase {
@McpTool(name = "smp_weather_inquiry", title = "날씨 조회 툴", description = "특정 도시의 현재 날씨, 온도, 풍속 정보를 조회합니다.")
@ToolHint(register = false, categoryKey = "smp", mappingId = "WEATHER_001")
WeatherResponse execute(WeatherRequest req);
WeatherResponse getWeather(WeatherRequest req);
}

View File

@@ -35,7 +35,7 @@ public class DailyQuoteToolUseCaseImpl implements DailyQuoteToolUseCase {
);
@Override
public DailyQuoteResponse execute(DailyQuoteRequest req) {
public DailyQuoteResponse getDailyQuote(DailyQuoteRequest req) {
int index = new Random().nextInt(quotes.size());
DailyQuoteResponse selected = quotes.get(index);

View File

@@ -32,7 +32,7 @@ public class ExchangeRateToolUseCaseImpl implements ExchangeRateToolUseCase {
}
@Override
public ExchangeRateResponse execute(ExchangeRateRequest req) {
public ExchangeRateResponse getExchangeRate(ExchangeRateRequest req) {
String targetCurrency = req.getCurrencyCode() != null ? req.getCurrencyCode().toUpperCase().trim() : "USD";
// 간단한 모의 데이터로 반환 (실제 구현 시 외부 연동)

View File

@@ -27,7 +27,7 @@ import lombok.extern.slf4j.Slf4j;
public class TeamMemberUseCaseImpl implements TeamMemberUseCase {
@Override
public Object execute(TeamMemberRequest req) {
public Object getTeamMember(TeamMemberRequest req) {
log.info("[A01] 신한라이프 MCP, TOOL 파트 구성원 조회 요청: {}", req);
String filter = req != null && req.getTeamName() != null ? req.getTeamName().toUpperCase() : "전체";

View File

@@ -35,7 +35,7 @@ public class WeatherToolUseCaseImpl implements WeatherToolUseCase {
public WeatherToolUseCaseImpl() {
this.restClient = RestClient.create();
}
public WeatherResponse execute(WeatherRequest req) {
public WeatherResponse getWeather(WeatherRequest req) {
String city = req.city() != null ? req.city().trim() : "서울";
// 지역별 위경도 매핑 (간단한 예시)

View File

@@ -23,5 +23,5 @@ public interface SolReqDetailUseCase {
@McpTool(name = "sol_request_detail", title = "SolReqDetail 툴", description = "SOL 의뢰서 상세 조회", annotations = @McpTool.McpAnnotations(openWorldHint = true, readOnlyHint = true))
@ToolHint(register = false, requiresApproval = false, categoryKey = "sol", mappingId = "SOLG00000002")
Object execute(SolReqDetailRequest req);
Object getSolRequestDetail(SolReqDetailRequest req);
}

View File

@@ -8,5 +8,5 @@ import io.shinhanlife.dap.mcc.biz.sol.dto.SolReqListRequest;
public interface SolReqListUseCase {
@McpTool(name = "sol_request_list", title = "SolReqList 툴", description = "SOL 의뢰서 목록 조회해줘", annotations = @McpTool.McpAnnotations(openWorldHint = true))
@ToolHint(register = false, requiresApproval = false, categoryKey = "sol", mappingId = "SOLG00000001")
Object execute(SolReqListRequest req);
Object searchSolRequests(SolReqListRequest req);
}

View File

@@ -40,7 +40,7 @@ public class SolReqDetailUseCaseImpl implements SolReqDetailUseCase {
private boolean mockEnabled;
@Override
public Object execute(SolReqDetailRequest req) {
public Object getSolRequestDetail(SolReqDetailRequest req) {
log.info("[MCI Tool] {} 요청 수신. 파라미터: {}", "solReqDetail", req);
if (req == null || req.getSrId() == null || req.getSrId().isBlank()) {
return Map.of("status", "ERROR", "message", "srId는 필수입니다.");

View File

@@ -39,7 +39,7 @@ public class SolReqListUseCaseImpl implements SolReqListUseCase {
private final SolReqListConverter converter;
@Override
public Object execute(SolReqListRequest req) {
public Object searchSolRequests(SolReqListRequest req) {
log.info("[MCI Tool] {} 요청 수신. 파라미터: {}", "solReqList", req);
try {
SOLG00000001_I mciRequest = converter.toLegacyRequest(req);

View File

@@ -10,7 +10,7 @@ class CustomerGuidanceToolUseCaseTest {
@Test
void createsToolRequestAndResponseDtos() {
assertNotNull(new CustomerGuidanceToolRequest());
assertNotNull(new CustomerGuidanceToolResponse());
assertNotNull(CustomerGuidanceToolRequest.builder().build());
assertNotNull(CustomerGuidanceToolResponse.builder().build());
}
}

View File

@@ -37,7 +37,7 @@ class Onnba3011UseCaseImplTest {
when(converter.toMciRequest(request)).thenReturn(mciRequest);
when(mciCfpaClient.callCfpa0001(mciRequest)).thenReturn("success");
Object result = useCase.execute(request);
Object result = useCase.callOnnba3011(request);
assertEquals("success", result);
verify(converter).toMciRequest(request);

View File

@@ -36,7 +36,7 @@ class SolReqDetailUseCaseImplTest {
SolReqDetailRequest request = new SolReqDetailRequest();
request.setSrId("SR-2026-001");
SolReqDetailResponse response = (SolReqDetailResponse) useCase.execute(request);
SolReqDetailResponse response = (SolReqDetailResponse) useCase.getSolRequestDetail(request);
assertThat(response.getSrId()).isEqualTo("SR-2026-001");
assertThat(response.getSrName()).isEqualTo("AX HUB 메인 화면 UI 개편");

View File

@@ -29,7 +29,7 @@ class SolReqListUseCaseImplTest {
when(mci.callTo(eq("SOLG00000001"), eq("SOLG00000001"), eq(mciRequest), eq(SOLG00000001_O.class)))
.thenReturn(new Transfer<>());
SolReqListResponse response = (SolReqListResponse) useCase.execute(request);
SolReqListResponse response = (SolReqListResponse) useCase.searchSolRequests(request);
verify(converter).toLegacyRequest(request);
verify(mci).callTo("SOLG00000001", "SOLG00000001", mciRequest, SOLG00000001_O.class);

View File

@@ -25,5 +25,5 @@ public interface ClaimSearchUseCase {
@ToolHint(register = false, categoryKey = "cmm", mappingId = "CLCNNB00001",
inputSchemaResource = "classpath:tool-schemas/cmm/claim-search-resource-input-schema.json",
outputSchemaResource = "classpath:tool-schemas/cmm/claim-search-resource-output-schema.json")
ClaimSearchResponse execute(ClaimSearchRequest req);
ClaimSearchResponse searchClaim(ClaimSearchRequest req);
}

View File

@@ -23,5 +23,5 @@ public interface MemoListRetrieverUseCase {
@McpTool(name = "cmm_memo_retriever", title = "의뢰서 목록 조회", description = "의뢰서 목록을 조회하여 의뢰 정보를 반환합니다.")
@ToolHint(register = false, categoryKey = "cmm", mappingId = "MEMO0000001")
MemoListRetrieverResponse execute(MemoListRetrieverRequest req);
MemoListRetrieverResponse retrieveMemoList(MemoListRetrieverRequest req);
}

View File

@@ -36,7 +36,7 @@ public class ClaimSearchUseCaseImpl implements ClaimSearchUseCase {
private final ClaimSearchConverter converter;
@Override
public ClaimSearchResponse execute(ClaimSearchRequest req) {
public ClaimSearchResponse searchClaim(ClaimSearchRequest req) {
log.info("[MCI Tool] {} 요청 수신.", "cmm_claim_search");
try {
// MapStruct를 이용한 자동 매핑 (AI DTO -> MCI DTO)

View File

@@ -18,7 +18,7 @@ public class MemoListRetrieverUseCaseImpl implements MemoListRetrieverUseCase {
private final MemoClient memoClient;
@Override
public MemoListRetrieverResponse execute(MemoListRetrieverRequest req) {
public MemoListRetrieverResponse retrieveMemoList(MemoListRetrieverRequest req) {
MemoListRetrieverHttpRequest httpRequest = converter.toHttpRequest(req);
MemoListRetrieverHttpResponse httpResponse = memoClient.call(httpRequest, MemoListRetrieverHttpResponse.class);