feat: add MCI request DTO scaffold conversion
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 31s

This commit is contained in:
jade
2026-09-07 09:18:43 +09:00
parent a58af8178a
commit ac432a95dd
5 changed files with 305 additions and 26 deletions

View File

@@ -13,7 +13,7 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Glow MCI 응답 전문({@code *_O.java})을 분석하여 LLM 노출용 Response DTO와
* Glow MCI 전문({@code *_I.java}/{@code *_O.java})을 분석하여 LLM 노출용 Request/Response DTO와
* MapStruct Converter 소스를 생성합니다. AI는 필드명 제안에만 사용하고 전문 구조와
* {@code GlowTrgmField.description}은 이 클래스가 원문에서 직접 추출합니다.
*/
@@ -60,6 +60,9 @@ public final class MciResponseScaffolder {
public record GeneratedSources(String responseSource, String converterSource) {
}
public record GeneratedRequestSources(String requestSource, String converterSource) {
}
public static ParsedSource parse(String source) {
if (source == null || source.isBlank()) {
throw new IllegalArgumentException("MCI 응답 Java 소스를 입력해주세요.");
@@ -113,6 +116,28 @@ public final class MciResponseScaffolder {
converterClassName, mappingBySource));
}
/**
* LLM Request DTO에서 Glow MCI 입력 전문으로 변환하는 MapStruct Converter를 생성합니다.
*/
public static GeneratedRequestSources generateRequest(ParsedSource parsed, String requestPackage,
String requestClassName, String converterPackage,
String converterClassName, List<FieldMapping> mappings) {
if (parsed == null || parsed.types() == null || parsed.types().isEmpty()) {
throw new IllegalArgumentException("분석된 MCI 요청 정보가 없습니다.");
}
validatePackage(requestPackage, "Request package");
validatePackage(converterPackage, "Converter package");
validateClassName(requestClassName, "Request class");
validateClassName(converterClassName, "Converter class");
Map<String, FieldMapping> mappingBySource = normalizeMappings(parsed, mappings);
validateTargetNames(parsed, mappingBySource);
return new GeneratedRequestSources(
responseSource(parsed, requestPackage, requestClassName, mappingBySource),
requestConverterSource(parsed, requestPackage, requestClassName, converterPackage,
converterClassName, mappingBySource));
}
private static List<ClassRange> findClassRanges(String source) {
List<ClassRangeDraft> drafts = new ArrayList<>();
Matcher matcher = CLASS_PATTERN.matcher(source);
@@ -356,6 +381,30 @@ public final class MciResponseScaffolder {
return source.append("}\n").toString();
}
private static String requestConverterSource(ParsedSource parsed, String requestPackage, String requestClassName,
String converterPackage, String converterClassName,
Map<String, FieldMapping> mappings) {
StringBuilder source = new StringBuilder("package ").append(converterPackage).append(";\n\n")
.append("import ").append(requestPackage).append('.').append(requestClassName).append(";\n")
.append("import ").append(parsed.packageName()).append('.').append(parsed.rootClassName()).append(";\n")
.append("import org.mapstruct.Mapper;\n")
.append("import org.mapstruct.Mapping;\n")
.append("import org.mapstruct.ReportingPolicy;\n\n")
.append("@Mapper(componentModel = \"spring\", unmappedTargetPolicy = ReportingPolicy.IGNORE)\n")
.append("public interface ").append(converterClassName).append(" {\n\n");
appendRequestMappingMethod(source, parsed.types().getFirst(), parsed.rootClassName(),
requestClassName, "toLegacyRequest", mappings);
Map<String, ParsedType> typeByName = new HashMap<>();
parsed.types().forEach(type -> typeByName.put(type.name(), type));
for (ParsedType type : parsed.types().stream().skip(1).toList()) {
appendRequestMappingMethod(source, type,
sourceTypePath(parsed.rootClassName(), type, typeByName),
requestClassName + "." + type.name(), "toLegacy" + type.name(), mappings);
}
return source.append("}\n").toString();
}
private static void appendMappingMethod(StringBuilder source, ParsedType type, String targetType,
String sourceType, String methodName,
Map<String, FieldMapping> mappings) {
@@ -370,6 +419,20 @@ public final class MciResponseScaffolder {
.append('(').append(sourceType).append(" source);\n\n");
}
private static void appendRequestMappingMethod(StringBuilder source, ParsedType type, String legacyTargetType,
String requestSourceType, String methodName,
Map<String, FieldMapping> mappings) {
for (ParsedField field : type.fields()) {
FieldMapping mapping = mappings.get(key(type.name(), field.name()));
if (mapping.include() && !field.name().equals(mapping.targetName())) {
source.append(" @Mapping(source = \"").append(mapping.targetName())
.append("\", target = \"").append(field.name()).append("\")\n");
}
}
source.append(" ").append(legacyTargetType).append(' ').append(methodName)
.append('(').append(requestSourceType).append(" source);\n\n");
}
private static String sourceTypePath(String rootClassName, ParsedType type,
Map<String, ParsedType> typeByName) {
List<String> names = new ArrayList<>();

View File

@@ -80,6 +80,35 @@ class MciResponseScaffolderTest {
"IndividualCustomerDetailInquiryResponse.CmnnPrafIfinOutDto toCmnnPrafIfinOutDto(ONBSZ0460_O.CmnnPrafIfinOutDto source);"));
}
@Test
void generatesLlmRequestAndMapsLlmFieldsBackToLegacyInput() {
String requestSource = MCI_SOURCE.replace("ONBSZ0460_O", "ONBSZ0460_I");
MciResponseScaffolder.ParsedSource parsed = MciResponseScaffolder.parse(requestSource);
List<MciResponseScaffolder.FieldMapping> mappings = List.of(
new MciResponseScaffolder.FieldMapping("ONBSZ0460_I", "employeeItems", "employees", true),
new MciResponseScaffolder.FieldMapping("CmnnPrafIfinOutDto", "prafNo", "employeeNumber", true),
new MciResponseScaffolder.FieldMapping("CmnnPrafIfinOutDto", "prafTypeCd", "employeeTypeCode", true),
new MciResponseScaffolder.FieldMapping("CmnnPrafIfinOutDto", "prafNm", "employeeName", true),
new MciResponseScaffolder.FieldMapping("CmnnPrafIfinOutDto", "rdreNo", "residentRegistrationNumber", true));
MciResponseScaffolder.GeneratedRequestSources generated = MciResponseScaffolder.generateRequest(
parsed,
"io.shinhanlife.dat.mcc.biz.pro.dto",
"IndividualCustomerDetailInquiryRequest",
"io.shinhanlife.dat.mcc.biz.pro.converter",
"IndividualCustomerDetailInquiryRequestConverter",
mappings);
assertTrue(generated.requestSource().contains("@Schema(description = \"인사번호\")"));
assertTrue(generated.requestSource().contains("private String employeeNumber;"));
assertTrue(generated.converterSource().contains(
"@Mapping(source = \"employeeNumber\", target = \"prafNo\")"));
assertTrue(generated.converterSource().contains(
"ONBSZ0460_I toLegacyRequest(IndividualCustomerDetailInquiryRequest source);"));
assertTrue(generated.converterSource().contains(
"ONBSZ0460_I.CmnnPrafIfinOutDto toLegacyCmnnPrafIfinOutDto("));
}
@Test
void rejectsDuplicateAiTargetNamesWithinTheSameType() {
MciResponseScaffolder.ParsedSource parsed = MciResponseScaffolder.parse(MCI_SOURCE);