update: apply recent changes from local workspace
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m54s
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m54s
This commit is contained in:
@@ -136,6 +136,25 @@ public class ScaffoldingController {
|
||||
}
|
||||
}
|
||||
|
||||
@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();
|
||||
|
||||
@@ -827,12 +827,13 @@
|
||||
<div class="mb-4 p-3 rounded" style="background:#18181b; border:1px solid #3f3f46;">
|
||||
<div class="d-flex justify-content-between align-items-center gap-3">
|
||||
<div>
|
||||
<label class="form-label mb-1">Multi Tool UseCase (MCI)</label>
|
||||
<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>
|
||||
@@ -1172,6 +1173,7 @@
|
||||
<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>
|
||||
@@ -1501,11 +1503,17 @@
|
||||
function currentToolDefinition() {
|
||||
const form = document.getElementById('toolForm');
|
||||
const data = Object.fromEntries(new FormData(form).entries());
|
||||
if (!data.baseName || !data.interfaceId || !data.clientSystemCode) {
|
||||
throw new Error('Base Name, Legacy Interface ID, Client System Code를 입력해주세요.');
|
||||
if (!data.baseName) {
|
||||
throw new Error('Base Name을 입력해주세요.');
|
||||
}
|
||||
if (data.routingType !== 'MCI') {
|
||||
throw new Error('여러 Tool을 하나의 UseCase로 생성하는 기능은 현재 MCI 전용입니다.');
|
||||
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,
|
||||
@@ -1542,6 +1550,35 @@
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
@@ -1706,17 +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];
|
||||
}
|
||||
updateFieldEditorPreview();
|
||||
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));
|
||||
});
|
||||
|
||||
const detailsInput = makeInput(
|
||||
field.type === 'Enum' ? (field.enumValues || []).join(', ') : (field.type === 'List' ? (field.itemType || 'String') : ''),
|
||||
'Enum: OPEN, CLOSED / List: String');
|
||||
detailsInput.dataset.field = 'details';
|
||||
} else {
|
||||
input = makeInput(type === 'Enum' ? value : '', 'Enum: OPEN, CLOSED');
|
||||
}
|
||||
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';
|
||||
@@ -1724,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, detailsInput].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, detailsInput, 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);
|
||||
@@ -1754,6 +1825,13 @@
|
||||
.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(),
|
||||
@@ -1761,7 +1839,7 @@
|
||||
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: []
|
||||
itemFields
|
||||
};
|
||||
})
|
||||
.filter(field => field.name);
|
||||
|
||||
@@ -11,7 +11,9 @@ 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;
|
||||
@@ -26,6 +28,23 @@ 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);
|
||||
|
||||
@@ -98,22 +98,33 @@ public class ToolScaffolder {
|
||||
Files.createDirectories(mockDir);
|
||||
|
||||
String bizPackage = BASE_PACKAGE + ".biz." + group;
|
||||
writeUtf8(useCaseDir.resolve(useCaseBaseName + "UseCase.java"),
|
||||
groupedUseCaseContent(bizPackage, useCaseBaseName, moduleName, tools));
|
||||
writeUtf8(implDir.resolve(useCaseBaseName + "UseCaseImpl.java"),
|
||||
groupedUseCaseImplContent(bizPackage, useCaseBaseName, tools));
|
||||
writeUtf8(converterDir.resolve(useCaseBaseName + "Converter.java"),
|
||||
groupedConverterContent(bizPackage, useCaseBaseName, tools));
|
||||
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("[Usecase Interface] ").append(useCaseDir.resolve(useCaseBaseName + "UseCase.java")).append("\n")
|
||||
.append("[Usecase Impl] ").append(implDir.resolve(useCaseBaseName + "UseCaseImpl.java")).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(converterDir.resolve(useCaseBaseName + "Converter.java")).append("\n");
|
||||
}
|
||||
log.append("[Converter] ").append(converterFile).append("\n");
|
||||
return log.toString();
|
||||
}
|
||||
|
||||
@@ -128,13 +139,18 @@ public class ToolScaffolder {
|
||||
if (!expectedGroup.equalsIgnoreCase(tool.group())) {
|
||||
throw new IllegalArgumentException("All Tool methods in one UseCase must use the same category.");
|
||||
}
|
||||
if (!"MCI".equalsIgnoreCase(tool.routingType())) {
|
||||
throw new IllegalArgumentException("Grouped Tool scaffolding currently supports MCI Tools only.");
|
||||
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 (tool.interfaceId() == null || tool.interfaceId().isBlank()
|
||||
|| tool.clientSystemCode() == null || tool.clientSystemCode().isBlank()) {
|
||||
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.");
|
||||
@@ -146,9 +162,10 @@ public class ToolScaffolder {
|
||||
Path mockDir, String bizPackage, ToolMethodDefinition tool,
|
||||
String moduleName, StringBuilder log) throws IOException {
|
||||
String baseName = toPascalCase(tool.baseName());
|
||||
String code = tool.clientSystemCode().toLowerCase(Locale.ROOT);
|
||||
String ioPackage = BASE_PACKAGE + ".infra.itrf.mci." + code;
|
||||
Path clientDir = sourceRoot.resolve(Paths.get("infra", "itrf", "mci", code));
|
||||
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"),
|
||||
@@ -157,6 +174,7 @@ public class ToolScaffolder {
|
||||
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"),
|
||||
@@ -165,6 +183,20 @@ public class ToolScaffolder {
|
||||
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,
|
||||
@@ -204,48 +236,154 @@ public class ToolScaffolder {
|
||||
StringBuilder methods = new StringBuilder();
|
||||
for (ToolMethodDefinition tool : tools) {
|
||||
String baseName = toPascalCase(tool.baseName());
|
||||
String code = tool.clientSystemCode().toLowerCase(Locale.ROOT);
|
||||
String clientVariable = Character.toLowerCase(baseName.charAt(0)) + baseName.substring(1) + "Client";
|
||||
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(BASE_PACKAGE).append(".infra.itrf.mci.").append(code).append(".").append(baseName).append("Client;\n")
|
||||
.append("import ").append(BASE_PACKAGE).append(".infra.itrf.mci.").append(code).append(".io.").append(baseName).append("_I;\n")
|
||||
.append("import ").append(BASE_PACKAGE).append(".infra.itrf.mci.").append(code).append(".io.").append(baseName).append("_O;\n");
|
||||
.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("_I request = converter.to").append(baseName).append("Request(req);\n")
|
||||
.append(" ").append(baseName).append("_O response = ").append(clientVariable).append(".call").append(baseName).append("(request);\n")
|
||||
.append(" ").append(baseName).append("Response toolResponse = converter.to").append(baseName).append("Response(response);\n")
|
||||
.append(" ").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 + ".converter." + useCaseBaseName + "Converter;\n"
|
||||
+ "import " + bizPackage + ".usecase." + useCaseBaseName + "UseCase;\n"
|
||||
+ "import lombok.RequiredArgsConstructor;\nimport org.springframework.stereotype.Service;\n" + imports
|
||||
+ "\n@Service\n@RequiredArgsConstructor\npublic class " + useCaseBaseName + "UseCaseImpl implements " + useCaseBaseName + "UseCase {\n\n"
|
||||
+ " private final " + useCaseBaseName + "Converter converter;\n" + fields + "\n" + methods + "}\n";
|
||||
+ fields + "\n" + methods + "}\n";
|
||||
}
|
||||
|
||||
private static String groupedConverterContent(String bizPackage, String useCaseBaseName,
|
||||
List<ToolMethodDefinition> tools) {
|
||||
StringBuilder imports = new StringBuilder();
|
||||
StringBuilder methods = new StringBuilder();
|
||||
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 code = tool.clientSystemCode().toLowerCase(Locale.ROOT);
|
||||
imports.append("import ").append(bizPackage).append(".dto.").append(baseName).append("Request;\n")
|
||||
.append("import ").append(bizPackage).append(".dto.").append(baseName).append("Response;\n")
|
||||
.append("import ").append(BASE_PACKAGE).append(".infra.itrf.mci.").append(code).append(".io.").append(baseName).append("_I;\n")
|
||||
.append("import ").append(BASE_PACKAGE).append(".infra.itrf.mci.").append(code).append(".io.").append(baseName).append("_O;\n");
|
||||
methods.append(" ").append(baseName).append("_I to").append(baseName).append("Request(").append(baseName).append("Request request);\n")
|
||||
.append(" ").append(baseName).append("Response to").append(baseName).append("Response(").append(baseName).append("_O response);\n\n");
|
||||
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);
|
||||
}
|
||||
return "package " + bizPackage + ".converter;\n\nimport org.mapstruct.Mapper;\nimport org.mapstruct.ReportingPolicy;\n"
|
||||
+ imports + "\n@Mapper(componentModel = \"spring\", unmappedTargetPolicy = ReportingPolicy.IGNORE)\n"
|
||||
+ "public interface " + useCaseBaseName + "Converter {\n\n" + methods + "}\n";
|
||||
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"
|
||||
+ " @ToolHint(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) {
|
||||
@@ -1325,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*"
|
||||
@@ -1396,8 +1534,8 @@ public class ToolScaffolder {
|
||||
@Data
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class %s {
|
||||
%s}
|
||||
""".formatted(packageName, listImport, className, body);
|
||||
%s%s}
|
||||
""".formatted(packageName, listImport, className, body, innerObjectListClasses(fields));
|
||||
}
|
||||
|
||||
private static String mciIoContent(String packageSuffix, String className, List<FieldDefinition> fields,
|
||||
@@ -1412,8 +1550,9 @@ public class ToolScaffolder {
|
||||
|
||||
@Data
|
||||
public class %s {
|
||||
%s}
|
||||
""".formatted(BASE_PACKAGE, packageSuffix, listImport, className, fieldLines(fields, Set.of(), className));
|
||||
%s%s}
|
||||
""".formatted(BASE_PACKAGE, packageSuffix, listImport, className, fieldLines(fields, Set.of(), className),
|
||||
innerObjectListClasses(fields));
|
||||
}
|
||||
|
||||
private static boolean hasListField(List<FieldDefinition> fields) {
|
||||
@@ -1465,15 +1604,10 @@ public class ToolScaffolder {
|
||||
}
|
||||
""".formatted(packageName, enumName, constants, enumName, enumName, enumName));
|
||||
}
|
||||
if ("List".equals(field.type()) && "Object".equals(field.itemType())) {
|
||||
List<FieldDefinition> itemFields = field.itemFields() == null ? List.of() : field.itemFields();
|
||||
if (itemFields.isEmpty()) {
|
||||
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());
|
||||
}
|
||||
String itemName = listItemClassName(ownerClass, field);
|
||||
writeUtf8(directory.resolve(itemName + ".java"), dtoContent(packageName, itemName, itemFields, "", "", true));
|
||||
writeStructuredFieldTypes(directory, packageName, itemName, itemFields);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1483,7 +1617,29 @@ public class ToolScaffolder {
|
||||
}
|
||||
|
||||
private static String listItemClassName(String ownerClass, FieldDefinition field) {
|
||||
return ownerClass + toPascalCase(field.name()) + "Item";
|
||||
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,
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,20 +9,6 @@ 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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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; // 시스템등록인사번호
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -51,6 +51,22 @@ class ToolScaffolderTest {
|
||||
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();
|
||||
@@ -75,8 +91,9 @@ class ToolScaffolderTest {
|
||||
assertTrue(request.contains("private ClaimStatus claimStatus;"), request);
|
||||
assertTrue(request.contains("private List<String> customerIds;"), request);
|
||||
assertTrue(Files.exists(dtoRoot.resolve("ClaimStatus.java")));
|
||||
assertTrue(response.contains("private List<ClaimSearchResponseGuidanceItemsItem> guidanceItems;"), response);
|
||||
assertTrue(Files.exists(dtoRoot.resolve("ClaimSearchResponseGuidanceItemsItem.java")));
|
||||
assertTrue(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);
|
||||
@@ -412,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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user