This commit is contained in:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user