feat: enhance tool scaffold and usecase naming
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 0s
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 0s
This commit is contained in:
@@ -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) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
// 간단한 모의 데이터로 반환 (실제 구현 시 외부 연동)
|
||||
|
||||
@@ -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() : "전체";
|
||||
|
||||
@@ -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() : "서울";
|
||||
|
||||
// 지역별 위경도 매핑 (간단한 예시)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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는 필수입니다.");
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -10,7 +10,7 @@ class CustomerGuidanceToolUseCaseTest {
|
||||
|
||||
@Test
|
||||
void createsToolRequestAndResponseDtos() {
|
||||
assertNotNull(new CustomerGuidanceToolRequest());
|
||||
assertNotNull(new CustomerGuidanceToolResponse());
|
||||
assertNotNull(CustomerGuidanceToolRequest.builder().build());
|
||||
assertNotNull(CustomerGuidanceToolResponse.builder().build());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 개편");
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user