feat: add AI scaffold drafts
Some checks failed
Deploy to OCIWP / deploy (push) Has been cancelled

This commit is contained in:
jade
2026-08-11 22:54:02 +09:00
parent 11f94cf721
commit 45e4332adb
3 changed files with 447 additions and 19 deletions

View File

@@ -444,6 +444,27 @@
<!-- Tool Creation Form -->
<div class="tab-pane fade" id="tool" role="tabpanel">
<form id="toolForm">
<div class="mb-4 p-3 rounded" style="background:#18181b; border:1px solid #3f3f46;">
<label class="form-label">AI Tool 초안 만들기</label>
<div class="row g-2">
<div class="col-md-6">
<input id="toolNaturalLanguage" type="text" class="form-control" placeholder="예: 고객번호로 계약 상태를 조회하는 Tool">
</div>
<div class="col-md-3">
<select id="aiModelSelect" class="form-select" aria-label="AI 모델 선택">
<option value="cohere/north-mini-code:free" selected>Cohere North Mini Code</option>
<option value="inclusionai/ling-3.0-flash:free">InclusionAI Ling 3 Flash</option>
<option value="openai/gpt-oss-20b:free">OpenAI GPT-OSS 20B</option>
<option value="google/gemma-4-31b-it:free">Google Gemma 4 31B</option>
<option value="nvidia/nemotron-3-nano-30b-a3b:free">NVIDIA Nemotron 3 Nano</option>
</select>
</div>
<div class="col-md-3 d-grid">
<button id="toolDraftButton" type="button" class="btn-action" onclick="createAiToolDraft()">AI로 Tool 채우기</button>
</div>
</div>
<div class="input-hint">선택한 AI가 Tool 기본 정보와 Input/Output Fields를 채웁니다. Legacy Interface ID와 Client System Code는 실제 연계 명세를 확인해 직접 입력하세요.</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<label class="form-label">Base Name (PascalCase)</label>
@@ -482,11 +503,10 @@
</div>
<div class="col-md-6 mt-3 mt-md-0">
<label class="form-label">Protocol</label>
<select class="form-select" name="routingType">
<option value="HTTP">HTTP (REST)</option>
<option value="TCP">TCP (Socket)</option>
<option value="MCI">MCI (Legacy)</option>
</select>
<select class="form-select" name="routingType">
<option value="HTTP">HTTP (REST)</option>
<option value="MCI">MCI (Legacy)</option>
</select>
</div>
</div>
@@ -627,6 +647,22 @@
</div>
<div class="modal-body">
<p class="input-hint mt-0 mb-3">행을 추가해 필드를 입력하면 저장할 때 JSON으로 자동 변환됩니다.</p>
<div class="d-flex flex-wrap gap-2 align-items-center mb-3">
<span class="input-hint mt-0 mb-0">빠른 입력:</span>
<button type="button" class="btn-secondary-action" onclick="applyFieldTemplate('customer')">고객조회</button>
<button type="button" class="btn-secondary-action" onclick="applyFieldTemplate('list')">목록조회</button>
<button type="button" class="btn-secondary-action" onclick="applyFieldTemplate('detail')">단건조회</button>
<button type="button" class="btn-secondary-action" onclick="applyFieldTemplate('write')">등록/변경</button>
<button type="button" class="btn-secondary-action" onclick="applyFieldTemplate('clear')">초기화</button>
</div>
<div class="mb-4">
<label class="form-label">자연어 초안 만들기</label>
<div class="input-group">
<input id="fieldNaturalLanguage" type="text" class="form-control" placeholder="예: 고객번호로 계약 상태를 조회">
<button id="fieldDraftButton" type="button" class="btn-action" onclick="createNaturalFieldDraft()">AI 초안 만들기</button>
</div>
<div class="input-hint">Chat 화면과 같은 AI 모델이 문장을 분석해 현재 Input 또는 Output 필드 초안을 만듭니다.</div>
</div>
<div class="table-responsive">
<table class="table align-middle" id="fieldEditorTable">
<thead>
@@ -643,6 +679,10 @@
</table>
</div>
<button type="button" class="btn-secondary-action" onclick="addFieldEditorRow()">+ 필드 추가</button>
<div class="mt-4">
<label class="form-label">JSON 미리보기</label>
<pre id="fieldEditorPreview" class="mb-0 p-3 rounded" style="background:#09090b; border:1px solid #27272a; color:#e4e4e7; font-size:0.78rem; max-height:190px; overflow:auto;"></pre>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn-secondary-action" data-bs-dismiss="modal" style="padding: 0.5rem 1rem; font-size: 0.875rem;">취소</button>
@@ -784,6 +824,53 @@
}
const fieldTypes = ['String', 'Integer', 'Long', 'Double', 'Boolean', 'BigDecimal'];
const typeExamples = {
String: 'example',
Integer: '1',
Long: '1',
Double: '1.0',
Boolean: 'true',
BigDecimal: '1000.00'
};
const fieldTemplates = {
customer: {
inputFields: [
{ name: 'customerId', type: 'String', description: 'Customer identifier', example: 'CUST00001', required: true }
],
outputFields: [
{ name: 'customerId', type: 'String', description: 'Customer identifier', example: 'CUST00001', required: true },
{ name: 'customerName', type: 'String', description: 'Customer name', example: 'Hong Gildong', required: false }
]
},
list: {
inputFields: [
{ name: 'page', type: 'Integer', description: 'Page number', example: '1', required: false },
{ name: 'pageSize', type: 'Integer', description: 'Items per page', example: '20', required: false }
],
outputFields: [
{ name: 'totalCount', type: 'Long', description: 'Total result count', example: '100', required: false }
]
},
detail: {
inputFields: [
{ name: 'id', type: 'String', description: 'Item identifier', example: 'ID00001', required: true }
],
outputFields: [
{ name: 'resultCode', type: 'String', description: 'Result code', example: 'SUCCESS', required: true },
{ name: 'resultMessage', type: 'String', description: 'Result message', example: 'Completed', required: false }
]
},
write: {
inputFields: [
{ name: 'requestId', type: 'String', description: 'Request identifier', example: 'REQ00001', required: true },
{ name: 'requestType', type: 'String', description: 'Registration or update type', example: 'CREATE', required: true }
],
outputFields: [
{ name: 'resultCode', type: 'String', description: 'Result code', example: 'SUCCESS', required: true },
{ name: 'resultMessage', type: 'String', description: 'Result message', example: 'Completed', required: false }
]
}
};
let fieldEditorTargetId = null;
let fieldEditorModalInstance = null;
@@ -807,6 +894,7 @@
const body = document.getElementById('fieldEditorBody');
body.replaceChildren();
(fields.length ? fields : [{}]).forEach(addFieldEditorRow);
updateFieldEditorPreview();
if (!fieldEditorModalInstance) {
fieldEditorModalInstance = new bootstrap.Modal(document.getElementById('fieldEditorModal'));
@@ -838,6 +926,12 @@
const option = new Option(type, type, false, (field.type || 'String') === type);
typeSelect.add(option);
});
typeSelect.addEventListener('change', () => {
if (!exampleInput.value.trim()) {
exampleInput.value = typeExamples[typeSelect.value];
}
updateFieldEditorPreview();
});
const requiredSelect = document.createElement('select');
requiredSelect.className = 'form-select form-select-sm';
@@ -845,6 +939,11 @@
requiredSelect.add(new Option('Required', 'true', false, field.required === true || field.required === 'true'));
requiredSelect.add(new Option('Optional', 'false', false, !(field.required === true || field.required === 'true')));
[nameInput, descriptionInput, exampleInput, requiredSelect].forEach(control => {
control.addEventListener('input', updateFieldEditorPreview);
control.addEventListener('change', updateFieldEditorPreview);
});
[nameInput, typeSelect, descriptionInput, exampleInput, requiredSelect].forEach(control => {
const cell = document.createElement('td');
cell.appendChild(control);
@@ -855,19 +954,122 @@
deleteButton.type = 'button';
deleteButton.className = 'btn-secondary-action';
deleteButton.textContent = '삭제';
deleteButton.addEventListener('click', () => row.remove());
deleteButton.addEventListener('click', () => {
row.remove();
updateFieldEditorPreview();
});
deleteCell.appendChild(deleteButton);
row.appendChild(deleteCell);
document.getElementById('fieldEditorBody').appendChild(row);
updateFieldEditorPreview();
}
function currentEditorFields() {
return [...document.querySelectorAll('#fieldEditorBody tr')]
.map(row => ({
name: row.querySelector('[data-field="name"]').value.trim(),
type: row.querySelector('[data-field="type"]').value,
description: row.querySelector('[data-field="description"]').value.trim(),
example: row.querySelector('[data-field="example"]').value.trim(),
required: row.querySelector('[data-field="required"]').value === 'true'
}))
.filter(field => field.name);
}
function updateFieldEditorPreview() {
const preview = document.getElementById('fieldEditorPreview');
if (preview) preview.textContent = JSON.stringify(currentEditorFields(), null, 2);
}
function applyFieldTemplate(templateName) {
const body = document.getElementById('fieldEditorBody');
body.replaceChildren();
const fields = templateName === 'clear'
? [{}]
: (fieldTemplates[templateName]?.[fieldEditorTargetId] || []);
(fields.length ? fields : [{}]).forEach(addFieldEditorRow);
updateFieldEditorPreview();
}
async function createNaturalFieldDraft() {
const source = document.getElementById('fieldNaturalLanguage').value.trim();
if (!source) {
alert('필드 초안을 만들 문장을 입력해주세요.');
return;
}
const button = document.getElementById('fieldDraftButton');
const originalLabel = button.textContent;
button.textContent = 'AI 생성 중...';
button.disabled = true;
try {
const response = await fetch('/api/v1/scaffold/field-draft', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ description: source, target: fieldEditorTargetId, model: selectedAiModel() })
});
const result = await response.json();
if (!response.ok) throw new Error(result.error || 'AI 초안 생성에 실패했습니다.');
if (!Array.isArray(result.fields) || result.fields.length === 0) {
throw new Error('AI가 사용할 수 있는 필드를 만들지 못했습니다.');
}
const body = document.getElementById('fieldEditorBody');
body.replaceChildren();
result.fields.forEach(addFieldEditorRow);
updateFieldEditorPreview();
} catch (error) {
alert(`AI 초안 생성 실패: ${error.message}`);
} finally {
button.textContent = originalLabel;
button.disabled = false;
}
}
function selectedAiModel() {
return document.getElementById('aiModelSelect').value;
}
async function createAiToolDraft() {
const source = document.getElementById('toolNaturalLanguage').value.trim();
if (!source) {
alert('AI가 만들 Tool 설명을 입력해주세요.');
return;
}
const button = document.getElementById('toolDraftButton');
const originalLabel = button.textContent;
button.textContent = 'AI 생성 중...';
button.disabled = true;
try {
const response = await fetch('/api/v1/scaffold/tool-draft', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ description: source, model: selectedAiModel() })
});
const result = await response.json();
if (!response.ok) throw new Error(result.error || 'AI Tool 초안 생성에 실패했습니다.');
const form = document.getElementById('toolForm');
form.elements.baseName.value = result.baseName || '';
form.elements.title.value = result.title || '';
form.elements.description.value = result.description || '';
form.elements.categoryKey.value = result.categoryKey || '';
form.elements.routingType.value = result.routingType || 'HTTP';
form.elements.httpApiName.value = result.httpApiName || 'sample';
document.getElementById('inputFields').value = JSON.stringify(result.inputFields || [], null, 2);
document.getElementById('outputFields').value = JSON.stringify(result.outputFields || [], null, 2);
alert('Tool 초안을 채웠습니다. Legacy Interface ID와 Client System Code는 실제 연계 명세를 확인해 입력해주세요.');
} catch (error) {
alert(`AI Tool 초안 생성 실패: ${error.message}`);
} finally {
button.textContent = originalLabel;
button.disabled = false;
}
}
function applyFieldEditor() {
const rows = [...document.querySelectorAll('#fieldEditorBody tr')];
const names = new Set();
const fields = [];
for (const row of rows) {
const name = row.querySelector('[data-field="name"]').value.trim();
if (!name) continue;
const fields = currentEditorFields();
for (const field of fields) {
const name = field.name;
if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name)) {
alert(`필드명 "${name}"은(는) Java 필드명 형식으로 입력해주세요.`);
return;
@@ -877,13 +1079,6 @@
return;
}
names.add(name);
fields.push({
name,
type: row.querySelector('[data-field="type"]').value,
description: row.querySelector('[data-field="description"]').value.trim(),
example: row.querySelector('[data-field="example"]').value.trim(),
required: row.querySelector('[data-field="required"]').value === 'true'
});
}
document.getElementById(fieldEditorTargetId).value = JSON.stringify(fields, null, 2);
fieldEditorModalInstance.hide();