feat: scaffold tools from declared fields
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m39s
All checks were successful
Deploy to OCIWP / deploy (push) Successful in 1m39s
This commit is contained in:
@@ -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>>() { });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>';
|
||||
|
||||
@@ -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-")
|
||||
|
||||
@@ -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);"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user