📦 Initialize Geulbeot structure and merge Prompts & test projects

This commit is contained in:
2026-03-05 11:32:29 +09:00
commit 555a954458
687 changed files with 205247 additions and 0 deletions

View 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;
}
}

View 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() 패치 완료');
});
}

View 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);
}
}

View 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);
}

File diff suppressed because it is too large Load Diff

View 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();
}
}

View 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();
}
}

View 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();
}

View 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);
}
}

View 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');
}
}