feat: add Excel document generator
This commit is contained in:
@@ -12,6 +12,383 @@
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource/geist-mono@5.0.1/400.css">
|
||||
<!-- Bootstrap CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<style id="document-generator-styles">
|
||||
.document-generator {
|
||||
--docgen-panel: #111113;
|
||||
--docgen-panel-soft: #18181b;
|
||||
--docgen-line: #2a2a2f;
|
||||
--docgen-line-strong: #3f3f46;
|
||||
--docgen-text: #f4f4f5;
|
||||
--docgen-muted: #92929d;
|
||||
--docgen-blue: #3b82f6;
|
||||
--docgen-blue-soft: rgba(59, 130, 246, 0.12);
|
||||
--docgen-green: #6ee7a0;
|
||||
--docgen-amber: #facc50;
|
||||
--docgen-danger: #fb7185;
|
||||
color: var(--docgen-text);
|
||||
}
|
||||
|
||||
.document-generator *,
|
||||
.document-generator *::before,
|
||||
.document-generator *::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.document-generator .docgen-workspace {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(340px, 0.9fr) minmax(460px, 1.35fr);
|
||||
margin: -2rem -1.5rem;
|
||||
min-height: 680px;
|
||||
}
|
||||
|
||||
.document-generator .docgen-config,
|
||||
.document-generator .docgen-preview {
|
||||
padding: 28px;
|
||||
}
|
||||
|
||||
.document-generator .docgen-config {
|
||||
border-right: 1px solid var(--docgen-line);
|
||||
}
|
||||
|
||||
.document-generator .docgen-section-title {
|
||||
margin: 0 0 6px;
|
||||
color: var(--docgen-text);
|
||||
font-size: 16px;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.document-generator .docgen-section-copy {
|
||||
margin: 0 0 24px;
|
||||
color: var(--docgen-muted);
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.document-generator .docgen-field {
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
|
||||
.document-generator .docgen-field-label {
|
||||
display: block;
|
||||
margin-bottom: 9px;
|
||||
color: var(--docgen-text);
|
||||
font-size: 13px;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.document-generator select,
|
||||
.document-generator input[type="text"] {
|
||||
width: 100%;
|
||||
padding: 13px 14px;
|
||||
border: 1px solid var(--docgen-line-strong);
|
||||
border-radius: 7px;
|
||||
outline: 0;
|
||||
background: var(--docgen-panel-soft);
|
||||
color: var(--docgen-text);
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.document-generator select:focus,
|
||||
.document-generator input[type="text"]:focus {
|
||||
border-color: var(--docgen-blue);
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.12);
|
||||
}
|
||||
|
||||
.document-generator .docgen-hint {
|
||||
margin-top: 9px;
|
||||
color: var(--docgen-muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.document-generator .docgen-two-col {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 150px;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.document-generator .docgen-check-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.document-generator .docgen-check-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 58px;
|
||||
gap: 11px;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid var(--docgen-line-strong);
|
||||
border-radius: 7px;
|
||||
background: var(--docgen-panel-soft);
|
||||
color: #d4d4d8;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.document-generator .docgen-check-card:hover {
|
||||
border-color: #52525b;
|
||||
}
|
||||
|
||||
.document-generator .docgen-check-card:has(input:checked) {
|
||||
border-color: rgba(59, 130, 246, 0.72);
|
||||
background: var(--docgen-blue-soft);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.document-generator input[type="checkbox"] {
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
margin: 0;
|
||||
accent-color: var(--docgen-blue);
|
||||
}
|
||||
|
||||
.document-generator .docgen-mini-badge {
|
||||
margin-left: auto;
|
||||
padding: 4px 7px;
|
||||
border: 1px solid var(--docgen-line-strong);
|
||||
border-radius: 5px;
|
||||
background: #27272a;
|
||||
color: #a1a1aa;
|
||||
font: 600 10px 'Geist Mono', monospace;
|
||||
}
|
||||
|
||||
.document-generator .docgen-action-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 18px;
|
||||
margin-top: 30px;
|
||||
padding-top: 24px;
|
||||
border-top: 1px solid var(--docgen-line);
|
||||
}
|
||||
|
||||
.document-generator .docgen-source-label {
|
||||
color: var(--docgen-muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.document-generator .docgen-primary {
|
||||
padding: 13px 22px;
|
||||
border: 0;
|
||||
border-radius: 7px;
|
||||
background: var(--docgen-blue);
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.document-generator .docgen-primary:hover {
|
||||
background: #2563eb;
|
||||
}
|
||||
|
||||
.document-generator .docgen-primary:disabled {
|
||||
opacity: 0.45;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.document-generator .docgen-meta-card {
|
||||
min-height: 155px;
|
||||
padding: 20px;
|
||||
border: 1px solid var(--docgen-line-strong);
|
||||
border-radius: 8px;
|
||||
background: var(--docgen-panel-soft);
|
||||
}
|
||||
|
||||
.document-generator .docgen-meta-title {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.document-generator .docgen-meta-title h2 {
|
||||
margin: 0 0 8px;
|
||||
color: var(--docgen-text);
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.document-generator .docgen-meta-title p {
|
||||
margin: 0;
|
||||
color: var(--docgen-muted);
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.document-generator .docgen-status {
|
||||
padding: 5px 8px;
|
||||
border: 1px solid rgba(110, 231, 160, 0.28);
|
||||
border-radius: 5px;
|
||||
background: rgba(110, 231, 160, 0.07);
|
||||
color: var(--docgen-green);
|
||||
font: 700 10px 'Geist Mono', monospace;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.document-generator .docgen-meta-badges {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.document-generator .docgen-tag {
|
||||
padding: 5px 8px;
|
||||
border: 1px solid #3f3f46;
|
||||
border-radius: 5px;
|
||||
background: #27272a;
|
||||
color: #a1a1aa;
|
||||
font: 600 10px 'Geist Mono', monospace;
|
||||
}
|
||||
|
||||
.document-generator .docgen-tag.docgen-blue {
|
||||
border-color: #2563eb;
|
||||
background: rgba(37, 99, 235, 0.08);
|
||||
color: #60a5fa;
|
||||
}
|
||||
|
||||
.document-generator .docgen-list {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.document-generator .docgen-row {
|
||||
display: grid;
|
||||
grid-template-columns: 42px 1fr auto;
|
||||
align-items: center;
|
||||
min-height: 68px;
|
||||
gap: 14px;
|
||||
padding: 13px 16px;
|
||||
border: 1px solid var(--docgen-line);
|
||||
border-radius: 7px;
|
||||
background: #151518;
|
||||
opacity: 0.42;
|
||||
transition: opacity 0.15s, border-color 0.15s;
|
||||
}
|
||||
|
||||
.document-generator .docgen-row.docgen-enabled {
|
||||
border-color: #303037;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.document-generator .docgen-number {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 6px;
|
||||
background: #27272a;
|
||||
color: #a1a1aa;
|
||||
font: 600 11px 'Geist Mono', monospace;
|
||||
}
|
||||
|
||||
.document-generator .docgen-name {
|
||||
margin-bottom: 5px;
|
||||
color: var(--docgen-text);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.document-generator .docgen-description {
|
||||
color: var(--docgen-muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.document-generator .docgen-mode {
|
||||
padding: 5px 8px;
|
||||
border: 1px solid #3f3f46;
|
||||
border-radius: 5px;
|
||||
background: #27272a;
|
||||
color: var(--docgen-muted);
|
||||
font: 700 10px 'Geist Mono', monospace;
|
||||
}
|
||||
|
||||
.document-generator .docgen-mode.docgen-auto { color: var(--docgen-green); }
|
||||
.document-generator .docgen-mode.docgen-mixed { color: var(--docgen-amber); }
|
||||
.document-generator .docgen-mode.docgen-separate {
|
||||
border-color: #2563eb;
|
||||
background: rgba(37, 99, 235, 0.08);
|
||||
color: #60a5fa;
|
||||
}
|
||||
|
||||
.document-generator .docgen-legend {
|
||||
margin: 16px 0 10px;
|
||||
color: var(--docgen-muted);
|
||||
font-size: 11px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.document-generator .docgen-files {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.document-generator .docgen-file {
|
||||
min-height: 72px;
|
||||
padding: 14px 16px;
|
||||
border: 1px dashed #3f3f46;
|
||||
border-radius: 7px;
|
||||
}
|
||||
|
||||
.document-generator .docgen-file.docgen-disabled {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.document-generator .docgen-file-kind {
|
||||
margin-bottom: 9px;
|
||||
color: var(--docgen-green);
|
||||
font: 700 10px 'Geist Mono', monospace;
|
||||
}
|
||||
|
||||
.document-generator .docgen-file-name {
|
||||
color: #a1a1aa;
|
||||
font: 500 11px 'Geist Mono', monospace;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.document-generator .docgen-loading { color: var(--docgen-muted); }
|
||||
.document-generator .docgen-error { color: var(--docgen-danger); }
|
||||
|
||||
.docgen-toast {
|
||||
position: fixed;
|
||||
right: 24px;
|
||||
bottom: 24px;
|
||||
z-index: 1080;
|
||||
padding: 13px 16px;
|
||||
border: 1px solid #3f3f46;
|
||||
border-radius: 8px;
|
||||
background: #18181b;
|
||||
box-shadow: 0 18px 50px rgba(0, 0, 0, 0.45);
|
||||
color: #e4e4e7;
|
||||
font-size: 12px;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transform: translateY(10px);
|
||||
transition: 0.2s;
|
||||
}
|
||||
|
||||
.docgen-toast.docgen-show {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.document-generator .docgen-workspace {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.document-generator .docgen-config {
|
||||
border-right: 0;
|
||||
border-bottom: 1px solid var(--docgen-line);
|
||||
}
|
||||
|
||||
.document-generator .docgen-two-col {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
:root {
|
||||
--bg-color: #09090b;
|
||||
@@ -405,6 +782,9 @@
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="tool-tab" data-bs-toggle="tab" data-bs-target="#tool" type="button" role="tab">Tool Function</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="document-tab" data-bs-toggle="tab" data-bs-target="#document" type="button" role="tab" onclick="loadDocumentGenerator()">Document Generator</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="list-tab" data-bs-toggle="tab" data-bs-target="#list" type="button" role="tab" onclick="loadToolList()">Registry</button>
|
||||
</li>
|
||||
@@ -595,6 +975,78 @@
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Document Generator -->
|
||||
<div class="tab-pane fade document-generator" id="document" role="tabpanel" aria-labelledby="document-tab">
|
||||
<div class="docgen-workspace">
|
||||
<section class="docgen-config">
|
||||
<h2 class="docgen-section-title">Document Generator</h2>
|
||||
<p class="docgen-section-copy">Tool Metadata를 기준으로 개발 산출물 Excel을 생성합니다. 선택된 문서만 파일과 시트에 포함됩니다.</p>
|
||||
|
||||
<div class="docgen-field">
|
||||
<label class="docgen-field-label" for="docgenToolSelect">Tool</label>
|
||||
<select id="docgenToolSelect" disabled><option>Tool Metadata를 불러오는 중...</option></select>
|
||||
<div id="docgenToolHint" class="docgen-hint">Gateway의 ToolMetadata 목록을 조회하고 있습니다.</div>
|
||||
</div>
|
||||
|
||||
<div class="docgen-two-col docgen-field">
|
||||
<div>
|
||||
<label class="docgen-field-label" for="docgenDocumentType">Document Type</label>
|
||||
<select id="docgenDocumentType"><option>Program Definition / Design</option></select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="docgen-field-label" for="docgenVersion">Version</label>
|
||||
<input id="docgenVersion" type="text" value="1.0" autocomplete="off">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="docgen-field">
|
||||
<label class="docgen-field-label">Included Sheets</label>
|
||||
<div class="docgen-check-grid">
|
||||
<label class="docgen-check-card"><input id="docgenProgramCheck" type="checkbox" checked><span>프로그램정의서</span></label>
|
||||
<label class="docgen-check-card"><input id="docgenProcessCheck" type="checkbox" checked><span>처리설계</span></label>
|
||||
<label class="docgen-check-card"><input id="docgenInterfaceCheck" type="checkbox" checked><span>인터페이스</span><span class="docgen-mini-badge">별도 파일</span></label>
|
||||
<label class="docgen-check-card"><input id="docgenRevisionCheck" type="checkbox" checked><span>개정이력</span></label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="docgen-action-row">
|
||||
<span class="docgen-source-label">Tool metadata · Generated XLSX</span>
|
||||
<button id="docgenGenerateBtn" class="docgen-primary" type="button" disabled>Generate EXCEL</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="docgen-preview">
|
||||
<div class="docgen-meta-card">
|
||||
<div class="docgen-meta-title">
|
||||
<div>
|
||||
<h2 id="docgenToolTitle" class="docgen-loading">Tool Metadata loading...</h2>
|
||||
<p id="docgenToolDescription">등록된 Tool 목록을 가져오는 중입니다.</p>
|
||||
</div>
|
||||
<span id="docgenMetadataStatus" class="docgen-status">SYNC</span>
|
||||
</div>
|
||||
<div class="docgen-meta-badges">
|
||||
<span id="docgenCategoryTag" class="docgen-tag">Category · -</span>
|
||||
<span id="docgenInterfaceTag" class="docgen-tag">Interface · -</span>
|
||||
<span class="docgen-tag docgen-blue">Tool Metadata</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="docgen-list">
|
||||
<div id="docgenProgramRow" class="docgen-row docgen-enabled"><span class="docgen-number">01</span><div><div class="docgen-name">프로그램정의서</div><div class="docgen-description">Basic information · Runtime · Process summary</div></div><span class="docgen-mode docgen-auto">AUTO</span></div>
|
||||
<div id="docgenProcessRow" class="docgen-row docgen-enabled"><span class="docgen-number">02</span><div><div class="docgen-name">처리설계</div><div class="docgen-description">Tool metadata + common design</div></div><span class="docgen-mode docgen-mixed">MIXED</span></div>
|
||||
<div id="docgenInterfaceRow" class="docgen-row docgen-enabled"><span class="docgen-number">03</span><div><div class="docgen-name">인터페이스</div><div class="docgen-description">Request In · Response Out · 2 sheets</div></div><span class="docgen-mode docgen-separate">2 SHEETS · SEPARATE</span></div>
|
||||
<div id="docgenRevisionRow" class="docgen-row docgen-enabled"><span class="docgen-number">04</span><div><div class="docgen-name">개정이력</div><div class="docgen-description">Version · Date · Change information</div></div><span class="docgen-mode">MANUAL</span></div>
|
||||
</div>
|
||||
|
||||
<p class="docgen-legend">AUTO: Tool metadata · MIXED: Metadata + common design · MANUAL: Review required<br>Output Files · 인터페이스 정의서는 별도 Excel 파일로 생성됩니다.</p>
|
||||
<div class="docgen-files">
|
||||
<div id="docgenProgramFile" class="docgen-file"><div class="docgen-file-kind">PROGRAM SPEC</div><div id="docgenProgramFileName" class="docgen-file-name">tool_프로그램정의서_v1.0_YYYYMMDD.xlsx</div></div>
|
||||
<div id="docgenInterfaceFile" class="docgen-file"><div class="docgen-file-kind">INTERFACE SPEC · REQUEST IN / RESPONSE OUT</div><div id="docgenInterfaceFileName" class="docgen-file-name">tool_인터페이스정의서_v1.0_YYYYMMDD.xlsx</div></div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tool List Form -->
|
||||
<div class="tab-pane fade" id="list" role="tabpanel">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
@@ -736,6 +1188,192 @@
|
||||
|
||||
<!-- Bootstrap JS -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script id="document-generator-script">
|
||||
(() => {
|
||||
'use strict';
|
||||
|
||||
const state = { tools: [], selected: null };
|
||||
let checks = null;
|
||||
let initialized = false;
|
||||
|
||||
const dg = (id) => document.getElementById(id);
|
||||
|
||||
function initialize() {
|
||||
if (!initialized) {
|
||||
checks = {
|
||||
program: dg('docgenProgramCheck'),
|
||||
process: dg('docgenProcessCheck'),
|
||||
interface: dg('docgenInterfaceCheck'),
|
||||
revision: dg('docgenRevisionCheck')
|
||||
};
|
||||
Object.entries(checks).forEach(([name, checkbox]) => checkbox.addEventListener('change', () => {
|
||||
dg(`docgen${capitalize(name)}Row`).classList.toggle('docgen-enabled', checkbox.checked);
|
||||
updateOutputPreview();
|
||||
}));
|
||||
dg('docgenToolSelect').addEventListener('change', selectTool);
|
||||
dg('docgenVersion').addEventListener('input', updateOutputPreview);
|
||||
dg('docgenGenerateBtn').addEventListener('click', generateFiles);
|
||||
initialized = true;
|
||||
}
|
||||
loadTools();
|
||||
}
|
||||
|
||||
async function loadTools() {
|
||||
const select = dg('docgenToolSelect');
|
||||
const button = dg('docgenGenerateBtn');
|
||||
select.disabled = true;
|
||||
button.disabled = true;
|
||||
dg('docgenToolHint').textContent = 'Gateway의 ToolMetadata 목록을 조회하고 있습니다.';
|
||||
try {
|
||||
const response = await fetch('/mcp/api/v1/tools/list', { cache: 'no-store' });
|
||||
if (!response.ok) throw new Error(`Gateway HTTP ${response.status}`);
|
||||
const payload = await response.json();
|
||||
const tools = payload?.result?.tools;
|
||||
if (!Array.isArray(tools)) throw new Error('ToolMetadata 응답 형식이 올바르지 않습니다.');
|
||||
state.tools = tools.slice().sort((a, b) =>
|
||||
(a.categoryKey || '').localeCompare(b.categoryKey || '') ||
|
||||
(a.name || '').localeCompare(b.name || '')
|
||||
);
|
||||
select.innerHTML = state.tools.map((tool, index) =>
|
||||
`<option value="${index}">[${escapeHtml(tool.categoryKey || 'etc')}] ${escapeHtml(tool.name || tool.displayName || tool.uid)}</option>`
|
||||
).join('');
|
||||
select.disabled = state.tools.length === 0;
|
||||
dg('docgenToolHint').textContent = `ToolMetadata에 등록된 ${state.tools.length}개 Tool을 모두 표시합니다.`;
|
||||
if (state.tools.length) {
|
||||
select.value = '0';
|
||||
selectTool();
|
||||
} else {
|
||||
showMetadataError('등록된 Tool이 없습니다.');
|
||||
}
|
||||
} catch (error) {
|
||||
state.tools = [];
|
||||
state.selected = null;
|
||||
showMetadataError(error.message);
|
||||
select.innerHTML = '<option>Tool Metadata 조회 실패</option>';
|
||||
dg('docgenToolHint').innerHTML = `<span class="docgen-error">${escapeHtml(error.message)}</span> · Gateway 실행 상태를 확인하세요.`;
|
||||
}
|
||||
}
|
||||
|
||||
function selectTool() {
|
||||
state.selected = state.tools[Number(dg('docgenToolSelect').value)] || null;
|
||||
const tool = state.selected;
|
||||
if (!tool) return;
|
||||
dg('docgenVersion').value = normalizeVersion(tool.semver || dg('docgenVersion').value || '1.0');
|
||||
dg('docgenToolTitle').classList.remove('docgen-loading', 'docgen-error');
|
||||
dg('docgenToolTitle').textContent = tool.displayName || tool.name;
|
||||
dg('docgenToolDescription').textContent = tool.description || '설명이 등록되지 않은 Tool입니다.';
|
||||
dg('docgenCategoryTag').textContent = `Category · ${tool.categoryKey || '-'}`;
|
||||
dg('docgenInterfaceTag').textContent = `Interface · ${tool.integrationType || '-'}`;
|
||||
dg('docgenMetadataStatus').textContent = tool.enabled === false ? 'DISABLED' : 'READY';
|
||||
updateOutputPreview();
|
||||
}
|
||||
|
||||
function showMetadataError(message) {
|
||||
dg('docgenToolTitle').className = 'docgen-error';
|
||||
dg('docgenToolTitle').textContent = 'Tool Metadata unavailable';
|
||||
dg('docgenToolDescription').textContent = message;
|
||||
dg('docgenMetadataStatus').textContent = 'ERROR';
|
||||
dg('docgenGenerateBtn').disabled = true;
|
||||
}
|
||||
|
||||
function updateOutputPreview() {
|
||||
const toolName = safeFilename(state.selected?.displayName || state.selected?.name || 'tool');
|
||||
const version = safeVersion(dg('docgenVersion').value);
|
||||
const date = yyyymmdd();
|
||||
const hasProgram = checks.program.checked || checks.process.checked || checks.revision.checked;
|
||||
dg('docgenProgramFile').classList.toggle('docgen-disabled', !hasProgram);
|
||||
dg('docgenInterfaceFile').classList.toggle('docgen-disabled', !checks.interface.checked);
|
||||
dg('docgenProgramFileName').textContent = `${toolName}_프로그램정의서_v${version}_${date}.xlsx`;
|
||||
dg('docgenInterfaceFileName').textContent = `${toolName}_인터페이스정의서_v${version}_${date}.xlsx`;
|
||||
dg('docgenGenerateBtn').disabled = !state.selected || (!hasProgram && !checks.interface.checked);
|
||||
}
|
||||
|
||||
async function generateFiles() {
|
||||
const tool = state.selected;
|
||||
if (!tool) return notify('생성할 Tool을 선택하세요.');
|
||||
const button = dg('docgenGenerateBtn');
|
||||
const version = safeVersion(dg('docgenVersion').value);
|
||||
const date = yyyymmdd();
|
||||
const toolName = safeFilename(tool.displayName || tool.name || 'tool');
|
||||
const hasProgram = checks.program.checked || checks.process.checked || checks.revision.checked;
|
||||
const request = {
|
||||
tool,
|
||||
version,
|
||||
includeProgram: checks.program.checked,
|
||||
includeProcess: checks.process.checked,
|
||||
includeRevision: checks.revision.checked
|
||||
};
|
||||
|
||||
button.disabled = true;
|
||||
button.textContent = 'Generating...';
|
||||
try {
|
||||
if (hasProgram) {
|
||||
await requestDownload('/mcp/api/v1/admin/documents/program', request,
|
||||
`${toolName}_프로그램정의서_v${version}_${date}.xlsx`);
|
||||
}
|
||||
if (checks.interface.checked) {
|
||||
await requestDownload('/mcp/api/v1/admin/documents/interface', request,
|
||||
`${toolName}_인터페이스정의서_v${version}_${date}.xlsx`);
|
||||
}
|
||||
notify('선택한 Excel 문서 다운로드를 완료했습니다.');
|
||||
} catch (error) {
|
||||
notify(error.message);
|
||||
} finally {
|
||||
button.textContent = 'Generate EXCEL';
|
||||
updateOutputPreview();
|
||||
}
|
||||
}
|
||||
|
||||
async function requestDownload(url, payload, filename) {
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
if (!response.ok) {
|
||||
let message = `문서 생성에 실패했습니다. (HTTP ${response.status})`;
|
||||
try {
|
||||
const error = await response.json();
|
||||
if (error?.error) message = error.error;
|
||||
} catch (_) {
|
||||
// JSON 오류 응답이 아닌 경우 기본 메시지를 사용합니다.
|
||||
}
|
||||
throw new Error(message);
|
||||
}
|
||||
const blob = await response.blob();
|
||||
const href = URL.createObjectURL(blob);
|
||||
const anchor = document.createElement('a');
|
||||
anchor.href = href;
|
||||
anchor.download = filename;
|
||||
document.body.appendChild(anchor);
|
||||
anchor.click();
|
||||
anchor.remove();
|
||||
URL.revokeObjectURL(href);
|
||||
}
|
||||
|
||||
function safeFilename(value) { return String(value).replace(/[\\/:*?"<>|]/g, '_').trim() || 'tool'; }
|
||||
function safeVersion(value) { return String(value || '1.0').replace(/^v/i, '').replace(/[^0-9A-Za-z._-]/g, '_') || '1.0'; }
|
||||
function normalizeVersion(value) { return String(value).replace(/^v/i, '') || '1.0'; }
|
||||
function yyyymmdd() {
|
||||
const date = new Date();
|
||||
return `${date.getFullYear()}${String(date.getMonth() + 1).padStart(2, '0')}${String(date.getDate()).padStart(2, '0')}`;
|
||||
}
|
||||
function capitalize(value) { return value.charAt(0).toUpperCase() + value.slice(1); }
|
||||
function escapeHtml(value) {
|
||||
return String(value).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||
.replace(/"/g, '"').replace(/'/g, ''');
|
||||
}
|
||||
function notify(message) {
|
||||
const toast = dg('docgenToast');
|
||||
toast.textContent = message;
|
||||
toast.classList.add('docgen-show');
|
||||
window.setTimeout(() => toast.classList.remove('docgen-show'), 2800);
|
||||
}
|
||||
|
||||
window.loadDocumentGenerator = initialize;
|
||||
})();
|
||||
</script>
|
||||
<div id="docgenToast" class="docgen-toast"></div>
|
||||
<script>
|
||||
function handleFormSubmit(formId, apiUrl) {
|
||||
document.getElementById(formId).addEventListener('submit', function(e) {
|
||||
|
||||
Reference in New Issue
Block a user