From c9701a5a94215fce2ae3a75e37d8336295a9a79c Mon Sep 17 00:00:00 2001 From: jade Date: Mon, 7 Sep 2026 15:41:57 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20MCI=20=EB=B3=80=ED=99=98=20AI=20?= =?UTF-8?q?=EB=B3=91=EB=A0=AC=20=EC=B2=AD=ED=82=B9=20=EB=B0=8F=20Converter?= =?UTF-8?q?=20=EC=83=9D=EC=84=B1=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ScaffoldingController: AI 분석 시 20개 단위 병렬 청킹(CompletableFuture) 적용 - ScaffoldingController: reserved 자동 제외 로직 제거, AI 명시적 include=false만 제외 - ScaffoldingController: AI 프롬프트 개선 - 모든 필드(List/reserved 포함) 번역 지시 - MciResponseScaffolder: Request Converter 메서드명 toLegacyRequest -> toRequest 변경 - scaffold.html: collectMciResponseMappings validation 방어 처리 (자동 fallback) --- .../presentation/ScaffoldingController.java | 115 ++++++++++-------- .../main/resources/static/admin/scaffold.html | 38 ++++-- .../dat/lib/util/MciResponseScaffolder.java | 4 +- 3 files changed, 92 insertions(+), 65 deletions(-) diff --git a/dat-gateway/src/main/java/io/shinhanlife/dat/mcg/presentation/ScaffoldingController.java b/dat-gateway/src/main/java/io/shinhanlife/dat/mcg/presentation/ScaffoldingController.java index ac458a17..6004028e 100644 --- a/dat-gateway/src/main/java/io/shinhanlife/dat/mcg/presentation/ScaffoldingController.java +++ b/dat-gateway/src/main/java/io/shinhanlife/dat/mcg/presentation/ScaffoldingController.java @@ -334,33 +334,8 @@ public class ScaffoldingController { } try { MciResponseScaffolder.ParsedSource parsed = MciResponseScaffolder.parse(request.source()); - String fieldsJson = objectMapper.writeValueAsString(parsed.types().stream() - .flatMap(type -> type.fields().stream().map(field -> Map.of( - "ownerType", type.name(), - "sourceName", field.name(), - "description", field.description(), - "javaType", field.type(), - "sensitive", field.sensitive()))) - .toList()); - String prompt = """ - You rename legacy Korean financial-system response fields for an MCP Tool response DTO. - Return JSON only with this exact shape: - {"mappings":[{"ownerType":"SourceOwnerClass","sourceName":"legacyField","targetName":"businessMeaningInEnglish","include":true}]} - - Rules: - - Return exactly one mapping for every input field, preserving ownerType and sourceName verbatim. - - targetName must be a concise, descriptive English Java camelCase identifier. - - Derive the business meaning primarily from description; use sourceName only as supporting metadata. - - Expand abbreviations: No -> Number, Cd -> Code, Nm -> Name, Ymd/Dt -> Date when the description supports it. - - Do not invent fields, examples, descriptions, values, or business rules. - - Keep include=true. Sensitive fields must still be named accurately; the UI will show a warning for human review. - - targetName values must be unique within each ownerType. - - Source fields: - %s - """.formatted(fieldsJson); - String aiResponse = generateAiContent(prompt, request.model()); - AiMciMappingDraft draft = objectMapper.readValue(stripCodeFence(aiResponse), AiMciMappingDraft.class); + List> allFields = extractFields(parsed); + AiMciMappingDraft draft = analyzeFieldsInChunks(allFields, request.model(), false); List mappings = normalizeAiMappings(parsed, draft); return ResponseEntity.ok(new MciResponseAnalyzeResponse(parsed, mappings)); } catch (IllegalArgumentException e) { @@ -401,33 +376,8 @@ public class ScaffoldingController { } try { MciResponseScaffolder.ParsedSource parsed = MciResponseScaffolder.parse(request.source()); - String fieldsJson = objectMapper.writeValueAsString(parsed.types().stream() - .flatMap(type -> type.fields().stream().map(field -> Map.of( - "ownerType", type.name(), - "sourceName", field.name(), - "description", field.description(), - "javaType", field.type(), - "sensitive", field.sensitive()))) - .toList()); - String prompt = """ - You rename legacy Korean financial-system request fields for an MCP Tool request DTO. - Return JSON only with this exact shape: - {"mappings":[{"ownerType":"SourceOwnerClass","sourceName":"legacyField","targetName":"businessMeaningInEnglish","include":true}]} - - Rules: - - Return exactly one mapping for every input field, preserving ownerType and sourceName verbatim. - - targetName must be a concise, descriptive English Java camelCase identifier. - - Derive the business meaning primarily from description; use sourceName only as supporting metadata. - - Expand abbreviations: No -> Number, Cd -> Code, Nm -> Name, Ymd/Dt -> Date when the description supports it. - - Do not invent fields, examples, descriptions, values, or business rules. - - Keep include=true. Sensitive fields must still be named accurately; the UI will show a warning for human review. - - targetName values must be unique within each ownerType. - - Source fields: - %s - """.formatted(fieldsJson); - String aiResponse = generateAiContent(prompt, request.model()); - AiMciMappingDraft draft = objectMapper.readValue(stripCodeFence(aiResponse), AiMciMappingDraft.class); + List> allFields = extractFields(parsed); + AiMciMappingDraft draft = analyzeFieldsInChunks(allFields, request.model(), true); List mappings = normalizeAiMappings(parsed, draft); return ResponseEntity.ok(new MciResponseAnalyzeResponse(parsed, mappings)); } catch (IllegalArgumentException e) { @@ -642,6 +592,63 @@ public class ScaffoldingController { return normalized.isEmpty() ? fallback : normalized; } + + private List> extractFields(MciResponseScaffolder.ParsedSource parsed) { + return parsed.types().stream() + .flatMap(type -> type.fields().stream().map(field -> Map.of( + "ownerType", (Object) type.name(), + "sourceName", field.name(), + "description", field.description(), + "javaType", field.type(), + "sensitive", field.sensitive()))) + .toList(); + } + + private AiMciMappingDraft analyzeFieldsInChunks(List> allFields, String requestedModel, boolean isRequest) throws Exception { + int CHUNK_SIZE = 20; + List>> chunks = new java.util.ArrayList<>(); + for (int i = 0; i < allFields.size(); i += CHUNK_SIZE) { + chunks.add(allFields.subList(i, Math.min(i + CHUNK_SIZE, allFields.size()))); + } + + String typeStr = isRequest ? "request" : "response"; + String promptTemplate = "You rename legacy Korean financial-system " + typeStr + " fields for an MCP Tool " + typeStr + " DTO.\n" + + "Return JSON only with this exact shape:\n" + + "{\"mappings\":[{\"ownerType\":\"SourceOwnerClass\",\"sourceName\":\"legacyField\",\"targetName\":\"businessMeaningInEnglish\",\"include\":true}]}\n\n" + + "Rules:\n" + + "- Return a mapping for EVERY field in the input list, including List-type, reserved, and filler fields.\n" + + "- targetName must be a concise, descriptive English Java camelCase identifier (e.g. 'employeeNumber', 'reservedField01').\n" + + "- Derive the business meaning primarily from description; use sourceName only as supporting metadata.\n" + + "- For reserved/dummy/filler fields (e.g. reserved01, filler, spare), use a clean camelCase form like 'reserved01', 'filler01' as targetName and set include=true.\n" + + "- Set include=false ONLY for fields that are explicitly obsolete or harmful to expose.\n\n" + + "Source fields:\n" + + "%s\n"; + + List> futures = chunks.stream() + .map(chunk -> java.util.concurrent.CompletableFuture.supplyAsync(() -> { + try { + String fieldsJson = objectMapper.writeValueAsString(chunk); + String prompt = String.format(promptTemplate, fieldsJson); + String aiResponse = generateAiContent(prompt, requestedModel); + return objectMapper.readValue(stripCodeFence(aiResponse), AiMciMappingDraft.class); + } catch (Exception e) { + throw new RuntimeException(e); + } + })) + .toList(); + + java.util.concurrent.CompletableFuture.allOf(futures.toArray(new java.util.concurrent.CompletableFuture[0])).join(); + + List mergedMappings = new java.util.ArrayList<>(); + for (var future : futures) { + AiMciMappingDraft draft = future.get(); + if (draft != null && draft.mappings() != null) { + mergedMappings.addAll(draft.mappings()); + } + } + return new AiMciMappingDraft(mergedMappings); + } + private String generateAiContent(String prompt, String requestedModel) { String resolvedModel = resolveModel(requestedModel); org.springframework.ai.chat.client.ChatClient activeChatClient; diff --git a/dat-gateway/src/main/resources/static/admin/scaffold.html b/dat-gateway/src/main/resources/static/admin/scaffold.html index 662463f3..f68646a6 100644 --- a/dat-gateway/src/main/resources/static/admin/scaffold.html +++ b/dat-gateway/src/main/resources/static/admin/scaffold.html @@ -2813,15 +2813,35 @@ function validateMciResponseMappingRows() { } function collectMciResponseMappings() { - if (!validateMciResponseMappingRows()) { - throw new Error('빨간색으로 표시된 LLM 필드명을 수정하세요. camelCase 형식이며 같은 타입 안에서 중복될 수 없습니다.'); - } - return Array.from(document.querySelectorAll('#mciResponseMappingBody tr')).map(row => ({ - ownerType: row.dataset.ownerType, - sourceName: row.dataset.sourceName, - targetName: row.querySelector('.mci-response-target').value.trim(), - include: row.querySelector('.mci-response-include').checked - })); + const usedByOwner = new Map(); + return Array.from(document.querySelectorAll('#mciResponseMappingBody tr')).map(row => { + const targetInput = row.querySelector('.mci-response-target'); + const includeChk = row.querySelector('.mci-response-include'); + let targetName = targetInput.value.trim(); + let include = includeChk.checked; + const owner = row.dataset.ownerType; + const used = usedByOwner.get(owner) || new Set(); + // targetName이 camelCase 패턴에 맞지 않거나 중복이면 자동으로 sourceName으로 fallback + if (include && (!/^[a-z][A-Za-z0-9]*$/.test(targetName) || used.has(targetName))) { + const fallback = row.dataset.sourceName; + if (/^[a-z][A-Za-z0-9]*$/.test(fallback) && !used.has(fallback)) { + targetName = fallback; + targetInput.value = fallback; + targetInput.classList.remove('is-invalid'); + } else { + include = false; + includeChk.checked = false; + } + } + if (include) used.add(targetName); + usedByOwner.set(owner, used); + return { + ownerType: row.dataset.ownerType, + sourceName: row.dataset.sourceName, + targetName, + include + }; + }); } async function generateMciResponseSources() { diff --git a/dat-was-lib/src/main/java/io/shinhanlife/dat/lib/util/MciResponseScaffolder.java b/dat-was-lib/src/main/java/io/shinhanlife/dat/lib/util/MciResponseScaffolder.java index 2d89dd98..e45f52b5 100644 --- a/dat-was-lib/src/main/java/io/shinhanlife/dat/lib/util/MciResponseScaffolder.java +++ b/dat-was-lib/src/main/java/io/shinhanlife/dat/lib/util/MciResponseScaffolder.java @@ -394,13 +394,13 @@ public final class MciResponseScaffolder { .append("public interface ").append(converterClassName).append(" {\n\n"); appendRequestMappingMethod(source, parsed.types().getFirst(), parsed.rootClassName(), - requestClassName, "toLegacyRequest", mappings); + requestClassName, "toRequest", mappings); Map 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); + requestClassName + "." + type.name(), "to" + type.name(), mappings); } return source.append("}\n").toString(); }