diff --git a/dap-gateway/src/main/java/io/shinhanlife/dap/mcg/presentation/ScaffoldingController.java b/dap-gateway/src/main/java/io/shinhanlife/dap/mcg/presentation/ScaffoldingController.java index dc87d206..d58132da 100644 --- a/dap-gateway/src/main/java/io/shinhanlife/dap/mcg/presentation/ScaffoldingController.java +++ b/dap-gateway/src/main/java/io/shinhanlife/dap/mcg/presentation/ScaffoldingController.java @@ -136,6 +136,25 @@ public class ScaffoldingController { } } + @GetMapping("/usecases") + public List 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 req) { String description = req.getOrDefault("description", "").trim(); diff --git a/dap-gateway/src/main/resources/static/admin/scaffold.html b/dap-gateway/src/main/resources/static/admin/scaffold.html index 6f73afbe..4188027c 100644 --- a/dap-gateway/src/main/resources/static/admin/scaffold.html +++ b/dap-gateway/src/main/resources/static/admin/scaffold.html @@ -827,12 +827,13 @@
- +
현재 Tool을 같은 UseCase에 추가하면, Tool별 Client를 호출하는 여러 MCP Tool 메서드가 생성됩니다.
+
묶음에 추가된 Tool 없음 — 일반 Generate Tool은 기존 단일 Tool 생성입니다.
@@ -1172,6 +1173,7 @@ Name Type Enum values / List item type + Object list item fields (JSON) Description Example Required @@ -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 = ''; + 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]; + 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(); - }); - - const detailsInput = makeInput( - field.type === 'Enum' ? (field.enumValues || []).join(', ') : (field.type === 'List' ? (field.itemType || 'String') : ''), - 'Enum: OPEN, CLOSED / List: String'); - detailsInput.dataset.field = 'details'; + 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); diff --git a/dap-gateway/src/test/java/io/shinhanlife/dap/mcg/presentation/ScaffoldingControllerToolDraftTest.java b/dap-gateway/src/test/java/io/shinhanlife/dap/mcg/presentation/ScaffoldingControllerToolDraftTest.java index 507ad145..e2588fba 100644 --- a/dap-gateway/src/test/java/io/shinhanlife/dap/mcg/presentation/ScaffoldingControllerToolDraftTest.java +++ b/dap-gateway/src/test/java/io/shinhanlife/dap/mcg/presentation/ScaffoldingControllerToolDraftTest.java @@ -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); diff --git a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/util/ToolScaffolder.java b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/util/ToolScaffolder.java index 514912f4..91e297bb 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/util/ToolScaffolder.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/dap/lib/util/ToolScaffolder.java @@ -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,14 +174,29 @@ 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()); - 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())); + 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, @@ -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 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 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); + } + 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); } - 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"; + 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 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 fields) { @@ -1465,14 +1604,9 @@ public class ToolScaffolder { } """.formatted(packageName, enumName, constants, enumName, enumName, enumName)); } - if ("List".equals(field.type()) && "Object".equals(field.itemType())) { - List itemFields = field.itemFields() == null ? List.of() : field.itemFields(); - if (itemFields.isEmpty()) { - throw new IllegalArgumentException("Object List field needs item fields: " + field.name()); - } - String itemName = listItemClassName(ownerClass, field); - writeUtf8(directory.resolve(itemName + ".java"), dtoContent(packageName, itemName, itemFields, "", "", true)); - writeStructuredFieldTypes(directory, packageName, itemName, itemFields); + 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()); } } } @@ -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 fields) { + StringBuilder source = new StringBuilder(); + Set generated = new LinkedHashSet<>(); + for (FieldDefinition field : fields == null ? List.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 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, diff --git a/dap-was-lib/src/main/java/io/shinhanlife/glow/BaseException.java b/dap-was-lib/src/main/java/io/shinhanlife/glow/BaseException.java index a2d8cffc..ae63fd96 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/glow/BaseException.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/glow/BaseException.java @@ -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 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ public class BaseException { // @Schema(description = "Error 코드 Meta 참조 운영. (예) 20001, 50001 등", shinhanlife = "20001") @@ -40,4 +26,4 @@ public class BaseException { @Builder.Default String exceptionDetail = ""; -} \ No newline at end of file +} diff --git a/dap-was-lib/src/main/java/io/shinhanlife/glow/BaseResponse.java b/dap-was-lib/src/main/java/io/shinhanlife/glow/BaseResponse.java index 7ddcfca7..e0d88e13 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/glow/BaseResponse.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/glow/BaseResponse.java @@ -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 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ @ToString @Getter @Builder @@ -36,4 +22,4 @@ public class BaseResponse { private BaseException error; -} \ No newline at end of file +} diff --git a/dap-was-lib/src/main/java/io/shinhanlife/glow/BizException.java b/dap-was-lib/src/main/java/io/shinhanlife/glow/BizException.java index 835c800d..5d3665e9 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/glow/BizException.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/glow/BizException.java @@ -1,20 +1,61 @@ package io.shinhanlife.glow; /** - * @package io.shinhanlife.glow - * @className BizException - * @description AX HUB 시스템 처리 클래스 - * @author 0986406 - * @create 2026.09.01 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
+ * 업무 예외. + * + *

두 가지 방식으로 쓸 수 있다.

+ *
    + *
  1. 메시지코드 방식(권장) — {@code throw new BizException("DAH00004", "사번")}
    + * 통합메시지(ZT_UNFC_MSG)에서 문구를 찾아 {0},{1}.. 을 인자로 치환해 응답한다. + * 문구가 화면·서버 한곳(관리 화면)에서 관리되고, 다국어 확장도 여기서 처리된다.
  2. + *
  3. 문구 직접 방식(기존 호환) — {@code throw new BizException("사번은 필수입니다.")}
    + * 메시지코드로 해석되지 않으면 문구 그대로 응답한다.
  4. + *
+ * + *

변환은 {@code common/config/GlobalExceptionHandler} 가 수행한다. + * 메시지코드 여부는 코드 형식(영문 대문자+숫자 8자리)으로 판별한다.

*/ 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]; } -} \ No newline at end of file + + /** + * 메시지코드 + 치환 인자. + * + * @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}$"); + } +} diff --git a/dap-was-lib/src/main/java/io/shinhanlife/glow/GlowAppServiceId.java b/dap-was-lib/src/main/java/io/shinhanlife/glow/GlowAppServiceId.java index fe4ec3ac..1772ba09 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/glow/GlowAppServiceId.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/glow/GlowAppServiceId.java @@ -1,20 +1,5 @@ package io.shinhanlife.glow; - -/** - * @package io.shinhanlife.glow - * @className GlowAppServiceId - * @description AX HUB 시스템 처리 클래스 - * @author 0986406 - * @create 2026.09.01 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ import java.lang.annotation.*; @Target(ElementType.METHOD) diff --git a/dap-was-lib/src/main/java/io/shinhanlife/glow/GlowControllerId.java b/dap-was-lib/src/main/java/io/shinhanlife/glow/GlowControllerId.java index d9e806c7..078ac61f 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/glow/GlowControllerId.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/glow/GlowControllerId.java @@ -1,20 +1,5 @@ package io.shinhanlife.glow; - -/** - * @package io.shinhanlife.glow - * @className GlowControllerId - * @description AX HUB 시스템 처리 클래스 - * @author 0986406 - * @create 2026.09.01 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ public @interface GlowControllerId { String value(); } diff --git a/dap-was-lib/src/main/java/io/shinhanlife/glow/GlowIndexPaging.java b/dap-was-lib/src/main/java/io/shinhanlife/glow/GlowIndexPaging.java index c03265ff..884d75f8 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/glow/GlowIndexPaging.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/glow/GlowIndexPaging.java @@ -1,20 +1,5 @@ package io.shinhanlife.glow; - -/** - * @package io.shinhanlife.glow - * @className GlowIndexPaging - * @description AX HUB 시스템 처리 클래스 - * @author 0986406 - * @create 2026.09.01 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ import java.lang.annotation.*; @Target(ElementType.METHOD) diff --git a/dap-was-lib/src/main/java/io/shinhanlife/glow/GlowLogTarget.java b/dap-was-lib/src/main/java/io/shinhanlife/glow/GlowLogTarget.java index a748a067..c16937ea 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/glow/GlowLogTarget.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/glow/GlowLogTarget.java @@ -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 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ enum Target { FILE, CONSOLE } -} \ No newline at end of file +} diff --git a/dap-was-lib/src/main/java/io/shinhanlife/glow/GlowLogger.java b/dap-was-lib/src/main/java/io/shinhanlife/glow/GlowLogger.java index 0cf9e84e..3da0e493 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/glow/GlowLogger.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/glow/GlowLogger.java @@ -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 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ @Component @Scope("prototype") public class GlowLogger { diff --git a/dap-was-lib/src/main/java/io/shinhanlife/glow/GlowMybatisMapper.java b/dap-was-lib/src/main/java/io/shinhanlife/glow/GlowMybatisMapper.java index 9c6c09f7..ca50c442 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/glow/GlowMybatisMapper.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/glow/GlowMybatisMapper.java @@ -1,20 +1,5 @@ package io.shinhanlife.glow; - -/** - * @package io.shinhanlife.glow - * @className GlowMybatisMapper - * @description AX HUB 시스템 처리 클래스 - * @author 0986406 - * @create 2026.09.01 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ import org.apache.ibatis.annotations.Mapper; import org.springframework.stereotype.Component; diff --git a/dap-was-lib/src/main/java/io/shinhanlife/glow/GlowServiceGroupId.java b/dap-was-lib/src/main/java/io/shinhanlife/glow/GlowServiceGroupId.java index f6c38166..375a3b6b 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/glow/GlowServiceGroupId.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/glow/GlowServiceGroupId.java @@ -1,20 +1,5 @@ package io.shinhanlife.glow; - -/** - * @package io.shinhanlife.glow - * @className GlowServiceGroupId - * @description AX HUB 시스템 처리 클래스 - * @author 0986406 - * @create 2026.09.01 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ public @interface GlowServiceGroupId { String value(); diff --git a/dap-was-lib/src/main/java/io/shinhanlife/glow/GlowTrgmField.java b/dap-was-lib/src/main/java/io/shinhanlife/glow/GlowTrgmField.java index 6e44dfae..883d7dda 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/glow/GlowTrgmField.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/glow/GlowTrgmField.java @@ -1,23 +1,7 @@ package io.shinhanlife.glow; - -/** - * @package io.shinhanlife.glow - * @className GlowTrgmField - * @description AX HUB 시스템 처리 클래스 - * @author 0986406 - * @create 2026.09.01 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ import java.lang.annotation.*; -@Deprecated(forRemoval = false) @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) @Documented diff --git a/dap-was-lib/src/main/java/io/shinhanlife/glow/PageInfo.java b/dap-was-lib/src/main/java/io/shinhanlife/glow/PageInfo.java index 985d76a4..a83c5a22 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/glow/PageInfo.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/glow/PageInfo.java @@ -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 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ @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(); } -} \ No newline at end of file +} diff --git a/dap-was-lib/src/main/java/io/shinhanlife/glow/ResponseCode.java b/dap-was-lib/src/main/java/io/shinhanlife/glow/ResponseCode.java index 84d60e95..63ffb693 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/glow/ResponseCode.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/glow/ResponseCode.java @@ -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 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ @Getter @RequiredArgsConstructor public enum ResponseCode { @@ -42,4 +28,4 @@ public enum ResponseCode { private final HttpStatus status; private final String message; -} \ No newline at end of file +} diff --git a/dap-was-lib/src/main/java/io/shinhanlife/glow/ResponseUtil.java b/dap-was-lib/src/main/java/io/shinhanlife/glow/ResponseUtil.java index cda4738e..9ebe6b97 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/glow/ResponseUtil.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/glow/ResponseUtil.java @@ -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 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ public final class ResponseUtil { private ResponseUtil() { @@ -122,4 +108,4 @@ public final class ResponseUtil { ); } -} \ No newline at end of file +} diff --git a/dap-was-lib/src/main/java/io/shinhanlife/glow/db/dto/AuditInfo.java b/dap-was-lib/src/main/java/io/shinhanlife/glow/db/dto/AuditInfo.java index b9717f8d..605362c1 100644 --- a/dap-was-lib/src/main/java/io/shinhanlife/glow/db/dto/AuditInfo.java +++ b/dap-was-lib/src/main/java/io/shinhanlife/glow/db/dto/AuditInfo.java @@ -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 - *
- * ---------- 개정이력 ----------
- * 수정일      수정자    수정내용
- * ---------- -------- ---------------------------
- * 2026.09.01  0986406    최초생성
- * 
- * 
- */ @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 -} \ No newline at end of file +} diff --git a/dap-was-lib/src/main/java/io/shinhanlife/glow/db/typehandler/CodeEnum.java b/dap-was-lib/src/main/java/io/shinhanlife/glow/db/typehandler/CodeEnum.java new file mode 100644 index 00000000..3e7b94d6 --- /dev/null +++ b/dap-was-lib/src/main/java/io/shinhanlife/glow/db/typehandler/CodeEnum.java @@ -0,0 +1,11 @@ +package io.shinhanlife.glow.db.typehandler; + +/** + * glow 프레임워크 스텁 (실제 라이브러리가 없는 로컬 개발 환경용 더미). + * 코드값을 갖는 enum이 공통으로 구현하는 인터페이스 — {@link CodeEnumTypeHandler}가 이 getCode()로 + * DB 컬럼(String)과 enum 상수를 상호 변환한다. + */ +public interface CodeEnum { + + String getCode(); +} diff --git a/dap-was-lib/src/main/java/io/shinhanlife/glow/db/typehandler/CodeEnumTypeHandler.java b/dap-was-lib/src/main/java/io/shinhanlife/glow/db/typehandler/CodeEnumTypeHandler.java new file mode 100644 index 00000000..b0dc8a8e --- /dev/null +++ b/dap-was-lib/src/main/java/io/shinhanlife/glow/db/typehandler/CodeEnumTypeHandler.java @@ -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 & CodeEnum> extends BaseTypeHandler { + + private final Class type; + + protected CodeEnumTypeHandler(Class 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()); + } +} diff --git a/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/ToolScaffolderTest.java b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/ToolScaffolderTest.java index e1ab620d..626f261d 100644 --- a/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/ToolScaffolderTest.java +++ b/dap-was-lib/src/test/java/io/shinhanlife/dap/lib/util/ToolScaffolderTest.java @@ -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 customerIds;"), request); assertTrue(Files.exists(dtoRoot.resolve("ClaimStatus.java"))); - assertTrue(response.contains("private List guidanceItems;"), response); - assertTrue(Files.exists(dtoRoot.resolve("ClaimSearchResponseGuidanceItemsItem.java"))); + assertTrue(response.contains("private List 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 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 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); + } }