feat: add MCI request DTO scaffold conversion
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 31s
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 31s
This commit is contained in:
@@ -385,6 +385,73 @@ public class ScaffoldingController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/mci-request/analyze")
|
||||||
|
public ResponseEntity<?> analyzeMciRequest(@RequestBody MciResponseAnalyzeRequest request) {
|
||||||
|
if (request == null || request.source() == null || request.source().isBlank()) {
|
||||||
|
return ResponseEntity.badRequest().body(Map.of("error", "XXXX_I.java 소스를 입력해주세요."));
|
||||||
|
}
|
||||||
|
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<MciResponseScaffolder.FieldMapping> mappings = normalizeAiMappings(parsed, draft);
|
||||||
|
return ResponseEntity.ok(new MciResponseAnalyzeResponse(parsed, mappings));
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
return ResponseEntity.badRequest().body(Map.of("error", safeMessage(e)));
|
||||||
|
} catch (Exception e) {
|
||||||
|
return ResponseEntity.internalServerError()
|
||||||
|
.body(Map.of("error", "MCI Request AI 분석 실패: " + safeMessage(e)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/mci-request/generate")
|
||||||
|
public ResponseEntity<?> generateMciRequest(@RequestBody MciRequestGenerateRequest request) {
|
||||||
|
if (request == null || request.source() == null || request.source().isBlank()) {
|
||||||
|
return ResponseEntity.badRequest().body(Map.of("error", "XXXX_I.java 소스를 입력해주세요."));
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
MciResponseScaffolder.ParsedSource parsed = MciResponseScaffolder.parse(request.source());
|
||||||
|
MciResponseScaffolder.GeneratedRequestSources generated = MciResponseScaffolder.generateRequest(
|
||||||
|
parsed,
|
||||||
|
request.requestPackage(),
|
||||||
|
request.requestClassName(),
|
||||||
|
request.converterPackage(),
|
||||||
|
request.converterClassName(),
|
||||||
|
request.mappings());
|
||||||
|
return ResponseEntity.ok(generated);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
return ResponseEntity.badRequest().body(Map.of("error", safeMessage(e)));
|
||||||
|
} catch (Exception e) {
|
||||||
|
return ResponseEntity.internalServerError()
|
||||||
|
.body(Map.of("error", "MCI Request 소스 생성 실패: " + safeMessage(e)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping("/tool/update")
|
@PostMapping("/tool/update")
|
||||||
public String updateTool(@RequestBody Map<String, String> req) {
|
public String updateTool(@RequestBody Map<String, String> req) {
|
||||||
try {
|
try {
|
||||||
@@ -667,6 +734,14 @@ public class ScaffoldingController {
|
|||||||
List<MciResponseScaffolder.FieldMapping> mappings) {
|
List<MciResponseScaffolder.FieldMapping> mappings) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public record MciRequestGenerateRequest(String source,
|
||||||
|
String requestPackage,
|
||||||
|
String requestClassName,
|
||||||
|
String converterPackage,
|
||||||
|
String converterClassName,
|
||||||
|
List<MciResponseScaffolder.FieldMapping> mappings) {
|
||||||
|
}
|
||||||
|
|
||||||
private record AiMciMappingDraft(List<AiMciFieldMapping> mappings) {
|
private record AiMciMappingDraft(List<AiMciFieldMapping> mappings) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1139,16 +1139,23 @@
|
|||||||
<div class="mb-4 p-3 rounded" style="background:#18181b; border:1px solid #3f3f46;">
|
<div class="mb-4 p-3 rounded" style="background:#18181b; border:1px solid #3f3f46;">
|
||||||
<div class="d-flex justify-content-between align-items-start gap-3 flex-wrap">
|
<div class="d-flex justify-content-between align-items-start gap-3 flex-wrap">
|
||||||
<div>
|
<div>
|
||||||
<h2 style="font-size:1.05rem; color:#f4f4f5; margin:0 0 0.45rem;">Glow MCI 응답을 LLM Response로 변환</h2>
|
<h2 id="mciTransformHeading" style="font-size:1.05rem; color:#f4f4f5; margin:0 0 0.45rem;">Glow MCI 응답을 LLM Response로 변환</h2>
|
||||||
<div class="input-hint mt-0">XXXX_O.java의 전문 구조와 <code>@GlowTrgmField.description</code>은 원문 그대로 읽고, AI는 축약 필드명을 업무 의미가 드러나는 영문 camelCase로만 제안합니다.</div>
|
<div id="mciTransformHint" class="input-hint mt-0">XXXX_O.java의 전문 구조와 <code>@GlowTrgmField.description</code>은 원문 그대로 읽고, AI는 축약 필드명을 업무 의미가 드러나는 영문 camelCase로만 제안합니다.</div>
|
||||||
</div>
|
</div>
|
||||||
<span class="badge-mono">구조: Parser · 이름: AI · 출력: MapStruct</span>
|
<span class="badge-mono">구조: Parser · 이름: AI · 출력: MapStruct</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row g-3 mb-4">
|
<div class="row g-3 mb-4">
|
||||||
<div class="col-md-8">
|
<div class="col-md-3">
|
||||||
<label class="form-label" for="mciResponseFile">XXXX_O.java 파일</label>
|
<label class="form-label" for="mciTransformMode">변환 대상</label>
|
||||||
|
<select id="mciTransformMode" class="form-select">
|
||||||
|
<option value="RESPONSE" selected>Response (_O.java)</option>
|
||||||
|
<option value="REQUEST">Request (_I.java)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-5">
|
||||||
|
<label id="mciSourceFileLabel" class="form-label" for="mciResponseFile">XXXX_O.java 파일</label>
|
||||||
<input class="form-control" type="file" id="mciResponseFile" accept=".java,text/x-java-source">
|
<input class="form-control" type="file" id="mciResponseFile" accept=".java,text/x-java-source">
|
||||||
<div class="input-hint">파일을 선택하면 아래 소스 입력란에 UTF-8 텍스트로 불러옵니다. 소스를 직접 붙여넣어도 됩니다.</div>
|
<div class="input-hint">파일을 선택하면 아래 소스 입력란에 UTF-8 텍스트로 불러옵니다. 소스를 직접 붙여넣어도 됩니다.</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1171,7 +1178,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label class="form-label" for="mciResponseSource">MCI 응답 소스</label>
|
<label id="mciSourceInputLabel" class="form-label" for="mciResponseSource">MCI 응답 소스</label>
|
||||||
<textarea id="mciResponseSource" class="form-control" rows="13" spellcheck="false" placeholder="package ...; public class ONBSZ0460_O { @GlowTrgmField(order = 1, length = 8, description = "인사번호") private String prafNo; }"></textarea>
|
<textarea id="mciResponseSource" class="form-control" rows="13" spellcheck="false" placeholder="package ...; public class ONBSZ0460_O { @GlowTrgmField(order = 1, length = 8, description = "인사번호") private String prafNo; }"></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1182,11 +1189,11 @@
|
|||||||
<div id="mciResponseResultArea" style="display:none;">
|
<div id="mciResponseResultArea" style="display:none;">
|
||||||
<div class="row g-3 mb-4">
|
<div class="row g-3 mb-4">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label" for="mciResponsePackage">Response Package</label>
|
<label id="mciDtoPackageLabel" class="form-label" for="mciResponsePackage">Response Package</label>
|
||||||
<input id="mciResponsePackage" type="text" class="form-control" value="io.shinhanlife.dat.mcc.biz.pro.dto">
|
<input id="mciResponsePackage" type="text" class="form-control" value="io.shinhanlife.dat.mcc.biz.pro.dto">
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<label class="form-label" for="mciResponseClassName">Response Class</label>
|
<label id="mciDtoClassLabel" class="form-label" for="mciResponseClassName">Response Class</label>
|
||||||
<input id="mciResponseClassName" type="text" class="form-control" placeholder="IndividualCustomerDetailInquiryResponse">
|
<input id="mciResponseClassName" type="text" class="form-control" placeholder="IndividualCustomerDetailInquiryResponse">
|
||||||
<div class="input-hint">업무 의미가 드러나는 PascalCase 이름으로 최종 확인하세요.</div>
|
<div class="input-hint">업무 의미가 드러나는 PascalCase 이름으로 최종 확인하세요.</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1234,7 +1241,7 @@
|
|||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
<div class="col-lg-6">
|
<div class="col-lg-6">
|
||||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||||
<label class="form-label mb-0">Response.java</label>
|
<label id="mciDtoPreviewLabel" class="form-label mb-0">Response.java</label>
|
||||||
<div class="d-flex gap-2">
|
<div class="d-flex gap-2">
|
||||||
<button type="button" class="btn-secondary-action" onclick="copyMciResponseSource('mciResponsePreview')">복사</button>
|
<button type="button" class="btn-secondary-action" onclick="copyMciResponseSource('mciResponsePreview')">복사</button>
|
||||||
<button type="button" class="btn-secondary-action" onclick="downloadMciResponseSource('response')">다운로드</button>
|
<button type="button" class="btn-secondary-action" onclick="downloadMciResponseSource('response')">다운로드</button>
|
||||||
@@ -2616,9 +2623,18 @@ const mciResponseState = {
|
|||||||
parsed: null,
|
parsed: null,
|
||||||
responseSource: '',
|
responseSource: '',
|
||||||
converterSource: '',
|
converterSource: '',
|
||||||
autoConverterName: ''
|
autoConverterName: '',
|
||||||
|
mode: 'RESPONSE'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
document.getElementById('mciTransformMode').addEventListener('change', function() {
|
||||||
|
mciResponseState.mode = this.value;
|
||||||
|
document.getElementById('mciResponseResultArea').style.display = 'none';
|
||||||
|
document.getElementById('mciResponsePreviewArea').style.display = 'none';
|
||||||
|
mciResponseState.parsed = null;
|
||||||
|
updateMciTransformMode();
|
||||||
|
});
|
||||||
|
|
||||||
document.getElementById('mciResponseFile').addEventListener('change', async function() {
|
document.getElementById('mciResponseFile').addEventListener('change', async function() {
|
||||||
const file = this.files && this.files[0];
|
const file = this.files && this.files[0];
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
@@ -2645,15 +2661,16 @@ document.getElementById('mciResponsePackage').addEventListener('input', function
|
|||||||
|
|
||||||
async function analyzeMciResponse() {
|
async function analyzeMciResponse() {
|
||||||
const source = document.getElementById('mciResponseSource').value.trim();
|
const source = document.getElementById('mciResponseSource').value.trim();
|
||||||
|
const labels = mciTransformLabels();
|
||||||
if (!source) {
|
if (!source) {
|
||||||
setMciResponseStatus('XXXX_O.java 파일을 선택하거나 소스를 붙여넣어 주세요.', true);
|
setMciResponseStatus(`XXXX_${labels.suffix}.java 파일을 선택하거나 소스를 붙여넣어 주세요.`, true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const button = document.getElementById('mciResponseAnalyzeButton');
|
const button = document.getElementById('mciResponseAnalyzeButton');
|
||||||
setMciResponseButtonBusy(button, true, 'AI 분석 중...');
|
setMciResponseButtonBusy(button, true, 'AI 분석 중...');
|
||||||
setMciResponseStatus('Glow 전문 구조를 분석하고 AI가 LLM 필드명을 제안하고 있습니다.', false);
|
setMciResponseStatus(`Glow ${labels.korean} 전문 구조를 분석하고 AI가 LLM 필드명을 제안하고 있습니다.`, false);
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/v1/scaffold/mci-response/analyze', {
|
const response = await fetch(`/api/v1/scaffold/mci-${labels.endpoint}/analyze`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {'Content-Type': 'application/json'},
|
headers: {'Content-Type': 'application/json'},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
@@ -2670,7 +2687,7 @@ async function analyzeMciResponse() {
|
|||||||
renderMciResponseMappings(result.parsed, result.mappings || []);
|
renderMciResponseMappings(result.parsed, result.mappings || []);
|
||||||
document.getElementById('mciResponseResultArea').style.display = 'block';
|
document.getElementById('mciResponseResultArea').style.display = 'block';
|
||||||
document.getElementById('mciResponsePreviewArea').style.display = 'none';
|
document.getElementById('mciResponsePreviewArea').style.display = 'none';
|
||||||
setMciResponseStatus('AI 제안을 불러왔습니다. 필드명과 개인정보 표시를 검토한 뒤 미리보기를 생성하세요.', false);
|
setMciResponseStatus(`AI 제안을 불러왔습니다. 필드명과 개인정보 표시를 검토한 뒤 ${labels.dto} 미리보기를 생성하세요.`, false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setMciResponseStatus(error.message, true);
|
setMciResponseStatus(error.message, true);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -2680,10 +2697,11 @@ async function analyzeMciResponse() {
|
|||||||
|
|
||||||
function initializeMciResponseNames(parsed) {
|
function initializeMciResponseNames(parsed) {
|
||||||
const rootName = parsed?.rootClassName || 'MciOutput';
|
const rootName = parsed?.rootClassName || 'MciOutput';
|
||||||
const baseName = rootName.replace(/_O$/, '') || 'MciOutput';
|
const labels = mciTransformLabels();
|
||||||
const responseName = `${baseName}Response`;
|
const baseName = rootName.replace(/_[OI]$/, '') || 'MciOutput';
|
||||||
const converterName = `${baseName}Converter`;
|
const dtoName = `${baseName}${labels.dto}`;
|
||||||
document.getElementById('mciResponseClassName').value = responseName;
|
const converterName = `${baseName}${labels.dto}Converter`;
|
||||||
|
document.getElementById('mciResponseClassName').value = dtoName;
|
||||||
document.getElementById('mciConverterClassName').value = converterName;
|
document.getElementById('mciConverterClassName').value = converterName;
|
||||||
mciResponseState.autoConverterName = converterName;
|
mciResponseState.autoConverterName = converterName;
|
||||||
|
|
||||||
@@ -2763,7 +2781,7 @@ function createMciResponseMappingRow(type, field, mapping) {
|
|||||||
badge.style.borderColor = '#7f1d1d';
|
badge.style.borderColor = '#7f1d1d';
|
||||||
badge.style.background = 'rgba(127,29,29,0.22)';
|
badge.style.background = 'rgba(127,29,29,0.22)';
|
||||||
badge.textContent = 'PII';
|
badge.textContent = 'PII';
|
||||||
badge.title = '개인정보 가능성이 있는 필드입니다. Tool 응답 노출 여부를 반드시 확인하세요.';
|
badge.title = '개인정보 가능성이 있는 필드입니다. Tool 입출력 노출 여부를 반드시 확인하세요.';
|
||||||
warningCell.appendChild(badge);
|
warningCell.appendChild(badge);
|
||||||
} else {
|
} else {
|
||||||
warningCell.textContent = '-';
|
warningCell.textContent = '-';
|
||||||
@@ -2811,37 +2829,61 @@ async function generateMciResponseSources() {
|
|||||||
setMciResponseStatus('먼저 AI 필드명 분석을 실행하세요.', true);
|
setMciResponseStatus('먼저 AI 필드명 분석을 실행하세요.', true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const labels = mciTransformLabels();
|
||||||
const button = document.getElementById('mciResponseGenerateButton');
|
const button = document.getElementById('mciResponseGenerateButton');
|
||||||
setMciResponseButtonBusy(button, true, '소스 생성 중...');
|
setMciResponseButtonBusy(button, true, `${labels.dto} 소스 생성 중...`);
|
||||||
try {
|
try {
|
||||||
const payload = {
|
const payload = {
|
||||||
source: document.getElementById('mciResponseSource').value,
|
source: document.getElementById('mciResponseSource').value,
|
||||||
responsePackage: document.getElementById('mciResponsePackage').value.trim(),
|
|
||||||
responseClassName: document.getElementById('mciResponseClassName').value.trim(),
|
|
||||||
converterPackage: document.getElementById('mciConverterPackage').value.trim(),
|
converterPackage: document.getElementById('mciConverterPackage').value.trim(),
|
||||||
converterClassName: document.getElementById('mciConverterClassName').value.trim(),
|
converterClassName: document.getElementById('mciConverterClassName').value.trim(),
|
||||||
mappings: collectMciResponseMappings()
|
mappings: collectMciResponseMappings()
|
||||||
};
|
};
|
||||||
const response = await fetch('/api/v1/scaffold/mci-response/generate', {
|
payload[labels.dto.toLowerCase() + 'Package'] = document.getElementById('mciResponsePackage').value.trim();
|
||||||
|
payload[labels.dto.toLowerCase() + 'ClassName'] = document.getElementById('mciResponseClassName').value.trim();
|
||||||
|
const response = await fetch(`/api/v1/scaffold/mci-${labels.endpoint}/generate`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {'Content-Type': 'application/json'},
|
headers: {'Content-Type': 'application/json'},
|
||||||
body: JSON.stringify(payload)
|
body: JSON.stringify(payload)
|
||||||
});
|
});
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
if (!response.ok) throw new Error(result.error || `소스 생성 실패 (HTTP ${response.status})`);
|
if (!response.ok) throw new Error(result.error || `소스 생성 실패 (HTTP ${response.status})`);
|
||||||
mciResponseState.responseSource = result.responseSource || '';
|
mciResponseState.responseSource = result[labels.dto.toLowerCase() + 'Source'] || '';
|
||||||
mciResponseState.converterSource = result.converterSource || '';
|
mciResponseState.converterSource = result.converterSource || '';
|
||||||
document.getElementById('mciResponsePreview').value = mciResponseState.responseSource;
|
document.getElementById('mciResponsePreview').value = mciResponseState.responseSource;
|
||||||
document.getElementById('mciConverterPreview').value = mciResponseState.converterSource;
|
document.getElementById('mciConverterPreview').value = mciResponseState.converterSource;
|
||||||
document.getElementById('mciResponsePreviewArea').style.display = 'block';
|
document.getElementById('mciResponsePreviewArea').style.display = 'block';
|
||||||
setMciResponseStatus('Response와 MapStruct Converter를 생성했습니다. 미리보기 검토 후 다운로드하세요.', false);
|
setMciResponseStatus(`${labels.dto}와 MapStruct Converter를 생성했습니다. 미리보기 검토 후 다운로드하세요.`, false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setMciResponseStatus(error.message, true);
|
setMciResponseStatus(error.message, true);
|
||||||
} finally {
|
} finally {
|
||||||
setMciResponseButtonBusy(button, false, 'Response / Converter 미리보기 생성');
|
setMciResponseButtonBusy(button, false, `${labels.dto} / Converter 미리보기 생성`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateMciTransformMode() {
|
||||||
|
const labels = mciTransformLabels();
|
||||||
|
document.getElementById('mciTransformHeading').textContent = `Glow MCI ${labels.korean}을 LLM ${labels.dto}로 변환`;
|
||||||
|
document.getElementById('mciTransformHint').innerHTML = `XXXX_${labels.suffix}.java의 전문 구조와 <code>@GlowTrgmField.description</code>은 원문 그대로 읽고, AI는 축약 필드명을 업무 의미가 드러나는 영문 camelCase로만 제안합니다.`;
|
||||||
|
document.getElementById('mciSourceFileLabel').textContent = `XXXX_${labels.suffix}.java 파일`;
|
||||||
|
document.getElementById('mciSourceInputLabel').textContent = `MCI ${labels.korean} 소스`;
|
||||||
|
document.getElementById('mciDtoPackageLabel').textContent = `${labels.dto} Package`;
|
||||||
|
document.getElementById('mciDtoClassLabel').textContent = `${labels.dto} Class`;
|
||||||
|
document.getElementById('mciDtoPreviewLabel').textContent = `${labels.dto}.java`;
|
||||||
|
document.getElementById('mciResponseClassName').placeholder = `IndividualCustomerDetailInquiry${labels.dto}`;
|
||||||
|
document.getElementById('mciConverterClassName').placeholder = `IndividualCustomerDetailInquiry${labels.dto}Converter`;
|
||||||
|
document.getElementById('mciResponseAnalyzeButton').textContent = 'AI로 필드명 분석';
|
||||||
|
document.getElementById('mciResponseGenerateButton').textContent = `${labels.dto} / Converter 미리보기 생성`;
|
||||||
|
document.getElementById('mciResponseSource').placeholder = `package ...;\n\npublic class ONBSZ0460_${labels.suffix} {\n @GlowTrgmField(order = 1, length = 8, description = \"인사번호\")\n private String prafNo;\n}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mciTransformLabels() {
|
||||||
|
const isRequest = document.getElementById('mciTransformMode').value === 'REQUEST';
|
||||||
|
return isRequest
|
||||||
|
? {dto: 'Request', korean: '요청', suffix: 'I', endpoint: 'request'}
|
||||||
|
: {dto: 'Response', korean: '응답', suffix: 'O', endpoint: 'response'};
|
||||||
|
}
|
||||||
|
|
||||||
async function copyMciResponseSource(elementId) {
|
async function copyMciResponseSource(elementId) {
|
||||||
const textarea = document.getElementById(elementId);
|
const textarea = document.getElementById(elementId);
|
||||||
if (!textarea.value) return;
|
if (!textarea.value) return;
|
||||||
|
|||||||
@@ -194,6 +194,76 @@ class ScaffoldingControllerToolDraftTest {
|
|||||||
"@Mapping(source = \"prafNo\", target = \"employeeNumber\")")));
|
"@Mapping(source = \"prafNo\", target = \"employeeNumber\")")));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void mciRequestGenerateReturnsRequestAndReverseConverterPreviews() throws Exception {
|
||||||
|
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(
|
||||||
|
new ScaffoldingController(mock(ChatClient.Builder.class), new ObjectMapper()))
|
||||||
|
.setMessageConverters(new MappingJackson2HttpMessageConverter())
|
||||||
|
.build();
|
||||||
|
String source = """
|
||||||
|
package io.shinhanlife.dat.mcc.infra.itrf.mci.onbsz.io;
|
||||||
|
public class ONBSZ0460_I {
|
||||||
|
@GlowTrgmField(order = 1, length = 8, description = "인사번호")
|
||||||
|
private String prafNo;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
String request = new ObjectMapper().writeValueAsString(java.util.Map.of(
|
||||||
|
"source", source,
|
||||||
|
"requestPackage", "io.shinhanlife.dat.mcc.biz.pro.dto",
|
||||||
|
"requestClassName", "IndividualCustomerDetailInquiryRequest",
|
||||||
|
"converterPackage", "io.shinhanlife.dat.mcc.biz.pro.converter",
|
||||||
|
"converterClassName", "IndividualCustomerDetailInquiryRequestConverter",
|
||||||
|
"mappings", java.util.List.of(java.util.Map.of(
|
||||||
|
"ownerType", "ONBSZ0460_I",
|
||||||
|
"sourceName", "prafNo",
|
||||||
|
"targetName", "employeeNumber",
|
||||||
|
"include", true))));
|
||||||
|
|
||||||
|
mockMvc.perform(post("/api/v1/scaffold/mci-request/generate")
|
||||||
|
.contentType(MediaType.APPLICATION_JSON).content(request))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.requestSource", org.hamcrest.Matchers.containsString(
|
||||||
|
"private String employeeNumber;")))
|
||||||
|
.andExpect(jsonPath("$.converterSource", org.hamcrest.Matchers.containsString(
|
||||||
|
"@Mapping(source = \"employeeNumber\", target = \"prafNo\")")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void mciRequestAnalyzeReturnsAiSuggestedRequestFieldNames() throws Exception {
|
||||||
|
ChatClient.Builder builder = mock(ChatClient.Builder.class);
|
||||||
|
ChatClient chatClient = mock(ChatClient.class);
|
||||||
|
ChatClient.ChatClientRequestSpec requestSpec = mock(ChatClient.ChatClientRequestSpec.class);
|
||||||
|
ChatClient.CallResponseSpec responseSpec = mock(ChatClient.CallResponseSpec.class);
|
||||||
|
when(builder.build()).thenReturn(chatClient);
|
||||||
|
when(chatClient.prompt()).thenReturn(requestSpec);
|
||||||
|
when(requestSpec.user(anyString())).thenReturn(requestSpec);
|
||||||
|
when(requestSpec.options(any(ChatOptions.class))).thenReturn(requestSpec);
|
||||||
|
when(requestSpec.call()).thenReturn(responseSpec);
|
||||||
|
when(responseSpec.content()).thenReturn("""
|
||||||
|
{"mappings":[{"ownerType":"ONBSZ0460_I","sourceName":"prafNo","targetName":"employeeNumber","include":true}]}
|
||||||
|
""");
|
||||||
|
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(
|
||||||
|
new ScaffoldingController(builder, new ObjectMapper()))
|
||||||
|
.setMessageConverters(new MappingJackson2HttpMessageConverter())
|
||||||
|
.build();
|
||||||
|
String source = """
|
||||||
|
package io.shinhanlife.dat.mcc.infra.itrf.mci.onbsz.io;
|
||||||
|
public class ONBSZ0460_I {
|
||||||
|
@GlowTrgmField(order = 1, length = 8, description = "인사번호")
|
||||||
|
private String prafNo;
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
mockMvc.perform(post("/api/v1/scaffold/mci-request/analyze")
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.content(new ObjectMapper().writeValueAsString(java.util.Map.of(
|
||||||
|
"source", source,
|
||||||
|
"model", "cohere/north-mini-code:free"))))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$.parsed.rootClassName").value("ONBSZ0460_I"))
|
||||||
|
.andExpect(jsonPath("$.mappings[0].targetName").value("employeeNumber"));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void podDraftUsesTheCurrentTargetModuleOptionsForConfusableServers() {
|
void podDraftUsesTheCurrentTargetModuleOptionsForConfusableServers() {
|
||||||
ChatClient.Builder builder = mock(ChatClient.Builder.class);
|
ChatClient.Builder builder = mock(ChatClient.Builder.class);
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import java.util.regex.Matcher;
|
|||||||
import java.util.regex.Pattern;
|
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는 필드명 제안에만 사용하고 전문 구조와
|
* MapStruct Converter 소스를 생성합니다. AI는 필드명 제안에만 사용하고 전문 구조와
|
||||||
* {@code GlowTrgmField.description}은 이 클래스가 원문에서 직접 추출합니다.
|
* {@code GlowTrgmField.description}은 이 클래스가 원문에서 직접 추출합니다.
|
||||||
*/
|
*/
|
||||||
@@ -60,6 +60,9 @@ public final class MciResponseScaffolder {
|
|||||||
public record GeneratedSources(String responseSource, String converterSource) {
|
public record GeneratedSources(String responseSource, String converterSource) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public record GeneratedRequestSources(String requestSource, String converterSource) {
|
||||||
|
}
|
||||||
|
|
||||||
public static ParsedSource parse(String source) {
|
public static ParsedSource parse(String source) {
|
||||||
if (source == null || source.isBlank()) {
|
if (source == null || source.isBlank()) {
|
||||||
throw new IllegalArgumentException("MCI 응답 Java 소스를 입력해주세요.");
|
throw new IllegalArgumentException("MCI 응답 Java 소스를 입력해주세요.");
|
||||||
@@ -113,6 +116,28 @@ public final class MciResponseScaffolder {
|
|||||||
converterClassName, mappingBySource));
|
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) {
|
private static List<ClassRange> findClassRanges(String source) {
|
||||||
List<ClassRangeDraft> drafts = new ArrayList<>();
|
List<ClassRangeDraft> drafts = new ArrayList<>();
|
||||||
Matcher matcher = CLASS_PATTERN.matcher(source);
|
Matcher matcher = CLASS_PATTERN.matcher(source);
|
||||||
@@ -356,6 +381,30 @@ public final class MciResponseScaffolder {
|
|||||||
return source.append("}\n").toString();
|
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,
|
private static void appendMappingMethod(StringBuilder source, ParsedType type, String targetType,
|
||||||
String sourceType, String methodName,
|
String sourceType, String methodName,
|
||||||
Map<String, FieldMapping> mappings) {
|
Map<String, FieldMapping> mappings) {
|
||||||
@@ -370,6 +419,20 @@ public final class MciResponseScaffolder {
|
|||||||
.append('(').append(sourceType).append(" source);\n\n");
|
.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,
|
private static String sourceTypePath(String rootClassName, ParsedType type,
|
||||||
Map<String, ParsedType> typeByName) {
|
Map<String, ParsedType> typeByName) {
|
||||||
List<String> names = new ArrayList<>();
|
List<String> names = new ArrayList<>();
|
||||||
|
|||||||
@@ -80,6 +80,35 @@ class MciResponseScaffolderTest {
|
|||||||
"IndividualCustomerDetailInquiryResponse.CmnnPrafIfinOutDto toCmnnPrafIfinOutDto(ONBSZ0460_O.CmnnPrafIfinOutDto source);"));
|
"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
|
@Test
|
||||||
void rejectsDuplicateAiTargetNamesWithinTheSameType() {
|
void rejectsDuplicateAiTargetNamesWithinTheSameType() {
|
||||||
MciResponseScaffolder.ParsedSource parsed = MciResponseScaffolder.parse(MCI_SOURCE);
|
MciResponseScaffolder.ParsedSource parsed = MciResponseScaffolder.parse(MCI_SOURCE);
|
||||||
|
|||||||
Reference in New Issue
Block a user