feat: MCI 변환 AI 병렬 청킹 및 Converter 생성 개선
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 28s

- ScaffoldingController: AI 분석 시 20개 단위 병렬 청킹(CompletableFuture) 적용
- ScaffoldingController: reserved 자동 제외 로직 제거, AI 명시적 include=false만 제외
- ScaffoldingController: AI 프롬프트 개선 - 모든 필드(List/reserved 포함) 번역 지시
- MciResponseScaffolder: Request Converter 메서드명 toLegacyRequest -> toRequest 변경
- scaffold.html: collectMciResponseMappings validation 방어 처리 (자동 fallback)
This commit is contained in:
jade
2026-09-07 15:41:57 +09:00
parent 91c34a2fb3
commit c9701a5a94
3 changed files with 92 additions and 65 deletions

View File

@@ -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<Map<String, Object>> allFields = extractFields(parsed);
AiMciMappingDraft draft = analyzeFieldsInChunks(allFields, request.model(), false);
List<MciResponseScaffolder.FieldMapping> 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<Map<String, Object>> allFields = extractFields(parsed);
AiMciMappingDraft draft = analyzeFieldsInChunks(allFields, request.model(), true);
List<MciResponseScaffolder.FieldMapping> 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<Map<String, Object>> 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<Map<String, Object>> allFields, String requestedModel, boolean isRequest) throws Exception {
int CHUNK_SIZE = 20;
List<List<Map<String, Object>>> 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<java.util.concurrent.CompletableFuture<AiMciMappingDraft>> 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<AiMciFieldMapping> 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;

View File

@@ -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() {