feat: enhance tool scaffold and usecase naming
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 0s
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 0s
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user