📦 Initialize Geulbeot structure and merge Prompts & test projects
This commit is contained in:
143
03. Code/geulbeot_10th/static/js/ai_edit.js
Normal file
143
03. Code/geulbeot_10th/static/js/ai_edit.js
Normal file
@@ -0,0 +1,143 @@
|
||||
// ===== AI 부분 수정 =====
|
||||
function setupIframeSelection() {
|
||||
const frame = document.getElementById('previewFrame');
|
||||
if (!frame.contentDocument) return;
|
||||
|
||||
// 블록 선택 시 텍스트만 저장 (팝업 안 띄움)
|
||||
frame.contentDocument.addEventListener('mouseup', function(e) {
|
||||
const selection = frame.contentWindow.getSelection();
|
||||
const text = selection.toString().trim();
|
||||
|
||||
if (text.length > 0) {
|
||||
selectedText = text;
|
||||
selectedRange = selection.getRangeAt(0).cloneRange();
|
||||
// 툴바의 AI 버튼 활성화 표시
|
||||
const aiBtn = document.getElementById('aiEditToolbarBtn');
|
||||
if (aiBtn) {
|
||||
aiBtn.classList.add('has-selection');
|
||||
aiBtn.title = `AI 수정 (${text.length}자 선택됨)`;
|
||||
}
|
||||
} else {
|
||||
selectedText = '';
|
||||
selectedRange = null;
|
||||
const aiBtn = document.getElementById('aiEditToolbarBtn');
|
||||
if (aiBtn) {
|
||||
aiBtn.classList.remove('has-selection');
|
||||
aiBtn.title = 'AI 수정 (텍스트를 먼저 선택하세요)';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 툴바 버튼 클릭 시 AI 편집 팝업 표시
|
||||
function triggerAiEdit() {
|
||||
if (!selectedText || selectedText.length === 0) {
|
||||
alert('먼저 수정할 텍스트를 드래그하여 선택해주세요.');
|
||||
return;
|
||||
}
|
||||
showAiEditPopup(selectedText);
|
||||
}
|
||||
|
||||
function showAiEditPopup(text) {
|
||||
const popup = document.getElementById('aiEditPopup');
|
||||
const textDisplay = document.getElementById('aiEditSelectedText');
|
||||
|
||||
const displayText = text.length > 150 ? text.substring(0, 150) + '...' : text;
|
||||
textDisplay.textContent = displayText;
|
||||
|
||||
popup.classList.add('show');
|
||||
document.getElementById('aiEditInput').focus();
|
||||
}
|
||||
|
||||
function closeAiEditPopup() {
|
||||
document.getElementById('aiEditPopup').classList.remove('show');
|
||||
document.getElementById('aiEditInput').value = '';
|
||||
}
|
||||
|
||||
async function submitAiEdit() {
|
||||
const request = document.getElementById('aiEditInput').value.trim();
|
||||
if (!request) {
|
||||
alert('수정 요청을 입력해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!selectedText) {
|
||||
alert('선택된 텍스트가 없습니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
const btn = document.querySelector('.ai-edit-btn');
|
||||
const originalText = btn.textContent;
|
||||
btn.textContent = '⏳ 수정 중...';
|
||||
btn.disabled = true;
|
||||
|
||||
setStatus('부분 수정 중...', true);
|
||||
|
||||
try {
|
||||
const response = await fetch('/refine-selection', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
current_html: generatedHTML,
|
||||
selected_text: selectedText,
|
||||
request: request,
|
||||
doc_type: currentDocType
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.error) {
|
||||
throw new Error(data.error);
|
||||
}
|
||||
|
||||
if (data.success && data.html) {
|
||||
const frame = document.getElementById('previewFrame');
|
||||
const doc = frame.contentDocument;
|
||||
|
||||
// 간단한 텍스트 교체
|
||||
const modifiedContent = data.html.replace(/```html\n?/g, '').replace(/```\n?/g, '').trim();
|
||||
|
||||
const searchStr = selectedText.substring(0, 30);
|
||||
const allElements = doc.body.getElementsByTagName('*');
|
||||
|
||||
for (const el of allElements) {
|
||||
if (el.textContent && el.textContent.includes(searchStr)) {
|
||||
let hasChildWithText = false;
|
||||
for (const child of el.children) {
|
||||
if (child.textContent && child.textContent.includes(searchStr)) {
|
||||
hasChildWithText = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!hasChildWithText) {
|
||||
el.innerHTML = modifiedContent;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
generatedHTML = '<!DOCTYPE html>' + doc.documentElement.outerHTML;
|
||||
|
||||
// 선택 초기화
|
||||
selectedText = '';
|
||||
selectedRange = null;
|
||||
const aiBtn = document.getElementById('aiEditToolbarBtn');
|
||||
if (aiBtn) {
|
||||
aiBtn.classList.remove('has-selection');
|
||||
aiBtn.title = 'AI 수정 (텍스트를 먼저 선택하세요)';
|
||||
}
|
||||
|
||||
setTimeout(setupIframeSelection, 500);
|
||||
closeAiEditPopup();
|
||||
setStatus('부분 수정 완료', true);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
alert('수정 오류: ' + error.message);
|
||||
setStatus('오류 발생', false);
|
||||
} finally {
|
||||
btn.textContent = originalText;
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
371
03. Code/geulbeot_10th/static/js/demo_mode.js
Normal file
371
03. Code/geulbeot_10th/static/js/demo_mode.js
Normal file
@@ -0,0 +1,371 @@
|
||||
/**
|
||||
* 글벗 Light v2.1 — 시연용 데모 모드
|
||||
*
|
||||
* 배치:
|
||||
* 1. output/result/ 폴더에 HTML 파일:
|
||||
* - report.html, brief_1.html, brief_2.html, slide.html
|
||||
* 2. index.html에 generator.js 뒤에 추가:
|
||||
* <script src="/static/js/demo_mode.js"></script>
|
||||
* 3. 시연 후 DEMO_MODE = false
|
||||
*/
|
||||
|
||||
const DEMO_MODE = true;
|
||||
|
||||
// ===== 가짜 목차 데이터 (보고서 6장) =====
|
||||
const DEMO_TOC_ITEMS = [
|
||||
{
|
||||
num: '1장',
|
||||
title: '한국 토목 엔지니어링 소프트웨어 시장 현황',
|
||||
guide: '국내 토목 설계 시장의 AutoCAD 독점 구조와 시장 점유율 현황을 분석하고, 독점적 지위의 배경과 그로 인한 구조적 문제점을 도출한다.',
|
||||
keywords: ['시장 점유율', 'AutoCAD 독점', '라이선스 비용', '기술 종속'],
|
||||
contents: [
|
||||
'📊 국내 CAD 소프트웨어 시장 점유율 비교표 (핵심역량강화방안.hwp p.8, 새로운시대준비된우리.hwp p.3)',
|
||||
'📊 연간 라이선스 비용 추이표 (핵심역량강화방안.hwp p.12)'
|
||||
]
|
||||
},
|
||||
{
|
||||
num: '2장',
|
||||
title: 'AutoCAD, 토목설계에 정말 적합한가?',
|
||||
guide: '건축과 토목의 근본적 차이를 분석하고, AutoCAD의 토목 분야 적용 시 기능적 한계와 기술적 비효율을 검증한다.',
|
||||
keywords: ['레고와 찰흙', '비정형 형상', '데이터 단절', 'BIM 부조화'],
|
||||
contents: [
|
||||
'📊 건축 vs 토목 설계 특성 비교표 (핵심역량강화방안.hwp p.15)',
|
||||
'🖼️ 비정형 지형 모델링 사례 이미지 (발표자료.pptx p.7)',
|
||||
'📊 측량→설계→시공 데이터 흐름도 (새로운시대준비된우리.hwp p.11)'
|
||||
]
|
||||
},
|
||||
{
|
||||
num: '3장',
|
||||
title: '시장의 족쇄: 관행인가, 필수인가?',
|
||||
guide: '익숙함의 함정과 전환 비용 인식, 라이선스 비용 압박 구조를 분석하여 독점 유지 메커니즘의 실체를 파악한다.',
|
||||
keywords: ['익숙함의 함정', '삼중 잠금', '비용 압박', '기술적 우위 허상'],
|
||||
contents: [
|
||||
'📊 삼중 잠금 효과 구조도 (핵심역량강화방안.hwp p.22)',
|
||||
'📊 AutoCAD vs 대안 SW 기능 비교표 (발표자료.pptx p.12, 핵심역량강화방안.hwp p.25)'
|
||||
]
|
||||
},
|
||||
{
|
||||
num: '4장',
|
||||
title: '지식재산권: 문제점과 해결 방안',
|
||||
guide: '비공개 DWG 포맷으로 인한 성과물 소유권 왜곡, 기술 종속, 데이터 주권 침해 문제를 분석하고 해결 방안을 제시한다.',
|
||||
keywords: ['DWG 종속', '성과물 소유권', '데이터 주권', '개방형 포맷'],
|
||||
contents: [
|
||||
'📊 DWG vs 개방형 포맷 비교표 (핵심역량강화방안.hwp p.30)',
|
||||
'🖼️ 데이터 종속 구조 다이어그램 (발표자료.pptx p.15)',
|
||||
'📊 공공조달 포맷 현황표 (새로운시대준비된우리.hwp p.18)'
|
||||
]
|
||||
},
|
||||
{
|
||||
num: '5장',
|
||||
title: '새로운 가능성: 대안을 찾아서',
|
||||
guide: '토목 엔지니어의 핵심 요구사항을 정리하고, 시장의 대안 소프트웨어 옵션 및 국산 솔루션 개발의 전략적 중요성을 분석한다.',
|
||||
keywords: ['핵심 요구사항', 'Civil 3D', 'OpenRoads', '국산 솔루션'],
|
||||
contents: [
|
||||
'📊 대안 소프트웨어 기능·비용 비교표 (핵심역량강화방안.hwp p.35, 발표자료.pptx p.18)',
|
||||
'📊 단계별 전환 로드맵 (새로운시대준비된우리.hwp p.22)',
|
||||
'🖼️ 국산 솔루션 아키텍처 구성도 (발표자료.pptx p.20)'
|
||||
]
|
||||
},
|
||||
{
|
||||
num: '6장',
|
||||
title: '결론 및 시사점',
|
||||
guide: '분석 결과를 종합하여 단계적 전환 로드맵을 제시하고, 비용 절감·데이터 주권 확보·기술 경쟁력 강화의 기대효과를 도출한다.',
|
||||
keywords: ['전환 로드맵', '비용 절감', '데이터 주권', '기술 경쟁력'],
|
||||
contents: [
|
||||
'📊 기대효과 종합표 (핵심역량강화방안.hwp p.40, 새로운시대준비된우리.hwp p.25)',
|
||||
'📊 Q1~Q4 실행 일정표 (발표자료.pptx p.22)'
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
// ===== 문서 유형별 HTML 경로 =====
|
||||
const DEMO_HTML_MAP = {
|
||||
'report': '/static/result/report.html',
|
||||
'briefing_1': '/static/result/brief_1.html',
|
||||
'briefing_2': '/static/result/brief_2.html',
|
||||
'slide': '/static/result/slide.html'
|
||||
};
|
||||
|
||||
|
||||
// ===== fetch 인터셉터 =====
|
||||
if (DEMO_MODE) {
|
||||
const _originalFetch = window.fetch;
|
||||
|
||||
window.fetch = async function(url, options) {
|
||||
|
||||
// --- /api/doc-types → presentation 활성화 ---
|
||||
if (typeof url === 'string' && url.includes('/api/doc-types') && !options) {
|
||||
const resp = await _originalFetch(url, options);
|
||||
const data = await resp.json();
|
||||
// presentation enabled로 변경
|
||||
const pres = data.find(t => t.id === 'presentation');
|
||||
if (pres) { pres.enabled = true; pres.badge = ''; }
|
||||
return { ok: true, json: async () => data };
|
||||
}
|
||||
|
||||
// --- /api/generate-toc → 가짜 목차 반환 ---
|
||||
if (typeof url === 'string' && url.includes('/api/generate-toc')) {
|
||||
console.log('[DEMO] 목차 생성 인터셉트');
|
||||
await new Promise(r => setTimeout(r, 800));
|
||||
return {
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
success: true,
|
||||
toc_items: DEMO_TOC_ITEMS
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
// --- /api/generate-report-from-toc → 데모 HTML 반환 ---
|
||||
if (typeof url === 'string' && url.includes('/api/generate-report-from-toc')) {
|
||||
console.log('[DEMO] 보고서 생성 인터셉트');
|
||||
|
||||
const docType = window.currentDocType || 'report';
|
||||
const htmlPath = _resolveHtmlPath(docType);
|
||||
|
||||
try {
|
||||
const resp = await _originalFetch(htmlPath);
|
||||
const html = await resp.text();
|
||||
return { ok: true, json: async () => ({ success: true, html }) };
|
||||
} catch (e) {
|
||||
console.error('[DEMO] HTML 로드 실패:', htmlPath, e);
|
||||
return { ok: true, json: async () => ({ success: false, error: '데모 HTML 로드 실패' }) };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// --- 그 외: 원래 fetch ---
|
||||
return _originalFetch(url, options);
|
||||
};
|
||||
|
||||
console.log('[DEMO] ✅ 데모 모드 활성화');
|
||||
}
|
||||
|
||||
// ===== HTML 경로 결정 =====
|
||||
function _resolveHtmlPath(docType) {
|
||||
if (docType === 'report' || (docType && docType.includes('보고'))) {
|
||||
return DEMO_HTML_MAP['report'];
|
||||
}
|
||||
if (docType === 'slide' || docType === 'ppt' || docType === 'presentation' || (docType && (docType.includes('발표') || docType.includes('slide')))) {
|
||||
return DEMO_HTML_MAP['slide'];
|
||||
}
|
||||
if (docType === 'briefing' || (docType && docType.includes('기획'))) {
|
||||
// briefing_1 vs briefing_2 판별: currentPageConfig 또는 기본값
|
||||
const pageConfig = window.currentPageConfig || '';
|
||||
if (pageConfig === 'body-only') {
|
||||
return DEMO_HTML_MAP['briefing_1'];
|
||||
}
|
||||
return DEMO_HTML_MAP['briefing_2'];
|
||||
}
|
||||
return DEMO_HTML_MAP['report'];
|
||||
}
|
||||
|
||||
|
||||
// ===== 문서 유형 전환 팝업 =====
|
||||
async function demoConvertDocument(targetType) {
|
||||
if (!DEMO_MODE) return false;
|
||||
|
||||
const typeNames = {
|
||||
'report': '📄 보고서',
|
||||
'briefing': '📋 기획서',
|
||||
'briefing_1': '📋 기획서 (1p)',
|
||||
'briefing_2': '📋 기획서 (2p)',
|
||||
'slide': '📊 발표자료',
|
||||
'ppt': '📊 발표자료'
|
||||
};
|
||||
|
||||
const typeName = typeNames[targetType] || targetType;
|
||||
|
||||
const overlay = document.createElement('div');
|
||||
overlay.id = 'demoConvertOverlay';
|
||||
overlay.innerHTML = `
|
||||
<div class="demo-convert-popup">
|
||||
<div class="demo-convert-header">
|
||||
<span class="demo-convert-icon">🔄</span>
|
||||
<span>${typeName} 변환 중</span>
|
||||
</div>
|
||||
<div class="demo-convert-steps" id="demoSteps">
|
||||
<div class="demo-step"><span class="demo-step-icon">⏳</span><span>원본 콘텐츠 분석</span></div>
|
||||
<div class="demo-step"><span class="demo-step-icon">⏳</span><span>문서 구조 재설계</span></div>
|
||||
<div class="demo-step"><span class="demo-step-icon">⏳</span><span>${typeName} 형식 적용</span></div>
|
||||
<div class="demo-step"><span class="demo-step-icon">⏳</span><span>레이아웃 최적화</span></div>
|
||||
<div class="demo-step"><span class="demo-step-icon">⏳</span><span>최종 퍼블리싱</span></div>
|
||||
</div>
|
||||
<div class="demo-convert-progress">
|
||||
<div class="demo-progress-bar" id="demoProgressBar"></div>
|
||||
</div>
|
||||
<div class="demo-convert-status" id="demoStatus">준비 중...</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
const steps = overlay.querySelectorAll('.demo-step');
|
||||
const progressBar = overlay.querySelector('#demoProgressBar');
|
||||
const statusEl = overlay.querySelector('#demoStatus');
|
||||
|
||||
const msgs = [
|
||||
'원본 콘텐츠를 분석하고 있습니다...',
|
||||
'문서 구조를 재설계하고 있습니다...',
|
||||
`${typeName} 형식을 적용하고 있습니다...`,
|
||||
'레이아웃을 최적화하고 있습니다...',
|
||||
'최종 퍼블리싱 중입니다...'
|
||||
];
|
||||
|
||||
for (let i = 0; i < steps.length; i++) {
|
||||
steps[i].querySelector('.demo-step-icon').textContent = '🔄';
|
||||
steps[i].classList.add('running');
|
||||
statusEl.textContent = msgs[i];
|
||||
progressBar.style.width = ((i + 1) / steps.length * 100) + '%';
|
||||
|
||||
await new Promise(r => setTimeout(r, 600 + Math.random() * 400));
|
||||
|
||||
steps[i].querySelector('.demo-step-icon').textContent = '✅';
|
||||
steps[i].classList.remove('running');
|
||||
steps[i].classList.add('done');
|
||||
}
|
||||
|
||||
// 발표자료면 16:9 비율로 전환, 아니면 A4 복원
|
||||
const a4Preview = document.getElementById('a4Preview');
|
||||
const a4Wrapper = document.getElementById('a4Wrapper');
|
||||
if (a4Preview && a4Wrapper) {
|
||||
if (targetType === 'slide' || targetType === 'presentation') {
|
||||
a4Preview.style.width = '1100px';
|
||||
a4Preview.style.height = '620px';
|
||||
a4Preview.style.aspectRatio = '16/9';
|
||||
a4Wrapper.style.width = '1100px';
|
||||
} else {
|
||||
a4Preview.style.width = '';
|
||||
a4Preview.style.height = '';
|
||||
a4Preview.style.aspectRatio = '';
|
||||
a4Wrapper.style.width = '';
|
||||
}
|
||||
}
|
||||
|
||||
statusEl.textContent = '✅ 변환 완료!';
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
|
||||
// HTML 로드 & 프리뷰 표시
|
||||
const htmlPath = _resolveHtmlPath(targetType);
|
||||
try {
|
||||
const resp = await fetch(htmlPath);
|
||||
const html = await resp.text();
|
||||
|
||||
window.generatedHTML = html;
|
||||
if (typeof generatedHTML !== 'undefined') generatedHTML = html;
|
||||
|
||||
const placeholder = document.getElementById('placeholder');
|
||||
if (placeholder) placeholder.style.display = 'none';
|
||||
|
||||
const frame = document.getElementById('previewFrame');
|
||||
if (frame) {
|
||||
frame.classList.add('active');
|
||||
frame.srcdoc = html;
|
||||
setTimeout(setupIframeSelection, 500);
|
||||
}
|
||||
|
||||
const feedbackBar = document.getElementById('feedbackBar');
|
||||
if (feedbackBar) feedbackBar.classList.add('show');
|
||||
|
||||
if (typeof setStatus === 'function') setStatus('생성 완료', true);
|
||||
} catch (e) {
|
||||
console.error('[DEMO] 변환 HTML 로드 실패:', e);
|
||||
}
|
||||
|
||||
overlay.classList.add('fade-out');
|
||||
setTimeout(() => overlay.remove(), 300);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// ===== 팝업 CSS 주입 (배지 없음) =====
|
||||
if (DEMO_MODE) {
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
#demoConvertOverlay {
|
||||
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: rgba(0,0,0,0.6); backdrop-filter: blur(4px);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
z-index: 99999; animation: demoFadeIn 0.2s ease;
|
||||
}
|
||||
#demoConvertOverlay.fade-out { animation: demoFadeOut 0.3s ease forwards; }
|
||||
|
||||
.demo-convert-popup {
|
||||
background: #1e1e2e; color: #e0e0e0;
|
||||
border-radius: 16px; padding: 35px 40px;
|
||||
width: 420px; box-shadow: 0 20px 60px rgba(0,0,0,0.5);
|
||||
border: 1px solid rgba(255,255,255,0.1);
|
||||
}
|
||||
.demo-convert-header {
|
||||
font-size: 18pt; font-weight: 800; margin-bottom: 25px;
|
||||
display: flex; align-items: center; gap: 12px; color: #fff;
|
||||
}
|
||||
.demo-convert-icon { font-size: 22pt; }
|
||||
.demo-convert-steps { display: flex; flex-direction: column; gap: 10px; margin-bottom: 20px; }
|
||||
.demo-step {
|
||||
display: flex; align-items: center; gap: 12px;
|
||||
padding: 10px 14px; border-radius: 8px;
|
||||
background: rgba(255,255,255,0.03);
|
||||
font-size: 11pt; color: #d1d1d1; transition: all 0.3s ease;
|
||||
}
|
||||
.demo-step.running { background: rgba(59,130,246,0.15); color: #93c5fd; font-weight: 600; }
|
||||
.demo-step.done { background: rgba(34,197,94,0.1); color: #86efac; }
|
||||
.demo-step-icon { font-size: 14pt; min-width: 24px; text-align: center; }
|
||||
.demo-convert-progress {
|
||||
height: 4px; background: rgba(255,255,255,0.1);
|
||||
border-radius: 2px; overflow: hidden; margin-bottom: 12px;
|
||||
}
|
||||
.demo-progress-bar {
|
||||
height: 100%; width: 0%;
|
||||
background: linear-gradient(90deg, #3b82f6, #22c55e);
|
||||
border-radius: 2px; transition: width 0.5s ease;
|
||||
}
|
||||
.demo-convert-status { font-size: 10pt; color: #cfcfcf; text-align: center; }
|
||||
|
||||
@keyframes demoFadeIn { from { opacity: 0; } to { opacity: 1; } }
|
||||
@keyframes demoFadeOut { from { opacity: 1; } to { opacity: 0; } }
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
|
||||
// ===== generate() 패치 — 문서 유형 전환 시 팝업 =====
|
||||
if (DEMO_MODE) {
|
||||
let _lastDemoKey = null;
|
||||
|
||||
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
const _origGenerate = window.generate;
|
||||
|
||||
if (typeof _origGenerate !== 'function') {
|
||||
console.warn('[DEMO] generate() 함수를 찾을 수 없음 — 패치 스킵');
|
||||
return;
|
||||
}
|
||||
|
||||
window.generate = async function() {
|
||||
const docType = window.currentDocType || 'report';
|
||||
const pageConfig = window.currentPageConfig || '';
|
||||
const demoKey = docType + '|' + pageConfig;
|
||||
|
||||
// 이미 결과 있으면 → 데모 전환
|
||||
if (window.generatedHTML && window.generatedHTML.trim() !== '') {
|
||||
// 완전히 같은 조합이면 원래 흐름
|
||||
if (_lastDemoKey === demoKey) {
|
||||
return _origGenerate();
|
||||
}
|
||||
|
||||
let demoType = docType;
|
||||
if (docType === 'presentation') demoType = 'slide';
|
||||
|
||||
await demoConvertDocument(demoType);
|
||||
_lastDemoKey = demoKey;
|
||||
return;
|
||||
}
|
||||
|
||||
// 첫 생성 → 원래 흐름 (fetch 인터셉터가 처리)
|
||||
_lastDemoKey = demoKey;
|
||||
return _origGenerate();
|
||||
};
|
||||
|
||||
console.log('[DEMO] ✅ generate() 패치 완료');
|
||||
});
|
||||
}
|
||||
587
03. Code/geulbeot_10th/static/js/doc_type.js
Normal file
587
03. Code/geulbeot_10th/static/js/doc_type.js
Normal file
@@ -0,0 +1,587 @@
|
||||
// ===== 문서 유형 분석 관련 =====
|
||||
const ANALYSIS_STEPS = [
|
||||
{id: 1, name: "문서 파싱"},
|
||||
{id: 2, name: "레이아웃 분석"},
|
||||
{id: 3, name: "맥락 분석"},
|
||||
{id: 4, name: "구조 분석"},
|
||||
{id: 5, name: "템플릿 추출"},
|
||||
{id: 6, name: "최종 검증"}
|
||||
];
|
||||
|
||||
|
||||
// ===== 문서 유형 로드 =====
|
||||
async function loadDocTypes() {
|
||||
const container = document.getElementById('docTypeList');
|
||||
container.classList.add('loading');
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/doc-types');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.error) {
|
||||
throw new Error(data.error);
|
||||
}
|
||||
|
||||
docTypes = data;
|
||||
renderDocTypeList();
|
||||
|
||||
// 첫 번째 활성화된 유형 선택
|
||||
const firstEnabled = docTypes.find(t => t.enabled);
|
||||
if (firstEnabled) {
|
||||
selectDocType(firstEnabled.id);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('문서 유형 로드 실패:', error);
|
||||
container.innerHTML = '<div style="color:var(--ui-error);font-size:12px;">로드 실패</div>';
|
||||
} finally {
|
||||
container.classList.remove('loading');
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 문서 유형 리스트 렌더링 =====
|
||||
function renderDocTypeList() {
|
||||
const container = document.getElementById('docTypeList');
|
||||
|
||||
// 기본 유형과 사용자 유형 분리
|
||||
const defaultTypes = docTypes.filter(t => t.isDefault);
|
||||
const userTypes = docTypes.filter(t => !t.isDefault);
|
||||
|
||||
let html = defaultTypes.map(type => createDocTypeHTML(type)).join('');
|
||||
|
||||
if (userTypes.length > 0) {
|
||||
html += '<div class="doc-type-divider"></div>';
|
||||
html += userTypes.map(type => createDocTypeHTML(type)).join('');
|
||||
}
|
||||
|
||||
container.innerHTML = html;
|
||||
attachDocTypeEvents();
|
||||
}
|
||||
|
||||
// ===== 개별 문서 유형 HTML 생성 =====
|
||||
function createDocTypeHTML(type) {
|
||||
const isSelected = currentDocType === type.id;
|
||||
const isDisabled = !type.enabled;
|
||||
|
||||
return `
|
||||
<div class="doc-type-item ${isSelected ? 'selected' : ''} ${isDisabled ? 'disabled' : ''}"
|
||||
data-type="${type.id}">
|
||||
<input type="radio" name="docType" ${isSelected ? 'checked' : ''} ${isDisabled ? 'disabled' : ''}>
|
||||
<span class="label">${type.icon} ${type.name}</span>
|
||||
${type.badge ? `<span class="badge">${type.badge}</span>` : ''}
|
||||
${!type.isDefault ? `<button class="delete-btn" onclick="event.stopPropagation(); deleteDocType('${type.id}')" title="삭제">✕</button>` : ''}
|
||||
|
||||
<div class="doc-type-preview">
|
||||
<div class="preview-thumbnail ${type.thumbnailType || 'custom'}">
|
||||
${thumbnailTemplates[type.thumbnailType] || thumbnailTemplates.custom}
|
||||
</div>
|
||||
<div class="preview-title">${type.name}</div>
|
||||
<div class="preview-desc">${type.description || ''}</div>
|
||||
<div class="preview-features">
|
||||
${(type.features || []).map(f => `
|
||||
<div class="preview-feature">
|
||||
<span class="icon">${f.icon || '✓'}</span> ${f.text || f}
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// ===== 문서 유형 이벤트 연결 =====
|
||||
function attachDocTypeEvents() {
|
||||
document.querySelectorAll('.doc-type-item').forEach(item => {
|
||||
if (item.classList.contains('disabled')) return;
|
||||
|
||||
// 클릭 이벤트
|
||||
item.onclick = () => selectDocType(item.dataset.type);
|
||||
|
||||
// 호버 프리뷰 이벤트
|
||||
const preview = item.querySelector('.doc-type-preview');
|
||||
if (preview) {
|
||||
item.addEventListener('mouseenter', () => {
|
||||
const rect = item.getBoundingClientRect();
|
||||
preview.style.top = (rect.top + rect.height / 2 - 150) + 'px';
|
||||
preview.style.left = (rect.left - 295) + 'px';
|
||||
preview.classList.add('show');
|
||||
});
|
||||
item.addEventListener('mouseleave', () => preview.classList.remove('show'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ===== 문서 유형 선택 =====
|
||||
function selectDocType(typeId) {
|
||||
const type = docTypes.find(t => t.id === typeId);
|
||||
if (!type || !type.enabled) return;
|
||||
|
||||
currentDocType = typeId;
|
||||
|
||||
// 선택 상태 업데이트
|
||||
document.querySelectorAll('.doc-type-item').forEach(item => {
|
||||
const isSelected = item.dataset.type === typeId;
|
||||
item.classList.toggle('selected', isSelected);
|
||||
const radio = item.querySelector('input[type="radio"]');
|
||||
if (radio) radio.checked = isSelected;
|
||||
});
|
||||
|
||||
// 옵션 렌더링
|
||||
renderDocTypeOptions(type);
|
||||
|
||||
// 버튼 텍스트 업데이트 (입력 상태도 반영)
|
||||
const generateBtnText = document.getElementById('generateBtnText');
|
||||
const hasFolder = typeof folderPath !== 'undefined' && folderPath && folderPath.trim() !== '';
|
||||
const hasLinks = typeof referenceLinks !== 'undefined' && referenceLinks && referenceLinks.length > 0;
|
||||
const hasHtml = typeof inputContent !== 'undefined' && inputContent && inputContent.trim() !== '';
|
||||
|
||||
if (type.generateFlow === 'draft-first' && (hasFolder || (hasLinks && hasHtml))) {
|
||||
generateBtnText.textContent = '📋 목차 확인하기';
|
||||
} else {
|
||||
generateBtnText.textContent = '🚀 생성하기';
|
||||
}
|
||||
|
||||
console.log('문서 유형 선택:', typeId);
|
||||
}
|
||||
|
||||
// ===== 문서 유형별 옵션 렌더링 =====
|
||||
function renderDocTypeOptions(type) {
|
||||
const container = document.getElementById('docTypeOptionsContainer');
|
||||
|
||||
if (!type.options) {
|
||||
container.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '';
|
||||
|
||||
// 페이지 구성 (기획서 - 새로운 구조)
|
||||
if (type.options.pageConfig) {
|
||||
const config = type.options.pageConfig;
|
||||
html += `
|
||||
<div class="option-section">
|
||||
<div class="option-title">페이지 구성</div>
|
||||
<div class="option-group">
|
||||
${config.choices.map((opt, idx) => `
|
||||
<div class="option-item ${opt.default ? 'selected' : ''}" onclick="selectPageConfig('${opt.value}', ${idx})">
|
||||
<input type="radio" name="pageConfig" value="${opt.value}" id="pageConfig${idx}" ${opt.default ? 'checked' : ''}>
|
||||
<label for="pageConfig${idx}">${opt.label}</label>
|
||||
${opt.hasInput ? `
|
||||
<input type="number"
|
||||
id="attachPages"
|
||||
class="page-input"
|
||||
value="${opt.inputDefault || 1}"
|
||||
min="${opt.inputMin || 1}"
|
||||
max="${opt.inputMax || 10}"
|
||||
onclick="event.stopPropagation(); selectPageConfig('${opt.value}', ${idx});"
|
||||
onchange="updateAttachPages(this.value)">
|
||||
<span class="page-input-suffix">${opt.inputSuffix || ''}</span>
|
||||
` : ''}
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// 기존 페이지 옵션 (하위 호환)
|
||||
if (type.options.pageOptions) {
|
||||
html += `
|
||||
<div class="option-section">
|
||||
<div class="option-title">페이지 구성</div>
|
||||
<div class="option-group">
|
||||
${type.options.pageOptions.map((opt, idx) => `
|
||||
<div class="option-item ${opt.default ? 'selected' : ''}" onclick="selectPageOption('${opt.value}')">
|
||||
<input type="radio" name="pages" value="${opt.value}" id="page${idx}" ${opt.default ? 'checked' : ''}>
|
||||
<label for="page${idx}">${opt.label}</label>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// 구성 요소 (보고서)
|
||||
if (type.options.components) {
|
||||
html += `
|
||||
<div class="option-section">
|
||||
<div class="option-title">보고서 구성</div>
|
||||
<div class="option-group">
|
||||
${type.options.components.map(comp => `
|
||||
<div class="option-item" style="cursor:${comp.required ? 'default' : 'pointer'}; ${comp.required ? 'opacity:0.6;' : ''}">
|
||||
<input type="checkbox" id="${comp.id}" ${comp.default ? 'checked' : ''} ${comp.required ? 'disabled' : ''}>
|
||||
<label for="${comp.id}">${comp.icon} ${comp.label}</label>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// 슬라이드 수 (발표자료)
|
||||
if (type.options.slideCount) {
|
||||
html += `
|
||||
<div class="option-section">
|
||||
<div class="option-title">슬라이드 수</div>
|
||||
<div class="option-group">
|
||||
${type.options.slideCount.map((opt, idx) => `
|
||||
<div class="option-item ${opt.default ? 'selected' : ''}" onclick="selectSlideCount('${opt.value}')">
|
||||
<input type="radio" name="slideCount" value="${opt.value}" id="slide${idx}" ${opt.default ? 'checked' : ''}>
|
||||
<label for="slide${idx}">${opt.label}</label>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
// ===== 페이지 구성 선택 (새로운 방식) =====
|
||||
var currentPageConfig = 'body-attach';
|
||||
let attachPageCount = 1;
|
||||
|
||||
function selectPageConfig(value, idx) {
|
||||
currentPageConfig = value;
|
||||
|
||||
document.querySelectorAll('#docTypeOptionsContainer .option-item').forEach(item => {
|
||||
const radio = item.querySelector('input[type="radio"][name="pageConfig"]');
|
||||
if (radio) {
|
||||
const isSelected = radio.value === value;
|
||||
item.classList.toggle('selected', isSelected);
|
||||
radio.checked = isSelected;
|
||||
}
|
||||
});
|
||||
|
||||
// 첨부 페이지 입력 활성화/비활성화
|
||||
const attachInput = document.getElementById('attachPages');
|
||||
if (attachInput) {
|
||||
attachInput.disabled = (value === 'body-only');
|
||||
attachInput.style.opacity = (value === 'body-only') ? '0.5' : '1';
|
||||
}
|
||||
}
|
||||
|
||||
function updateAttachPages(value) {
|
||||
attachPageCount = parseInt(value) || 1;
|
||||
console.log('첨부 페이지 수:', attachPageCount);
|
||||
}
|
||||
|
||||
// ===== 슬라이드 수 선택 =====
|
||||
function selectSlideCount(count) {
|
||||
document.querySelectorAll('#docTypeOptionsContainer .option-item').forEach(item => {
|
||||
const radio = item.querySelector('input[type="radio"][name="slideCount"]');
|
||||
if (radio) {
|
||||
const isSelected = radio.value === count;
|
||||
item.classList.toggle('selected', isSelected);
|
||||
radio.checked = isSelected;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 모달 열기
|
||||
function openDocTypeModal() {
|
||||
resetDocTypeModal();
|
||||
document.getElementById('addDocTypeModal').classList.add('active');
|
||||
}
|
||||
|
||||
// 모달 닫기
|
||||
function closeAddDocTypeModal() {
|
||||
document.getElementById('addDocTypeModal').classList.remove('active');
|
||||
resetDocTypeModal();
|
||||
}
|
||||
|
||||
// 모달 초기화
|
||||
function resetDocTypeModal() {
|
||||
analysisResult = null;
|
||||
|
||||
// Step 표시 초기화
|
||||
document.getElementById('docTypeStep1').style.display = 'block';
|
||||
document.getElementById('docTypeStep2').style.display = 'none';
|
||||
document.getElementById('docTypeStep3').style.display = 'none';
|
||||
|
||||
// 제목 초기화
|
||||
document.getElementById('addDocTypeModalTitle').textContent = '📄 문서 유형 추가';
|
||||
|
||||
// 입력 초기화
|
||||
document.getElementById('newDocTypeName').value = '';
|
||||
document.getElementById('newDocTypeDesc').value = '';
|
||||
document.getElementById('newDocTypeFile').value = '';
|
||||
|
||||
// 버튼 초기화
|
||||
const footer = document.getElementById('docTypeModalFooter');
|
||||
footer.style.display = 'flex';
|
||||
|
||||
const actionBtn = document.getElementById('docTypeActionBtn');
|
||||
actionBtn.textContent = '분석 시작';
|
||||
actionBtn.onclick = startDocTypeAnalysis;
|
||||
}
|
||||
|
||||
// 분석 시작
|
||||
function startDocTypeAnalysis() {
|
||||
const name = document.getElementById('newDocTypeName').value.trim();
|
||||
const file = document.getElementById('newDocTypeFile').files[0];
|
||||
|
||||
if (!name) {
|
||||
alert('문서 유형 이름을 입력해주세요.');
|
||||
return;
|
||||
}
|
||||
if (!file) {
|
||||
alert('샘플 문서를 선택해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 2로 전환
|
||||
document.getElementById('docTypeStep1').style.display = 'none';
|
||||
document.getElementById('docTypeStep2').style.display = 'block';
|
||||
document.getElementById('addDocTypeModalTitle').textContent = '🔄 문서 분석 중...';
|
||||
|
||||
// 푸터 버튼 숨기기
|
||||
document.getElementById('docTypeModalFooter').style.display = 'none';
|
||||
|
||||
// 진행 단계 UI 생성
|
||||
renderAnalysisSteps();
|
||||
|
||||
// 분석 시작
|
||||
performAnalysis(name, file);
|
||||
}
|
||||
|
||||
// 진행 단계 렌더링
|
||||
function renderAnalysisSteps() {
|
||||
const container = document.getElementById('analysisSteps');
|
||||
container.innerHTML = ANALYSIS_STEPS.map(step => `
|
||||
<div class="analysis-step" id="analysisStep${step.id}">
|
||||
<span class="step-icon">⏳</span>
|
||||
<span class="step-name">Step ${step.id}: ${step.name}</span>
|
||||
<span class="step-status">대기</span>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// 단계 상태 업데이트 (메시지 지원)
|
||||
function updateAnalysisStep(stepId, status, message = '') {
|
||||
const stepEl = document.getElementById(`analysisStep${stepId}`);
|
||||
if (!stepEl) return;
|
||||
|
||||
const iconEl = stepEl.querySelector('.step-icon');
|
||||
const statusEl = stepEl.querySelector('.step-status');
|
||||
|
||||
if (status === 'running') {
|
||||
iconEl.textContent = '🔄';
|
||||
iconEl.classList.add('spinning');
|
||||
statusEl.textContent = message || '진행중...';
|
||||
stepEl.classList.add('running');
|
||||
} else if (status === 'done') {
|
||||
iconEl.textContent = '✅';
|
||||
iconEl.classList.remove('spinning');
|
||||
statusEl.textContent = message || '완료';
|
||||
stepEl.classList.remove('running');
|
||||
stepEl.classList.add('done');
|
||||
} else if (status === 'error') {
|
||||
iconEl.textContent = '❌';
|
||||
iconEl.classList.remove('spinning');
|
||||
statusEl.textContent = message || '실패';
|
||||
stepEl.classList.add('error');
|
||||
}
|
||||
|
||||
// 진행률 업데이트
|
||||
const doneCount = document.querySelectorAll('.analysis-step.done').length;
|
||||
const progress = Math.round((doneCount / ANALYSIS_STEPS.length) * 100);
|
||||
document.getElementById('analysisProgressBar').style.width = progress + '%';
|
||||
document.getElementById('analysisProgressText').textContent = `${progress}% 완료`;
|
||||
}
|
||||
|
||||
// 분석 수행 (SSE 방식)
|
||||
async function performAnalysis(name, file) {
|
||||
console.log('🚀 performAnalysis 시작 (SSE 모드)');
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('name', name);
|
||||
formData.append('description', document.getElementById('newDocTypeDesc').value.trim());
|
||||
formData.append('file', file);
|
||||
|
||||
// XMLHttpRequest로 SSE 연결 (FormData 전송)
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', '/api/doc-types/analyze-stream', true);
|
||||
|
||||
let buffer = '';
|
||||
|
||||
xhr.onprogress = function() {
|
||||
const newData = xhr.responseText.substring(buffer.length);
|
||||
buffer = xhr.responseText;
|
||||
|
||||
// SSE 메시지 파싱
|
||||
const lines = newData.split('\n');
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('data: ')) {
|
||||
try {
|
||||
const data = JSON.parse(line.substring(6));
|
||||
handleSSEMessage(data);
|
||||
} catch (e) {
|
||||
console.warn('SSE 파싱 오류:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onerror = function() {
|
||||
console.error('❌ SSE 연결 오류');
|
||||
alert('서버 연결 오류');
|
||||
closeAddDocTypeModal();
|
||||
};
|
||||
|
||||
xhr.ontimeout = function() {
|
||||
console.error('❌ SSE 타임아웃');
|
||||
alert('분석 시간 초과');
|
||||
closeAddDocTypeModal();
|
||||
};
|
||||
|
||||
xhr.timeout = 120000; // 2분
|
||||
xhr.send(formData);
|
||||
}
|
||||
|
||||
// SSE 메시지 처리
|
||||
function handleSSEMessage(data) {
|
||||
console.log('📨 SSE:', data);
|
||||
|
||||
switch(data.type) {
|
||||
case 'progress':
|
||||
updateAnalysisStep(data.step, data.status, data.message);
|
||||
break;
|
||||
|
||||
case 'result':
|
||||
console.log('✅ 분석 완료!');
|
||||
analysisResult = data.data;
|
||||
showAnalysisResult(data.data);
|
||||
break;
|
||||
|
||||
case 'error':
|
||||
console.error('❌ 분석 오류:', data.error);
|
||||
alert('분석 중 오류: ' + data.error.message);
|
||||
closeAddDocTypeModal();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 분석 결과 표시 (v2.0 맥락 기반)
|
||||
function showAnalysisResult(data) {
|
||||
document.getElementById('docTypeStep2').style.display = 'none';
|
||||
document.getElementById('docTypeStep3').style.display = 'block';
|
||||
document.getElementById('addDocTypeModalTitle').textContent = '✅ 분석 완료';
|
||||
|
||||
// 푸터 버튼 복원 및 변경
|
||||
const footer = document.getElementById('docTypeModalFooter');
|
||||
footer.style.display = 'flex';
|
||||
|
||||
const actionBtn = document.getElementById('docTypeActionBtn');
|
||||
actionBtn.textContent = '저장';
|
||||
actionBtn.onclick = saveAnalyzedDocType;
|
||||
|
||||
// v2.0: 맥락 정보
|
||||
const context = data.context || {};
|
||||
const structure = data.structure || {};
|
||||
const config = data.config || {};
|
||||
|
||||
// 요약 표시
|
||||
document.getElementById('analysisResultSummary').innerHTML = `
|
||||
<div class="summary-item"><strong>유형:</strong> ${context.documentType || '?'}</div>
|
||||
<div class="summary-item"><strong>페이지:</strong> ${structure.pageEstimate || '?'}p</div>
|
||||
<div class="summary-item"><strong>섹션:</strong> ${(structure.sectionGuides || structure.sections)?.length || '?'}개</div>
|
||||
`;
|
||||
|
||||
// 상세 결과 표시
|
||||
const sections = structure.sectionGuides || structure.sections || [];
|
||||
|
||||
document.getElementById('analysisResultToc').innerHTML = `
|
||||
<div style="margin-bottom: 16px; padding: 12px; background: var(--ui-bg); border-radius: 6px;">
|
||||
<h5 style="margin-bottom: 8px; font-size: 11px; color: var(--ui-accent);">📋 문서 맥락</h5>
|
||||
<p style="font-size: 11px; color: var(--ui-dim); margin-bottom: 4px;"><strong>목적:</strong> ${context.purpose || '-'}</p>
|
||||
<p style="font-size: 11px; color: var(--ui-dim); margin-bottom: 4px;"><strong>대상:</strong> ${context.audience || '-'}</p>
|
||||
<p style="font-size: 11px; color: var(--ui-dim);"><strong>톤:</strong> ${context.tone || '-'}</p>
|
||||
</div>
|
||||
|
||||
<h5 style="margin-bottom: 10px; font-size: 12px; color: var(--ui-dim);">📐 문서 구조</h5>
|
||||
<p style="font-size: 11px; color: var(--ui-text); margin-bottom: 10px;"><strong>논리 흐름:</strong> ${structure.logicFlow || '-'}</p>
|
||||
|
||||
${sections.length > 0 ? `
|
||||
<ul class="toc-list">
|
||||
${sections.map(s => `
|
||||
<li class="toc-level-${s.level || 1}">
|
||||
<strong>${s.name || s.title}</strong>
|
||||
<span style="font-size: 10px; color: var(--ui-dim); display: block;">${s.role || ''}</span>
|
||||
</li>
|
||||
`).join('')}
|
||||
</ul>
|
||||
` : `
|
||||
<p style="font-size: 12px; color: var(--ui-warning);">⚠️ 구조를 파악하지 못했습니다.</p>
|
||||
`}
|
||||
`;
|
||||
}
|
||||
|
||||
// 분석 결과 저장
|
||||
async function saveAnalyzedDocType() {
|
||||
if (!analysisResult || !analysisResult.config) {
|
||||
alert('저장할 분석 결과가 없습니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
const savedName = analysisResult.config.name; // ← 미리 저장!
|
||||
|
||||
const actionBtn = document.getElementById('docTypeActionBtn');
|
||||
actionBtn.disabled = true;
|
||||
actionBtn.textContent = '저장 중...';
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/doc-types', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(analysisResult.config)
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.error) {
|
||||
throw new Error(data.error);
|
||||
}
|
||||
|
||||
closeAddDocTypeModal();
|
||||
loadDocTypes();
|
||||
loadUserTemplates();
|
||||
setStatus(`문서 유형 "${savedName}" 추가 완료`, true); // ← 저장한 값 사용!
|
||||
|
||||
} catch (error) {
|
||||
alert('저장 실패: ' + error.message);
|
||||
} finally {
|
||||
actionBtn.disabled = false;
|
||||
actionBtn.textContent = '저장';
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 문서 유형 삭제 =====
|
||||
async function deleteDocType(typeId) {
|
||||
if (!confirm('이 문서 유형을 삭제하시겠습니까?')) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/doc-types/${typeId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('삭제 실패');
|
||||
}
|
||||
|
||||
docTypes = docTypes.filter(t => t.id !== typeId);
|
||||
renderDocTypeList();
|
||||
|
||||
// 삭제된 유형이 선택되어 있었으면 첫 번째로 변경
|
||||
if (currentDocType === typeId) {
|
||||
const firstEnabled = docTypes.find(t => t.enabled);
|
||||
if (firstEnabled) selectDocType(firstEnabled.id);
|
||||
}
|
||||
|
||||
setStatus('문서 유형 삭제 완료', true);
|
||||
|
||||
} catch (error) {
|
||||
alert('삭제 오류: ' + error.message);
|
||||
}
|
||||
}
|
||||
288
03. Code/geulbeot_10th/static/js/domain_selector.js
Normal file
288
03. Code/geulbeot_10th/static/js/domain_selector.js
Normal file
@@ -0,0 +1,288 @@
|
||||
/**
|
||||
* domain_selector.js
|
||||
*
|
||||
* 도메인 지식 선택 시스템
|
||||
* - 폴더 설정 직후 자동으로 도메인 선택 모달 표시
|
||||
* - 체크박스로 도메인 선택 → 선택된 .txt 합쳐서 서버 전달
|
||||
* - "세부 분야별" 선택 시 하위 항목 펼침
|
||||
*/
|
||||
|
||||
// ===== 상태 =====
|
||||
let domainConfig = null; // 서버에서 로드한 config
|
||||
let selectedDomains = []; // 선택된 도메인 ID 배열
|
||||
let domainLoaded = false;
|
||||
|
||||
// ===== 초기화 =====
|
||||
async function loadDomainConfig() {
|
||||
try {
|
||||
const resp = await fetch('/api/domain-config');
|
||||
if (!resp.ok) throw new Error('도메인 설정 로드 실패');
|
||||
domainConfig = await resp.json();
|
||||
domainLoaded = true;
|
||||
console.log('[Domain] 설정 로드 완료:', domainConfig.categories.length, '카테고리');
|
||||
} catch (e) {
|
||||
console.error('[Domain] 설정 로드 실패:', e);
|
||||
// fallback: 빈 config
|
||||
domainConfig = { categories: [] };
|
||||
}
|
||||
}
|
||||
|
||||
// 페이지 로드 시 config 가져오기
|
||||
document.addEventListener('DOMContentLoaded', loadDomainConfig);
|
||||
|
||||
|
||||
// ===== 모달 열기/닫기 =====
|
||||
|
||||
function openDomainModal() {
|
||||
if (!domainConfig || !domainConfig.categories) {
|
||||
alert('도메인 설정을 불러오는 중입니다. 잠시 후 다시 시도해주세요.');
|
||||
return;
|
||||
}
|
||||
renderDomainModal();
|
||||
document.getElementById('domainModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
function closeDomainModal() {
|
||||
document.getElementById('domainModal').style.display = 'none';
|
||||
}
|
||||
|
||||
|
||||
// ===== 모달 렌더링 =====
|
||||
|
||||
function renderDomainModal() {
|
||||
const container = document.getElementById('domainCategoryList');
|
||||
if (!container) return;
|
||||
|
||||
container.innerHTML = '';
|
||||
|
||||
domainConfig.categories.forEach(cat => {
|
||||
const catDiv = document.createElement('div');
|
||||
catDiv.className = 'domain-category';
|
||||
catDiv.dataset.id = cat.id;
|
||||
|
||||
// 메인 체크박스 행
|
||||
const isChecked = selectedDomains.includes(cat.id);
|
||||
catDiv.innerHTML = `
|
||||
<label class="domain-cat-label ${isChecked ? 'checked' : ''}">
|
||||
<input type="checkbox" value="${cat.id}"
|
||||
${isChecked ? 'checked' : ''}
|
||||
onchange="toggleDomainCategory('${cat.id}', this.checked)">
|
||||
<span class="domain-cat-icon">${cat.icon}</span>
|
||||
<span class="domain-cat-text">
|
||||
<span class="domain-cat-name">${cat.label}</span>
|
||||
<span class="domain-cat-desc">${cat.description}</span>
|
||||
</span>
|
||||
${cat.children && cat.children.length > 0 ?
|
||||
`<span class="domain-cat-expand ${isChecked ? 'open' : ''}" id="expand_${cat.id}">▼</span>` : ''}
|
||||
</label>
|
||||
`;
|
||||
|
||||
// 하위 항목이 있으면 서브 패널 추가
|
||||
if (cat.children && cat.children.length > 0) {
|
||||
const subPanel = document.createElement('div');
|
||||
subPanel.className = 'domain-sub-panel';
|
||||
subPanel.id = `sub_${cat.id}`;
|
||||
subPanel.style.display = isChecked ? 'block' : 'none';
|
||||
|
||||
// 그룹별로 묶기
|
||||
const groups = {};
|
||||
cat.children.forEach(child => {
|
||||
const g = child.group || '기타';
|
||||
if (!groups[g]) groups[g] = [];
|
||||
groups[g].push(child);
|
||||
});
|
||||
|
||||
Object.entries(groups).forEach(([groupName, children]) => {
|
||||
const groupDiv = document.createElement('div');
|
||||
groupDiv.className = 'domain-sub-group';
|
||||
groupDiv.innerHTML = `<div class="domain-sub-group-label">${groupName}</div>`;
|
||||
|
||||
const chipsDiv = document.createElement('div');
|
||||
chipsDiv.className = 'domain-sub-chips';
|
||||
|
||||
children.forEach(child => {
|
||||
const childChecked = selectedDomains.includes(child.id);
|
||||
chipsDiv.innerHTML += `
|
||||
<label class="domain-chip ${childChecked ? 'selected' : ''}">
|
||||
<input type="checkbox" value="${child.id}"
|
||||
${childChecked ? 'checked' : ''}
|
||||
onchange="toggleDomainChild('${cat.id}', '${child.id}', this.checked)">
|
||||
<span>${child.label}</span>
|
||||
</label>
|
||||
`;
|
||||
});
|
||||
|
||||
groupDiv.appendChild(chipsDiv);
|
||||
subPanel.appendChild(groupDiv);
|
||||
});
|
||||
|
||||
catDiv.appendChild(subPanel);
|
||||
}
|
||||
|
||||
container.appendChild(catDiv);
|
||||
});
|
||||
|
||||
// 선택 요약 업데이트
|
||||
updateDomainSummary();
|
||||
}
|
||||
|
||||
|
||||
// ===== 선택 로직 =====
|
||||
|
||||
function toggleDomainCategory(catId, checked) {
|
||||
const cat = domainConfig.categories.find(c => c.id === catId);
|
||||
if (!cat) return;
|
||||
|
||||
if (checked) {
|
||||
// 카테고리 추가
|
||||
if (!selectedDomains.includes(catId)) {
|
||||
selectedDomains.push(catId);
|
||||
}
|
||||
|
||||
// 하위 항목이 있으면 서브 패널 펼침
|
||||
if (cat.children && cat.children.length > 0) {
|
||||
const subPanel = document.getElementById(`sub_${catId}`);
|
||||
if (subPanel) subPanel.style.display = 'block';
|
||||
const expand = document.getElementById(`expand_${catId}`);
|
||||
if (expand) expand.classList.add('open');
|
||||
}
|
||||
} else {
|
||||
// 카테고리 제거
|
||||
selectedDomains = selectedDomains.filter(d => d !== catId);
|
||||
|
||||
// 하위 항목도 전부 제거
|
||||
if (cat.children) {
|
||||
cat.children.forEach(child => {
|
||||
selectedDomains = selectedDomains.filter(d => d !== child.id);
|
||||
});
|
||||
const subPanel = document.getElementById(`sub_${catId}`);
|
||||
if (subPanel) subPanel.style.display = 'none';
|
||||
const expand = document.getElementById(`expand_${catId}`);
|
||||
if (expand) expand.classList.remove('open');
|
||||
}
|
||||
}
|
||||
|
||||
renderDomainModal();
|
||||
}
|
||||
|
||||
function toggleDomainChild(parentId, childId, checked) {
|
||||
if (checked) {
|
||||
if (!selectedDomains.includes(childId)) {
|
||||
selectedDomains.push(childId);
|
||||
}
|
||||
} else {
|
||||
selectedDomains = selectedDomains.filter(d => d !== childId);
|
||||
}
|
||||
|
||||
// 칩 UI 업데이트
|
||||
renderDomainModal();
|
||||
}
|
||||
|
||||
|
||||
// ===== 선택 요약 =====
|
||||
|
||||
function updateDomainSummary() {
|
||||
const summaryEl = document.getElementById('domainSummaryText');
|
||||
if (!summaryEl) return;
|
||||
|
||||
if (selectedDomains.length === 0) {
|
||||
summaryEl.textContent = '선택된 도메인이 없습니다. AI가 자동으로 분야를 판단합니다.';
|
||||
summaryEl.className = 'domain-summary-text empty';
|
||||
return;
|
||||
}
|
||||
|
||||
// 선택된 항목 이름 수집
|
||||
const names = [];
|
||||
domainConfig.categories.forEach(cat => {
|
||||
if (selectedDomains.includes(cat.id)) {
|
||||
names.push(cat.label);
|
||||
}
|
||||
if (cat.children) {
|
||||
cat.children.forEach(child => {
|
||||
if (selectedDomains.includes(child.id)) {
|
||||
names.push(child.label);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
summaryEl.textContent = names.join(', ');
|
||||
summaryEl.className = 'domain-summary-text';
|
||||
}
|
||||
|
||||
|
||||
// ===== 확인 (서버에 전달) =====
|
||||
|
||||
async function submitDomainSelection() {
|
||||
if (selectedDomains.length === 0) {
|
||||
// 선택 없으면 step3 자동 분석 모드
|
||||
closeDomainModal();
|
||||
updateDomainDisplay('자동 분석 (AI 판단)');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const resp = await fetch('/api/domain-combine', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ selected: selectedDomains })
|
||||
});
|
||||
|
||||
const data = await resp.json();
|
||||
|
||||
if (data.success) {
|
||||
closeDomainModal();
|
||||
|
||||
// 사이드바에 선택 결과 표시
|
||||
const names = data.selected_names || selectedDomains;
|
||||
updateDomainDisplay(names.join(', '));
|
||||
|
||||
console.log('[Domain] 도메인 지식 결합 완료:', data.combined_length, '자');
|
||||
} else {
|
||||
alert('도메인 지식 결합 실패: ' + (data.error || '알 수 없는 오류'));
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[Domain] 서버 전달 실패:', e);
|
||||
alert('서버 통신 오류가 발생했습니다.');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ===== 사이드바 표시 업데이트 =====
|
||||
|
||||
function updateDomainDisplay(text) {
|
||||
const el = document.getElementById('domainDisplayText');
|
||||
if (el) {
|
||||
el.textContent = text;
|
||||
el.className = 'domain-display-text' + (text.includes('자동') ? ' auto' : ' selected');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ===== 전체 선택/해제 =====
|
||||
|
||||
function selectAllDomains() {
|
||||
selectedDomains = [];
|
||||
domainConfig.categories.forEach(cat => {
|
||||
selectedDomains.push(cat.id);
|
||||
if (cat.children) {
|
||||
cat.children.forEach(child => selectedDomains.push(child.id));
|
||||
}
|
||||
});
|
||||
renderDomainModal();
|
||||
}
|
||||
|
||||
function clearAllDomains() {
|
||||
selectedDomains = [];
|
||||
renderDomainModal();
|
||||
}
|
||||
|
||||
|
||||
// ===== 폴더 설정 후 자동 호출 =====
|
||||
// 기존 submitFolder() 함수에서 성공 후 이 함수 호출
|
||||
function onFolderSetComplete() {
|
||||
// 약간의 딜레이 후 도메인 선택 모달 표시
|
||||
setTimeout(() => {
|
||||
openDomainModal();
|
||||
}, 500);
|
||||
}
|
||||
1208
03. Code/geulbeot_10th/static/js/editor.js
Normal file
1208
03. Code/geulbeot_10th/static/js/editor.js
Normal file
File diff suppressed because it is too large
Load Diff
72
03. Code/geulbeot_10th/static/js/export.js
Normal file
72
03. Code/geulbeot_10th/static/js/export.js
Normal file
@@ -0,0 +1,72 @@
|
||||
// ===== 저장/출력 =====
|
||||
function saveHtml() {
|
||||
if (!generatedHTML) {
|
||||
alert('먼저 문서를 생성해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
const frame = document.getElementById('previewFrame');
|
||||
const html = frame.contentDocument ?
|
||||
'<!DOCTYPE html>' + frame.contentDocument.documentElement.outerHTML :
|
||||
generatedHTML;
|
||||
|
||||
const blob = new Blob([html], { type: 'text/html' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `report_${new Date().toISOString().slice(0,10)}.html`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
async function exportHwp() {
|
||||
if (!generatedHTML) {
|
||||
alert('먼저 문서를 생성해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
const frame = document.getElementById('previewFrame');
|
||||
const html = frame.contentDocument ?
|
||||
'<!DOCTYPE html>' + frame.contentDocument.documentElement.outerHTML :
|
||||
generatedHTML;
|
||||
|
||||
setStatus('HWP 변환 중...', true);
|
||||
|
||||
try {
|
||||
const response = await fetch('/export-hwp', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
html: html,
|
||||
doc_type: currentDocType,
|
||||
style_grouping: true
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.error || 'HWP 변환 실패');
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `report_${new Date().toISOString().slice(0,10)}.hwp`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
setStatus('HWP 변환 완료', true);
|
||||
|
||||
} catch (error) {
|
||||
alert('HWP 변환 오류: ' + error.message);
|
||||
setStatus('오류 발생', false);
|
||||
}
|
||||
}
|
||||
|
||||
function printDoc() {
|
||||
const frame = document.getElementById('previewFrame');
|
||||
if (frame.contentWindow) {
|
||||
frame.contentWindow.print();
|
||||
}
|
||||
}
|
||||
484
03. Code/geulbeot_10th/static/js/generator.js
Normal file
484
03. Code/geulbeot_10th/static/js/generator.js
Normal file
@@ -0,0 +1,484 @@
|
||||
function updateGenerateBtnText() {
|
||||
const btnText = document.getElementById('generateBtnText');
|
||||
if (!btnText) return;
|
||||
const hasFolder = folderPath && folderPath.trim() !== '';
|
||||
const hasLinks = referenceLinks.length > 0;
|
||||
const hasHtml = inputContent && inputContent.trim() !== '';
|
||||
|
||||
if (hasFolder || (hasLinks && hasHtml)) {
|
||||
btnText.textContent = '📋 목차 확인하기';
|
||||
} else {
|
||||
btnText.textContent = '🚀 생성하기';
|
||||
}
|
||||
}
|
||||
|
||||
async function generate() {
|
||||
const type = docTypes.find(t => t.id === currentDocType);
|
||||
if (!type) return;
|
||||
|
||||
// ★ 입력 타입 자동 판별
|
||||
const hasFolder = folderPath && folderPath.trim() !== '';
|
||||
const hasLinks = referenceLinks.length > 0;
|
||||
const hasHtml = inputContent && inputContent.trim() !== '';
|
||||
|
||||
if (hasFolder || (hasLinks && hasHtml)) {
|
||||
// 입력 1,2,3: 폴더/링크 → 파이프라인 → 목차 → 생성
|
||||
await generateDraft();
|
||||
} else if (hasHtml) {
|
||||
// 입력 4: HTML → 형식 변환
|
||||
await generateBriefing();
|
||||
} else {
|
||||
alert('먼저 폴더 위치, 참고 링크, 또는 HTML을 입력해주세요.');
|
||||
}
|
||||
}
|
||||
|
||||
async function generateBriefing() {
|
||||
if (!inputContent && !folderPath && referenceLinks.length === 0) {
|
||||
alert('먼저 폴더 위치, 참고 링크, 또는 HTML을 입력해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
const btn = document.getElementById('generateBtn');
|
||||
const btnText = document.getElementById('generateBtnText');
|
||||
const spinner = document.getElementById('generateSpinner');
|
||||
|
||||
btn.disabled = true;
|
||||
btnText.textContent = '생성 중...';
|
||||
spinner.style.display = 'block';
|
||||
resetSteps();
|
||||
updateStep(0, 'done');
|
||||
setStatus('생성 중...', true);
|
||||
|
||||
try {
|
||||
for (let i = 1; i <= 7; i++) {
|
||||
updateStep(i, 'running');
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
updateStep(i, 'done');
|
||||
}
|
||||
|
||||
updateStep(8, 'running');
|
||||
|
||||
// 페이지 구성 값 가져오기
|
||||
let pageOption = '2'; // 기본값
|
||||
if (currentPageConfig === 'body-only') {
|
||||
pageOption = '1';
|
||||
} else if (currentPageConfig === 'body-attach') {
|
||||
pageOption = String(1 + attachPageCount); // 본문 1p + 첨부 np
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('content', inputContent);
|
||||
formData.append('doc_type', currentDocType);
|
||||
formData.append('page_option', pageOption);
|
||||
formData.append('attach_pages', attachPageCount);
|
||||
formData.append('instruction', document.getElementById('globalInstructionInput').value);
|
||||
formData.append('write_mode', currentWriteMode);
|
||||
formData.append('folder_path', folderPath || '');
|
||||
formData.append('links', JSON.stringify(referenceLinks || []));
|
||||
|
||||
const response = await fetch('/generate', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.error) {
|
||||
throw new Error(data.error);
|
||||
}
|
||||
|
||||
updateStep(8, 'done');
|
||||
updateStep(9, 'running');
|
||||
await new Promise(r => setTimeout(r, 300));
|
||||
updateStep(9, 'done');
|
||||
|
||||
if (data.success && data.html) {
|
||||
generatedHTML = data.html;
|
||||
document.getElementById('placeholder').style.display = 'none';
|
||||
const frame = document.getElementById('previewFrame');
|
||||
frame.classList.add('active');
|
||||
frame.srcdoc = generatedHTML;
|
||||
setTimeout(setupIframeSelection, 500);
|
||||
document.getElementById('feedbackBar').classList.add('show');
|
||||
setStatus('생성 완료', true);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
alert('생성 오류: ' + error.message);
|
||||
setStatus('오류 발생', false);
|
||||
for (let i = 0; i <= 9; i++) {
|
||||
const item = document.querySelector(`.step-item[data-step="${i}"]`);
|
||||
if (item && item.classList.contains('running')) {
|
||||
updateStep(i, 'error');
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btnText.textContent = '🚀 생성하기';
|
||||
spinner.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
async function generateDraft() {
|
||||
if (!folderPath && !inputContent && referenceLinks.length === 0) {
|
||||
alert('먼저 폴더 위치, 참고 링크, 또는 HTML을 입력해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
const btn = document.getElementById('generateBtn');
|
||||
const btnText = document.getElementById('generateBtnText');
|
||||
const spinner = document.getElementById('generateSpinner');
|
||||
|
||||
btn.disabled = true;
|
||||
btnText.textContent = '분석 중...';
|
||||
spinner.style.display = 'block';
|
||||
resetSteps();
|
||||
updateStep(0, 'done');
|
||||
setStatus('목차 생성 중...', true);
|
||||
|
||||
try {
|
||||
const hasFolder = folderPath && folderPath.trim() !== '';
|
||||
const hasLinks = referenceLinks && referenceLinks.length > 0;
|
||||
const hasHtml = inputContent && inputContent.trim() !== '';
|
||||
const needsPipeline = hasFolder || (hasLinks && hasHtml);
|
||||
|
||||
if (needsPipeline) {
|
||||
// 파이프라인 모드: 서버에서 step3~7 실행
|
||||
updateStep(1, 'running');
|
||||
const tocResp = await fetch('/api/generate-toc', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
folder_path: folderPath,
|
||||
links: referenceLinks,
|
||||
domain_selected: selectedDomains && selectedDomains.length > 0,
|
||||
write_mode: currentWriteMode
|
||||
})
|
||||
});
|
||||
const tocData = await tocResp.json();
|
||||
|
||||
if (!tocData.success) {
|
||||
throw new Error(tocData.error || '목차 생성 실패');
|
||||
}
|
||||
|
||||
// step 1~7 완료 표시
|
||||
for (let i = 1; i <= 7; i++) updateStep(i, 'done');
|
||||
|
||||
// 목차 애니메이션 표시
|
||||
if (tocData.toc_items && typeof displayTocWithAnimation === 'function') {
|
||||
displayTocWithAnimation(tocData.toc_items);
|
||||
}
|
||||
} else {
|
||||
// HTML 입력 모드: 가짜 진행 (기존)
|
||||
for (let i = 1; i <= 7; i++) {
|
||||
updateStep(i, 'running');
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
updateStep(i, 'done');
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('feedbackBar').classList.remove('show');
|
||||
|
||||
setStatus('목차 생성 완료 - 확인 후 승인해주세요', true);
|
||||
|
||||
} catch (error) {
|
||||
alert('목차 생성 오류: ' + error.message);
|
||||
setStatus('오류 발생', false);
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btnText.textContent = '📋 목차 확인하기';
|
||||
spinner.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 목차 애니메이션 표시 =====
|
||||
function displayTocWithAnimation(tocItems) {
|
||||
const tocDisplay = document.getElementById('tocDisplayArea');
|
||||
if (!tocDisplay) return;
|
||||
|
||||
tocDisplay.style.display = 'block';
|
||||
tocDisplay.innerHTML = '';
|
||||
|
||||
tocItems.forEach((item, index) => {
|
||||
const tocEl = document.createElement('div');
|
||||
tocEl.className = 'toc-anim-item';
|
||||
tocEl.style.opacity = '0';
|
||||
tocEl.style.transform = 'translateY(12px)';
|
||||
|
||||
// 키워드 HTML 생성
|
||||
const keywordsHtml = (item.keywords || [])
|
||||
.map(k => `<span class="toc-anim-keyword">${k}</span>`)
|
||||
.join('');
|
||||
|
||||
const contentsHtml = (item.contents || [])
|
||||
.map(c => `<div class="toc-anim-content-item">${c}</div>`)
|
||||
.join('');
|
||||
|
||||
tocEl.innerHTML = `
|
||||
<div class="toc-anim-num">${item.num || (index + 1) + '장'}</div>
|
||||
<div class="toc-anim-title">${item.title}</div>
|
||||
${item.guide ? `<div class="toc-anim-guide">${item.guide}</div>` : ''}
|
||||
${contentsHtml ? `<div class="toc-anim-contents">${contentsHtml}</div>` : ''}
|
||||
${keywordsHtml ? `<div class="toc-anim-keywords">${keywordsHtml}</div>` : ''}
|
||||
`;
|
||||
|
||||
tocDisplay.appendChild(tocEl);
|
||||
|
||||
// 순차적으로 나타남
|
||||
setTimeout(() => {
|
||||
tocEl.style.transition = 'all 0.4s ease';
|
||||
tocEl.style.opacity = '1';
|
||||
tocEl.style.transform = 'translateY(0)';
|
||||
}, 300 + (index * 600));
|
||||
});
|
||||
|
||||
// 모든 항목 표시 후 액션바 표시
|
||||
const totalDelay = 300 + (tocItems.length * 600) + 400;
|
||||
setTimeout(() => {
|
||||
document.getElementById('tocActionBar').classList.add('show');
|
||||
document.getElementById('feedbackBar').classList.remove('show');
|
||||
setStatus('목차 생성 완료 - 확인 후 승인해주세요', true);
|
||||
}, totalDelay);
|
||||
}
|
||||
|
||||
// ===== 목차 표시 초기화 =====
|
||||
function hideTocDisplay() {
|
||||
const tocDisplay = document.getElementById('tocDisplayArea');
|
||||
if (tocDisplay) {
|
||||
tocDisplay.style.display = 'none';
|
||||
tocDisplay.innerHTML = '';
|
||||
}
|
||||
}
|
||||
|
||||
function editToc() {
|
||||
const tocContainer = document.getElementById('tocContainer');
|
||||
if (tocContainer) {
|
||||
tocContainer.contentEditable = true;
|
||||
tocContainer.style.outline = '2px solid var(--ui-accent)';
|
||||
}
|
||||
|
||||
// 애니메이션 목차도 편집 가능하게
|
||||
const tocDisplay = document.getElementById('tocDisplayArea');
|
||||
if (tocDisplay && tocDisplay.style.display !== 'none') {
|
||||
tocDisplay.contentEditable = true;
|
||||
tocDisplay.style.outline = '2px solid var(--ui-accent)';
|
||||
}
|
||||
|
||||
setStatus('목차 편집 모드 - 직접 수정 가능합니다', true);
|
||||
}
|
||||
|
||||
async function approveToc() {
|
||||
const btn = document.getElementById('approveBtn');
|
||||
btn.disabled = true;
|
||||
btn.textContent = '⏳ 생성 중...';
|
||||
|
||||
document.getElementById('tocActionBar').classList.remove('show');
|
||||
hideTocDisplay();
|
||||
setStatus('최종 문서 생성 중...', true);
|
||||
|
||||
// 진행률 표시
|
||||
const progressArea = document.getElementById('genProgressArea');
|
||||
if (progressArea) {
|
||||
progressArea.style.display = 'block';
|
||||
}
|
||||
|
||||
try {
|
||||
// Step 8: 콘텐츠 생성
|
||||
updateStep(8, 'running');
|
||||
updateGenProgress(10, '📚 RAG 검색 중...');
|
||||
|
||||
await generateReport();
|
||||
|
||||
updateGenProgress(100, '✅ 생성 완료');
|
||||
updateStep(8, 'done');
|
||||
updateStep(9, 'running');
|
||||
await new Promise(r => setTimeout(r, 300));
|
||||
updateStep(9, 'done');
|
||||
|
||||
// 진행률 숨기기
|
||||
setTimeout(() => {
|
||||
if (progressArea) progressArea.style.display = 'none';
|
||||
}, 1000);
|
||||
|
||||
} catch (error) {
|
||||
alert('문서 생성 오류: ' + error.message);
|
||||
setStatus('오류 발생', false);
|
||||
if (progressArea) progressArea.style.display = 'none';
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = '✅ 승인 & 생성하기';
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 생성 진행률 업데이트 =====
|
||||
function updateGenProgress(percent, message) {
|
||||
const bar = document.getElementById('genProgressBar');
|
||||
const text = document.getElementById('genProgressText');
|
||||
if (bar) bar.style.width = percent + '%';
|
||||
if (text) text.textContent = message;
|
||||
}
|
||||
|
||||
// === generateReport() 함수 전체 교체 ===
|
||||
async function generateReport() {
|
||||
const coverCheck = document.getElementById('cover');
|
||||
const tocCheck = document.getElementById('toc');
|
||||
const dividerCheck = document.getElementById('divider');
|
||||
|
||||
updateGenProgress(20, '📋 목차 기반 구조화 중...');
|
||||
|
||||
// ★ 입력1,2,3일 때 (폴더/링크 있으면)
|
||||
const hasFolder = folderPath && folderPath.trim() !== '';
|
||||
const hasLinks = referenceLinks && referenceLinks.length > 0;
|
||||
const hasHtml = inputContent && inputContent.trim() !== '';
|
||||
if (hasFolder || (hasLinks && hasHtml)) {
|
||||
// 편집된 목차 수집
|
||||
const editedToc = collectEditedToc();
|
||||
|
||||
const response = await fetch('/api/generate-report-from-toc', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
toc_items: editedToc,
|
||||
write_mode: currentWriteMode,
|
||||
instruction: document.getElementById('globalInstructionInput').value,
|
||||
cover: coverCheck ? coverCheck.checked : true,
|
||||
toc: tocCheck ? tocCheck.checked : true,
|
||||
})
|
||||
});
|
||||
|
||||
updateGenProgress(60, '📝 본문 작성 중...');
|
||||
const data = await response.json();
|
||||
|
||||
if (data.error) throw new Error(data.error);
|
||||
|
||||
updateGenProgress(85, '🎨 레이아웃 조립 중...');
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
|
||||
if (data.success && data.html) {
|
||||
generatedHTML = data.html;
|
||||
showGeneratedHtml(generatedHTML);
|
||||
}
|
||||
|
||||
} else {
|
||||
const response = await fetch('/generate-report', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
content: inputContent,
|
||||
folder_path: folderPath,
|
||||
cover: coverCheck ? coverCheck.checked : true,
|
||||
toc: tocCheck ? tocCheck.checked : true,
|
||||
divider: dividerCheck ? dividerCheck.checked : false,
|
||||
instruction: document.getElementById('globalInstructionInput').value
|
||||
})
|
||||
});
|
||||
|
||||
updateGenProgress(60, '📝 본문 작성 중...');
|
||||
const data = await response.json();
|
||||
if (data.error) throw new Error(data.error);
|
||||
|
||||
updateGenProgress(85, '🎨 레이아웃 조립 중...');
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
|
||||
if (data.success && data.html) {
|
||||
generatedHTML = data.html;
|
||||
showGeneratedHtml(generatedHTML);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ★ 공통 HTML 표시 함수 추가
|
||||
function showGeneratedHtml(html) {
|
||||
document.getElementById('placeholder').style.display = 'none';
|
||||
const frame = document.getElementById('previewFrame');
|
||||
frame.classList.add('active');
|
||||
frame.srcdoc = html;
|
||||
setTimeout(setupIframeSelection, 500);
|
||||
document.getElementById('feedbackBar').classList.add('show');
|
||||
updateGenProgress(100, '✅ 생성 완료');
|
||||
setStatus('생성 완료', true);
|
||||
}
|
||||
|
||||
// ★ 편집된 목차 수집 함수 추가
|
||||
function collectEditedToc() {
|
||||
const tocDisplay = document.getElementById('tocDisplayArea');
|
||||
if (!tocDisplay) return [];
|
||||
|
||||
const items = [];
|
||||
tocDisplay.querySelectorAll('.toc-anim-item').forEach(el => {
|
||||
items.push({
|
||||
num: el.querySelector('.toc-anim-num')?.textContent || '',
|
||||
title: el.querySelector('.toc-anim-title')?.textContent || '',
|
||||
guide: el.querySelector('.toc-anim-guide')?.textContent || '',
|
||||
keywords: Array.from(el.querySelectorAll('.toc-anim-keyword'))
|
||||
.map(k => k.textContent)
|
||||
});
|
||||
});
|
||||
return items;
|
||||
}
|
||||
|
||||
|
||||
// ===== 피드백 =====
|
||||
async function submitFeedback() {
|
||||
const feedback = document.getElementById('feedbackInput').value.trim();
|
||||
if (!feedback) {
|
||||
alert('수정 내용을 입력해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!generatedHTML) {
|
||||
alert('먼저 문서를 생성해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
const btn = document.getElementById('feedbackBtn');
|
||||
const btnText = document.getElementById('feedbackBtnText');
|
||||
const spinner = document.getElementById('feedbackSpinner');
|
||||
|
||||
btn.disabled = true;
|
||||
btnText.textContent = '⏳ 수정 중...';
|
||||
spinner.style.display = 'inline-block';
|
||||
|
||||
setStatus('수정 중...', true);
|
||||
|
||||
try {
|
||||
const response = await fetch('/refine', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
feedback: feedback,
|
||||
current_html: generatedHTML
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.error) {
|
||||
throw new Error(data.error);
|
||||
}
|
||||
|
||||
if (data.success && data.html) {
|
||||
generatedHTML = data.html;
|
||||
document.getElementById('previewFrame').srcdoc = generatedHTML;
|
||||
document.getElementById('feedbackInput').value = '';
|
||||
|
||||
setTimeout(setupIframeSelection, 500);
|
||||
setStatus('수정 완료', true);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
alert('수정 오류: ' + error.message);
|
||||
setStatus('오류 발생', false);
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btnText.textContent = '🔄 수정 반영';
|
||||
spinner.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function regenerate() {
|
||||
if (confirm('현재 결과를 버리고 다시 생성하시겠습니까?')) {
|
||||
hideTocDisplay();
|
||||
generate();
|
||||
}
|
||||
}
|
||||
135
03. Code/geulbeot_10th/static/js/modals.js
Normal file
135
03. Code/geulbeot_10th/static/js/modals.js
Normal file
@@ -0,0 +1,135 @@
|
||||
// ===== modals.js 맨 첫줄에 추가 =====
|
||||
function updateDomainSectionVisibility() {
|
||||
const section = document.getElementById('domainSection');
|
||||
if (!section) return;
|
||||
const hasFolder = typeof folderPath !== 'undefined' && folderPath && folderPath.trim() !== '';
|
||||
const hasLinks = typeof referenceLinks !== 'undefined' && referenceLinks && referenceLinks.length > 0;
|
||||
section.style.display = (hasFolder || hasLinks) ? 'block' : 'none';
|
||||
}
|
||||
// ===== 폴더 모달 =====
|
||||
function openFolderModal() {
|
||||
document.getElementById('folderModal').classList.add('active');
|
||||
document.getElementById('folderPath').focus();
|
||||
}
|
||||
|
||||
function closeFolderModal() {
|
||||
document.getElementById('folderModal').classList.remove('active');
|
||||
}
|
||||
|
||||
function submitFolder() {
|
||||
const path = document.getElementById('folderPath').value.trim();
|
||||
if (!path) {
|
||||
alert('폴더 경로를 입력해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
folderPath = document.getElementById('folderPath').value;
|
||||
closeFolderModal();
|
||||
updateInputStatus();
|
||||
setStatus('폴더 경로 설정됨', true);
|
||||
|
||||
// 서버에 폴더 검토 요청
|
||||
fetch('/api/check-folder', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ folder_path: folderPath })
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
document.getElementById('totalCount').textContent = data.total + '개';
|
||||
document.getElementById('okCount').textContent = data.ok + '개 ✓';
|
||||
document.getElementById('unknownCount').textContent = data.unknown + '개';
|
||||
|
||||
// 미확인 파일 목록
|
||||
const listEl = document.getElementById('unknownFilesList');
|
||||
if (listEl && data.unknown_list) {
|
||||
listEl.innerHTML = data.unknown_list
|
||||
.map(name => `<div style="font-size:11px; color:var(--ui-dim); padding:2px 0;">${name}</div>`)
|
||||
.join('');
|
||||
}
|
||||
} else {
|
||||
document.getElementById('totalCount').textContent = '오류';
|
||||
alert('폴더 검토 실패: ' + (data.error || ''));
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
document.getElementById('totalCount').textContent = '오류';
|
||||
console.error('[Folder]', err);
|
||||
});
|
||||
updateDomainSectionVisibility();
|
||||
if (typeof onFolderSetComplete === 'function') {
|
||||
onFolderSetComplete();
|
||||
}
|
||||
}
|
||||
|
||||
function toggleUnknownFiles() {
|
||||
document.getElementById('unknownFilesBox').classList.toggle('show');
|
||||
}
|
||||
|
||||
function openFolder() {
|
||||
alert('폴더 열기는 Engine이 실행 중일 때만 가능합니다.');
|
||||
}
|
||||
|
||||
// ===== 링크 모달 =====
|
||||
function openLinkModal() {
|
||||
document.getElementById('linkModal').classList.add('active');
|
||||
}
|
||||
|
||||
function closeLinkModal() {
|
||||
document.getElementById('linkModal').classList.remove('active');
|
||||
}
|
||||
|
||||
function addLinkInput() {
|
||||
const container = document.getElementById('linkInputList');
|
||||
const input = document.createElement('input');
|
||||
input.type = 'text';
|
||||
input.className = 'link-input';
|
||||
input.placeholder = 'https://...';
|
||||
input.style = 'width:100%; padding:10px; border-radius:6px; border:1px solid var(--ui-border); background:var(--ui-bg); color:var(--ui-text); font-size:12px; margin-bottom:8px;';
|
||||
container.appendChild(input);
|
||||
}
|
||||
|
||||
function submitLinks() {
|
||||
const inputs = document.querySelectorAll('#linkInputList .link-input');
|
||||
referenceLinks = [];
|
||||
inputs.forEach(input => {
|
||||
const val = input.value.trim();
|
||||
if (val) referenceLinks.push(val);
|
||||
});
|
||||
|
||||
closeLinkModal();
|
||||
updateInputStatus();
|
||||
|
||||
if (referenceLinks.length > 0) {
|
||||
setStatus(`참고 링크 ${referenceLinks.length}개 설정됨`, true);
|
||||
}
|
||||
updateDomainSectionVisibility();
|
||||
if (typeof onFolderSetComplete === 'function') {
|
||||
onFolderSetComplete();
|
||||
}
|
||||
}
|
||||
|
||||
// ===== HTML 모달 =====
|
||||
function openHtmlModal() {
|
||||
document.getElementById('htmlModal').classList.add('active');
|
||||
document.getElementById('htmlContent').focus();
|
||||
}
|
||||
|
||||
function closeHtmlModal() {
|
||||
document.getElementById('htmlModal').classList.remove('active');
|
||||
}
|
||||
|
||||
function submitHtml() {
|
||||
const html = document.getElementById('htmlContent').value.trim();
|
||||
if (!html) {
|
||||
alert('HTML을 입력해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
inputContent = html;
|
||||
closeHtmlModal();
|
||||
updateInputStatus();
|
||||
setStatus('HTML 입력 완료', true);
|
||||
updateDomainSectionVisibility();
|
||||
}
|
||||
189
03. Code/geulbeot_10th/static/js/template.js
Normal file
189
03. Code/geulbeot_10th/static/js/template.js
Normal file
@@ -0,0 +1,189 @@
|
||||
// ===== 템플릿 모달 =====
|
||||
function openTemplateModal() {
|
||||
document.getElementById('templateModal').classList.add('active');
|
||||
document.getElementById('templateNameInput').value = '';
|
||||
removeTemplateFile();
|
||||
}
|
||||
|
||||
function closeTemplateModal() {
|
||||
document.getElementById('templateModal').classList.remove('active');
|
||||
}
|
||||
|
||||
function handleTemplateFile(input) {
|
||||
if (input.files.length > 0) {
|
||||
const file = input.files[0];
|
||||
const validExtensions = ['.hwpx', '.hwp', '.pdf'];
|
||||
const ext = '.' + file.name.split('.').pop().toLowerCase();
|
||||
|
||||
if (!validExtensions.includes(ext)) {
|
||||
alert('지원하지 않는 파일 형식입니다.\n(HWPX, HWP, PDF만 지원)');
|
||||
return;
|
||||
}
|
||||
|
||||
selectedTemplateFile = file;
|
||||
document.getElementById('templateFileName').textContent = file.name;
|
||||
document.getElementById('templateFileInfo').classList.add('show');
|
||||
document.getElementById('templateDropzone').style.display = 'none';
|
||||
|
||||
const nameInput = document.getElementById('templateNameInput');
|
||||
if (!nameInput.value.trim()) {
|
||||
nameInput.value = file.name.replace(/\.[^/.]+$/, '');
|
||||
}
|
||||
|
||||
updateTemplateSubmitBtn();
|
||||
}
|
||||
}
|
||||
|
||||
function removeTemplateFile() {
|
||||
selectedTemplateFile = null;
|
||||
document.getElementById('templateFileInput').value = '';
|
||||
document.getElementById('templateFileInfo').classList.remove('show');
|
||||
document.getElementById('templateDropzone').style.display = 'block';
|
||||
updateTemplateSubmitBtn();
|
||||
}
|
||||
|
||||
function updateTemplateSubmitBtn() {
|
||||
const nameInput = document.getElementById('templateNameInput');
|
||||
const btn = document.getElementById('templateSubmitBtn');
|
||||
btn.disabled = !selectedTemplateFile || !nameInput.value.trim();
|
||||
}
|
||||
|
||||
async function submitTemplate() {
|
||||
const name = document.getElementById('templateNameInput').value.trim();
|
||||
if (!name || !selectedTemplateFile) return;
|
||||
|
||||
const btn = document.getElementById('templateSubmitBtn');
|
||||
const spinner = document.getElementById('templateSpinner');
|
||||
const text = document.getElementById('templateSubmitText');
|
||||
|
||||
btn.disabled = true;
|
||||
spinner.style.display = 'inline-block';
|
||||
text.textContent = '분석 중...';
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('name', name);
|
||||
formData.append('file', selectedTemplateFile);
|
||||
|
||||
const response = await fetch('/analyze-template', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.error) {
|
||||
throw new Error(data.error);
|
||||
}
|
||||
|
||||
userTemplates.push(data.meta);
|
||||
renderUserTemplates();
|
||||
closeTemplateModal();
|
||||
|
||||
setStatus(`템플릿 "${name}" 추가 완료`, true);
|
||||
|
||||
} catch (error) {
|
||||
alert('템플릿 분석 오류: ' + error.message);
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
spinner.style.display = 'none';
|
||||
text.textContent = '✨ 분석 및 추가';
|
||||
}
|
||||
}
|
||||
|
||||
function selectTemplate(templateId) {
|
||||
currentTemplate = templateId;
|
||||
|
||||
document.querySelectorAll('.template-item').forEach(item => {
|
||||
item.classList.remove('selected');
|
||||
const radio = item.querySelector('input[type="radio"]');
|
||||
if (radio) radio.checked = false;
|
||||
});
|
||||
|
||||
const selectedItem = document.querySelector(`.template-item[data-template="${templateId}"]`);
|
||||
if (selectedItem) {
|
||||
selectedItem.classList.add('selected');
|
||||
const radio = selectedItem.querySelector('input[type="radio"]');
|
||||
if (radio) radio.checked = true;
|
||||
}
|
||||
|
||||
const elementsPanel = document.getElementById('templateElementOptions');
|
||||
if (templateId === 'default') {
|
||||
elementsPanel.style.display = 'none';
|
||||
} else {
|
||||
showTemplateElements(templateId);
|
||||
elementsPanel.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
function showTemplateElements(templateId) {
|
||||
const template = userTemplates.find(t => t.id === templateId);
|
||||
if (!template || !template.elements) return;
|
||||
|
||||
const container = document.querySelector('#templateElementOptions .elements-list');
|
||||
container.innerHTML = template.elements.map(el => `
|
||||
<label class="element-checkbox">
|
||||
<input type="checkbox"
|
||||
data-element="${el.type}"
|
||||
${el.default ? 'checked' : ''}
|
||||
onchange="toggleTemplateElement('${templateId}', '${el.type}', this.checked)">
|
||||
<span class="element-icon">${el.icon}</span>
|
||||
<span>${el.name}</span>
|
||||
</label>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function toggleTemplateElement(templateId, elementType, checked) {
|
||||
if (!templateElements[templateId]) {
|
||||
templateElements[templateId] = {};
|
||||
}
|
||||
templateElements[templateId][elementType] = checked;
|
||||
}
|
||||
|
||||
function renderUserTemplates() {
|
||||
const container = document.getElementById('userTemplatesListNew');
|
||||
|
||||
if (userTemplates.length === 0) {
|
||||
container.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = userTemplates.map(tpl => `
|
||||
<div class="template-item" data-template="${tpl.id}" onclick="selectTemplate('${tpl.id}')">
|
||||
<input type="radio" name="template">
|
||||
<span class="label">📑 ${tpl.name}</span>
|
||||
<button class="delete-btn" onclick="event.stopPropagation(); deleteTemplate('${tpl.id}')" title="삭제">✕</button>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
async function loadUserTemplates() {
|
||||
try {
|
||||
const response = await fetch('/api/templates');
|
||||
const data = await response.json();
|
||||
if (Array.isArray(data)) {
|
||||
userTemplates = data;
|
||||
renderUserTemplates();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('템플릿 목록 로드 실패:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteTemplate(templateId) {
|
||||
if (!confirm('이 템플릿을 삭제하시겠습니까?')) return;
|
||||
|
||||
try {
|
||||
await fetch(`/api/templates/${templateId}`, { method: 'DELETE' });
|
||||
userTemplates = userTemplates.filter(t => t.id !== templateId);
|
||||
renderUserTemplates();
|
||||
|
||||
if (currentTemplate === templateId) {
|
||||
selectTemplate('default');
|
||||
}
|
||||
|
||||
setStatus('템플릿 삭제 완료', true);
|
||||
} catch (error) {
|
||||
alert('삭제 오류: ' + error.message);
|
||||
}
|
||||
}
|
||||
91
03. Code/geulbeot_10th/static/js/ui.js
Normal file
91
03. Code/geulbeot_10th/static/js/ui.js
Normal file
@@ -0,0 +1,91 @@
|
||||
// ===== 상태 표시 =====
|
||||
function setStatus(msg, connected = false) {
|
||||
document.getElementById('statusMessage').textContent = msg;
|
||||
document.getElementById('statusDot').classList.toggle('connected', connected);
|
||||
}
|
||||
|
||||
// ===== 입력 상태 업데이트 =====
|
||||
function updateInputStatus() {
|
||||
const hasFolder = folderPath.length > 0;
|
||||
const hasLinks = referenceLinks.length > 0;
|
||||
const hasHtml = inputContent.length > 0;
|
||||
|
||||
const pathEl = document.getElementById('folderPathDisplay');
|
||||
if (hasFolder) {
|
||||
pathEl.textContent = folderPath;
|
||||
pathEl.classList.remove('empty');
|
||||
} else {
|
||||
pathEl.textContent = '폴더 경로가 설정되지 않음';
|
||||
pathEl.classList.add('empty');
|
||||
}
|
||||
|
||||
document.getElementById('linkCount').textContent = referenceLinks.length + '개';
|
||||
|
||||
const htmlStatus = document.getElementById('htmlInputStatus');
|
||||
if (hasHtml) {
|
||||
htmlStatus.textContent = '✓ 입력됨';
|
||||
htmlStatus.classList.add('ok');
|
||||
} else {
|
||||
htmlStatus.textContent = '없음';
|
||||
htmlStatus.classList.remove('ok');
|
||||
}
|
||||
|
||||
const canGenerate = hasHtml || hasFolder || hasLinks;
|
||||
document.getElementById('generateBtn').disabled = !canGenerate;
|
||||
// 버튼 텍스트: 폴더/링크 있으면 목차 확인, 아니면 생성하기
|
||||
const btnText = document.getElementById('generateBtnText');
|
||||
if (btnText) {
|
||||
if (hasFolder || (hasLinks && hasHtml)) {
|
||||
btnText.textContent = '📋 목차 확인하기';
|
||||
} else {
|
||||
btnText.textContent = '🚀 생성하기';
|
||||
}
|
||||
}
|
||||
|
||||
if (canGenerate) {
|
||||
updateStep(0, 'done');
|
||||
} else {
|
||||
updateStep(0, 'pending');
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 진행 상태 =====
|
||||
function updateStep(num, status) {
|
||||
const item = document.querySelector(`.step-item[data-step="${num}"]`);
|
||||
if (!item) return;
|
||||
item.classList.remove('pending', 'running', 'done', 'error');
|
||||
item.classList.add(status);
|
||||
item.querySelector('.status').textContent =
|
||||
status === 'pending' ? '○' :
|
||||
status === 'running' ? '◐' :
|
||||
status === 'done' ? '●' : '✕';
|
||||
}
|
||||
|
||||
function resetSteps() {
|
||||
for (let i = 0; i <= 9; i++) {
|
||||
updateStep(i, 'pending');
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 줌 =====
|
||||
function setZoom(value) {
|
||||
currentZoom = parseInt(value);
|
||||
document.getElementById('a4Wrapper').style.transform = `scale(${currentZoom / 100})`;
|
||||
}
|
||||
|
||||
// ===== 작성 방식 선택 =====
|
||||
function selectWriteMode(mode) {
|
||||
currentWriteMode = mode;
|
||||
|
||||
document.querySelectorAll('.write-mode-tab').forEach(tab => {
|
||||
tab.classList.remove('selected');
|
||||
tab.querySelector('input[type="radio"]').checked = false;
|
||||
});
|
||||
|
||||
const selectedTab = document.querySelector(`.write-mode-tab input[value="${mode}"]`);
|
||||
if (selectedTab) {
|
||||
selectedTab.checked = true;
|
||||
selectedTab.closest('.write-mode-tab').classList.add('selected');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user