371 lines
16 KiB
JavaScript
371 lines
16 KiB
JavaScript
/**
|
|
* 글벗 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() 패치 완료');
|
|
});
|
|
} |