feat: scaffold tools from declared fields
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m39s

This commit is contained in:
jade
2026-08-09 17:50:06 +09:00
parent c0567ec1c2
commit 813c846f05
4 changed files with 196 additions and 2 deletions

View File

@@ -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<ToolScaffolder.FieldDefinition> inputFields = parseFields(req.get("inputFields"));
List<ToolScaffolder.FieldDefinition> 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<ToolScaffolder.FieldDefinition> parseFields(String source) throws Exception {
if (source == null || source.isBlank()) {
return List.of();
}
return new ObjectMapper().readValue(source, new TypeReference<List<ToolScaffolder.FieldDefinition>>() { });
}
}

View File

@@ -495,6 +495,29 @@
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<div class="d-flex justify-content-between align-items-center mb-1">
<label class="form-label mb-0">Input Fields (JSON)</label>
<div class="d-flex gap-1">
<button type="button" class="btn-secondary-action" style="padding: 0.2rem 0.45rem; font-size: 0.72rem;" onclick="loadFieldExample('inputFields')">예제 넣기</button>
<button type="button" class="btn-secondary-action" style="padding: 0.2rem 0.45rem; font-size: 0.72rem;" onclick="copyFieldJson('inputFields')">복사</button>
</div>
</div>
<textarea id="inputFields" class="form-control" name="inputFields" rows="4" placeholder='[{"name":"employeeId","type":"String","description":"Employee ID","example":"EMP10001","required":true}]'></textarea>
</div>
<div class="col-md-6 mt-3 mt-md-0">
<div class="d-flex justify-content-between align-items-center mb-1">
<label class="form-label mb-0">Output Fields (JSON)</label>
<div class="d-flex gap-1">
<button type="button" class="btn-secondary-action" style="padding: 0.2rem 0.45rem; font-size: 0.72rem;" onclick="loadFieldExample('outputFields')">예제 넣기</button>
<button type="button" class="btn-secondary-action" style="padding: 0.2rem 0.45rem; font-size: 0.72rem;" onclick="copyFieldJson('outputFields')">복사</button>
</div>
</div>
<textarea id="outputFields" class="form-control" name="outputFields" rows="4" placeholder='[{"name":"employeeName","type":"String","description":"Employee name","example":"Hong Gildong","required":true}]'></textarea>
</div>
</div>
<div class="row mb-4">
<div class="col-md-6">
<label class="form-label">Target Module</label>
@@ -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 = '<tr><td colspan="5" class="text-center py-5 text-muted">Loading data...</td></tr>';

View File

@@ -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<FieldDefinition> inputFields, List<FieldDefinition> 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<Object> 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<Object> callTo(String interfaceId, String dummy, Object mciReq, Class<Object> resType) throws Exception {
public <O> Transfer<O> callTo(String interfaceId, String dummy, Object mciReq, Class<O> 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<FieldDefinition> 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<FieldDefinition> 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<FieldDefinition> fields) {
StringBuilder source = new StringBuilder();
for (FieldDefinition field : fields == null ? List.<FieldDefinition>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-")

View File

@@ -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<ToolScaffolder.FieldDefinition> inputFields = List.of(
new ToolScaffolder.FieldDefinition("employeeId", "String", "Employee identifier", "EMP10001", true),
new ToolScaffolder.FieldDefinition("page", "Integer", "Page number", "1", false));
List<ToolScaffolder.FieldDefinition> 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);"));
}
}