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 975a7d79..c905bcf0 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 @@ -18,6 +18,8 @@ package io.shinhanlife.dap.mcg.presentation; import io.shinhanlife.dap.lib.util.PodScaffolder; import io.shinhanlife.dap.lib.util.ToolScaffolder; import io.shinhanlife.dap.lib.util.ToolSourceUpdater; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; import java.io.File; import java.time.LocalDate; import java.time.format.DateTimeFormatter; @@ -66,8 +68,13 @@ public class ScaffoldingController { String clientSystemCode = req.get("clientSystemCode"); String inputSchemaResource = req.get("inputSchemaResource"); String outputSchemaResource = req.get("outputSchemaResource"); + List inputFields = parseFields(req.get("inputFields")); + List outputFields = parseFields(req.get("outputFields")); + if (inputFields.isEmpty()) { + inputFields = List.of(new ToolScaffolder.FieldDefinition("query", "String", "Search query", "example", false)); + } - return ToolScaffolder.scaffold(baseName, interfaceId, description, group, routingType, moduleName, author, date, register, clientSystemCode, inputSchemaResource, outputSchemaResource); + return ToolScaffolder.scaffold(baseName, interfaceId, description, group, routingType, moduleName, author, date, register, clientSystemCode, inputSchemaResource, outputSchemaResource, inputFields, outputFields); } catch (Exception e) { return "오류 발생: " + e.getMessage(); } @@ -107,4 +114,11 @@ public class ScaffoldingController { return List.of("dap-was-oth", "dap-was-hr", "dap-was-sms"); } } + + private List parseFields(String source) throws Exception { + if (source == null || source.isBlank()) { + return List.of(); + } + return new ObjectMapper().readValue(source, new TypeReference>() { }); + } } diff --git a/dap-gateway/src/main/resources/static/admin/scaffold.html b/dap-gateway/src/main/resources/static/admin/scaffold.html index 1bd6cd9f..976fedba 100644 --- a/dap-gateway/src/main/resources/static/admin/scaffold.html +++ b/dap-gateway/src/main/resources/static/admin/scaffold.html @@ -495,6 +495,29 @@ +
+
+
+ +
+ + +
+
+ +
+
+
+ +
+ + +
+
+ +
+
+
@@ -685,6 +708,33 @@ handleFormSubmit('podForm', '/api/v1/scaffold/pod'); handleFormSubmit('toolForm', '/api/v1/scaffold/tool'); + const fieldExamples = { + inputFields: [ + { name: 'employeeId', type: 'String', description: 'Employee identifier', example: 'EMP10001', required: true }, + { name: 'page', type: 'Integer', description: 'Page number', example: '1', required: false } + ], + outputFields: [ + { name: 'employeeName', type: 'String', description: 'Employee name', example: 'Hong Gildong', required: true } + ] + }; + + function loadFieldExample(fieldId) { + document.getElementById(fieldId).value = JSON.stringify(fieldExamples[fieldId], null, 2); + } + + async function copyFieldJson(fieldId) { + const textarea = document.getElementById(fieldId); + const value = textarea.value.trim() || JSON.stringify(fieldExamples[fieldId], null, 2); + try { + await navigator.clipboard.writeText(value); + } catch (error) { + textarea.value = value; + textarea.select(); + document.execCommand('copy'); + textarea.setSelectionRange(0, 0); + } + } + function loadToolList() { const tbody = document.getElementById('toolListBody'); tbody.innerHTML = 'Loading data...'; 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 c318202f..ac351078 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 @@ -7,6 +7,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.time.LocalDate; import java.time.format.DateTimeFormatter; +import java.util.List; import java.util.Locale; import java.util.Scanner; @@ -41,6 +42,9 @@ public class ToolScaffolder { private static final String BASE_PACKAGE = "io.shinhanlife.dap.mcc"; private static final String BASE_PACKAGE_PATH = "src/main/java/io/shinhanlife/dap/mcc"; + public record FieldDefinition(String name, String type, String description, String example, boolean required) { + } + public static void main(String[] args) throws IOException { Scanner scanner = new Scanner(System.in); @@ -94,6 +98,12 @@ public class ToolScaffolder { } public static String scaffold(String baseName, String interfaceId, String description, String group, String routingType, String moduleName, String author, String createDate, boolean register, String clientSystemCode, String inputSchemaResource, String outputSchemaResource) throws IOException { + return scaffold(baseName, interfaceId, description, group, routingType, moduleName, author, createDate, + register, clientSystemCode, inputSchemaResource, outputSchemaResource, + List.of(new FieldDefinition("query", "String", "Search query", "example", false)), List.of()); + } + + public static String scaffold(String baseName, String interfaceId, String description, String group, String routingType, String moduleName, String author, String createDate, boolean register, String clientSystemCode, String inputSchemaResource, String outputSchemaResource, List inputFields, List outputFields) throws IOException { baseName = toPascalCase(baseName); String envSourceDir = System.getenv("AXHUB_SOURCE_DIR"); Path rootDir = envSourceDir != null ? Paths.get(envSourceDir) : Paths.get("."); @@ -185,6 +195,7 @@ public class ToolScaffolder { .replaceAll("(?m)^\\s*-\\(\\?:[^\\r\\n]*\\R", "") .replace("private String phoneNumber;", "@Schema(example = \"01012345678\")\n private String phoneNumber;") .replace("private String message;", "@Schema(example = \"테스트 메시지입니다.\")\n private String message;"); + reqContent = dtoContent(bizPackage, baseName + "Request", inputFields, author, createDate, true); Files.writeString(dtoDir.resolve(baseName + "Request.java"), reqContent); // Generate Response DTO @@ -218,6 +229,7 @@ public class ToolScaffolder { // TODO: Add response fields here. Do not include PII in the Tool response. } """.formatted(bizPackage, bizPackage, baseName, author, createDate, createDate, author, baseName); + resContent = dtoContent(bizPackage, baseName + "Response", outputFields, author, createDate, false); Files.writeString(dtoDir.resolve(baseName + "Response.java"), resContent); String toolName = toToolName(moduleName, group, baseName); @@ -368,6 +380,14 @@ public class ToolScaffolder { baseName, baseName ); + String mciIoPackage = BASE_PACKAGE + "." + mciGroupPath.replace("/", "."); + serviceImplContent = serviceImplContent + .replace("import " + mciIoPackage + "." + interfaceId + "_I;", + "import " + mciIoPackage + "." + interfaceId + "_I;\nimport " + mciIoPackage + "." + interfaceId + "_O;") + .replace("Transfer resTransfer", "Transfer<" + interfaceId + "_O> resTransfer") + .replace("Object.class", interfaceId + "_O.class") + .replace("response.setResultCode(\"SUCCESS\");", + "if (resTransfer.getBody() != null) {\n response = converter.toResponse(resTransfer.getBody());\n }\n response.setResultCode(\"SUCCESS\");"); } else { serviceImplContent = """ package %s.usecase.impl; @@ -461,6 +481,7 @@ public class ToolScaffolder { private String content; } """.formatted(BASE_PACKAGE, mciGroupPath.replace("/", "."), BASE_PACKAGE, mciGroupPath.replace("/", "."), interfaceId, author, createDate, createDate, author, interfaceId); + mciReqContent = mciIoContent(mciGroupPath.replace("/", "."), interfaceId + "_I", inputFields, author, createDate); Files.writeString(mciIoDir.resolve(interfaceId + "_I.java"), mciReqContent); String mciResContent = """ @@ -487,6 +508,7 @@ public class ToolScaffolder { // TODO: Add response fields here } """.formatted(BASE_PACKAGE, mciGroupPath.replace("/", "."), BASE_PACKAGE, mciGroupPath.replace("/", "."), interfaceId, author, createDate, createDate, author, interfaceId); + mciResContent = mciIoContent(mciGroupPath.replace("/", "."), interfaceId + "_O", outputFields, author, createDate); Files.writeString(mciIoDir.resolve(interfaceId + "_O.java"), mciResContent); String converterContent = """ @@ -538,6 +560,7 @@ public class ToolScaffolder { baseName, interfaceId, baseName, interfaceId ); + converterContent = mciConverterContent(bizPackage, baseName, mciGroupPath.replace("/", "."), interfaceId); Files.writeString(converterDir.resolve(baseName + "Converter.java"), converterContent); log.append("\n=========================================\n"); @@ -579,7 +602,7 @@ public class ToolScaffolder { public class Mci%sClient { private final AxhubMciComponent mci; - public Transfer callTo(String interfaceId, String dummy, Object mciReq, Class resType) throws Exception { + public Transfer callTo(String interfaceId, String dummy, Object mciReq, Class resType) throws Exception { return mci.callTo(interfaceId, dummy, mciReq, resType); } } @@ -764,6 +787,84 @@ public class ToolScaffolder { .toLowerCase(Locale.ROOT); } + private static String dtoContent(String packageName, String className, List fields, + String author, String createDate, boolean request) { + String body = fieldLines(fields); + if (!request) { + body = " private String resultCode;\n\n private String resultMessage;\n" + body; + } + return """ + package %s; + + import com.fasterxml.jackson.annotation.JsonInclude; + import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; + + @Data + @JsonInclude(JsonInclude.Include.NON_NULL) + public class %s { + %s} + """.formatted(packageName, className, body); + } + + private static String mciIoContent(String packageSuffix, String className, List fields, + String author, String createDate) { + return """ + package %s.%s; + + import io.swagger.v3.oas.annotations.media.Schema; + import lombok.Data; + + @Data + public class %s { + %s} + """.formatted(BASE_PACKAGE, packageSuffix, className, fieldLines(fields)); + } + + private static String mciConverterContent(String bizPackage, String baseName, String mciPackage, String interfaceId) { + return """ + package %s.converter; + + import %s.dto.%sRequest; + import %s.dto.%sResponse; + import %s.%s.io.%s_I; + import %s.%s.io.%s_O; + import org.mapstruct.Mapper; + + @Mapper(componentModel = "spring") + public interface %sConverter { + %s_I toLegacyRequest(%sRequest request); + %sRequest toRequest(%s_I mciRequest); + %sResponse toResponse(%s_O mciRes); + } + """.formatted(bizPackage, bizPackage, baseName, bizPackage, baseName, + BASE_PACKAGE, mciPackage, interfaceId, BASE_PACKAGE, mciPackage, interfaceId, + baseName, interfaceId, baseName, baseName, interfaceId, baseName, interfaceId); + } + + private static String fieldLines(List fields) { + StringBuilder source = new StringBuilder(); + for (FieldDefinition field : fields == null ? List.of() : fields) { + String type = supportedType(field.type()); + String description = field.description() == null ? "" : field.description().replace("\"", "\\\""); + String example = field.example() == null ? "" : field.example().replace("\"", "\\\""); + source.append(" @Schema(description = \"").append(description).append("\", example = \"") + .append(example).append("\""); + if (field.required()) { + source.append(", requiredMode = Schema.RequiredMode.REQUIRED"); + } + source.append(")\n private ").append(type).append(' ').append(field.name()).append(";\n\n"); + } + return source.toString(); + } + + private static String supportedType(String type) { + return switch (type == null ? "String" : type) { + case "String", "Integer", "Long", "Double", "Boolean", "BigDecimal" -> type; + default -> throw new IllegalArgumentException("Unsupported field type: " + type); + }; + } + private static String toToolName(String moduleName, String group, String baseName) { String moduleDirectory = Path.of(moduleName).getFileName().toString(); String pod = moduleDirectory.startsWith("dap-was-") 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 39ac1fd0..05434f10 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 @@ -5,6 +5,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.nio.file.Files; import java.nio.file.Path; +import java.util.List; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -78,4 +79,32 @@ class ToolScaffolderTest { assertTrue(implementation.contains("public SearchHrResponse execute(SearchHrRequest req)")); assertTrue(implementation.contains("response.setResultCode(\"SUCCESS\")")); } + + @Test + void generatesMciToolFromDeclaredInputAndOutputFields() throws Exception { + String moduleName = root.resolve("dap-was-pay").toString(); + List inputFields = List.of( + new ToolScaffolder.FieldDefinition("employeeId", "String", "Employee identifier", "EMP10001", true), + new ToolScaffolder.FieldDefinition("page", "Integer", "Page number", "1", false)); + List outputFields = List.of( + new ToolScaffolder.FieldDefinition("employeeName", "String", "Employee name", "Hong Gildong", true)); + + ToolScaffolder.scaffold("search hr", "SHEARCH_01", "HR search", "pay", "MCI", moduleName, + "tester", "2026.08.09", true, "DFAG", null, null, inputFields, outputFields); + + Path sourceRoot = root.resolve("dap-was-pay/src/main/java/io/shinhanlife/dap/mcc"); + String request = Files.readString(sourceRoot.resolve("biz/pay/dto/SearchHrRequest.java")); + String response = Files.readString(sourceRoot.resolve("biz/pay/dto/SearchHrResponse.java")); + String mciRequest = Files.readString(sourceRoot.resolve("infra/itrf/mci/dfa/g/io/SHEARCH_01_I.java")); + String mciResponse = Files.readString(sourceRoot.resolve("infra/itrf/mci/dfa/g/io/SHEARCH_01_O.java")); + String converter = Files.readString(sourceRoot.resolve("biz/pay/converter/SearchHrConverter.java")); + + assertTrue(request.contains("private String employeeId;")); + assertTrue(request.contains("private Integer page;")); + assertFalse(request.contains("phoneNumber")); + assertTrue(response.contains("private String employeeName;")); + assertTrue(mciRequest.contains("private String employeeId;")); + assertTrue(mciResponse.contains("private String employeeName;")); + assertTrue(converter.contains("SearchHrResponse toResponse(SHEARCH_01_O mciRes);")); + } }