12 Commits

Author SHA1 Message Date
jade
2723d12568 Refactor: rename ToolHint to GrowToolHint and update annotations 2026-08-13 16:20:00 +09:00
jade
df03037d3f update: apply recent changes from local workspace
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m54s
2026-08-13 09:52:30 +09:00
윤희준
fc235f05b9 ci: configure SizeAndTimeBasedRollingPolicy in logback to prevent disk space issues
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 2m54s
2026-08-13 09:48:07 +09:00
jade
97d380afe0 feat: enhance tool scaffold and usecase naming
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 0s
2026-08-12 23:42:14 +09:00
jade
bd4d2a7624 revert: restore register=false check in HeartbeatSender
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 3s
2026-08-12 23:34:42 +09:00
jade
b39c66000a docs: define multi-tool scaffold design 2026-08-12 22:53:57 +09:00
jade
11d78a0119 fix: ignore register=false to expose all tools
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 2m18s
2026-08-12 22:18:00 +09:00
jade
1d4e8b8ebd fix: add missing V17 YAML definition for cmm_customer_tool
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m52s
2026-08-12 18:24:10 +09:00
jade
a661e07a9f fix: resolve SSE stream completion issue, fix mapstruct typo, and remove duplicate GlowLogger
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 14s
2026-08-12 18:20:48 +09:00
Boram
4ecff183c6 어노테이션수정
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 14s
2026-08-12 18:06:25 +09:00
Boram
999b4ebae0 Merge remote-tracking branch 'origin/main'
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 17s
2026-08-12 17:48:15 +09:00
Boram
69e89e0d52 고객 통합 안내이력 조회 sample 생성 2026-08-12 17:48:00 +09:00
76 changed files with 1834 additions and 371 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,45 @@ 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);
}
}
@GetMapping("/usecases")
public List<String> listUseCases(@RequestParam String moduleName, @RequestParam String categoryKey) {
if (moduleName == null || !moduleName.matches("^dap-was-[a-z0-9-]+$")) {
throw new IllegalArgumentException("Invalid target module.");
}
if (categoryKey == null || !categoryKey.matches("^[a-z0-9]{3}$")) {
throw new IllegalArgumentException("Invalid domain category.");
}
String sourceDir = System.getenv("AXHUB_SOURCE_DIR");
if (sourceDir == null || sourceDir.isBlank()) sourceDir = System.getProperty("user.dir");
File useCaseDir = new File(sourceDir, moduleName + "/src/main/java/io/shinhanlife/dap/mcc/biz/"
+ categoryKey + "/usecase");
File[] files = useCaseDir.listFiles(file -> file.isFile() && file.getName().endsWith("UseCase.java"));
if (files == null) return List.of();
return Arrays.stream(files).map(File::getName)
.map(name -> name.substring(0, name.length() - ".java".length()))
.sorted().toList();
}
@PostMapping("/field-draft")
public ResponseEntity<?> generateFieldDraft(@RequestBody Map<String, String> req) {
String description = req.getOrDefault("description", "").trim();
@@ -133,8 +172,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 +202,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 +288,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 +299,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 +307,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을 생성했습니다.");
@@ -319,8 +375,7 @@ public class ScaffoldingController {
return chatClientBuilder.build().prompt()
.user(prompt)
.options(org.springframework.ai.openai.OpenAiChatOptions.builder()
.model(resolveModel(requestedModel))
.build())
.model(resolveModel(requestedModel)).build())
.call()
.content();
}
@@ -355,4 +410,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

