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 {
|
try {
|
||||||
MciResponseScaffolder.ParsedSource parsed = MciResponseScaffolder.parse(request.source());
|
MciResponseScaffolder.ParsedSource parsed = MciResponseScaffolder.parse(request.source());
|
||||||
String fieldsJson = objectMapper.writeValueAsString(parsed.types().stream()
|
List<Map<String, Object>> allFields = extractFields(parsed);
|
||||||
.flatMap(type -> type.fields().stream().map(field -> Map.of(
|
AiMciMappingDraft draft = analyzeFieldsInChunks(allFields, request.model(), false);
|
||||||
"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<MciResponseScaffolder.FieldMapping> mappings = normalizeAiMappings(parsed, draft);
|
List<MciResponseScaffolder.FieldMapping> mappings = normalizeAiMappings(parsed, draft);
|
||||||
return ResponseEntity.ok(new MciResponseAnalyzeResponse(parsed, mappings));
|
return ResponseEntity.ok(new MciResponseAnalyzeResponse(parsed, mappings));
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
@@ -401,33 +376,8 @@ public class ScaffoldingController {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
MciResponseScaffolder.ParsedSource parsed = MciResponseScaffolder.parse(request.source());
|
MciResponseScaffolder.ParsedSource parsed = MciResponseScaffolder.parse(request.source());
|
||||||
String fieldsJson = objectMapper.writeValueAsString(parsed.types().stream()
|
List<Map<String, Object>> allFields = extractFields(parsed);
|
||||||
.flatMap(type -> type.fields().stream().map(field -> Map.of(
|
AiMciMappingDraft draft = analyzeFieldsInChunks(allFields, request.model(), true);
|
||||||
"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<MciResponseScaffolder.FieldMapping> mappings = normalizeAiMappings(parsed, draft);
|
List<MciResponseScaffolder.FieldMapping> mappings = normalizeAiMappings(parsed, draft);
|
||||||
return ResponseEntity.ok(new MciResponseAnalyzeResponse(parsed, mappings));
|
return ResponseEntity.ok(new MciResponseAnalyzeResponse(parsed, mappings));
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
@@ -642,6 +592,63 @@ public class ScaffoldingController {
|
|||||||
return normalized.isEmpty() ? fallback : normalized;
|
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) {
|
private String generateAiContent(String prompt, String requestedModel) {
|
||||||
String resolvedModel = resolveModel(requestedModel);
|
String resolvedModel = resolveModel(requestedModel);
|
||||||
org.springframework.ai.chat.client.ChatClient activeChatClient;
|
org.springframework.ai.chat.client.ChatClient activeChatClient;
|
||||||
|
|||||||
@@ -2813,15 +2813,35 @@ function validateMciResponseMappingRows() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function collectMciResponseMappings() {
|
function collectMciResponseMappings() {
|
||||||
if (!validateMciResponseMappingRows()) {
|
const usedByOwner = new Map();
|
||||||
throw new Error('빨간색으로 표시된 LLM 필드명을 수정하세요. camelCase 형식이며 같은 타입 안에서 중복될 수 없습니다.');
|
return Array.from(document.querySelectorAll('#mciResponseMappingBody tr')).map(row => {
|
||||||
}
|
const targetInput = row.querySelector('.mci-response-target');
|
||||||
return Array.from(document.querySelectorAll('#mciResponseMappingBody tr')).map(row => ({
|
const includeChk = row.querySelector('.mci-response-include');
|
||||||
ownerType: row.dataset.ownerType,
|
let targetName = targetInput.value.trim();
|
||||||
sourceName: row.dataset.sourceName,
|
let include = includeChk.checked;
|
||||||
targetName: row.querySelector('.mci-response-target').value.trim(),
|
const owner = row.dataset.ownerType;
|
||||||
include: row.querySelector('.mci-response-include').checked
|
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() {
|
async function generateMciResponseSources() {
|
||||||
|
|||||||
@@ -394,13 +394,13 @@ public final class MciResponseScaffolder {
|
|||||||
.append("public interface ").append(converterClassName).append(" {\n\n");
|
.append("public interface ").append(converterClassName).append(" {\n\n");
|
||||||
|
|
||||||
appendRequestMappingMethod(source, parsed.types().getFirst(), parsed.rootClassName(),
|
appendRequestMappingMethod(source, parsed.types().getFirst(), parsed.rootClassName(),
|
||||||
requestClassName, "toLegacyRequest", mappings);
|
requestClassName, "toRequest", mappings);
|
||||||
Map<String, ParsedType> typeByName = new HashMap<>();
|
Map<String, ParsedType> typeByName = new HashMap<>();
|
||||||
parsed.types().forEach(type -> typeByName.put(type.name(), type));
|
parsed.types().forEach(type -> typeByName.put(type.name(), type));
|
||||||
for (ParsedType type : parsed.types().stream().skip(1).toList()) {
|
for (ParsedType type : parsed.types().stream().skip(1).toList()) {
|
||||||
appendRequestMappingMethod(source, type,
|
appendRequestMappingMethod(source, type,
|
||||||
sourceTypePath(parsed.rootClassName(), type, typeByName),
|
sourceTypePath(parsed.rootClassName(), type, typeByName),
|
||||||
requestClassName + "." + type.name(), "toLegacy" + type.name(), mappings);
|
requestClassName + "." + type.name(), "to" + type.name(), mappings);
|
||||||
}
|
}
|
||||||
return source.append("}\n").toString();
|
return source.append("}\n").toString();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user