feat: enhance tool scaffold and usecase naming
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 0s

This commit is contained in:
jade
2026-08-12 23:42:14 +09:00
parent bd4d2a7624
commit 97d380afe0
35 changed files with 657 additions and 60 deletions

View File

@@ -824,6 +824,19 @@
<!-- 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;">
<div class="d-flex justify-content-between align-items-center gap-3">
<div>
<label class="form-label mb-1">Multi Tool UseCase (MCI)</label>
<div class="input-hint mt-0">현재 Tool을 같은 UseCase에 추가하면, Tool별 Client를 호출하는 여러 MCP Tool 메서드가 생성됩니다.</div>
</div>
<button type="button" class="btn-secondary-action" onclick="addCurrentToolToGroup()">현재 Tool 묶음에 추가</button>
</div>
<div class="row g-2 mt-1">
<div class="col-md-5"><input id="toolGroupUseCaseName" type="text" class="form-control" placeholder="UseCase 이름 e.g. Customer"></div>
<div class="col-md-7"><div id="toolGroupSummary" class="input-hint pt-2">묶음에 추가된 Tool 없음 — 일반 Generate Tool은 기존 단일 Tool 생성입니다.</div></div>
</div>
</div>
<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">
@@ -1158,6 +1171,7 @@
<tr>
<th style="min-width: 150px;">Name</th>
<th style="min-width: 125px;">Type</th>
<th style="min-width: 170px;">Enum values / List item type</th>
<th style="min-width: 210px;">Description</th>
<th style="min-width: 180px;">Example</th>
<th style="min-width: 125px;">Required</th>
@@ -1469,6 +1483,95 @@
});
handleFormSubmit('podForm', '/api/v1/scaffold/pod');
const groupedTools = [];
function toMethodName(baseName) {
return baseName ? baseName.charAt(0).toLowerCase() + baseName.slice(1) : '';
}
function parseToolFields(id) {
const text = document.getElementById(id).value.trim();
if (!text) return [];
const fields = JSON.parse(text);
if (!Array.isArray(fields)) throw new Error(`${id} must be a JSON array.`);
return fields;
}
function currentToolDefinition() {
const form = document.getElementById('toolForm');
const data = Object.fromEntries(new FormData(form).entries());
if (!data.baseName || !data.interfaceId || !data.clientSystemCode) {
throw new Error('Base Name, Legacy Interface ID, Client System Code를 입력해주세요.');
}
if (data.routingType !== 'MCI') {
throw new Error('여러 Tool을 하나의 UseCase로 생성하는 기능은 현재 MCI 전용입니다.');
}
return {
baseName: data.baseName,
methodName: toMethodName(data.baseName),
interfaceId: data.interfaceId,
title: data.title || data.baseName,
description: data.description || '',
group: data.categoryKey,
routingType: data.routingType,
register: data.register === 'true',
clientSystemCode: data.clientSystemCode,
httpApiName: data.httpApiName || null,
inputFields: parseToolFields('inputFields'),
outputFields: parseToolFields('outputFields'),
definitionOptions: {
functionDescription: data.functionDescription || '', displayDescription: data.displayDescription || '',
whenToUse: data.whenToUse || '', whenNotToUse: data.whenNotToUse || '', ioLimits: data.ioLimits || '',
exampleQueries: (data.exampleQueries || '').split(/[\n,]+/).map(value => value.trim()).filter(Boolean),
tags: (data.tags || '').split(',').map(value => value.trim()).filter(Boolean), ownerOrg: data.ownerOrg || 'MCP_TOOL'
}
};
}
function addCurrentToolToGroup() {
try {
const tool = currentToolDefinition();
if (groupedTools.some(item => item.methodName === tool.methodName || item.baseName === tool.baseName)) {
throw new Error('같은 Base Name 또는 메서드명이 이미 묶음에 있습니다.');
}
groupedTools.push(tool);
renderToolGroupSummary();
} catch (error) {
alert(error.message);
}
}
function renderToolGroupSummary() {
const summary = document.getElementById('toolGroupSummary');
summary.textContent = groupedTools.length
? `${groupedTools.length}개 Tool: ${groupedTools.map(tool => `${tool.baseName}${tool.methodName}()`).join(', ')}`
: '묶음에 추가된 Tool 없음 — 일반 Generate Tool은 기존 단일 Tool 생성입니다.';
}
document.getElementById('toolForm').addEventListener('submit', function(e) {
if (!groupedTools.length) return;
e.preventDefault();
e.stopImmediatePropagation();
try {
const useCaseName = document.getElementById('toolGroupUseCaseName').value.trim();
if (!/^[A-Z][A-Za-z0-9]*$/.test(useCaseName)) throw new Error('UseCase 이름은 PascalCase로 입력해주세요. 예: Customer');
const current = currentToolDefinition();
const tools = groupedTools.some(item => item.methodName === current.methodName) ? groupedTools : [...groupedTools, current];
const data = Object.fromEntries(new FormData(this).entries());
fetch('/api/v1/scaffold/tool-group', {
method: 'POST', headers: {'Content-Type': 'application/json'},
body: JSON.stringify({useCaseName, moduleName: data.moduleName, author: data.author, date: data.date, tools})
}).then(response => response.text()).then(result => {
const resultBox = document.getElementById('resultBox');
resultBox.style.display = 'block'; resultBox.className = result.startsWith('Error:') ? 'error' : 'success';
resultBox.textContent = result;
});
} catch (error) {
alert(error.message);
}
});
handleFormSubmit('toolForm', '/api/v1/scaffold/tool');
const fieldExamples = {
@@ -1498,14 +1601,16 @@
}
}
const fieldTypes = ['String', 'Integer', 'Long', 'Double', 'Boolean', 'BigDecimal'];
const fieldTypes = ['String', 'Integer', 'Long', 'Double', 'Boolean', 'BigDecimal', 'Enum', 'List'];
const typeExamples = {
String: 'example',
Integer: '1',
Long: '1',
Double: '1.0',
Boolean: 'true',
BigDecimal: '1000.00'
BigDecimal: '1000.00',
Enum: 'OPEN',
List: 'C001'
};
const fieldTemplates = {
customer: {
@@ -1608,18 +1713,23 @@
updateFieldEditorPreview();
});
const detailsInput = makeInput(
field.type === 'Enum' ? (field.enumValues || []).join(', ') : (field.type === 'List' ? (field.itemType || 'String') : ''),
'Enum: OPEN, CLOSED / List: String');
detailsInput.dataset.field = 'details';
const requiredSelect = document.createElement('select');
requiredSelect.className = 'form-select form-select-sm';
requiredSelect.dataset.field = 'required';
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 => {
[nameInput, descriptionInput, exampleInput, requiredSelect, detailsInput].forEach(control => {
control.addEventListener('input', updateFieldEditorPreview);
control.addEventListener('change', updateFieldEditorPreview);
});
[nameInput, typeSelect, descriptionInput, exampleInput, requiredSelect].forEach(control => {
[nameInput, typeSelect, detailsInput, descriptionInput, exampleInput, requiredSelect].forEach(control => {
const cell = document.createElement('td');
cell.appendChild(control);
row.appendChild(cell);
@@ -1641,13 +1751,19 @@
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'
}))
.map(row => {
const type = row.querySelector('[data-field="type"]').value;
const details = row.querySelector('[data-field="details"]').value.trim();
return {
name: row.querySelector('[data-field="name"]').value.trim(), type,
description: row.querySelector('[data-field="description"]').value.trim(),
example: row.querySelector('[data-field="example"]').value.trim(),
required: row.querySelector('[data-field="required"]').value === 'true',
enumValues: type === 'Enum' ? details.split(',').map(value => value.trim()).filter(Boolean) : [],
itemType: type === 'List' ? (details || 'String') : null,
itemFields: []
};
})
.filter(field => field.name);
}