diff --git a/dap-gateway/src/main/resources/static/admin/scaffold.html b/dap-gateway/src/main/resources/static/admin/scaffold.html
index ec270593..8db12a4c 100644
--- a/dap-gateway/src/main/resources/static/admin/scaffold.html
+++ b/dap-gateway/src/main/resources/static/admin/scaffold.html
@@ -1203,6 +1203,41 @@
+
+
@@ -1726,6 +1761,81 @@
]
}
};
+ let currentSubFieldTargetInput = null;
+ let subFieldEditorModalInstance = null;
+
+ function openSubFieldEditor(inputEl) {
+ currentSubFieldTargetInput = inputEl;
+ let fields = [];
+ if (inputEl.value.trim()) {
+ try { fields = JSON.parse(inputEl.value); } catch(e) {}
+ }
+ document.getElementById('subFieldEditorBody').replaceChildren();
+ (fields.length ? fields : [{}]).forEach(addSubFieldEditorRow);
+ if (!subFieldEditorModalInstance) {
+ subFieldEditorModalInstance = new bootstrap.Modal(document.getElementById('subFieldEditorModal'));
+ }
+ subFieldEditorModalInstance.show();
+ }
+
+ function addSubFieldEditorRow(field = {}) {
+ const row = document.createElement('tr');
+ const makeInput = (val, ph) => {
+ const el = document.createElement('input');
+ el.type = 'text'; el.className = 'form-control form-control-sm';
+ el.value = val || ''; el.placeholder = ph;
+ return el;
+ };
+ const n = makeInput(field.name, 'name'); n.dataset.field = 'name';
+ const d = makeInput(field.description, 'desc'); d.dataset.field = 'description';
+ const e = makeInput(field.example, 'ex'); e.dataset.field = 'example';
+ const t = document.createElement('select');
+ t.className = 'form-select form-select-sm'; t.dataset.field = 'type';
+ ['String', 'Integer', 'Long', 'Double', 'Boolean', 'BigDecimal', 'Enum'].forEach(type => {
+ t.add(new Option(type, type, false, (field.type || 'String') === type));
+ });
+ const det = makeInput(field.type === 'Enum' ? (field.enumValues||[]).join(',') : '', 'Enum csv');
+ det.dataset.field = 'details';
+ t.addEventListener('change', () => {
+ if (t.value === 'Enum') det.placeholder = 'Enum csv';
+ else det.value = '';
+ });
+ const req = document.createElement('select');
+ req.className = 'form-select form-select-sm'; req.dataset.field = 'required';
+ req.add(new Option('Required', 'true', false, field.required === true || field.required === 'true'));
+ req.add(new Option('Optional', 'false', false, !(field.required === true || field.required === 'true')));
+
+ [n, t, det, d, e, req].forEach(ctrl => {
+ const td = document.createElement('td'); td.appendChild(ctrl); row.appendChild(td);
+ });
+ const del = document.createElement('button');
+ del.type = 'button'; del.className = 'btn-secondary-action'; del.textContent = 'X';
+ del.style.padding = '0.1rem 0.4rem';
+ del.onclick = () => row.remove();
+ const delTd = document.createElement('td'); delTd.appendChild(del); row.appendChild(delTd);
+ document.getElementById('subFieldEditorBody').appendChild(row);
+ }
+
+ function applySubFieldEditor() {
+ const fields = [...document.querySelectorAll('#subFieldEditorBody tr')].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(s=>s.trim()).filter(Boolean) : [],
+ itemType: null, itemFields: []
+ };
+ }).filter(f => f.name);
+ currentSubFieldTargetInput.value = JSON.stringify(fields);
+ currentSubFieldTargetInput.dispatchEvent(new Event('input'));
+ currentSubFieldTargetInput.dispatchEvent(new Event('change'));
+ subFieldEditorModalInstance.hide();
+ }
+
let fieldEditorTargetId = null;
let fieldEditorModalInstance = null;
@@ -1804,14 +1914,32 @@
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')));
+ const itemFieldsContainer = document.createElement('div');
+ itemFieldsContainer.className = 'd-flex gap-1';
+
const itemFieldsInput = makeInput(field.type === 'List' && field.itemType === 'Object'
- ? JSON.stringify(field.itemFields || []) : '', 'Object fields JSON e.g. [{"name":"date","type":"String"}]');
+ ? JSON.stringify(field.itemFields || []) : '', 'JSON 직접입력 또는 우측 편집 클릭');
itemFieldsInput.dataset.field = 'itemFields';
+ itemFieldsInput.style.flex = '1';
+
+ const editItemBtn = document.createElement('button');
+ editItemBtn.type = 'button';
+ editItemBtn.className = 'btn-secondary-action';
+ editItemBtn.textContent = '편집';
+ editItemBtn.style.padding = '0.2rem 0.5rem';
+ editItemBtn.onclick = () => {
+ if (typeof openSubFieldEditor === 'function') openSubFieldEditor(itemFieldsInput);
+ };
+
+ itemFieldsContainer.appendChild(itemFieldsInput);
+ itemFieldsContainer.appendChild(editItemBtn);
+
const updateObjectListFieldsState = () => {
const objectList = typeSelect.value === 'List' && detailsInput.value === 'Object';
itemFieldsInput.disabled = !objectList;
+ editItemBtn.disabled = !objectList;
itemFieldsInput.placeholder = objectList
- ? 'Object fields JSON e.g. [{"name":"date","type":"String"}]'
+ ? 'JSON 직접입력 또는 우측 편집 클릭'
: 'Select List > Object to enter item fields';
if (!objectList) itemFieldsInput.value = '';
};
@@ -1838,7 +1966,7 @@
control.addEventListener('change', updateFieldEditorPreview);
});
- [nameInput, typeSelect, detailsInput, itemFieldsInput, descriptionInput, exampleInput, requiredSelect].forEach(control => {
+ [nameInput, typeSelect, detailsInput, itemFieldsContainer, descriptionInput, exampleInput, requiredSelect].forEach(control => {
const cell = document.createElement('td');
cell.appendChild(control);
row.appendChild(cell);
@@ -1866,9 +1994,12 @@
let itemFields = [];
if (type === 'List' && details === 'Object') {
const itemFieldsText = row.querySelector('[data-field="itemFields"]').value.trim();
- if (!itemFieldsText) throw new Error('List Object는 항목 필드를 입력해야 합니다.');
- itemFields = JSON.parse(itemFieldsText);
- if (!Array.isArray(itemFields) || itemFields.length === 0) throw new Error('List Object 항목 필드는 JSON 배열이어야 합니다.');
+ try {
+ itemFields = itemFieldsText ? JSON.parse(itemFieldsText) : [];
+ if (!Array.isArray(itemFields)) itemFields = [];
+ } catch (e) {
+ itemFields = [];
+ }
}
return {
name: row.querySelector('[data-field="name"]').value.trim(), type,
@@ -1958,7 +2089,10 @@
form.elements.baseName.value = result.baseName || '';
form.elements.title.value = result.title || '';
form.elements.description.value = result.description || '';
- form.elements.categoryKey.value = result.categoryKey || '';
+ if (!form.elements.categoryKey.value) {
+ form.elements.categoryKey.value = result.categoryKey || '';
+ if (typeof loadUseCasesForSelection === 'function') loadUseCasesForSelection();
+ }
form.elements.routingType.value = result.routingType || 'HTTP';
form.elements.httpApiName.value = result.httpApiName || '';
form.elements.functionDescription.value = result.functionDescription || '';