feat: MCI 변환 AI 병렬 청킹 및 Converter 생성 개선
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 28s
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:
@@ -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;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user