@@ -179,7 +179,7 @@ public class CustomWebMvcSseServerTransportProvider implements McpServerTranspor
log.info("Message sent to session handler");
if (emitter != null && !map.containsKey("id")) {
emitter.complete();
// emitter.complete();
log.info("Completed emitter for notification (disabled for keep-alive)");
}
@@ -220,7 +220,7 @@ public class CustomWebMvcSseServerTransportProvider implements McpServerTranspor
// Custom 프로토콜: 1회 요청당 1응답 후 종료 (스트림을 닫아버림)
// 클라이언트가 한 번의 POST 후 응답을 받고 연결을 끊기 때문
this.emitter.complete();
// this.emitter.complete(); // MCP 표준 클라이언트 지원을 위해 스트림 강제 종료 제거
}
} catch (Exception e) {
log.error("Error sending message to SSE emitter", e);

View File

@@ -17,11 +17,13 @@
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>/swlog/dap-gateway/A01/${HOSTNAME}_A01.log</file> <!-- 현재 로그가 쌓이는 파일 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- 매일 자정에 날짜를 붙여서 지난 로그를 분리 저장 -->
<fileNamePattern>/swlog/dap-gateway/A01/${HOSTNAME}_A01_%d{yyyyMMdd}.log</fileNamePattern>
<!-- 최대 30일치 로그만 보관하고 오래된 것은 자동 삭제 -->
<fileNamePattern>/swlog/dap-gateway/A01/${HOSTNAME}_A01_%d{yyyyMMdd}.%i.log</fileNamePattern>
<!-- 개별 로그 파일 크기를 100MB로 제한하고, 전체 보관 일수는 30일, 총 로그 누적 용량은 1GB로 제한 -->
<maxFileSize>100MB</maxFileSize>
<maxHistory>30</maxHistory>
<totalSizeCap>1GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>${LOG_PATTERN}</pattern>

View File

@@ -824,6 +824,20 @@
<!-- 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 (HTTP / 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-4"><select id="toolGroupUseCaseSelect" class="form-select"><option value="">새 UseCase 생성</option></select></div>
<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">
@@ -1022,7 +1036,7 @@
<div class="docgen-field">
<label class="docgen-field-label" for="docgenToolSelect">Tool</label>
<select id="docgenToolSelect" disabled><option>Tool Metadata를 불러오는 중...</option></select>
<div id="docgenToolHint" class="docgen-hint">Gateway의 ToolMetadata 목록을 조회하고 있습니다.</div>
<div id="docgenGrowToolHint" class="docgen-hint">Gateway의 ToolMetadata 목록을 조회하고 있습니다.</div>
</div>
<div class="docgen-two-col docgen-field">
@@ -1158,6 +1172,8 @@
<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: 260px;">Object list item fields (JSON)</th>
<th style="min-width: 210px;">Description</th>
<th style="min-width: 180px;">Example</th>
<th style="min-width: 125px;">Required</th>
@@ -1260,7 +1276,7 @@
const button = dg('docgenGenerateBtn');
select.disabled = true;
button.disabled = true;
dg('docgenToolHint').textContent = 'Gateway의 ToolMetadata 목록을 조회하고 있습니다.';
dg('docgenGrowToolHint').textContent = 'Gateway의 ToolMetadata 목록을 조회하고 있습니다.';
try {
const response = await fetch('/mcp/api/v1/tools/list', { cache: 'no-store' });
if (!response.ok) throw new Error(`Gateway HTTP ${response.status}`);
@@ -1275,7 +1291,7 @@
`<option value="${index}">[${escapeHtml(tool.categoryKey || 'etc')}] ${escapeHtml(tool.name || tool.displayName || tool.uid)}</option>`
).join('');
select.disabled = state.tools.length === 0;
dg('docgenToolHint').textContent = `ToolMetadata에 등록된 ${state.tools.length}개 Tool을 모두 표시합니다.`;
dg('docgenGrowToolHint').textContent = `ToolMetadata에 등록된 ${state.tools.length}개 Tool을 모두 표시합니다.`;
if (state.tools.length) {
select.value = '0';
selectTool();
@@ -1287,7 +1303,7 @@
state.selected = null;
showMetadataError(error.message);
select.innerHTML = '<option>Tool Metadata 조회 실패</option>';
dg('docgenToolHint').innerHTML = `<span class="docgen-error">${escapeHtml(error.message)}</span> · Gateway 실행 상태를 확인하세요.`;
dg('docgenGrowToolHint').innerHTML = `<span class="docgen-error">${escapeHtml(error.message)}</span> · Gateway 실행 상태를 확인하세요.`;
}
}
@@ -1469,6 +1485,130 @@
});
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) {
throw new Error('Base Name을 입력해주세요.');
}
if (data.routingType === 'MCI' && (!data.interfaceId || !data.clientSystemCode)) {
throw new Error('MCI Tool은 Legacy Interface ID와 Target System Code를 입력해야 합니다.');
}
if (data.routingType === 'HTTP' && !data.httpApiName) {
throw new Error('HTTP Tool은 HTTP API Name을 입력해야 합니다.');
}
if (!['MCI', 'HTTP'].includes(data.routingType)) {
throw new Error('여러 Tool UseCase는 MCI 또는 HTTP 프로토콜만 지원합니다.');
}
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);
}
}
async function loadUseCasesForSelection() {
const moduleName = document.getElementById('targetModuleSelect').value;
const categoryKey = document.querySelector('#toolForm [name="categoryKey"]').value.trim();
const select = document.getElementById('toolGroupUseCaseSelect');
select.innerHTML = '<option value="">새 UseCase 생성</option>';
if (!/^[a-z0-9]{3}$/.test(categoryKey)) return;
try {
const response = await fetch(`/api/v1/scaffold/usecases?moduleName=${encodeURIComponent(moduleName)}&categoryKey=${encodeURIComponent(categoryKey)}`);
if (!response.ok) throw new Error('UseCase 목록 조회 실패');
const useCases = await response.json();
useCases.forEach(useCaseName => select.add(new Option(`기존 ${useCaseName}에 함수 추가`, useCaseName)));
} catch (error) {
console.warn(error);
}
}
document.querySelector('#toolForm [name="categoryKey"]').addEventListener('change', loadUseCasesForSelection);
document.getElementById('targetModuleSelect').addEventListener('change', loadUseCasesForSelection);
document.getElementById('toolGroupUseCaseSelect').addEventListener('change', function() {
const nameInput = document.getElementById('toolGroupUseCaseName');
if (this.value) {
nameInput.value = this.value.replace(/UseCase$/, '');
nameInput.readOnly = true;
} else {
nameInput.value = '';
nameInput.readOnly = false;
}
});
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 +1638,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: {
@@ -1601,12 +1743,22 @@
const option = new Option(type, type, false, (field.type || 'String') === type);
typeSelect.add(option);
});
typeSelect.addEventListener('change', () => {
if (!exampleInput.value.trim()) {
exampleInput.value = typeExamples[typeSelect.value];
const createDetailsInput = (type, value) => {
let input;
if (type === 'List') {
input = document.createElement('select');
input.className = 'form-select form-select-sm';
['String', 'Integer', 'Long', 'Double', 'Boolean', 'BigDecimal', 'Object'].forEach(itemType => {
input.add(new Option(itemType, itemType, false, (value || 'String') === itemType));
});
} else {
input = makeInput(type === 'Enum' ? value : '', 'Enum: OPEN, CLOSED');
}
updateFieldEditorPreview();
});
input.dataset.field = 'details';
return input;
};
let detailsInput = createDetailsInput(field.type || 'String',
field.type === 'Enum' ? (field.enumValues || []).join(', ') : field.itemType);
const requiredSelect = document.createElement('select');
requiredSelect.className = 'form-select form-select-sm';
@@ -1614,12 +1766,41 @@
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 => {
const itemFieldsInput = makeInput(field.type === 'List' && field.itemType === 'Object'
? JSON.stringify(field.itemFields || []) : '', 'Object fields JSON e.g. [{"name":"date","type":"String"}]');
itemFieldsInput.dataset.field = 'itemFields';
const updateObjectListFieldsState = () => {
const objectList = typeSelect.value === 'List' && detailsInput.value === 'Object';
itemFieldsInput.disabled = !objectList;
itemFieldsInput.placeholder = objectList
? 'Object fields JSON e.g. [{"name":"date","type":"String"}]'
: 'Select List > Object to enter item fields';
if (!objectList) itemFieldsInput.value = '';
};
const bindDetailsInput = () => {
detailsInput.addEventListener('input', updateFieldEditorPreview);
detailsInput.addEventListener('change', () => {
updateObjectListFieldsState();
updateFieldEditorPreview();
});
};
typeSelect.addEventListener('change', () => {
if (!exampleInput.value.trim()) exampleInput.value = typeExamples[typeSelect.value];
const replacement = createDetailsInput(typeSelect.value, typeSelect.value === 'List' ? 'String' : '');
detailsInput.replaceWith(replacement);
detailsInput = replacement;
bindDetailsInput();
updateObjectListFieldsState();
updateFieldEditorPreview();
});
bindDetailsInput();
updateObjectListFieldsState();
[nameInput, descriptionInput, exampleInput, requiredSelect, itemFieldsInput].forEach(control => {
control.addEventListener('input', updateFieldEditorPreview);
control.addEventListener('change', updateFieldEditorPreview);
});
[nameInput, typeSelect, descriptionInput, exampleInput, requiredSelect].forEach(control => {
[nameInput, typeSelect, detailsInput, itemFieldsInput, descriptionInput, exampleInput, requiredSelect].forEach(control => {
const cell = document.createElement('td');
cell.appendChild(control);
row.appendChild(cell);
@@ -1641,13 +1822,26 @@
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();
let itemFields = [];
if (type === 'List' && details === 'Object') {
const itemFieldsText = row.querySelector('[data-field="itemFields"]').value.trim();
if (!itemFieldsText) throw new Error('List Object는 항목 필드를 입력해야 합니다.');
itemFields = JSON.parse(itemFieldsText);
if (!Array.isArray(itemFields) || itemFields.length === 0) throw new Error('List Object 항목 필드는 JSON 배열이어야 합니다.');
}
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,7 +8,12 @@ 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 java.nio.file.Files;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
@@ -16,9 +21,50 @@ 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 listsExistingUseCasesForSelectedModuleAndCategory() throws Exception {
Path useCaseDir = root.resolve("dap-was-sample/src/main/java/io/shinhanlife/dap/mcc/biz/smp/usecase");
Files.createDirectories(useCaseDir);
Files.writeString(useCaseDir.resolve("EmployeeSearchUseCase.java"), "interface EmployeeSearchUseCase {}\n");
Files.writeString(useCaseDir.resolve("Ignored.java"), "class Ignored {}\n");
String previousUserDir = System.getProperty("user.dir");
System.setProperty("user.dir", root.toString());
try {
ScaffoldingController controller = new ScaffoldingController(mock(ChatClient.Builder.class), new ObjectMapper());
assertEquals(java.util.List.of("EmployeeSearchUseCase"), controller.listUseCases("dap-was-sample", "smp"));
} finally {
System.setProperty("user.dir", previousUserDir);
}
}
@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

@@ -8,7 +8,7 @@ import java.lang.annotation.Target;
/**
* Spring AI @Tool 어노테이션을 보완하여 MCP 시스템 메타데이터를 추가 제공하는 힌트 어노테이션
* @package io.shinhanlife.dap.lib.annotation
* @className ToolHint
* @className GrowToolHint
* @description 비즈니스 로직(Tool) 시스템 제어 메타데이터 분리
* @author 0986406
* @create 2026.09.01
@@ -22,7 +22,7 @@ import java.lang.annotation.Target;
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ToolHint {
public @interface GrowToolHint {
boolean register() default false;
boolean requiresApproval() default false;
String categoryKey() default "com";

View File

@@ -1,6 +1,6 @@
package io.shinhanlife.dap.lib.mcp;
import io.shinhanlife.dap.lib.annotation.ToolHint;
import io.shinhanlife.dap.lib.annotation.GrowToolHint;
import io.shinhanlife.dap.lib.config.McpProperties;
import java.lang.reflect.Method;
import java.util.LinkedHashMap;
@@ -45,7 +45,7 @@ public class McpToolMethodRegistry {
bean,
findInvocableMethod(bean, declaredMethod),
annotation,
AnnotationUtils.findAnnotation(declaredMethod, ToolHint.class));
AnnotationUtils.findAnnotation(declaredMethod, GrowToolHint.class));
register(discovered, annotation.name(), tool);
registerNamespaceAlias(discovered, annotation.name(), tool);
}
@@ -82,6 +82,6 @@ public class McpToolMethodRegistry {
}
}
public record RegisteredTool(Object bean, Method method, McpTool annotation, ToolHint hint) {
public record RegisteredTool(Object bean, Method method, McpTool annotation, GrowToolHint hint) {
}
}

View File

@@ -17,7 +17,7 @@ package io.shinhanlife.dap.lib.mcp;
*/
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springaicommunity.mcp.annotation.McpTool;
import io.shinhanlife.dap.lib.annotation.ToolHint;
import io.shinhanlife.dap.lib.annotation.GrowToolHint;
import io.shinhanlife.dap.lib.config.McpProperties;
import io.shinhanlife.dap.mcc.dto.ToolMetadata;
import io.shinhanlife.dap.lib.util.ToolSchemaResolver;
@@ -102,7 +102,7 @@ public class ToolRegistryHeartbeatSender {
for (Method method : targetClass.getDeclaredMethods()) {
McpTool functionAnnotation = AnnotationUtils.findAnnotation(method, McpTool.class);
ToolHint hintAnnotation = AnnotationUtils.findAnnotation(method, ToolHint.class);
GrowToolHint hintAnnotation = AnnotationUtils.findAnnotation(method, GrowToolHint.class);
if (functionAnnotation != null) {
String baseName = functionAnnotation.name();
@@ -110,7 +110,7 @@ public class ToolRegistryHeartbeatSender {
String subToolName = mcpProperties.getNamespace() != null && !mcpProperties.getNamespace().isEmpty()
? mcpProperties.getNamespace() + "_" + rawSubToolName
: rawSubToolName;
// @ToolHint(register = false)인 Tool은 메타데이터 조회에는 남기되,
// @GrowToolHint(register = false)인 Tool은 메타데이터 조회에는 남기되,
// Gateway 등록 및 heartbeat 전송 대상에서는 제외합니다.
// ToolHint가 없는 기존 Tool은 이전 동작과 동일하게 등록합니다.
boolean isRegister = hintAnnotation == null || hintAnnotation.register();
@@ -184,7 +184,7 @@ public class ToolRegistryHeartbeatSender {
}
}
private void enrichWithDefinition(ToolMetadata meta, String rawToolName, ToolHint hintAnnotation) {
private void enrichWithDefinition(ToolMetadata meta, String rawToolName, GrowToolHint hintAnnotation) {
if (toolDefinitionRepository == null) {
return;
}
@@ -192,7 +192,7 @@ public class ToolRegistryHeartbeatSender {
.ifPresent(definition -> applyDefinition(meta, definition, hintAnnotation));
}
private void applyDefinition(ToolMetadata meta, ToolDefinition definition, ToolHint hintAnnotation) {
private void applyDefinition(ToolMetadata meta, ToolDefinition definition, GrowToolHint hintAnnotation) {
meta.setDisplayName(definition.displayName());
meta.setSemver(definition.version());
meta.setCategoryKey(definition.categoryKey());

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,344 @@ 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;
Path useCaseFile = useCaseDir.resolve(useCaseBaseName + "UseCase.java");
Path useCaseImplFile = implDir.resolve(useCaseBaseName + "UseCaseImpl.java");
Path converterFile = converterDir.resolve(useCaseBaseName + "Converter.java");
boolean existingUseCase = Files.exists(useCaseFile);
if (existingUseCase) {
appendGroupedUseCaseSources(useCaseFile, useCaseImplFile, converterFile, bizPackage, useCaseBaseName,
moduleName, tools);
} else {
writeUtf8(useCaseFile, groupedUseCaseContent(bizPackage, useCaseBaseName, moduleName, tools));
writeUtf8(useCaseImplFile, groupedUseCaseImplContent(bizPackage, useCaseBaseName, tools));
writeUtf8(converterFile, groupedConverterContent(bizPackage, useCaseBaseName, tools));
}
StringBuilder log = new StringBuilder("\n=========================================\n")
.append(" Multi Tool Scaffolding Complete\n")
.append("=========================================\n")
.append(existingUseCase ? " Existing UseCase Extended\n" : " New UseCase Created\n")
.append("[Usecase Interface] ").append(useCaseFile).append("\n")
.append("[Usecase Impl] ").append(useCaseImplFile).append("\n");
for (ToolMethodDefinition tool : tools) {
writeGroupedToolFiles(moduleRoot, sourceRoot, dtoDir, definitionDir, mockDir, bizPackage, tool, moduleName, log);
if ("HTTP".equalsIgnoreCase(tool.routingType())) {
ensureLocalHttpApiConfiguration(moduleRoot, tool.httpApiName(),
toToolName(moduleName, tool.group(), toPascalCase(tool.baseName())));
}
}
log.append("[Converter] ").append(converterFile).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.");
}
boolean mci = "MCI".equalsIgnoreCase(tool.routingType());
boolean http = "HTTP".equalsIgnoreCase(tool.routingType());
if (!mci && !http) {
throw new IllegalArgumentException("Grouped Tool supports only MCI or HTTP routing.");
}
if (mci && (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.");
}
if (http && (tool.httpApiName() == null || tool.httpApiName().isBlank())) {
throw new IllegalArgumentException("HTTP Tool needs an HTTP API name.");
}
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());
boolean mci = "MCI".equalsIgnoreCase(tool.routingType());
String code = mci ? tool.clientSystemCode().toLowerCase(Locale.ROOT) : toPackageSegment(tool.httpApiName());
String ioPackage = BASE_PACKAGE + (mci ? ".infra.itrf.mci." : ".infra.itrf.http.") + code;
Path clientDir = sourceRoot.resolve(Paths.get("infra", "itrf", mci ? "mci" : "http", 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());
if (mci) {
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()));
writeUtf8(sourceRoot.resolve(Paths.get("biz", tool.group().toLowerCase(Locale.ROOT), "converter", baseName + "Converter.java")),
groupedMciConverterContent(bizPackage, baseName, ioPackage));
} else {
writeUtf8(ioDir.resolve(baseName + "HttpRequest.java"),
dtoContent(ioPackage + ".io", baseName + "HttpRequest", tool.inputFields(), "", "", true));
writeUtf8(ioDir.resolve(baseName + "HttpResponse.java"),
dtoContent(ioPackage + ".io", baseName + "HttpResponse", tool.outputFields(), "", "", false));
writeStructuredFieldTypes(ioDir, ioPackage + ".io", baseName + "HttpRequest", tool.inputFields());
writeStructuredFieldTypes(ioDir, ioPackage + ".io", baseName + "HttpResponse", tool.outputFields());
writeUtf8(clientDir.resolve(baseName + "Client.java"),
httpClientContent(ioPackage, baseName + "Client", tool.httpApiName()));
writeUtf8(sourceRoot.resolve(Paths.get("biz", tool.group().toLowerCase(Locale.ROOT), "converter", baseName + "Converter.java")),
groupedHttpConverterContent(bizPackage, baseName, ioPackage));
}
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(" @GrowToolHint(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.GrowToolHint;\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 clientVariable = Character.toLowerCase(baseName.charAt(0)) + baseName.substring(1) + "Client";
String converterVariable = Character.toLowerCase(baseName.charAt(0)) + baseName.substring(1) + "Converter";
boolean mci = "MCI".equalsIgnoreCase(tool.routingType());
String integrationPackage = BASE_PACKAGE + (mci ? ".infra.itrf.mci." + tool.clientSystemCode().toLowerCase(Locale.ROOT)
: ".infra.itrf.http." + toPackageSegment(tool.httpApiName()));
imports.append("import ").append(bizPackage).append(".dto.").append(baseName).append("Request;\n")
.append("import ").append(bizPackage).append(".dto.").append(baseName).append("Response;\n")
.append("import ").append(bizPackage).append(".converter.").append(baseName).append("Converter;\n")
.append("import ").append(integrationPackage).append(".").append(baseName).append("Client;\n")
.append("import ").append(integrationPackage).append(".io.").append(baseName)
.append(mci ? "_I;\n" : "HttpRequest;\n")
.append("import ").append(integrationPackage).append(".io.").append(baseName)
.append(mci ? "_O;\n" : "HttpResponse;\n");
fields.append(" private final ").append(baseName).append("Client ").append(clientVariable).append(";\n");
fields.append(" private final ").append(baseName).append("Converter ").append(converterVariable).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(mci ? "_I" : "HttpRequest").append(" request = ").append(converterVariable).append(".toRequest(req);\n")
.append(" ").append(baseName).append(mci ? "_O" : "HttpResponse").append(" response = ").append(clientVariable).append(mci ? ".call" + baseName + "(request);\n" : ".call(request, " + baseName + "HttpResponse.class);\n")
.append(" ").append(baseName).append("Response toolResponse = ").append(converterVariable).append(".toResponse(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 + ".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"
+ fields + "\n" + methods + "}\n";
}
private static String groupedConverterContent(String bizPackage, String useCaseBaseName,
List<ToolMethodDefinition> tools) {
return "package " + bizPackage + ".converter;\n\n/** Per-Tool converters are generated beside this compatibility marker. */\n"
+ "public interface " + useCaseBaseName + "Converter {\n}\n";
}
private static String groupedMciConverterContent(String bizPackage, String baseName, String ioPackage) {
return "package " + bizPackage + ".converter;\n\n"
+ "import " + bizPackage + ".dto." + baseName + "Request;\n"
+ "import " + bizPackage + ".dto." + baseName + "Response;\n"
+ "import " + ioPackage + ".io." + baseName + "_I;\n"
+ "import " + ioPackage + ".io." + baseName + "_O;\n"
+ "import org.mapstruct.Mapper;\nimport org.mapstruct.ReportingPolicy;\n\n"
+ "@Mapper(componentModel = \"spring\", unmappedTargetPolicy = ReportingPolicy.IGNORE)\n"
+ "public interface " + baseName + "Converter {\n"
+ " " + baseName + "_I toRequest(" + baseName + "Request request);\n"
+ " " + baseName + "Response toResponse(" + baseName + "_O response);\n}\n";
}
private static String groupedHttpConverterContent(String bizPackage, String baseName, String ioPackage) {
return "package " + bizPackage + ".converter;\n\n"
+ "import " + bizPackage + ".dto." + baseName + "Request;\n"
+ "import " + bizPackage + ".dto." + baseName + "Response;\n"
+ "import " + ioPackage + ".io." + baseName + "HttpRequest;\n"
+ "import " + ioPackage + ".io." + baseName + "HttpResponse;\n"
+ "import org.mapstruct.Mapper;\nimport org.mapstruct.ReportingPolicy;\n\n"
+ "@Mapper(componentModel = \"spring\", unmappedTargetPolicy = ReportingPolicy.IGNORE)\n"
+ "public interface " + baseName + "Converter {\n"
+ " " + baseName + "HttpRequest toRequest(" + baseName + "Request request);\n"
+ " " + baseName + "Response toResponse(" + baseName + "HttpResponse response);\n}\n";
}
private static void appendGroupedUseCaseSources(Path useCaseFile, Path useCaseImplFile, Path converterFile,
String bizPackage, String useCaseBaseName, String moduleName,
List<ToolMethodDefinition> tools) throws IOException {
if (!Files.exists(useCaseImplFile)) {
throw new IllegalArgumentException("UseCase implementation not found: " + useCaseImplFile);
}
String useCase = Files.readString(useCaseFile, StandardCharsets.UTF_8);
String implementation = Files.readString(useCaseImplFile, StandardCharsets.UTF_8);
for (ToolMethodDefinition tool : tools) {
String baseName = toPascalCase(tool.baseName());
String methodName = tool.methodName();
String toolName = toToolName(moduleName, tool.group(), baseName);
if (useCase.matches("(?s).*\\b" + java.util.regex.Pattern.quote(methodName) + "\\s*\\(.*")
|| useCase.contains("name = \"" + toolName + "\"")) {
throw new IllegalArgumentException("Tool method or MCP Tool name already exists: " + methodName);
}
boolean mci = "MCI".equalsIgnoreCase(tool.routingType());
String integrationPackage = BASE_PACKAGE + (mci ? ".infra.itrf.mci." + tool.clientSystemCode().toLowerCase(Locale.ROOT)
: ".infra.itrf.http." + toPackageSegment(tool.httpApiName()));
String requestType = baseName + "Request";
String responseType = baseName + "Response";
String requestIo = baseName + (mci ? "_I" : "HttpRequest");
String responseIo = baseName + (mci ? "_O" : "HttpResponse");
String clientVariable = Character.toLowerCase(baseName.charAt(0)) + baseName.substring(1) + "Client";
String converterVariable = Character.toLowerCase(baseName.charAt(0)) + baseName.substring(1) + "Converter";
useCase = addImport(useCase, "import " + bizPackage + ".dto." + requestType + ";") ;
useCase = addImport(useCase, "import " + bizPackage + ".dto." + responseType + ";") ;
String declaration = "\n @McpTool(name = \"" + toolName + "\", title = \"" + javaText(option(tool.title(), baseName))
+ "\", description = \"" + javaText(option(tool.description(), "")) + "\")\n"
+ " @GrowToolHint(register = " + tool.register() + ", categoryKey = \"" + tool.group().toLowerCase(Locale.ROOT)
+ "\", mappingId = \"" + javaText(option(tool.interfaceId(), tool.httpApiName())) + "\")\n"
+ " " + responseType + " " + methodName + "(" + requestType + " req);\n";
useCase = insertBeforeLastBrace(useCase, declaration);
implementation = addImport(implementation, "import " + bizPackage + ".dto." + requestType + ";");
implementation = addImport(implementation, "import " + bizPackage + ".dto." + responseType + ";");
implementation = addImport(implementation, "import " + bizPackage + ".converter." + baseName + "Converter;");
implementation = addImport(implementation, "import " + integrationPackage + "." + baseName + "Client;");
implementation = addImport(implementation, "import " + integrationPackage + ".io." + requestIo + ";");
implementation = addImport(implementation, "import " + integrationPackage + ".io." + responseIo + ";");
implementation = insertConstructorField(implementation, " private final " + baseName + "Client " + clientVariable + ";");
implementation = insertConstructorField(implementation, " private final " + baseName + "Converter " + converterVariable + ";");
String call = mci
? clientVariable + ".call" + baseName + "(request)"
: clientVariable + ".call(request, " + responseIo + ".class)";
String method = "\n @Override\n public " + responseType + " " + methodName + "(" + requestType + " req) {\n"
+ " " + requestIo + " request = " + converterVariable + ".toRequest(req);\n"
+ " " + responseIo + " response = " + call + ";\n"
+ " " + responseType + " toolResponse = " + converterVariable + ".toResponse(response);\n"
+ " if (toolResponse == null) toolResponse = new " + responseType + "();\n"
+ " toolResponse.setResultCode(\"SUCCESS\");\n"
+ " return toolResponse;\n }\n";
implementation = insertBeforeLastBrace(implementation, method);
}
writeUtf8(useCaseFile, useCase);
writeUtf8(useCaseImplFile, implementation);
if (!Files.exists(converterFile)) {
writeUtf8(converterFile, groupedConverterContent(bizPackage, useCaseBaseName, tools));
}
}
private static String addImport(String content, String importLine) {
if (content.contains(importLine)) return content;
int lastImport = content.lastIndexOf("import ");
if (lastImport < 0) {
int packageEnd = content.indexOf(';');
return content.substring(0, packageEnd + 1) + "\n\n" + importLine + content.substring(packageEnd + 1);
}
int lineEnd = content.indexOf('\n', lastImport);
return content.substring(0, lineEnd + 1) + importLine + "\n" + content.substring(lineEnd + 1);
}
private static String insertConstructorField(String content, String field) {
if (content.contains(field)) return content;
int constructorField = content.indexOf("private final ");
if (constructorField < 0) return insertBeforeLastBrace(content, "\n" + field + "\n");
int lineEnd = content.indexOf('\n', constructorField);
return content.substring(0, lineEnd + 1) + field + "\n" + content.substring(lineEnd + 1);
}
private static String insertBeforeLastBrace(String content, String addition) {
int brace = content.lastIndexOf('}');
if (brace < 0) throw new IllegalArgumentException("Java source closing brace not found.");
return content.substring(0, brace) + addition + content.substring(brace);
}
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 +603,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,23 +638,24 @@ 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);
String toolHintLine;
if (useSchemaResource) {
toolHintLine = (" @ToolHint(register = %s, categoryKey = \"%s\", mappingId = \"%s\",\n" +
toolHintLine = (" @GrowToolHint(register = %s, categoryKey = \"%s\", mappingId = \"%s\",\n" +
" inputSchemaResource = \"%s\",\n" +
" outputSchemaResource = \"%s\")").formatted(register, group.toLowerCase(Locale.ROOT), interfaceId, inputSchemaClasspath, outputSchemaClasspath);
} else {
toolHintLine = " @ToolHint(register = %s, categoryKey = \"%s\", mappingId = \"%s\")".formatted(register, group.toLowerCase(Locale.ROOT), interfaceId);
toolHintLine = " @GrowToolHint(register = %s, categoryKey = \"%s\", mappingId = \"%s\")".formatted(register, group.toLowerCase(Locale.ROOT), interfaceId);
}
String serviceInterfaceContent = """
package %s.usecase;
import org.springaicommunity.mcp.annotation.McpTool;
import io.shinhanlife.dap.lib.annotation.ToolHint;
import io.shinhanlife.dap.lib.annotation.GrowToolHint;
import %s.dto.%sRequest;
import %s.dto.%sResponse;
@@ -551,6 +902,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 +930,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 +1045,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 +1280,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 +1349,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 +1421,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", " ");
@@ -1088,7 +1463,7 @@ public class ToolScaffolder {
}
private static void ensureLocalHttpApiConfiguration(Path projectRoot, String httpApiName, String toolName) throws IOException {
Path localConfigPath = projectRoot.resolve("dap-was-lib/src/main/resources/glow/application-glow-local.yml");
Path localConfigPath = projectRoot.resolve("src/main/resources/glow/application-glow-local.yml");
Files.createDirectories(localConfigPath.getParent());
String existing = Files.exists(localConfigPath) ? Files.readString(localConfigPath, StandardCharsets.UTF_8) : "";
if (java.util.regex.Pattern.compile("(?m)^\\s*-\\s+name:\\s*"
@@ -1143,36 +1518,128 @@ 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);
%s%s}
""".formatted(packageName, listImport, className, body, innerObjectListClasses(fields));
}
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));
%s%s}
""".formatted(BASE_PACKAGE, packageSuffix, listImport, className, fieldLines(fields, Set.of(), className),
innerObjectListClasses(fields));
}
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())
&& (field.itemFields() == null || field.itemFields().isEmpty())) {
throw new IllegalArgumentException("Object List field needs item fields: " + field.name());
}
}
}
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 toPascalCase(field.name()) + "Item";
}
private static String innerObjectListClasses(List<FieldDefinition> fields) {
StringBuilder source = new StringBuilder();
Set<String> generated = new LinkedHashSet<>();
for (FieldDefinition field : fields == null ? List.<FieldDefinition>of() : fields) {
if (field == null || !"List".equals(field.type()) || !"Object".equals(field.itemType())
|| field.name() == null || field.name().isBlank()) {
continue;
}
String itemName = listItemClassName("", field);
if (!generated.add(itemName)) continue;
List<FieldDefinition> itemFields = field.itemFields() == null ? List.of() : field.itemFields();
if (itemFields.isEmpty()) {
throw new IllegalArgumentException("Object List field needs item fields: " + field.name());
}
source.append("\n @Data\n public static class ").append(itemName).append(" {\n")
.append(fieldLines(itemFields, Set.of(), itemName))
.append(innerObjectListClasses(itemFields))
.append(" }\n");
}
return source.toString();
}
private static String httpUseCaseImplContent(String bizPackage, String baseName, String httpPackage,
@@ -1334,10 +1801,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 +1819,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 +1831,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 +1891,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

@@ -3,7 +3,7 @@ package io.shinhanlife.dap.lib.util;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.shinhanlife.dap.lib.annotation.McpOutputSchema;
import io.shinhanlife.dap.lib.annotation.ToolHint;
import io.shinhanlife.dap.lib.annotation.GrowToolHint;
import java.io.InputStream;
import java.util.Map;
import org.springframework.core.io.ClassPathResource;
@@ -18,7 +18,7 @@ public class ToolSchemaResolver {
}
public Map<String, Object> resolve(org.springaicommunity.mcp.annotation.McpTool function,
ToolHint hint, Class<?> requestType) {
GrowToolHint hint, Class<?> requestType) {
if (hint != null && !hint.inputSchemaResource().isBlank()) {
return loadResource(hint.inputSchemaResource());
}
@@ -30,7 +30,7 @@ public class ToolSchemaResolver {
* A JSON resource has precedence over a DTO marker annotation.
*/
public Map<String, Object> resolveOutput(org.springaicommunity.mcp.annotation.McpTool function,
Class<?> responseType, ToolHint hint) {
Class<?> responseType, GrowToolHint hint) {
if (hint != null && !hint.outputSchemaResource().isBlank()) {
return loadResource(hint.outputSchemaResource());
}

View File

@@ -63,15 +63,15 @@ public final class ToolSourceUpdater {
}
private static String updateToolHint(String content, String categoryKey, boolean register, Boolean requiresApproval) {
AnnotationRange range = annotationArguments(content, "ToolHint", null);
AnnotationRange range = annotationArguments(content, "GrowToolHint", null);
if (range == null) {
throw new IllegalArgumentException("ToolHint declaration not found next to McpTool");
throw new IllegalArgumentException("GrowToolHint declaration not found next to McpTool");
}
String updated = replaceAttribute(content, range, "register", Boolean.toString(register));
range = annotationArguments(updated, "ToolHint", null);
range = annotationArguments(updated, "GrowToolHint", null);
if (requiresApproval != null) {
updated = replaceAttribute(updated, range, "requiresApproval", Boolean.toString(requiresApproval));
range = annotationArguments(updated, "ToolHint", null);
range = annotationArguments(updated, "GrowToolHint", null);
}
if (categoryKey != null && !categoryKey.isBlank()) {
updated = replaceAttribute(updated, range, "categoryKey", quote(categoryKey));

View File

@@ -10,20 +10,6 @@ import lombok.NoArgsConstructor;
@NoArgsConstructor
@AllArgsConstructor
//@Schema(description = "응답 에러 객체. 성공 케이스일 경우 null. 실제 에러가 발생할 경우에만 예외명, 예외 메시지 필드 세팅 예정.")
/**
* @package io.shinhanlife.glow
* @className BaseException
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
public class BaseException {
// @Schema(description = "Error 코드 Meta 참조 운영. (예) 20001, 50001 등", shinhanlife = "20001")
@@ -40,4 +26,4 @@ public class BaseException {
@Builder.Default
String exceptionDetail = "";
}
}

View File

@@ -5,20 +5,6 @@ import lombok.Builder;
import lombok.Getter;
import lombok.ToString;
/**
* @package io.shinhanlife.glow
* @className BaseResponse
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
@ToString
@Getter
@Builder
@@ -36,4 +22,4 @@ public class BaseResponse<T> {
private BaseException error;
}
}

View File

@@ -1,20 +1,61 @@
package io.shinhanlife.glow;
/**
* @package io.shinhanlife.glow
* @className BizException
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
* 업무 예외.
*
* <p>두 가지 방식으로 쓸 수 있다.</p>
* <ol>
* <li><b>메시지코드 방식(권장)</b> — {@code throw new BizException("DAH00004", "사번")}<br>
* 통합메시지(ZT_UNFC_MSG)에서 문구를 찾아 {0},{1}.. 을 인자로 치환해 응답한다.
* 문구가 화면·서버 한곳(관리 화면)에서 관리되고, 다국어 확장도 여기서 처리된다.</li>
* <li><b>문구 직접 방식(기존 호환)</b> — {@code throw new BizException("사번은 필수입니다.")}<br>
* 메시지코드로 해석되지 않으면 문구 그대로 응답한다.</li>
* </ol>
*
* <p>변환은 {@code common/config/GlobalExceptionHandler} 가 수행한다.
* 메시지코드 여부는 코드 형식(영문 대문자+숫자 8자리)으로 판별한다.</p>
*/
public class BizException extends RuntimeException {
public BizException(String s) {
/** 통합메시지코드 (문구 직접 방식이면 null) */
private final String msgCd;
/** 메시지 치환 인자 */
private final Object[] msgArgs;
/**
* 문구를 직접 지정하거나, 메시지코드만 던진다.
*
* @param messageOrCode 메시지 문구 또는 통합메시지코드
*/
public BizException(String messageOrCode) {
super(messageOrCode);
this.msgCd = isMessageCode(messageOrCode) ? messageOrCode : null;
this.msgArgs = new Object[0];
}
}
/**
* 메시지코드 + 치환 인자.
*
* @param msgCd 통합메시지코드 (예: DAH00004)
* @param msgArgs {0},{1}.. 에 순서대로 치환될 인자
*/
public BizException(String msgCd, Object... msgArgs) {
super(msgCd);
this.msgCd = msgCd;
this.msgArgs = msgArgs == null ? new Object[0] : msgArgs;
}
public String getMsgCd() {
return msgCd;
}
public Object[] getMsgArgs() {
return msgArgs;
}
/** 통합메시지코드 형식인지 — 영문 대문자 3자리 + 숫자 5자리 (예: DAH00001) */
private static boolean isMessageCode(String value) {
return value != null && value.matches("^[A-Z]{3}\\d{5}$");
}
}

View File

@@ -1,20 +1,5 @@
package io.shinhanlife.glow;
/**
* @package io.shinhanlife.glow
* @className GlowAppServiceId
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import java.lang.annotation.*;
@Target(ElementType.METHOD)

View File

@@ -1,20 +1,5 @@
package io.shinhanlife.glow;
/**
* @package io.shinhanlife.glow
* @className GlowControllerId
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
public @interface GlowControllerId {
String value();
}

View File

@@ -1,20 +1,5 @@
package io.shinhanlife.glow;
/**
* @package io.shinhanlife.glow
* @className GlowIndexPaging
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import java.lang.annotation.*;
@Target(ElementType.METHOD)

View File

@@ -9,21 +9,7 @@ public @interface GlowLogTarget {
Target[] value() default {};
/**
* @package io.shinhanlife.glow
* @className Target
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
enum Target {
FILE, CONSOLE
}
}
}

View File

@@ -5,20 +5,6 @@ import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
/**
* @package io.shinhanlife.glow
* @className GlowLogger
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
@Component
@Scope("prototype")
public class GlowLogger {

View File

@@ -1,20 +1,5 @@
package io.shinhanlife.glow;
/**
* @package io.shinhanlife.glow
* @className GlowMybatisMapper
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Component;

View File

@@ -1,20 +1,5 @@
package io.shinhanlife.glow;
/**
* @package io.shinhanlife.glow
* @className GlowServiceGroupId
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
public @interface GlowServiceGroupId {
String value();

View File

@@ -1,23 +1,7 @@
package io.shinhanlife.glow;
/**
* @package io.shinhanlife.glow
* @className GlowTrgmField
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
import java.lang.annotation.*;
@Deprecated(forRemoval = false)
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented

View File

@@ -2,7 +2,6 @@ package io.shinhanlife.glow;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import io.shinhanlife.glow.communication.annotation.GlowTrgmField;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
@@ -11,20 +10,6 @@ import org.apache.ibatis.session.RowBounds;
import java.io.Serial;
import java.io.Serializable;
/**
* @package io.shinhanlife.glow
* @className PageInfo
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
@Getter
@Setter
@JsonIgnoreProperties(ignoreUnknown = true)
@@ -35,25 +20,25 @@ public class PageInfo extends RowBounds implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 페이지번호 ( 입력값 )
* 페이지번호 (입력값)
*/
@GlowTrgmField(order = 1, length = 5, description = "페이지번호")
private int pageNo;
/**
* 페이지 데이터 건수 ( 열 건수, 입력값 )
* 페이지 데이터 건수 (열 건수, 입력값)
*/
@GlowTrgmField(order = 2, length = 5, description = "페이지데이터건수")
private int pageDataCc;
/**
* 총페이지 수 ( 리턴값 )
* 총페이지 수 (리턴값)
*/
@GlowTrgmField(order = 3, length = 10, description = "총페이지수")
private int totaPageCn;
/**
* 총 페이지 데이터 건수 ( 리턴값 )
* 총 페이지 데이터 건수 (리턴값)
*/
@GlowTrgmField(order = 4, length = 10, description = "총페이지데이터건수")
private int totaPageDataCc;
@@ -80,4 +65,4 @@ public class PageInfo extends RowBounds implements Serializable {
return super.getLimit();
}
}
}

View File

@@ -4,20 +4,6 @@ import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
/**
* @package io.shinhanlife.glow
* @className ResponseCode
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
@Getter
@RequiredArgsConstructor
public enum ResponseCode {
@@ -42,4 +28,4 @@ public enum ResponseCode {
private final HttpStatus status;
private final String message;
}
}

View File

@@ -3,20 +3,6 @@ package io.shinhanlife.glow;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
/**
* @package io.shinhanlife.glow
* @className ResponseUtil
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
public final class ResponseUtil {
private ResponseUtil() {
@@ -122,4 +108,4 @@ public final class ResponseUtil {
);
}
}
}

View File

@@ -2,30 +2,11 @@ package io.shinhanlife.glow.db.dto;
import lombok.Getter;
import lombok.Setter;
import lombok.Builder;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import java.util.Date;
/**
* @package io.shinhanlife.glow.db.dto
* @className AuditInfo
* @description AX HUB 시스템 처리 클래스
* @author 0986406
* @create 2026.09.01
* <pre>
* ---------- 개정이력 ----------
* 수정일 수정자 수정내용
* ---------- -------- ---------------------------
* 2026.09.01 0986406 최초생성
*
* </pre>
*/
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class AuditInfo {
private Date systRgiDt; // 시스템등록일시
private String systRgiPrafNo; // 시스템등록인사번호
@@ -37,4 +18,4 @@ public class AuditInfo {
private String systChgOgnzNo; // 시스템변경조직번호
private String systChgSystCd; // 시스템변경시스템코드
private String systChgPrgrId; // 시스템변경프로그램ID
}
}

View File

@@ -0,0 +1,11 @@
package io.shinhanlife.glow.db.typehandler;
/**
* glow 프레임워크 스텁 (실제 라이브러리가 없는 로컬 개발 환경용 더미).
* 코드값을 갖는 enum이 공통으로 구현하는 인터페이스 — {@link CodeEnumTypeHandler}가 이 getCode()로
* DB 컬럼(String)과 enum 상수를 상호 변환한다.
*/
public interface CodeEnum {
String getCode();
}

View File

@@ -0,0 +1,59 @@
package io.shinhanlife.glow.db.typehandler;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
/**
* glow 프레임워크 스텁 (실제 라이브러리가 없는 로컬 개발 환경용 더미).
* {@link CodeEnum}을 구현하는 코드 enum과 DB 문자열 컬럼(코드값)을 상호 변환하는 MyBatis TypeHandler
* 공통 베이스. common/enums/type의 {@code {ClassName}TypeHandler}는 모두 이 클래스를 상속하고,
* 생성자에서 자신의 enum 타입을 super(...)로 넘기기만 한다.
*/
public abstract class CodeEnumTypeHandler<E extends Enum<E> & CodeEnum> extends BaseTypeHandler<E> {
private final Class<E> type;
protected CodeEnumTypeHandler(Class<E> type) {
if (type == null) {
throw new IllegalArgumentException("Type argument cannot be null");
}
this.type = type;
}
@Override
public void setNonNullParameter(PreparedStatement ps, int i, E parameter, JdbcType jdbcType) throws SQLException {
ps.setString(i, parameter.getCode());
}
@Override
public E getNullableResult(ResultSet rs, String columnName) throws SQLException {
return toEnum(rs.getString(columnName));
}
@Override
public E getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
return toEnum(rs.getString(columnIndex));
}
@Override
public E getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
return toEnum(cs.getString(columnIndex));
}
private E toEnum(String code) {
if (code == null) {
return null;
}
for (E constant : type.getEnumConstants()) {
if (constant.getCode().equals(code)) {
return constant;
}
}
throw new IllegalArgumentException("알 수 없는 코드 [" + code + "] - " + type.getName());
}
}

View File

@@ -3,7 +3,7 @@ package io.shinhanlife.dap.lib.mcp;
import static org.junit.jupiter.api.Assertions.assertEquals;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.shinhanlife.dap.lib.annotation.ToolHint;
import io.shinhanlife.dap.lib.annotation.GrowToolHint;
import io.shinhanlife.dap.lib.config.McpProperties;
import io.shinhanlife.dap.lib.util.ToolSchemaResolver;
import io.shinhanlife.dap.lib.validation.ToolArgumentSchemaValidator;
@@ -47,7 +47,7 @@ class McpToolExecutionServiceTest {
static class EchoTool {
@McpTool(name = "sample.cmm.value.echo", description = "Echoes a value")
@ToolHint
@GrowToolHint
public Map<String, Object> execute(EchoRequest request) {
return Map.of("value", request.value);
}

View File

@@ -5,7 +5,7 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.shinhanlife.dap.lib.annotation.ToolHint;
import io.shinhanlife.dap.lib.annotation.GrowToolHint;
import io.shinhanlife.dap.lib.config.McpProperties;
import io.shinhanlife.dap.lib.util.ToolSchemaResolver;
import java.lang.reflect.Field;
@@ -49,7 +49,7 @@ class ToolRegistryHeartbeatSenderTest {
static class DisabledTool {
@McpTool(name = "test_disabled_tool")
@ToolHint(register = false)
@GrowToolHint(register = false)
void execute() {
}
}

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,87 @@ 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 groupedHttpToolRegistersItsGlowApiCatalogEntry() throws Exception {
String moduleName = root.resolve("dap-was-http").toString();
ToolScaffolder.scaffoldUseCase("Employee", moduleName, "tester", "2026.08.13", List.of(
new ToolScaffolder.ToolMethodDefinition(
"EmployeeSearch", "searchEmployee", null, "Employee search", "Search employee", "smp", "HTTP",
false, null, "employee-search",
List.of(new ToolScaffolder.FieldDefinition("employeeNo", "String", "Employee number", "10001", true)),
List.of(new ToolScaffolder.FieldDefinition("employeeName", "String", "Employee name", "Hong", false)), null)));
Path glowConfig = root.resolve("dap-was-http/src/main/resources/glow/application-glow-local.yml");
assertTrue(Files.exists(glowConfig));
assertTrue(Files.readString(glowConfig).contains("- name: employee-search"));
}
@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<GuidanceItemsItem> guidanceItems;"), response);
assertTrue(response.contains("public static class GuidanceItemsItem"), response);
assertFalse(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();
@@ -119,7 +201,7 @@ class ToolScaffolderTest {
String response = Files.readString(root.resolve("dto/ClaimSearchResponse.java"));
assertTrue(useCase.contains("name = \"cmm_claim_search\""));
assertTrue(useCase.contains("@ToolHint(register = true, categoryKey = \"cmm\", mappingId = \"CLM0001\")"));
assertTrue(useCase.contains("@GrowToolHint(register = true, categoryKey = \"cmm\", mappingId = \"CLM0001\")"));
assertTrue(response.contains("private String resultCode;"));
assertTrue(response.contains("private String resultMessage;"));
}
@@ -149,7 +231,7 @@ class ToolScaffolderTest {
Path schemas = root.resolve("dap-was-sample/src/main/resources/tool-schemas/cmm");
assertTrue(Files.exists(schemas.resolve("claim-search-resource-input-schema.json")));
assertTrue(Files.exists(schemas.resolve("claim-search-resource-output-schema.json"))); String useCase = Files.readString(root.resolve("dap-was-sample/src/main/java/io/shinhanlife/dap/mcc/biz/cmm/usecase/ClaimSearchUseCase.java"));
assertTrue(useCase.contains("@ToolHint(register = true, categoryKey = \"cmm\", mappingId = \"CLM0001\","));
assertTrue(useCase.contains("@GrowToolHint(register = true, categoryKey = \"cmm\", mappingId = \"CLM0001\","));
assertTrue(useCase.contains("inputSchemaResource = \"classpath:tool-schemas/cmm/claim-search-resource-input-schema.json\""));
assertTrue(useCase.contains("outputSchemaResource = \"classpath:tool-schemas/cmm/claim-search-resource-output-schema.json\""));
}
@@ -194,7 +276,7 @@ class ToolScaffolderTest {
String mciResponse = Files.readString(sourceRoot.resolve("infra/itrf/mci/dfag/io/SHEARCH_01_O.java"));
String converter = Files.readString(sourceRoot.resolve("biz/pay/converter/SearchHrConverter.java")); String useCase = Files.readString(sourceRoot.resolve("biz/pay/usecase/SearchHrUseCase.java"));
String implementation = Files.readString(sourceRoot.resolve("biz/pay/usecase/impl/SearchHrUseCaseImpl.java"));
assertTrue(mciRequest.contains("package io.shinhanlife.dap.mcc.infra.itrf.mci.dfag.io;"), mciRequest); assertTrue(useCase.contains("@ToolHint(register = true, categoryKey = \"pay\", mappingId = \"SHEARCH_01\")"));
assertTrue(mciRequest.contains("package io.shinhanlife.dap.mcc.infra.itrf.mci.dfag.io;"), mciRequest); assertTrue(useCase.contains("@GrowToolHint(register = true, categoryKey = \"pay\", mappingId = \"SHEARCH_01\")"));
assertTrue(implementation.contains("import io.shinhanlife.dap.mcc.infra.itrf.mci.dfag.io.SHEARCH_01_O;"));
assertTrue(request.contains("private String employeeId;"));
@@ -347,4 +429,59 @@ class ToolScaffolderTest {
assertTrue(yaml.contains(" biz-pod: false\n mci:"), yaml);
assertTrue(yaml.contains(" - name: insurance"), yaml);
}
@Test
void generatesObjectListAsNestedInnerClassWithoutSeparateItemSource() throws Exception {
String moduleName = root.resolve("dap-was-inner-list").toString();
List<ToolScaffolder.FieldDefinition> fields = List.of(
new ToolScaffolder.FieldDefinition("data", "List", "activity data", "", false,
List.of(), "Object", List.of(
new ToolScaffolder.FieldDefinition("date", "String", "date", "2026-08-13", true),
new ToolScaffolder.FieldDefinition("users", "Integer", "users", "10", false))));
ToolScaffolder.scaffold("ga activity status", "GA001", "GA status", "GA status", "ana", "HTTP",
moduleName, "tester", "2026.08.13", false, null, null, null, List.of(), fields);
Path dtoDir = root.resolve("dap-was-inner-list/src/main/java/io/shinhanlife/dap/mcc/biz/ana/dto");
String response = Files.readString(dtoDir.resolve("GaActivityStatusResponse.java"));
assertTrue(response.contains("private List<DataItem> data;"), response);
assertTrue(response.contains("public static class DataItem"), response);
assertTrue(response.contains("private String date;"), response);
assertFalse(Files.exists(dtoDir.resolve("GaActivityStatusResponseDataItem.java")));
}
@Test
void groupedUseCaseSupportsHttpAndMciTools() throws Exception {
String moduleName = root.resolve("dap-was-mixed").toString();
ToolScaffolder.scaffoldUseCase("Customer", moduleName, "tester", "2026.08.13", List.of(
new ToolScaffolder.ToolMethodDefinition("CustomerProfile", "getProfile", "CUST001", "Profile", "profile", "cmm", "MCI",
false, "CSTM", null, List.of(), List.of(), null),
new ToolScaffolder.ToolMethodDefinition("CustomerNotice", "getNotice", "NOTICE001", "Notice", "notice", "cmm", "HTTP",
false, null, "customer-notice", List.of(), List.of(), null)));
Path sourceRoot = root.resolve("dap-was-mixed/src/main/java/io/shinhanlife/dap/mcc");
String impl = Files.readString(sourceRoot.resolve("biz/cmm/usecase/impl/CustomerUseCaseImpl.java"));
assertTrue(impl.contains("getProfile(CustomerProfileRequest req)"), impl);
assertTrue(impl.contains("getNotice(CustomerNoticeRequest req)"), impl);
assertTrue(Files.exists(sourceRoot.resolve("infra/itrf/http/customer_notice/CustomerNoticeClient.java")));
}
@Test
void addsToolMethodToExistingUseCaseInsteadOfOverwritingIt() throws Exception {
String moduleName = root.resolve("dap-was-existing").toString();
ToolScaffolder.scaffoldUseCase("Customer", moduleName, "tester", "2026.08.13", List.of(
new ToolScaffolder.ToolMethodDefinition("CustomerProfile", "getProfile", "CUST001", "Profile", "profile", "cmm", "MCI",
false, "CSTM", null, List.of(), List.of(), null)));
ToolScaffolder.scaffoldUseCase("Customer", moduleName, "tester", "2026.08.13", List.of(
new ToolScaffolder.ToolMethodDefinition("CustomerNotice", "getNotice", "NOTICE001", "Notice", "notice", "cmm", "HTTP",
false, null, "customer-notice", List.of(), List.of(), null)));
Path sourceRoot = root.resolve("dap-was-existing/src/main/java/io/shinhanlife/dap/mcc");
String useCase = Files.readString(sourceRoot.resolve("biz/cmm/usecase/CustomerUseCase.java"));
String impl = Files.readString(sourceRoot.resolve("biz/cmm/usecase/impl/CustomerUseCaseImpl.java"));
assertTrue(useCase.contains("getProfile(CustomerProfileRequest req)"), useCase);
assertTrue(useCase.contains("getNotice(CustomerNoticeRequest req)"), useCase);
assertTrue(impl.contains("private final CustomerProfileClient customerProfileClient;"), impl);
assertTrue(impl.contains("private final CustomerNoticeClient customerNoticeClient;"), impl);
}
}

View File

@@ -19,10 +19,10 @@ class ToolSourceUpdaterTest {
Files.writeString(source, """
package example;
import org.springaicommunity.mcp.annotation.McpTool;
import io.shinhanlife.dap.lib.annotation.ToolHint;
import io.shinhanlife.dap.lib.annotation.GrowToolHint;
interface SampleUseCase {
@McpTool(name = "cmm_sample_search", description = "old")
@ToolHint(register = false, requiresApproval = false)
@GrowToolHint(register = false, requiresApproval = false)
void search();
}
""");
@@ -31,7 +31,7 @@ class ToolSourceUpdaterTest {
String updated = Files.readString(source);
assertTrue(updated.contains("@McpTool(name = \"cmm_sample_search\", description = \"new\")"));
assertTrue(updated.contains("@ToolHint(register = true, requiresApproval = true"));
assertTrue(updated.contains("@GrowToolHint(register = true, requiresApproval = true"));
assertTrue(updated.contains("categoryKey = \"customer\""), updated);
}
}

View File

@@ -0,0 +1,29 @@
package io.shinhanlife.dap.mcc.biz.cmm.converter;
import io.shinhanlife.dap.mcc.biz.cmm.dto.CcCstUnfcNotiCarhDto;
import io.shinhanlife.dap.mcc.biz.cmm.dto.CustomerGuidanceToolRequest;
import io.shinhanlife.dap.mcc.infra.itrf.mci.nil.d.io.ONILD0320_I;
import io.shinhanlife.dap.mcc.infra.itrf.mci.nil.d.io.ONILD0320_O.CstUnfcNotiCarhInqrOutDto;
import org.mapstruct.*;
import java.util.List;
@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface CustomerGuidanceToolConverter {
/**
* 고객통합이력조회 전사 Request -> I/F
*/
@Mapping(target = "cstUnfcNotiCarhInqrInDto.inqrEndYmd", source = "inqrEndYmd")
@Mapping(target = "cstUnfcNotiCarhInqrInDto.inqrStrtYmd", source = "inqrStrYmd")
@Mapping(target = "cstUnfcNotiCarhInqrInDto.notiPmlMdCd", source = "notiPmlMdCd")
@Mapping(target = "cstUnfcNotiCarhInqrInDto.ntleCd", source = "ntleCd")
@Mapping(target = "cstUnfcNotiCarhInqrInDto.csNo", source = "csNo")
public abstract ONILD0320_I toONILD0320_I(CustomerGuidanceToolRequest req);
@Named("toCcCstUnfcNotiCarhDto")
public abstract CcCstUnfcNotiCarhDto toCcCstUnfcNotiCarhDto(CstUnfcNotiCarhInqrOutDto cstUnfcNotiCarhInqrOutDto);
@IterableMapping(qualifiedByName = "toCcCstUnfcNotiCarhDto")
public abstract List<CcCstUnfcNotiCarhDto> toCcCstUnfcNotiCarhDtoList(List<CstUnfcNotiCarhInqrOutDto> cstUnfcNotiCarhInqrOutDto);
}

View File

@@ -0,0 +1,29 @@
package io.shinhanlife.dap.mcc.biz.cmm.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class AnalysisDataQueryRequest {
@Schema(description = "데이터 조회를 시작할 날짜", example = "2024-01-01")
private String startDate;
@Schema(description = "데이터 조회를 종료할 날짜", example = "2024-12-31")
private String endDate;
@Schema(description = "분석할 데이터 카테고리", example = "sales")
private String category;
@Schema(description = "지역 필터", example = "seoul")
private String region;
@Schema(description = "조회할 결과 수 제한", example = "100")
private Integer limit;
@Schema(description = "조회할 결과 시작 위치", example = "0")
private Integer offset;
}

View File

@@ -0,0 +1,97 @@
package io.shinhanlife.dap.mcc.biz.cmm.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Builder
@Data
public class CcCstUnfcNotiCarhDto {
@Schema(description = "고객번호", example = "CUST123")
private String csNo;
@Schema(description = "안내일시", example = "20260812093000")
private String notiDt;
@Schema(description = "발송일련번호", example = "1")
private int pmlSriaNo;
@Schema(description = "안내신청상세일시", example = "20260812092500")
private String notiPetDtptDt;
@Schema(description = "안내장코드", example = "NT001")
private String ntleCd;
@Schema(description = "안내장명", example = "카드대금 결제일 안내")
private String ntleNm;
@Schema(description = "안내발송일련번호", example = "10")
private int notiPmlSriaNo;
@Schema(description = "안내발송방법코드", example = "SMS")
private String notiPmlMdCd;
@Schema(description = "안내수신정보암호화내용", example = "AbC123EncRypTed==")
private String notiRcvInfoEncrCt;
@Schema(description = "메시지ID", example = "MSG20260812000001")
private String msgId;
@Schema(description = "안내내용", example = "8월 카드대금 결제일은 8월 25일입니다.")
private String notiCt;
@Schema(description = "통합안내발송결과코드", example = "00")
private String unfcNotiPmlRsltCd;
@Schema(description = "통합안내발송결과상세코드", example = "0001")
private String unfcNotiPmlRsltDtptCd;
@Schema(description = "발송시스템코드", example = "SYS01")
private String pmlSystCd;
@Schema(description = "발송조직번호", example = "1001")
private String pmlOgnzNo;
@Schema(description = "발송조직명", example = "고객지원팀")
private String pmlOgnzNm;
@Schema(description = "발신기관조직명", example = "카드사업본부")
private String msdilttOgnzNm;
@Schema(description = "조회구분명", example = "전체")
private String inqrDvsnNm;
@Schema(description = "등록일시", example = "20260812090000")
private String rgiDt;
@Schema(description = "등록자명", example = "홍길동")
private String rgstNm;
@Schema(description = "등록자조직명", example = "고객지원팀")
private String rgstOgnzNm;
@Schema(description = "최종변경자명", example = "김철수")
private String lstPrwpNm;
@Schema(description = "최종변경일시", example = "20260812101500")
private String lstChgDt;
@Schema(description = "최종변경자조직명", example = "운영지원팀")
private String lstPrwpOgnzNm;
@Schema(description = "원천회사구분코드", example = "01")
private String oriCpnyDvsnCd;
@Schema(description = "고객한글명", example = "이보람")
private String cstHanNm;
@Schema(description = "시스템등록인사번호", example = "20260001")
private String systRgiPrafNo;
@Schema(description = "시스템변경인사번호", example = "20260002")
private String systChgPrafNo;
}

View File

@@ -0,0 +1,27 @@
package io.shinhanlife.dap.mcc.biz.cmm.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;
@Data
@Getter
@Builder
public class CustomerGuidanceToolRequest {
@Schema(description = "고객번호", example = "CUST123")
private String csNo;
@Schema(description = "안내장코드", example = "ab12321")
private String ntleCd;
@Schema(description = "안내발송방법코드", example = "aa")
private String notiPmlMdCd;
@Schema(description = "조회시작일자", example = "20260101")
private String inqrStrYmd;
@Schema(description = "조회종료일자", example = "20260101")
private String inqrEndYmd;
}

View File

@@ -0,0 +1,21 @@
package io.shinhanlife.dap.mcc.biz.cmm.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;
import java.util.List;
@Getter
@Builder
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class CustomerGuidanceToolResponse {
private String resultCode;
private String resultMessage;
private List<CcCstUnfcNotiCarhDto> ccCstUnfcNotiCarhDtoList;
}

View File

@@ -0,0 +1,30 @@
package io.shinhanlife.dap.mcc.biz.cmm.usecase;
import io.shinhanlife.glow.BaseResponse;
import org.springaicommunity.mcp.annotation.McpTool;
import io.shinhanlife.dap.lib.annotation.GrowToolHint;
import io.shinhanlife.dap.mcc.biz.cmm.dto.CustomerGuidanceToolRequest;
import io.shinhanlife.dap.mcc.biz.cmm.dto.CustomerGuidanceToolResponse;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestBody;
/**
* @package io.shinhanlife.dap.mcc.biz.cmm.usecase
* @className CustomerGuidanceToolUseCase
* @description AX HUB ?쒖뒪??泥섎━ ?대옒??
* @author Boram
* @create 2026.08.12
* <pre>
* ---------- 媛쒖젙?대젰 ----------
* ?섏젙?? ?섏젙?? ?섏젙?댁슜
* ---------- -------- ---------------------------
* 2026.08.12 Boram 理쒖큹?앹꽦
*
* </pre>
*/
public interface CustomerGuidanceToolUseCase {
@McpTool(name = "cmm_customer_tool", title = "고객 통합 안내이력 조회", description = "고객의 통합 안내 이력을 조회하는 도구입니다. 고객 ID와 조회 기간을 입력하면 해당 기간 동안의 안내 이력을 반환합니다.")
@GrowToolHint(register = false, categoryKey = "cmm", mappingId = "ONILD0320")
CustomerGuidanceToolResponse searchCustomerGuidance(CustomerGuidanceToolRequest req) throws Exception;
}

View File

@@ -1,7 +1,7 @@
package io.shinhanlife.dap.mcc.biz.cmm.usecase;
import org.springaicommunity.mcp.annotation.McpTool;
import io.shinhanlife.dap.lib.annotation.ToolHint;
import io.shinhanlife.dap.lib.annotation.GrowToolHint;
import io.shinhanlife.dap.mcc.biz.cmm.dto.MetaCommonCodeRequest;
@@ -21,6 +21,6 @@ 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);
@GrowToolHint(register = false, requiresApproval = false, categoryKey = "cmm", mappingId = "CLCNNB00001")
Object searchCommonCode(MetaCommonCodeRequest req);
}

View File

@@ -1,7 +1,7 @@
package io.shinhanlife.dap.mcc.biz.cmm.usecase;
import org.springaicommunity.mcp.annotation.McpTool;
import io.shinhanlife.dap.lib.annotation.ToolHint;
import io.shinhanlife.dap.lib.annotation.GrowToolHint;
import io.shinhanlife.dap.mcc.biz.cmm.dto.MetaTableRequest;
@@ -21,6 +21,6 @@ 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);
@GrowToolHint(register = false, requiresApproval = false, categoryKey = "cmm", mappingId = "CLCNNB00001")
Object searchMetaTable(MetaTableRequest req);
}

View File

@@ -2,13 +2,13 @@ package io.shinhanlife.dap.mcc.biz.cmm.usecase;
import org.springaicommunity.mcp.annotation.McpTool;
import io.shinhanlife.dap.lib.annotation.ToolHint;
import io.shinhanlife.dap.lib.annotation.GrowToolHint;
import io.shinhanlife.dap.mcc.biz.cmm.dto.*;
import java.util.Map;
public interface TemplateUtilityUseCase {
@McpTool(name = "cmm_template_url", title = "템플릿 유틸리티 툴", description = "요청한 템플릿(엑셀, 워드 등) 파일(양식)을 다운로드 받을 수 있는 시스템 URL을 반환합니다. AI는 이 URL을 사용자에게 마크다운 링크 형태로 제공해야 합니다.")
@ToolHint(categoryKey = "cmm")
@GrowToolHint(categoryKey = "cmm")
Map<String, Object> getTemplateFileUrl(TemplateDownloadRequest req);
}

View File

@@ -0,0 +1,58 @@
package io.shinhanlife.dap.mcc.biz.cmm.usecase.impl;
import io.shinhanlife.dap.mcc.biz.cmm.dto.CcCstUnfcNotiCarhDto;
import io.shinhanlife.dap.mcc.biz.cmm.dto.CustomerGuidanceToolRequest;
import io.shinhanlife.dap.mcc.biz.cmm.dto.CustomerGuidanceToolResponse;
import io.shinhanlife.dap.mcc.biz.cmm.usecase.CustomerGuidanceToolUseCase;
import io.shinhanlife.glow.BaseResponse;
import io.shinhanlife.glow.ResponseUtil;
import io.shinhanlife.glow.communication.dto.Transfer;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import io.shinhanlife.dap.mcc.biz.cmm.converter.CustomerGuidanceToolConverter;
import io.shinhanlife.dap.mcc.infra.itrf.mci.nil.d.io.ONILD0320_I;
import io.shinhanlife.dap.mcc.infra.itrf.mci.nil.d.io.ONILD0320_O;
import io.shinhanlife.dap.mcc.infra.itrf.mci.nil.d.MciNildClient;
import java.util.List;
import java.util.Map;
/**
* @package io.shinhanlife.dap.mcc.biz.cmm.usecase.impl
* @className CustomerGuidanceToolUseCaseImpl
* @description AX HUB ?쒖뒪??泥섎━ ?대옒??
* @author Boram
* @create 2026.08.12
* <pre>
* ---------- 媛쒖젙?대젰 ----------
* ?섏젙?? ?섏젙?? ?섏젙?댁슜
* ---------- -------- ---------------------------
* 2026.08.12 Boram 理쒖큹?앹꽦
*
* </pre>
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class CustomerGuidanceToolUseCaseImpl implements CustomerGuidanceToolUseCase {
private final MciNildClient mci;
private final CustomerGuidanceToolConverter converter;
@Override
public CustomerGuidanceToolResponse searchCustomerGuidance(CustomerGuidanceToolRequest req) throws Exception {
ONILD0320_I onild0320_i = converter.toONILD0320_I(req);
ONILD0320_O onild0320_o = mci.callOnild0320(onild0320_i);
List<CcCstUnfcNotiCarhDto> ccCstUnfcNotiCarhDtoList = converter.toCcCstUnfcNotiCarhDtoList(onild0320_o.getCstUnfcNotiCarhInqrOutDto());
return CustomerGuidanceToolResponse.builder()
.ccCstUnfcNotiCarhDtoList(ccCstUnfcNotiCarhDtoList)
.resultCode("SUCCESS")
.resultMessage("")
.build();
}
}

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

@@ -1,7 +1,7 @@
package io.shinhanlife.dap.mcc.biz.ins.usecase;
import org.springaicommunity.mcp.annotation.McpTool;
import io.shinhanlife.dap.lib.annotation.ToolHint;
import io.shinhanlife.dap.lib.annotation.GrowToolHint;
import io.shinhanlife.dap.mcc.biz.ins.dto.InsuranceClaimProcessorRequest;
import io.shinhanlife.dap.mcc.biz.ins.dto.InsuranceClaimProcessorResponse;
@@ -22,6 +22,6 @@ import io.shinhanlife.dap.mcc.biz.ins.dto.InsuranceClaimProcessorResponse;
public interface InsuranceClaimProcessorUseCase {
@McpTool(name = "ins_insurance_processor", title = "보험금 청구", description = "보험금 청구 요청을 처리하고 결과를 반환하는 LLM 도구 가이드")
@ToolHint(register = false, categoryKey = "ins", mappingId = "CLAIM0000001")
InsuranceClaimProcessorResponse execute(InsuranceClaimProcessorRequest req);
@GrowToolHint(register = false, categoryKey = "ins", mappingId = "CLAIM0000001")
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

@@ -1,11 +1,11 @@
package io.shinhanlife.dap.mcc.biz.oth.usecase;
import org.springaicommunity.mcp.annotation.McpTool;
import io.shinhanlife.dap.lib.annotation.ToolHint;
import io.shinhanlife.dap.lib.annotation.GrowToolHint;
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);
@GrowToolHint(categoryKey = "oth", register = false)
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

@@ -2,12 +2,12 @@ package io.shinhanlife.dap.mcc.biz.smp.usecase;
import org.springaicommunity.mcp.annotation.McpTool;
import io.shinhanlife.dap.lib.annotation.ToolHint;
import io.shinhanlife.dap.lib.annotation.GrowToolHint;
import io.shinhanlife.dap.mcc.biz.smp.dto.DailyQuoteRequest;
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);
@GrowToolHint(register = false, categoryKey = "smp", mappingId = "QUOTE_001")
DailyQuoteResponse getDailyQuote(DailyQuoteRequest req);
}

View File

@@ -2,7 +2,7 @@ package io.shinhanlife.dap.mcc.biz.smp.usecase;
import org.springaicommunity.mcp.annotation.McpTool;
import io.shinhanlife.dap.lib.annotation.ToolHint;
import io.shinhanlife.dap.lib.annotation.GrowToolHint;
import io.shinhanlife.dap.mcc.biz.smp.dto.ExchangeRateRequest;
import io.shinhanlife.dap.mcc.biz.smp.dto.ExchangeRateResponse;
import io.shinhanlife.dap.mcc.biz.smp.dto.ExchangeRateRequest;
@@ -10,6 +10,6 @@ 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);
@GrowToolHint(register = false, categoryKey = "smp", mappingId = "EXCHANGE_001")
ExchangeRateResponse getExchangeRate(ExchangeRateRequest req);
}

View File

@@ -1,12 +1,12 @@
package io.shinhanlife.dap.mcc.biz.smp.usecase;
import org.springaicommunity.mcp.annotation.McpTool;
import io.shinhanlife.dap.lib.annotation.ToolHint;
import io.shinhanlife.dap.lib.annotation.GrowToolHint;
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);
@GrowToolHint(register = false, requiresApproval = false, categoryKey = "smp", mappingId = "DIRECT0001")
Object getTeamMember(TeamMemberRequest req);
}

View File

@@ -2,11 +2,11 @@ package io.shinhanlife.dap.mcc.biz.smp.usecase;
import org.springaicommunity.mcp.annotation.McpTool;
import io.shinhanlife.dap.lib.annotation.ToolHint;
import io.shinhanlife.dap.lib.annotation.GrowToolHint;
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);
@GrowToolHint(register = false, categoryKey = "smp", mappingId = "WEATHER_001")
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

@@ -1,7 +1,7 @@
package io.shinhanlife.dap.mcc.biz.sol.usecase;
import org.springaicommunity.mcp.annotation.McpTool;
import io.shinhanlife.dap.lib.annotation.ToolHint;
import io.shinhanlife.dap.lib.annotation.GrowToolHint;
import io.shinhanlife.dap.mcc.biz.sol.dto.SolReqDetailRequest;
@@ -22,6 +22,6 @@ import io.shinhanlife.dap.mcc.biz.sol.dto.SolReqDetailRequest;
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);
@GrowToolHint(register = false, requiresApproval = false, categoryKey = "sol", mappingId = "SOLG00000002")
Object getSolRequestDetail(SolReqDetailRequest req);
}

View File

@@ -1,12 +1,12 @@
package io.shinhanlife.dap.mcc.biz.sol.usecase;
import org.springaicommunity.mcp.annotation.McpTool;
import io.shinhanlife.dap.lib.annotation.ToolHint;
import io.shinhanlife.dap.lib.annotation.GrowToolHint;
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);
@GrowToolHint(register = false, requiresApproval = false, categoryKey = "sol", mappingId = "SOLG00000001")
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

@@ -0,0 +1,49 @@
package io.shinhanlife.dap.mcc.infra.itrf.mci.nil.d;
import io.shinhanlife.dap.mcc.biz.cmm.dto.CustomerGuidanceToolResponse;
import io.shinhanlife.dap.mcc.infra.itrf.mci.nil.d.io.ONILD0320_I;
import io.shinhanlife.dap.mcc.infra.itrf.mci.nil.d.io.ONILD0320_O;
import io.shinhanlife.glow.BizException;
import io.shinhanlife.glow.GlowLogger;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import lombok.RequiredArgsConstructor;
import io.shinhanlife.dap.lib.integration.mci.component.AxhubMciComponent;
import io.shinhanlife.glow.communication.dto.Transfer;
/**
* @package io.shinhanlife.dap.mcc.infra.itrf.mci.nild
* @className MciNildClient
* @description AX HUB ?쒖뒪??泥섎━ ?대옒??
* @author Boram
* @create 2026.08.12
* <pre>
* ---------- 媛쒖젙?대젰 ----------
* ?섏젙?? ?섏젙?? ?섏젙?댁슜
* ---------- -------- ---------------------------
* 2026.08.12 Boram 理쒖큹?앹꽦
*
* </pre>
*/
@Component
@RequiredArgsConstructor
public class MciNildClient {
private final AxhubMciComponent mci;
public ONILD0320_O callOnild0320(ONILD0320_I onild0320_i){
try {
Transfer<ONILD0320_O> resTransfer = mci.callTo(
"CTMNILO00007",
null,
onild0320_i,
ONILD0320_O.class
);
ONILD0320_O onild0320_o = resTransfer.getBody();
return onild0320_o;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}

View File

@@ -0,0 +1,41 @@
package io.shinhanlife.dap.mcc.infra.itrf.mci.nil.d.io;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import io.shinhanlife.glow.GlowMciFieldInfo;
import io.shinhanlife.glow.communication.annotation.GlowTrgmField;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Data
@Getter
@Setter
@NoArgsConstructor
public class ONILD0320_I {
@GlowTrgmField(order = 1, description = "고객 통합 안내이력 조회", type="gs")
private CstUnfcNotiCarhInqrInDto cstUnfcNotiCarhInqrInDto;
@NoArgsConstructor
@Getter
@Setter
public static class CstUnfcNotiCarhInqrInDto {
@GlowMciFieldInfo(order = 1, length = 5, description = "고객번호")
private String csNo;
@GlowMciFieldInfo(order = 2, length = 5, description = "안내장코드")
private String ntleCd;
@GlowMciFieldInfo(order = 3, length = 5, description = "안내발송방법코드")
private String notiPmlMdCd;
@GlowMciFieldInfo(order = 4, length = 5, description = "조회시작일자")
private String inqrStrtYmd;
@GlowMciFieldInfo(order = 5, length = 5, description = "조회종료일자")
private String inqrEndYmd;
}
}

View File

@@ -0,0 +1,109 @@
package io.shinhanlife.dap.mcc.infra.itrf.mci.nil.d.io;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import io.shinhanlife.glow.GlowMciFieldInfo;
import io.shinhanlife.glow.communication.annotation.GlowTrgmField;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;
import java.util.List;
@NoArgsConstructor
@Getter
@Setter
public class ONILD0320_O {
@GlowTrgmField(order = 1, description = "고객 통합 안내이력 조회out", type="gm")
private List<CstUnfcNotiCarhInqrOutDto> cstUnfcNotiCarhInqrOutDto;
@NoArgsConstructor
@Getter
@Setter
public static class CstUnfcNotiCarhInqrOutDto {
@GlowMciFieldInfo(order = 1, length = 5, description = "고객번호")
private String csNo;
@GlowMciFieldInfo(order = 2, length = 5, description = "안내일시")
private String notiDt;
@GlowMciFieldInfo(order = 3, length = 5, description = "발송일련번호")
private int pmlSriaNo;
@GlowMciFieldInfo(order = 4, length = 5, description = "안내신청상세일시")
private String notiPetDtptDt;
@GlowMciFieldInfo(order = 5, length = 5, description = "안내장코드")
private String ntleCd;
@GlowMciFieldInfo(order = 6, length = 5, description = "안내장명")
private String ntleNm;
@GlowMciFieldInfo(order = 7, length = 5, description = "안내발송일련번호")
private int notiPmlSriaNo;
@GlowMciFieldInfo(order = 8, length = 5, description = "안내발송방법코드")
private String notiPmlMdCd;
@GlowMciFieldInfo(order = 9, length = 5, description = "안내수신정보암호화내용")
private String notiRcvInfoEncrCt;
@GlowMciFieldInfo(order = 10, length = 5, description = "메시지ID")
private String msgId;
@GlowMciFieldInfo(order = 11, length = 5, description = "안내내용")
private String notiCt;
@GlowMciFieldInfo(order = 12, length = 5, description = "통합안내발송결과코드")
private String unfcNotiPmlRsltCd;
@GlowMciFieldInfo(order = 13, length = 5, description = "통합안내발송결과상세코드")
private String unfcNotiPmlRsltDtptCd;
@GlowMciFieldInfo(order = 14, length = 5, description = "발송시스템코드")
private String pmlSystCd;
@GlowMciFieldInfo(order = 15, length = 5, description = "발송조직번호")
private String pmlOgnzNo;
@GlowMciFieldInfo(order = 16, length = 5, description = "발송조직명")
private String pmlOgnzNm;
@GlowMciFieldInfo(order = 17, length = 5, description = "발신기관조직명")
private String msdilttOgnzNm;
@GlowMciFieldInfo(order = 18, length = 5, description = "조회구분명")
private String inqrDvsnNm;
@GlowMciFieldInfo(order = 19, length = 5, description = "등록일시")
private String rgiDt;
@GlowMciFieldInfo(order = 20, length = 5, description = "등록자명")
private String rgstNm;
@GlowMciFieldInfo(order = 21, length = 5, description = "등록자조직명")
private String rgstOgnzNm;
@GlowMciFieldInfo(order = 22, length = 5, description = "최종변경자명")
private String lstPrwpNm;
@GlowMciFieldInfo(order = 23, length = 5, description = "최종변경일시")
private String lstChgDt;
@GlowMciFieldInfo(order = 24, length = 5, description = "최종변경자조직명")
private String lstPrwpOgnzNm;
@GlowMciFieldInfo(order = 25, length = 5, description = "원천회사구분코드")
private String oriCpnyDvsnCd;
@GlowMciFieldInfo(order = 26, length = 5, description = "고객한글명")
private String cstHanNm;
@GlowMciFieldInfo(order = 27, length = 5, description = "시스템등록인사번호")
private String systRgiPrafNo;
@GlowMciFieldInfo(order = 28, length = 5, description = "시스템변경인사번호")
private String systChgPrafNo;
}
}

View File

@@ -17,11 +17,13 @@
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>/swlog/dap-was-oth/A01/${HOSTNAME}_A01.log</file> <!-- 현재 로그가 쌓이는 파일 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- 매일 자정에 날짜를 붙여서 지난 로그를 분리 저장 -->
<fileNamePattern>/swlog/dap-was-oth/A01/${HOSTNAME}_A01_%d{yyyyMMdd}.log</fileNamePattern>
<!-- 최대 30일치 로그만 보관하고 오래된 것은 자동 삭제 -->
<fileNamePattern>/swlog/dap-was-oth/A01/${HOSTNAME}_A01_%d{yyyyMMdd}.%i.log</fileNamePattern>
<!-- 개별 로그 파일 크기를 100MB로 제한하고, 전체 보관 일수는 30일, 총 로그 누적 용량은 1GB로 제한 -->
<maxFileSize>100MB</maxFileSize>
<maxHistory>30</maxHistory>
<totalSizeCap>1GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>${LOG_PATTERN}</pattern>

View File

@@ -0,0 +1,4 @@
{
"resultCode" : "SUCCESS",
"data" : "[{\"date\":\"2024-01-05\",\"message\":\"안내 내용\"}]"
}

View File

@@ -0,0 +1,28 @@
name: cmm_customer_tool
display_name: 고객 통합 안내이력 조회
version: 1.0.0
category_key: cmm
description:
function: 고객의 통합 안내 이력을 조회하는 도구입니다.
when_to_use: 고객 ID와 조회 기간을 기반으로 해당 기간 동안의 안내 이력을 확인할 때 사용합니다.
when_not_to_use: 안내 이력을 생성하거나 수정할 때는 사용하지 않습니다.
io_limits: 안내 이력 조회만 수행하며 데이터를 변경하지 않습니다.
display_description: 고객의 통합 안내 이력을 조회합니다.
example_queries: ["고객 통합 안내이력 조회해줘", "특정 기간의 안내 이력을 확인해줘", "고객 번호로 안내 이력을 찾아줘"]
read_only: true
destructive: false
idempotent: true
parameters_schema:
type: object
properties:
csNo: {type: string, description: 고객번호}
ntleCd: {type: string, description: 안내장코드}
notiPmlMdCd: {type: string, description: 안내발송방법코드}
inqrStrYmd: {type: string, description: 조회시작일자}
inqrEndYmd: {type: string, description: 조회종료일자}
required: [csNo]
additionalProperties: false
tags: [고객, 통합안내이력]
legacy_interface_id: ONILD0320
required_env_keys: []
owner_org: MCP_TOOL

View File

@@ -0,0 +1,16 @@
package io.shinhanlife.dap.mcc.biz.cmm.usecase;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import io.shinhanlife.dap.mcc.biz.cmm.dto.CustomerGuidanceToolRequest;
import io.shinhanlife.dap.mcc.biz.cmm.dto.CustomerGuidanceToolResponse;
import org.junit.jupiter.api.Test;
class CustomerGuidanceToolUseCaseTest {
@Test
void createsToolRequestAndResponseDtos() {
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

@@ -1,7 +1,7 @@
package io.shinhanlife.dap.mcc.biz.cmm.usecase;
import org.springaicommunity.mcp.annotation.McpTool;
import io.shinhanlife.dap.lib.annotation.ToolHint;
import io.shinhanlife.dap.lib.annotation.GrowToolHint;
import io.shinhanlife.dap.mcc.biz.cmm.dto.ClaimSearchRequest;
import io.shinhanlife.dap.mcc.biz.cmm.dto.ClaimSearchResponse;
@@ -22,8 +22,8 @@ import io.shinhanlife.dap.mcc.biz.cmm.dto.ClaimSearchResponse;
public interface ClaimSearchUseCase {
@McpTool(name = "cmm_claim_search", title = "청구번호 또는 계약번호로 보험금 청구 상태를 조회한다.", description = "청구번호 또는 계약번호로 보험금 청구 상태를 조회한다.")
@ToolHint(register = false, categoryKey = "cmm", mappingId = "CLCNNB00001",
@GrowToolHint(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

@@ -1,7 +1,7 @@
package io.shinhanlife.dap.mcc.biz.cmm.usecase;
import org.springaicommunity.mcp.annotation.McpTool;
import io.shinhanlife.dap.lib.annotation.ToolHint;
import io.shinhanlife.dap.lib.annotation.GrowToolHint;
import io.shinhanlife.dap.mcc.biz.cmm.dto.MemoListRetrieverRequest;
import io.shinhanlife.dap.mcc.biz.cmm.dto.MemoListRetrieverResponse;
@@ -22,6 +22,6 @@ import io.shinhanlife.dap.mcc.biz.cmm.dto.MemoListRetrieverResponse;
public interface MemoListRetrieverUseCase {
@McpTool(name = "cmm_memo_retriever", title = "의뢰서 목록 조회", description = "의뢰서 목록을 조회하여 의뢰 정보를 반환합니다.")
@ToolHint(register = false, categoryKey = "cmm", mappingId = "MEMO0000001")
MemoListRetrieverResponse execute(MemoListRetrieverRequest req);
@GrowToolHint(register = false, categoryKey = "cmm", mappingId = "MEMO0000001")
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);

View File

@@ -17,11 +17,13 @@
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>/swlog/dap-was-sms/A01/${HOSTNAME}_A01.log</file> <!-- 현재 로그가 쌓이는 파일 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- 매일 자정에 날짜를 붙여서 지난 로그를 분리 저장 -->
<fileNamePattern>/swlog/dap-was-sms/A01/${HOSTNAME}_A01_%d{yyyyMMdd}.log</fileNamePattern>
<!-- 최대 30일치 로그만 보관하고 오래된 것은 자동 삭제 -->
<fileNamePattern>/swlog/dap-was-sms/A01/${HOSTNAME}_A01_%d{yyyyMMdd}.%i.log</fileNamePattern>
<!-- 개별 로그 파일 크기를 100MB로 제한하고, 전체 보관 일수는 30일, 총 로그 누적 용량은 1GB로 제한 -->
<maxFileSize>100MB</maxFileSize>
<maxHistory>30</maxHistory>
<totalSizeCap>1GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>${LOG_PATTERN}</pattern>

View File

@@ -0,0 +1,77 @@
# Multi-tool Scaffold Design
## Goal
Extend the Tool Scaffold so one UseCase can expose multiple MCP Tools, and each
Tool can call its own typed integration Client using the same flow as
`CustomerGuidanceToolUseCaseImpl`.
## Generated structure
For a Scaffold group named `Customer`, the generator creates one UseCase and
implementation, with one method per Tool:
```java
public interface CustomerUseCase {
@McpTool(name = "cmm_customer_guidance", ...)
CustomerGuidanceResponse searchGuidance(CustomerGuidanceRequest request);
@McpTool(name = "cmm_customer_contract", ...)
CustomerContractResponse searchContract(CustomerContractRequest request);
}
```
Each Tool has independent metadata:
- MCP Tool name, title, description, category and registration flag
- Java method name
- integration interface ID
- Client class and Client method name
- request and response fields
- generated V17 Tool definition YAML
The implementation follows the CustomerGuidance pattern per method:
```text
Tool Request -> MapStruct Converter -> Typed *Client -> MapStruct Converter -> Tool Response
```
For MCI, the generator creates a typed Client method such as
`callOnild0320(ONILD0320_I)` rather than having the UseCase call generic
`callTo(...)` directly. The Client remains the only layer that calls
`AxhubMciComponent`.
## Field model
Existing scalar types remain supported: `String`, `Integer`, `Long`, `Double`,
`Boolean`, and `BigDecimal`.
Two structured choices are added:
1. **Enum**: the user supplies allowed values. The generator creates a named
enum class beside the DTO, uses it as the field type, and emits the same
values in the MCP parameter schema.
2. **List**: the user selects an item kind.
- Primitive lists generate e.g. `List<String>`.
- Object lists contain user-entered item fields and generate a separate
`...Item` DTO plus `List<...Item>`.
The Scaffold UI presents enum values and list item fields in dedicated dialogs,
instead of asking the user to hand-author Java or JSON type expressions.
## Compatibility
- The existing one-Tool request payload continues to work and generates the
current single-Tool shape.
- The new multi-Tool payload is additive and is used by the updated UI.
- Existing generated source is not rewritten.
- Each generated Tool continues to receive its own V17 YAML file, which keeps
runtime tool discovery and validation unchanged.
## Validation and tests
- Validate unique Tool names and Java method names within a UseCase group.
- Validate a Client class/method and interface ID for each MCI Tool.
- Validate enum values and object-list item fields.
- Add generator tests for multiple methods, typed MCI Client calls, enum DTOs,
primitive lists, object lists, and V17 schema output.