Compare commits
12 Commits
6aa62a5499
...
feature/to
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2723d12568 | ||
|
|
df03037d3f | ||
|
|
fc235f05b9 | ||
|
|
97d380afe0 | ||
|
|
bd4d2a7624 | ||
|
|
b39c66000a | ||
|
|
11d78a0119 | ||
|
|
1d4e8b8ebd | ||
|
|
a661e07a9f | ||
|
|
4ecff183c6 | ||
|
|
999b4ebae0 | ||
|
|
69e89e0d52 |
@@ -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) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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";
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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 = "";
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}$");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package io.shinhanlife.glow.db.typehandler;
|
||||
|
||||
/**
|
||||
* glow 프레임워크 스텁 (실제 라이브러리가 없는 로컬 개발 환경용 더미).
|
||||
* 코드값을 갖는 enum이 공통으로 구현하는 인터페이스 — {@link CodeEnumTypeHandler}가 이 getCode()로
|
||||
* DB 컬럼(String)과 enum 상수를 상호 변환한다.
|
||||
*/
|
||||
public interface CodeEnum {
|
||||
|
||||
String getCode();
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,7 @@ public class MetaCommonCodeUseCaseImpl implements MetaCommonCodeUseCase {
|
||||
private final MetaCommonCodeConverter converter;
|
||||
|
||||
@Override
|
||||
public Object execute(MetaCommonCodeRequest req) {
|
||||
public Object searchCommonCode(MetaCommonCodeRequest req) {
|
||||
log.info("[MCI Tool] {} 요청 수신. 파라미터: {}", "metaCommonCode", req);
|
||||
try {
|
||||
CLCNNB00001_I mciRequest = converter.toLegacyRequest(req);
|
||||
|
||||
@@ -38,7 +38,7 @@ public class MetaTableUseCaseImpl implements MetaTableUseCase {
|
||||
private final MetaTableConverter converter;
|
||||
|
||||
@Override
|
||||
public Object execute(MetaTableRequest req) {
|
||||
public Object searchMetaTable(MetaTableRequest req) {
|
||||
log.info("[MCI Tool] {} 요청 수신. 파라미터: {}", "metaTable", req);
|
||||
try {
|
||||
CLCNNB00001_I mciRequest = converter.toLegacyRequest(req);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ public class DailyQuoteToolUseCaseImpl implements DailyQuoteToolUseCase {
|
||||
);
|
||||
|
||||
@Override
|
||||
public DailyQuoteResponse execute(DailyQuoteRequest req) {
|
||||
public DailyQuoteResponse getDailyQuote(DailyQuoteRequest req) {
|
||||
int index = new Random().nextInt(quotes.size());
|
||||
DailyQuoteResponse selected = quotes.get(index);
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ public class ExchangeRateToolUseCaseImpl implements ExchangeRateToolUseCase {
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExchangeRateResponse execute(ExchangeRateRequest req) {
|
||||
public ExchangeRateResponse getExchangeRate(ExchangeRateRequest req) {
|
||||
String targetCurrency = req.getCurrencyCode() != null ? req.getCurrencyCode().toUpperCase().trim() : "USD";
|
||||
|
||||
// 간단한 모의 데이터로 반환 (실제 구현 시 외부 연동)
|
||||
|
||||
@@ -27,7 +27,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
public class TeamMemberUseCaseImpl implements TeamMemberUseCase {
|
||||
|
||||
@Override
|
||||
public Object execute(TeamMemberRequest req) {
|
||||
public Object getTeamMember(TeamMemberRequest req) {
|
||||
log.info("[A01] 신한라이프 MCP, TOOL 파트 구성원 조회 요청: {}", req);
|
||||
|
||||
String filter = req != null && req.getTeamName() != null ? req.getTeamName().toUpperCase() : "전체";
|
||||
|
||||
@@ -35,7 +35,7 @@ public class WeatherToolUseCaseImpl implements WeatherToolUseCase {
|
||||
public WeatherToolUseCaseImpl() {
|
||||
this.restClient = RestClient.create();
|
||||
}
|
||||
public WeatherResponse execute(WeatherRequest req) {
|
||||
public WeatherResponse getWeather(WeatherRequest req) {
|
||||
String city = req.city() != null ? req.city().trim() : "서울";
|
||||
|
||||
// 지역별 위경도 매핑 (간단한 예시)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ public class SolReqDetailUseCaseImpl implements SolReqDetailUseCase {
|
||||
private boolean mockEnabled;
|
||||
|
||||
@Override
|
||||
public Object execute(SolReqDetailRequest req) {
|
||||
public Object getSolRequestDetail(SolReqDetailRequest req) {
|
||||
log.info("[MCI Tool] {} 요청 수신. 파라미터: {}", "solReqDetail", req);
|
||||
if (req == null || req.getSrId() == null || req.getSrId().isBlank()) {
|
||||
return Map.of("status", "ERROR", "message", "srId는 필수입니다.");
|
||||
|
||||
@@ -39,7 +39,7 @@ public class SolReqListUseCaseImpl implements SolReqListUseCase {
|
||||
private final SolReqListConverter converter;
|
||||
|
||||
@Override
|
||||
public Object execute(SolReqListRequest req) {
|
||||
public Object searchSolRequests(SolReqListRequest req) {
|
||||
log.info("[MCI Tool] {} 요청 수신. 파라미터: {}", "solReqList", req);
|
||||
try {
|
||||
SOLG00000001_I mciRequest = converter.toLegacyRequest(req);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"resultCode" : "SUCCESS",
|
||||
"data" : "[{\"date\":\"2024-01-05\",\"message\":\"안내 내용\"}]"
|
||||
}
|
||||
@@ -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
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,7 @@ class Onnba3011UseCaseImplTest {
|
||||
when(converter.toMciRequest(request)).thenReturn(mciRequest);
|
||||
when(mciCfpaClient.callCfpa0001(mciRequest)).thenReturn("success");
|
||||
|
||||
Object result = useCase.execute(request);
|
||||
Object result = useCase.callOnnba3011(request);
|
||||
|
||||
assertEquals("success", result);
|
||||
verify(converter).toMciRequest(request);
|
||||
|
||||
@@ -36,7 +36,7 @@ class SolReqDetailUseCaseImplTest {
|
||||
SolReqDetailRequest request = new SolReqDetailRequest();
|
||||
request.setSrId("SR-2026-001");
|
||||
|
||||
SolReqDetailResponse response = (SolReqDetailResponse) useCase.execute(request);
|
||||
SolReqDetailResponse response = (SolReqDetailResponse) useCase.getSolRequestDetail(request);
|
||||
|
||||
assertThat(response.getSrId()).isEqualTo("SR-2026-001");
|
||||
assertThat(response.getSrName()).isEqualTo("AX HUB 메인 화면 UI 개편");
|
||||
|
||||
@@ -29,7 +29,7 @@ class SolReqListUseCaseImplTest {
|
||||
when(mci.callTo(eq("SOLG00000001"), eq("SOLG00000001"), eq(mciRequest), eq(SOLG00000001_O.class)))
|
||||
.thenReturn(new Transfer<>());
|
||||
|
||||
SolReqListResponse response = (SolReqListResponse) useCase.execute(request);
|
||||
SolReqListResponse response = (SolReqListResponse) useCase.searchSolRequests(request);
|
||||
|
||||
verify(converter).toLegacyRequest(request);
|
||||
verify(mci).callTo("SOLG00000001", "SOLG00000001", mciRequest, SOLG00000001_O.class);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ public class ClaimSearchUseCaseImpl implements ClaimSearchUseCase {
|
||||
private final ClaimSearchConverter converter;
|
||||
|
||||
@Override
|
||||
public ClaimSearchResponse execute(ClaimSearchRequest req) {
|
||||
public ClaimSearchResponse searchClaim(ClaimSearchRequest req) {
|
||||
log.info("[MCI Tool] {} 요청 수신.", "cmm_claim_search");
|
||||
try {
|
||||
// MapStruct를 이용한 자동 매핑 (AI DTO -> MCI DTO)
|
||||
|
||||
@@ -18,7 +18,7 @@ public class MemoListRetrieverUseCaseImpl implements MemoListRetrieverUseCase {
|
||||
private final MemoClient memoClient;
|
||||
|
||||
@Override
|
||||
public MemoListRetrieverResponse execute(MemoListRetrieverRequest req) {
|
||||
public MemoListRetrieverResponse retrieveMemoList(MemoListRetrieverRequest req) {
|
||||
MemoListRetrieverHttpRequest httpRequest = converter.toHttpRequest(req);
|
||||
MemoListRetrieverHttpResponse httpResponse = memoClient.call(httpRequest, MemoListRetrieverHttpResponse.class);
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user