📦 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,297 @@
/* ===== 편집 바 스타일 ===== */
.format-bar {
display: none;
align-items: center;
padding: 8px 12px;
background: var(--ui-panel);
border-bottom: 1px solid var(--ui-border);
gap: 6px;
flex-wrap: wrap;
}
.format-bar.active { display: flex; }
/* 편집 바 2줄 구조 */
.format-row {
display: flex;
align-items: center;
gap: 6px;
width: 100%;
}
.format-row:first-child {
border-bottom: 1px solid var(--ui-border);
padding-bottom: 8px;
}
.format-btn {
padding: 6px 10px;
background: none;
border: 1px solid transparent;
border-radius: 4px;
cursor: pointer;
color: var(--ui-text);
font-size: 14px;
position: relative;
}
.format-btn:hover { background: var(--ui-hover); }
.format-btn.active { background: rgba(0, 200, 83, 0.3); color: var(--ui-accent); }
.format-select {
padding: 5px 8px;
border: 1px solid var(--ui-border);
border-radius: 4px;
background: var(--ui-bg);
color: var(--ui-text);
font-size: 12px;
}
.format-divider {
width: 1px;
height: 24px;
background: var(--ui-border);
margin: 0 6px;
}
/* 툴팁 */
.format-btn .tooltip {
position: absolute;
bottom: -28px;
left: 50%;
transform: translateX(-50%);
background: #333;
color: #fff;
padding: 4px 8px;
border-radius: 4px;
font-size: 11px;
white-space: nowrap;
opacity: 0;
pointer-events: none;
transition: opacity 0.2s;
z-index: 100;
}
.format-btn:hover .tooltip { opacity: 1; }
/* 페이지 버튼 스타일 */
.format-btn.page-btn {
padding: 6px 12px;
font-size: 12px;
white-space: nowrap;
flex-shrink: 0;
min-width: fit-content;
}
/* 페이지 브레이크 표시 */
.page-break-forced {
border-top: 3px solid #e65100 !important;
margin-top: 10px;
}
.move-to-prev-page {
border-top: 3px dashed #1976d2 !important;
margin-top: 10px;
}
/* 색상 선택기 */
.color-picker-btn {
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
.color-picker-btn input[type="color"] {
position: absolute;
width: 100%;
height: 100%;
opacity: 0;
cursor: pointer;
}
/* 편집 모드 활성 블록 */
.active-block {
outline: 2px dashed var(--ui-accent) !important;
outline-offset: 2px;
}
/* 표 삽입 모달 */
.table-modal {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
z-index: 2000;
align-items: center;
justify-content: center;
}
.table-modal.active { display: flex; }
.table-modal-content {
background: var(--ui-panel);
border-radius: 12px;
padding: 24px;
width: 320px;
border: 1px solid var(--ui-border);
}
.table-modal-title {
font-size: 16px;
font-weight: 700;
color: var(--ui-text);
margin-bottom: 20px;
}
.table-modal-row {
display: flex;
align-items: center;
margin-bottom: 12px;
}
.table-modal-row label {
flex: 1;
font-size: 13px;
color: var(--ui-dim);
}
.table-modal-row input[type="number"] {
width: 60px;
padding: 6px 8px;
border: 1px solid var(--ui-border);
border-radius: 4px;
background: var(--ui-bg);
color: var(--ui-text);
text-align: center;
}
.table-modal-row input[type="checkbox"] {
width: 18px;
height: 18px;
}
.table-modal-buttons {
display: flex;
gap: 10px;
margin-top: 20px;
}
.table-modal-btn {
flex: 1;
padding: 10px;
border-radius: 6px;
border: none;
font-size: 13px;
cursor: pointer;
}
.table-modal-btn.primary {
background: var(--ui-accent);
color: #003300;
font-weight: 600;
}
.table-modal-btn.secondary {
background: var(--ui-border);
color: var(--ui-text);
}
/* 토스트 메시지 */
.toast-container {
position: fixed;
bottom: 80px;
left: 50%;
transform: translateX(-50%);
z-index: 3000;
}
.toast {
background: #333;
color: #fff;
padding: 10px 20px;
border-radius: 8px;
font-size: 13px;
animation: toastIn 0.3s ease, toastOut 0.3s ease 2.7s forwards;
}
.resizable-container { position: relative; display: inline-block; max-width: 100%; }
.resizable-container.block-type { display: block; }
.resize-handle {
position: absolute;
right: -2px;
bottom: -2px;
width: 18px;
height: 18px;
background: #00C853;
cursor: se-resize;
opacity: 0;
transition: opacity 0.2s;
z-index: 100;
border-radius: 3px 0 3px 0;
display: flex;
align-items: center;
justify-content: center;
}
.resize-handle::after {
content: '⤡';
color: white;
font-size: 12px;
font-weight: bold;
}
.resizable-container:hover .resize-handle { opacity: 0.8; }
.resize-handle:hover { opacity: 1 !important; transform: scale(1.1); }
.resizable-container.resizing { outline: 2px dashed #00C853 !important; }
.resizable-container.resizing .resize-handle { opacity: 1; background: #FF9800; }
/* 표 전용 */
.resizable-container.table-resize .resize-handle { background: #2196F3; }
.resizable-container.table-resize.resizing .resize-handle { background: #FF5722; }
/* 이미지 전용 */
.resizable-container.figure-resize img { display: block; }
/* 크기 표시 툴팁 */
.size-tooltip {
position: absolute;
bottom: 100%;
right: 0;
background: rgba(0,0,0,0.8);
color: white;
padding: 2px 6px;
border-radius: 3px;
font-size: 10px;
white-space: nowrap;
opacity: 0;
transition: opacity 0.2s;
pointer-events: none;
}
.resizable-container:hover .size-tooltip,
.resizable-container.resizing .size-tooltip { opacity: 1; }
@keyframes toastIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes toastOut {
from { opacity: 1; }
to { opacity: 0; }
}
/* 인쇄 시 숨김 */
@media print {
.format-bar,
.table-modal,
.toast-container {
display: none !important;
}
}

File diff suppressed because it is too large Load Diff

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

View File

@@ -0,0 +1,315 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>한국 토목 엔지니어링의 딜레마 - 기획서</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700;900&display=swap');
:root {
--primary-blue: #3057B9;
--gray-light: #F2F2F2;
--gray-medium: #E6E6E6;
--gray-dark: #666666;
--border-light: #DDDDDD;
--text-black: #000000;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
-webkit-print-color-adjust: exact;
}
body {
font-family: 'Noto Sans KR', sans-serif;
background-color: #525659;
color: var(--text-black);
line-height: 1.35;
display: flex;
justify-content: center;
padding: 10px 0;
}
.sheet {
background-color: white;
width: 210mm;
height: 297mm;
padding: 20mm 20mm;
box-shadow: 0 0 15px rgba(0,0,0,0.1);
position: relative;
display: flex;
flex-direction: column;
overflow: hidden;
}
@media print {
body { background: none; padding: 0; }
.sheet { box-shadow: none; margin: 0; border: none; }
}
.page-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 15px;
font-size: 8.5pt;
color: var(--gray-dark);
}
.header-title {
font-size: 24pt;
font-weight: 900;
margin-bottom: 8px;
letter-spacing: -1.5px;
color: #111;
}
.title-divider {
height: 4px;
background-color: var(--primary-blue);
width: 100%;
margin-bottom: 20px;
}
.lead-box {
background-color: var(--gray-light);
padding: 18px 20px;
margin-bottom: 5px;
border-radius: 2px;
text-align: center;
}
.lead-box div {
font-size: 13pt;
font-weight: 700;
color: var(--primary-blue);
letter-spacing: -0.5px;
}
.lead-notes {
font-size: 8.5pt;
color: #777;
margin-bottom: 20px;
padding-left: 5px;
text-align: right;
}
.body-content {
flex: 1;
}
.section {
margin-bottom: 22px;
}
.section-title {
font-size: 13pt;
font-weight: 700;
display: flex;
align-items: center;
margin-bottom: 10px;
color: #111;
}
.section-title::before {
content: "";
display: inline-block;
width: 10px;
height: 10px;
background-color: #999;
margin-right: 10px;
}
ul {
list-style: none;
padding-left: 12px;
}
li {
font-size: 10pt;
position: relative;
margin-bottom: 6px;
padding-left: 15px;
color: #333;
text-align: justify;
}
li::before {
content: "•";
position: absolute;
left: 0;
color: #AAA;
font-size: 11pt;
}
table {
width: 100%;
border-collapse: collapse;
margin: 8px 0;
font-size: 9.5pt;
border-top: 1.5px solid #333;
}
th {
background-color: var(--gray-medium);
font-weight: 700;
padding: 10px;
border: 1px solid var(--border-light);
}
td {
padding: 10px;
border: 1px solid var(--border-light);
vertical-align: middle;
}
.group-cell {
background-color: #F9F9F9;
font-weight: 700;
width: 20%;
text-align: center;
color: var(--primary-blue);
}
.bottom-box {
border: 1.5px dashed var(--primary-blue);
display: flex;
margin-top: 15px;
min-height: 75px;
}
.bottom-left {
width: 20%;
background-color: #F0F4FA;
padding: 12px;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
font-weight: 700;
font-size: 10pt;
color: var(--primary-blue);
}
.bottom-right {
width: 80%;
background-color: var(--gray-light);
padding: 15px 25px;
font-size: 10pt;
line-height: 1.6;
}
.page-footer {
margin-top: 15px;
padding-top: 10px;
display: flex;
justify-content: space-between;
font-size: 8.5pt;
color: var(--gray-dark);
border-top: 1px solid #EEE;
}
.footer-page { flex: 1; text-align: center; }
b { font-weight: 700; color: var(--primary-blue); }
.keyword { font-weight: 700; color: #000; }
</style>
</head>
<body>
<div class="sheet">
<header class="page-header">
<div class="header-left">
조직: <b>건설 DX 추진팀</b> / 기술혁신본부<br>
보고: 토목 설계 소프트웨어 전환 전략 기획
</div>
<div class="header-right">
문서번호: 2026-DX-003<br>
작성일자: 2026. 02. 10.
</div>
</header>
<div class="title-block">
<h1 class="header-title">한국 토목 엔지니어링의 딜레마 </h1>
<div class="title-divider"></div>
</div>
<div class="body-content">
<div class="lead-box">
<div>"AutoCAD 독점 구조의 기술적·경제적 리스크를 분석하고,<br> 3D/BIM 시대에 대응하는 단계적 전환 로드맵을 제시"</div>
</div>
<div class="section">
<div class="section-title">추진 배경 및 전략적 목적</div>
<ul>
<li><span class="keyword">독점 시장 구조 타파:</span> 국내 토목 설계 시장의 <b>AutoCAD 점유율 85% 이상</b>, 연간 라이선스 비용(인당 280만원) 지속 인상으로 경제적 부담 심화.</li>
<li><span class="keyword">기술 종속 해소:</span> 비공개 독점 포맷(.dwg) 기반 성과물 관리로 <b>데이터 주권</b> 및 지식재산권 제약 발생, 장기적 기술 자립 저해.</li>
<li><span class="keyword">토목 특성 대응:</span> AutoCAD는 건축 직교 체계에 최적화되어 토목 분야의 <b>비정형 형상</b>(지형·비탈면) 설계에 구조적 한계 존재.</li>
<li><span class="keyword">3D/BIM 전환 대비:</span> 측량→설계→시공 간 <b>데이터 단절</b> 문제 해결 및 개방형 포맷(IFC/LandXML) 기반 워크플로우 구축 필요.</li>
</ul>
</div>
<div class="section">
<div class="section-title">핵심 요구사항 및 차별화 전략</div>
<ul>
<li><span class="keyword">비정형 지형 모델링:</span> <b>토공량 자동 산출</b> 및 3차원 지형 분석 기능을 통해 토목 고유 업무의 디지털 전환 가속.</li>
<li><span class="keyword">원스톱 데이터 연계:</span> 측량·설계·시공 <b>전주기 데이터 흐름</b>을 단절 없이 연결하여 수작업 변환 오류 제거.</li>
<li><span class="keyword">개방형 포맷 전환:</span> <b>IFC/LandXML</b> 국제 표준 포맷 채택으로 특정 소프트웨어 종속성 탈피 및 데이터 주권 확보.</li>
<li><span class="keyword">국산 솔루션 육성:</span> 국내 실정에 맞는 토목 전용 솔루션 개발을 통한 <b>기술 자립</b> 및 장기적 비용 절감 실현.</li>
</ul>
</div>
<div class="section">
<div class="section-title">단계별 전환 로드맵</div>
<table>
<thead>
<tr>
<th>단계</th>
<th>핵심 수행 전략</th>
<th>적용 기술 / 기간</th>
</tr>
</thead>
<tbody>
<tr>
<td class="group-cell">현황 진단</td>
<td>부서별 CAD 사용 현황 및 비용 구조 분석, 리스크 도출</td>
<td><b>Q1 '26</b> / 내부 실태 조사</td>
</tr>
<tr>
<td class="group-cell">대안 PoC</td>
<td>Civil 3D, OpenRoads, 국산 솔루션 대상 기능 비교, 시범 적용</td>
<td><b>Q2 '26</b> / 벤치마크</td>
</tr>
<tr>
<td class="group-cell">파일럿 적용</td>
<td>선정 솔루션 실 프로젝트 투입, 기존 워크플로우 호환성 검증</td>
<td><b>Q3 '26</b> / 시범 프로젝트</td>
</tr>
<tr>
<td class="group-cell">전사 확산</td>
<td>교육 체계 수립, 라이선스 전환, 전 부서 단계적 롤아웃</td>
<td><b>Q4 '26</b> / 전사 배포</td>
</tr>
</tbody>
</table>
</div>
<div class="bottom-box">
<div class="bottom-left">기대 효과 및<br>향후 계획</div>
<div class="bottom-right">
<b>라이선스 비용</b> 연간 30% 이상 절감 및 특정 벤더 종속 리스크 해소<br>
• 개방형 포맷 전환을 통한 <b>데이터 주권 확보</b> 및 장기적 기술 자립 기반 마련<br>
• 3D/BIM 기반 <b>원스톱 워크플로우</b> 구축으로 설계 생산성 향상 및 오류 최소화
</div>
</div>
</div>
<footer class="page-footer">
<div class="footer-slogan"><span style="color:#3057B9">기술</span><span style="color:#D32F2F">사람</span>의 가치를 높이고 <span style="color:#388E3C">자연</span>스러운 업무 혁신을 이룹니다</div>
<div class="footer-page">- 1 -</div>
<div class="footer-info">CAD Dependency &amp; Alternative Strategy</div>
</footer>
</div>
</body>
</html>

View File

@@ -0,0 +1,427 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>한국 토목 엔지니어링의 딜레마 - 기획서</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700;900&display=swap');
:root {
--primary-blue: #3057B9;
--gray-light: #F2F2F2;
--gray-medium: #E6E6E6;
--gray-dark: #666666;
--border-light: #DDDDDD;
--text-black: #000000;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
-webkit-print-color-adjust: exact;
}
body {
font-family: 'Noto Sans KR', sans-serif;
background-color: #525659;
color: var(--text-black);
line-height: 1.35;
display: flex;
flex-direction: column;
align-items: center;
padding: 20px 0;
gap: 20px;
}
.sheet {
background-color: white;
width: 210mm;
height: 297mm;
padding: 20mm 20mm;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
position: relative;
display: flex;
flex-direction: column;
overflow: hidden;
}
@media print {
body { background: none; padding: 0; gap: 0; }
.sheet { box-shadow: none; margin: 0; border: none; page-break-after: always; }
.sheet:last-child { page-break-after: auto; }
}
.page-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 15px;
font-size: 8.5pt;
color: var(--gray-dark);
}
.header-title {
font-size: 24pt;
font-weight: 900;
margin-bottom: 8px;
letter-spacing: -1.5px;
color: #111;
line-height: 1.2;
}
.title-divider {
height: 4px;
background-color: var(--primary-blue);
width: 100%;
margin-bottom: 25px;
}
.lead-box {
background-color: var(--gray-light);
padding: 20px 20px;
margin-bottom: 25px;
border-radius: 2px;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
min-height: 80px;
}
.lead-box div {
font-size: 13pt;
font-weight: 700;
color: var(--primary-blue);
letter-spacing: -0.5px;
line-height: 1.4;
word-break: keep-all;
}
.body-content {
flex: 1;
display: flex;
flex-direction: column;
}
.section {
margin-bottom: 25px;
}
.section-title {
font-size: 13pt;
font-weight: 700;
display: flex;
align-items: center;
margin-bottom: 12px;
color: #111;
}
.section-title::before {
content: "";
display: inline-block;
width: 10px;
height: 10px;
background-color: #999;
margin-right: 10px;
}
ul {
list-style: none;
padding-left: 12px;
}
li {
font-size: 11pt;
position: relative;
margin-bottom: 8px;
padding-left: 15px;
color: #333;
text-align: justify;
line-height: 1.5;
letter-spacing: -0.3px;
}
li::before {
content: "•";
position: absolute;
left: 0;
color: #AAA;
font-size: 11pt;
top: 2px;
}
.bottom-box {
border: 1.5px dashed var(--primary-blue);
display: flex;
margin-top: auto;
min-height: 85px;
margin-bottom: 10px;
}
.bottom-left {
width: 20%;
background-color: #F0F4FA;
padding: 12px;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
font-weight: 700;
font-size: 11pt;
color: var(--primary-blue);
line-height: 1.4;
}
.bottom-right {
width: 80%;
background-color: var(--gray-light);
padding: 15px 25px;
font-size: 11pt;
line-height: 1.6;
display: flex;
flex-direction: column;
justify-content: center;
color: #333;
}
.page-footer {
margin-top: 10px;
padding-top: 10px;
display: flex;
justify-content: space-between;
font-size: 8.5pt;
color: var(--gray-dark);
border-top: 1px solid #EEE;
}
.footer-page { flex: 1; text-align: center; }
b { font-weight: 700; color: var(--primary-blue); }
.keyword { font-weight: 700; color: #000; }
/* [Page 2] 목차 표 */
.toc-table {
width: 100%;
height: 100%;
border-collapse: collapse;
font-size: 9.5pt;
border-top: 2px solid var(--primary-blue);
border-bottom: 2px solid #999;
margin-top: 10px;
}
.toc-table th {
background-color: var(--gray-medium);
color: #333;
font-weight: 700;
padding: 12px 5px;
border: 1px solid #ccc;
text-align: center;
height: 45px;
word-break: keep-all;
}
.toc-table td {
border: 1px solid #ddd;
padding: 8px 10px;
vertical-align: middle;
color: #333;
word-break: keep-all;
}
.col-idx { width: 6%; text-align: center; color: #666; font-weight: bold; background-color: #fcfcfc; }
.col-major { width: 16%; font-weight: 700; color: var(--primary-blue); text-align: center; background-color: #ffffff; line-height: 1.3; }
.col-middle { width: 20%; font-weight: 600; color: #444; line-height: 1.3; padding-left: 12px !important; }
.col-sub { width: 22%; color: #333; padding-left: 12px !important; }
.col-content { width: 36%; font-size: 9pt; color: #555; letter-spacing: -0.3px; line-height: 1.4; text-align: left; padding-left: 12px !important; }
.major-row-start td { border-top: 2px solid #ccc; }
</style>
</head>
<body>
<!-- [PAGE 1] 기획 개요서 -->
<div class="sheet">
<header class="page-header">
<div class="header-left">
조직: 건설 DX 추진팀 / 기술혁신본부
</div>
<div class="header-right">
2026. 02. 10.
</div>
</header>
<div class="title-block">
<h1 class="header-title">한국 토목 엔지니어링의 딜레마<br>— AutoCAD 독점과 대안 전략</h1>
<div class="title-divider"></div>
</div>
<div class="body-content">
<div class="lead-box">
<div>"AutoCAD 독점 구조의 기술적·경제적 리스크를 분석하고,<br>3D/BIM 시대에 대응하는 단계적 전환 로드맵을 제시"</div>
</div>
<div class="section">
<div class="section-title">추진 배경 및 목적</div>
<ul>
<li><span class="keyword">독점 시장 구조 타파:</span> 국내 토목 설계 시장의 <b>AutoCAD 점유율 85% 이상</b>, 연간 라이선스 비용(인당 280만원) 지속 인상으로 경제적 부담 심화.</li>
<li><span class="keyword">기술 종속 해소:</span> 비공개 독점 포맷(.dwg) 기반 성과물 관리로 <b>데이터 주권</b> 및 지식재산권 제약 발생, 장기적 기술 자립 저해.</li>
<li><span class="keyword">토목 특성 대응:</span> AutoCAD는 건축 직교 체계에 최적화되어 토목 분야의 <b>비정형 형상</b>(지형·비탈면) 설계에 구조적 한계 존재.</li>
</ul>
</div>
<div class="section">
<div class="section-title">핵심 요구사항</div>
<ul>
<li><span class="keyword">비정형 지형 모델링:</span> <b>토공량 자동 산출</b>, 3차원 지형 분석 기능을 통해 토목 고유 업무의 디지털 전환 가속.</li>
<li><span class="keyword">원스톱 데이터 연계:</span> 측량·설계·시공 <b>전주기 데이터 흐름</b>을 단절 없이 연결하여 수작업 변환 오류 제거.</li>
<li><span class="keyword">개방형 포맷 전환:</span> <b>IFC/LandXML</b> 등 국제 표준 포맷 채택으로 특정 소프트웨어 종속성 탈피.</li>
<li><span class="keyword">국산 솔루션 육성:</span> 국내 실정에 맞는 토목 전용 솔루션 개발을 통한 <b>기술 자립</b> 및 장기적 비용 절감 실현.</li>
</ul>
</div>
<div class="section">
<div class="section-title">기대 효과</div>
<ul>
<li><span class="keyword">비용 절감:</span> 라이선스 비용 연간 <b>30% 이상 절감</b> 및 특정 벤더 종속 리스크 해소.</li>
<li><span class="keyword">데이터 주권 확보:</span> 개방형 포맷 전환을 통한 성과물 소유권 보장 및 장기적 기술 자립 기반 마련.</li>
<li><span class="keyword">생산성 향상:</span> 3D/BIM 기반 원스톱 워크플로우 구축으로 설계 생산성 향상 및 오류 최소화.</li>
</ul>
</div>
<div class="bottom-box">
<div class="bottom-left">실행 내용<br>및 계획</div>
<div class="bottom-right">
- 현황 검토(1m): 부서별 CAD 사용 현황 및 비용 구조 분석, 리스크 도출<br>
- 대안 검토(2m): 국산 솔루션 대상 기능 비교 및 검토<br>
</div>
</div>
</div>
<footer class="page-footer">
<div class="footer-page">- 1 -</div>
<div class="footer-info">CAD Dependency &amp; Alternative Strategy</div>
</footer>
</div>
<!-- [PAGE 2] 보고서 상세 목차 -->
<div class="sheet">
<header class="page-header">
<div class="header-left">
[첨부] 보고서 상세 구성안
</div>
<div class="header-right">
Base: AutoCAD 독점 분석 보고서
</div>
</header>
<div class="title-block">
<h1 class="header-title" style="font-size: 18pt; margin-bottom: 5px;">보고서 상세 목차 구성안 (Table of Contents)</h1>
<div class="title-divider" style="height: 2px; margin-bottom: 15px;"></div>
</div>
<div class="body-content" style="display: block;">
<table class="toc-table">
<thead>
<tr>
<th class="col-idx">NO</th>
<th class="col-major">대목차</th>
<th class="col-middle">중목차</th>
<th class="col-sub">소목차</th>
<th class="col-content">주요 내용</th>
</tr>
</thead>
<tbody>
<!-- 1. 시장 현황 -->
<tr class="major-row-start">
<td class="col-idx" rowspan="2">1</td>
<td class="col-major" rowspan="2">1. 토목 소프트<br>웨어 시장 현황</td>
<td class="col-middle">1.1 시장 점유율 현황<br>및 독점적 지위</td>
<td class="col-sub">1.1.1 국내외 점유율 비교<br>1.1.2 독점 구조 형성 배경</td>
<td class="col-content">AutoCAD 85%+ 독점, 교육·관행·호환성 삼중 잠금 효과 분석</td>
</tr>
<tr>
<td class="col-middle">1.2 독점적 지위의<br>배경과 문제점</td>
<td class="col-sub">1.2.1 비용 구조<br>1.2.2 기술 종속</td>
<td class="col-content">라이선스 비용 인상, DWG 포맷 종속, 대안 부재 악순환</td>
</tr>
<!-- 2. 기술 적합성 -->
<tr class="major-row-start">
<td class="col-idx" rowspan="3">2</td>
<td class="col-major" rowspan="3">2. AutoCAD의<br>토목 적합성<br>검증</td>
<td class="col-middle">2.1 토목과 건축의 차이</td>
<td class="col-sub">2.1.1 레고와 찰흙 비유<br>2.1.2 비정형 형상 한계</td>
<td class="col-content">건축(직교/모듈) vs 토목(비정형/지형), CAD 설계 체계 부적합</td>
</tr>
<tr>
<td class="col-middle">2.2 실무적 기능 한계</td>
<td class="col-sub">2.2.1 토공량 산출 불가<br>2.2.2 지형 모델링 한계</td>
<td class="col-content">3차원 지형 분석, 토공량 자동 산출 기능 부재</td>
</tr>
<tr>
<td class="col-middle">2.3 기술 트렌드와의<br>부조화</td>
<td class="col-sub">2.3.1 데이터 단절<br>2.3.2 BIM 전환 지연</td>
<td class="col-content">측량→설계→시공 워크플로우 단절, 수작업 변환 오류 누적</td>
</tr>
<!-- 3. 시장의 족쇄 -->
<tr class="major-row-start">
<td class="col-idx" rowspan="2">3</td>
<td class="col-major" rowspan="2">3. 시장의 족쇄:<br>관행인가,<br>필수인가</td>
<td class="col-middle">3.1 익숙함의 함정</td>
<td class="col-sub">3.1.1 기술적 편의성<br>3.1.2 굳어진 관행</td>
<td class="col-content">전환 비용 인식, 학습 곡선, 업계 표준화 관성 분석</td>
</tr>
<tr>
<td class="col-middle">3.2 선택의 제약과<br>기술적 우위의 허상</td>
<td class="col-sub">3.2.1 라이선스 압박<br>3.2.2 기술 우위 검증</td>
<td class="col-content">비용 인상 구조, 대안 대비 실질적 기술 우위 객관적 평가</td>
</tr>
<!-- 4. 지식재산권 -->
<tr class="major-row-start">
<td class="col-idx" rowspan="2">4</td>
<td class="col-major" rowspan="2">4. 지식재산권:<br>문제점과<br>해결 방안</td>
<td class="col-middle">4.1 성과물 소유권의<br>왜곡과 종속성</td>
<td class="col-sub">4.1.1 DWG 포맷 종속<br>4.1.2 데이터 주권 침해</td>
<td class="col-content">비공개 포맷 의존, 성과물 소유권 귀속 문제, 보안 리스크</td>
</tr>
<tr>
<td class="col-middle">4.2 해결 방안</td>
<td class="col-sub">4.2.1 개방형 포맷 전환<br>4.2.2 법적·제도적 개선</td>
<td class="col-content">IFC/LandXML 채택, 공공조달 포맷 다양화 정책 제안</td>
</tr>
<!-- 5. 대안 -->
<tr class="major-row-start">
<td class="col-idx" rowspan="2">5</td>
<td class="col-major" rowspan="2">5. 새로운 가능성:<br>대안을 찾아서</td>
<td class="col-middle">5.1 엔지니어의<br>핵심 요구사항</td>
<td class="col-sub">5.1.1 기능 요구사항<br>5.1.2 워크플로우 요건</td>
<td class="col-content">비정형 모델링, 토공량 산출, 전주기 데이터 연계 필수 기능</td>
</tr>
<tr>
<td class="col-middle">5.2 대안 소프트웨어<br>및 국산 솔루션</td>
<td class="col-sub">5.2.1 Civil 3D/OpenRoads<br>5.2.2 국산 솔루션 전략</td>
<td class="col-content">대안별 강점 비교, 국내 개발의 전략적 중요성 및 육성 방안</td>
</tr>
<!-- 6. 결론 -->
<tr class="major-row-start">
<td class="col-idx">6</td>
<td class="col-major">6. 결론 및<br>시사점</td>
<td class="col-middle">6.1 결론<br>6.2 시사점</td>
<td class="col-sub">6.1.1 종합 평가<br>6.2.1 전환 로드맵</td>
<td class="col-content">단계별 전환 전략(Q1~Q4), 비용 절감·데이터 주권 확보 기대효과</td>
</tr>
</tbody>
</table>
</div>
<footer class="page-footer">
<div class="footer-page">- 2 -</div>
<div class="footer-info">Report Detailed Table of Contents</div>
</footer>
</div>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,513 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>한국 토목 엔지니어링의 딜레마 - 발표자료</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700;900&display=swap');
:root {
--primary-dark: #1a365d;
--primary-light: #2b6cb0;
--bg-gray: #f4f6f8;
--point-red-bg: #ffebee;
--point-red-border: #ef5350;
--point-red-text: #b71c1c;
--text-main: #333333;
--white: #FFFFFF;
--slide-width: 1120px;
--slide-height: 630px;
}
body {
margin: 0; padding: 40px;
background-color: #525659;
font-family: 'Noto Sans KR', sans-serif;
display: flex; flex-direction: column;
align-items: center; gap: 40px;
}
.slide {
width: var(--slide-width); height: var(--slide-height);
background: var(--white);
box-shadow: 0 15px 35px rgba(0,0,0,0.2);
position: relative; overflow: hidden;
display: flex; flex-direction: column;
box-sizing: border-box; border-radius: 4px;
}
/* ===== 표지 ===== */
.slide.cover {
background: linear-gradient(135deg, var(--primary-dark) 0%, #0a1628 100%);
color: var(--white); justify-content: center; padding: 60px;
}
.cover::after {
content: ''; position: absolute; top: 0; right: 0;
width: 35%; height: 100%;
background: rgba(255,255,255,0.05);
clip-path: polygon(20% 0%, 100% 0%, 100% 100%, 0% 100%);
}
.cover-content { z-index: 1; margin-left: 20px; }
.cover-content h1 {
font-size: 48pt; font-weight: 900; margin: 0 0 20px 0;
line-height: 1.1; letter-spacing: -2px;
}
.cover-content h2 {
font-size: 20pt; font-weight: 300; margin: 0 0 60px 0;
opacity: 0.9; padding-left: 25px; border-left: 6px solid #63b3ed;
}
.cover-footer {
position: absolute; bottom: 50px; left: 80px;
font-size: 14pt; opacity: 0.8; display: flex; gap: 40px;
}
/* ===== 목차 ===== */
.slide.index { padding: 45px 80px; display: flex; flex-direction: column; }
.slide-title {
font-size: 28pt; font-weight: 900; color: var(--primary-dark);
margin-bottom: 25px; padding-bottom: 15px;
border-bottom: 3px solid #eee; display: inline-block;
flex-shrink: 0;
}
.index-list { display: flex; flex-direction: column; gap: 12px; flex: 1; justify-content: center; }
.index-item {
display: flex; align-items: center; font-size: 16pt; font-weight: 700;
color: var(--text-main); padding: 12px 18px; background: var(--bg-gray);
border-radius: 10px; transition: 0.3s;
}
.index-num {
font-size: 20pt; font-weight: 900; color: var(--primary-light);
margin-right: 20px; opacity: 0.3; min-width: 40px;
}
.index-item:hover { transform: translateX(10px); background: #ebf4ff; }
.index-item:hover .index-num { opacity: 1; }
/* ===== 내지 공통 ===== */
.slide.content { padding: 40px 50px; }
.header {
display: flex; justify-content: space-between; align-items: flex-end;
border-bottom: 2px solid #eee; padding-bottom: 10px; margin-bottom: 15px;
height: 50px; flex-shrink: 0;
}
.header h3 {
font-size: 20pt; font-weight: 800; color: var(--primary-dark); margin: 0;
display: flex; align-items: center; gap: 15px;
}
.header-num {
background: var(--primary-dark); color: white; font-size: 14pt;
padding: 2px 12px; border-radius: 4px;
}
.header span { font-size: 11pt; color: #888; }
/* 2열 레이아웃 */
.content-body {
display: flex; gap: 30px; flex: 1;
margin-bottom: 15px; overflow: hidden;
}
.col-box { flex: 1; display: flex; flex-direction: column; }
.box-title {
font-size: 15pt; font-weight: 700; color: var(--primary-dark);
margin-bottom: 10px; padding-left: 10px; border-left: 5px solid var(--primary-light);
}
/* 표 */
.data-table {
width: 100%; border-collapse: collapse; font-size: 11.5pt;
border: 1px solid #ddd; border-radius: 8px; overflow: hidden;
box-shadow: 0 2px 5px rgba(0,0,0,0.05);
}
.data-table th {
background: #2d3748; color: white; padding: 8px 10px;
font-weight: 600; text-align: center;
}
.data-table td {
border-bottom: 1px solid #eee; padding: 8px 10px;
text-align: center; color: #444; vertical-align: middle;
}
.data-table tr:last-child td { border-bottom: none; }
.bg-accent { background-color: #ebf4ff; font-weight: bold; color: var(--primary-dark); }
.text-point { color: #c53030; font-weight: bold; }
/* 리스트 */
.slide-list { margin: 0; padding-left: 0; list-style: none; }
.slide-list li {
font-size: 13pt; line-height: 1.6; margin-bottom: 10px;
padding-left: 20px; position: relative; color: #444;
}
.slide-list li::before {
content: "▸"; position: absolute; left: 0;
color: var(--primary-light); font-weight: bold;
}
.slide-list strong { color: var(--primary-dark); }
/* 리스크/액션 */
.state-box {
padding: 14px; border-radius: 8px; margin-bottom: 10px;
}
.state-risk { background: #fff5f5; border: 1px solid #ffcdd2; }
.state-action { background: #ebf4ff; border: 1px solid #bee3f8; }
.state-title { font-weight: 800; font-size: 13pt; margin-bottom: 8px; }
.state-list { margin: 0; padding-left: 18px; font-size: 11pt; line-height: 1.5; color: #555; }
/* 하단 메시지 */
.bottom-message {
height: 70px; flex-shrink: 0;
background-color: var(--point-red-bg);
border: 2px solid var(--point-red-border);
color: var(--point-red-text);
border-radius: 12px;
display: flex; align-items: center; justify-content: center;
text-align: center;
font-size: 17pt; font-weight: 500;
}
.bottom-message strong {
font-weight: 900; color: #b71c1c; margin: 0 5px;
text-decoration: underline; text-underline-offset: 4px;
}
/* 하단 메시지 파란 */
.bottom-message-blue {
height: 70px; flex-shrink: 0;
background-color: #ebf4ff;
border: 2px solid var(--primary-light);
color: var(--primary-dark);
border-radius: 12px;
display: flex; align-items: center; justify-content: center;
text-align: center;
font-size: 17pt; font-weight: 500;
}
.bottom-message-blue strong {
font-weight: 900; color: var(--primary-dark); margin: 0 5px;
}
/* 프로세스 */
.process-flow {
display: flex; align-items: center; justify-content: center;
gap: 0; margin: 15px 0;
}
.process-step {
background: #ebf4ff; border: 1px solid #bee3f8;
border-radius: 8px; padding: 10px 20px; text-align: center;
font-size: 12pt; font-weight: 600; color: var(--primary-dark);
}
.process-arrow { font-size: 18pt; color: var(--primary-light); margin: 0 6px; }
@media print {
body { margin: 0; padding: 0; background: none; }
.slide { margin: 0; page-break-after: always; box-shadow: none; border: 1px solid #ddd; }
}
</style>
</head>
<body>
<!-- [1. 표지] -->
<div class="slide cover">
<div class="cover-content">
<h1>한국 토목<br>엔지니어링의 딜레마</h1>
<h2>AutoCAD 독점과<br>미래를 위한 대안 모색</h2>
</div>
<div class="cover-footer">
<p><strong>DATE.</strong> 2026. 02. 10</p>
<p><strong>REPORT.</strong> 건설 DX 추진팀</p>
</div>
</div>
<!-- [2. 목차] -->
<div class="slide index">
<div class="slide-title">Table of Contents</div>
<div class="index-list">
<div class="index-item"><span class="index-num">01</span><span>한국 토목 엔지니어링 소프트웨어 시장 현황</span></div>
<div class="index-item"><span class="index-num">02</span><span>AutoCAD, 토목설계에 정말 적합한가?</span></div>
<div class="index-item"><span class="index-num">03</span><span>시장의 족쇄: 관행인가, 필수인가?</span></div>
<div class="index-item"><span class="index-num">04</span><span>지식재산권: 문제점과 해결 방안</span></div>
<div class="index-item"><span class="index-num">05</span><span>새로운 가능성: 대안을 찾아서</span></div>
<div class="index-item"><span class="index-num">06</span><span>결론 및 시사점</span></div>
</div>
</div>
<!-- [3. 시장 현황] -->
<div class="slide content">
<div class="header">
<h3><span class="header-num">01</span> 토목 소프트웨어 시장 현황</h3>
<span>시장 점유율 및 독점 구조 분석</span>
</div>
<div class="content-body">
<div class="col-box">
<div class="box-title">시장 점유율 현황</div>
<table class="data-table">
<thead>
<tr><th>구분</th><th>현황</th><th>리스크</th></tr>
</thead>
<tbody>
<tr><td>시장 점유율</td><td>AutoCAD <strong>85%+</strong></td><td class="text-point">독점 종속</td></tr>
<tr><td>라이선스</td><td>연 280만원/인</td><td class="text-point">매년 인상</td></tr>
<tr><td>DWG 포맷</td><td>비공개 독점</td><td class="text-point">데이터 종속</td></tr>
<tr><td>3D/BIM 대응</td><td>건축 중심</td><td class="text-point">토목 부적합</td></tr>
</tbody>
</table>
</div>
<div class="col-box">
<div class="box-title">독점 구조 형성 배경</div>
<ul class="slide-list">
<li><strong>교육 잠금:</strong> 대학·기관에서 AutoCAD 중심 교육 → 입사 시 이미 숙련</li>
<li><strong>관행 잠금:</strong> "다 쓰니까 우리도" — 업계 전체가 DWG 기반</li>
<li><strong>호환성 잠금:</strong> 발주처·협력사 모두 DWG 요구 → 전환 불가</li>
<li><strong>악순환:</strong> 대안 없음 → 비용 인상 수용 → 종속 심화</li>
</ul>
</div>
</div>
<div class="bottom-message">
교육 → 관행 → 호환성의&nbsp;<strong>삼중 잠금 효과</strong>가 독점을 유지하는 핵심 메커니즘
</div>
</div>
<!-- [4. 토목 적합성] -->
<div class="slide content">
<div class="header">
<h3><span class="header-num">02</span> AutoCAD, 토목설계에 적합한가?</h3>
<span>건축 vs 토목의 근본적 차이</span>
</div>
<div class="content-body">
<div class="col-box">
<div class="box-title">건축 vs 토목: 레고와 찰흙</div>
<table class="data-table">
<thead>
<tr><th>구분</th><th style="background:#78909c;">건축 (레고)</th><th style="background:#2b6cb0;">토목 (찰흙)</th></tr>
</thead>
<tbody>
<tr><td>형상</td><td>직교·모듈</td><td class="bg-accent">비정형·곡면</td></tr>
<tr><td>대상</td><td>건물·실내</td><td class="bg-accent">지형·비탈면</td></tr>
<tr><td>설계</td><td>2D 도면 중심</td><td class="bg-accent">3D 모델 필수</td></tr>
<tr><td>CAD 적합도</td><td>✅ 최적화</td><td class="text-point">❌ 구조적 한계</td></tr>
</tbody>
</table>
</div>
<div class="col-box">
<div class="box-title">실무적 기능 한계</div>
<div class="state-box state-risk">
<div class="state-title" style="color:#c53030;">⚠️ 기능 부재</div>
<ul class="state-list">
<li><strong>토공량 자동 산출</strong> 불가 — 수작업 계산 의존</li>
<li><strong>3차원 지형 분석</strong> 미지원 — 별도 SW 필요</li>
<li>비정형 곡면 모델링 한계</li>
</ul>
</div>
<div class="state-box state-action">
<div class="state-title" style="color:#2b6cb0;">🔗 데이터 단절</div>
<ul class="state-list">
<li>측량 → 설계 → 시공 간 <strong>수작업 변환</strong> 반복</li>
<li>변환 과정에서 <strong>오류 누적</strong> → 품질 저하</li>
<li>BIM 전환 지연의 근본 원인</li>
</ul>
</div>
</div>
</div>
<div class="bottom-message">
AutoCAD는&nbsp;<strong>건축 직교 체계</strong>에 최적화 — 토목의&nbsp;<strong>비정형 지형</strong>에는 구조적 부적합
</div>
</div>
<!-- [5. 시장의 족쇄] -->
<div class="slide content">
<div class="header">
<h3><span class="header-num">03</span> 시장의 족쇄: 관행인가, 필수인가?</h3>
<span>익숙함의 함정과 선택의 제약</span>
</div>
<div class="content-body">
<div class="col-box">
<div class="box-title">익숙함의 함정</div>
<ul class="slide-list">
<li><strong>전환 비용 인식:</strong> "바꾸면 6개월 생산성 저하" → 현상 유지 선택</li>
<li><strong>학습 곡선:</strong> 신입 교육부터 AutoCAD → 다른 도구 경험 부재</li>
<li><strong>업계 관성:</strong> 발주처가 DWG를 요구하는 한 전환 동기 약함</li>
</ul>
<div class="state-box state-risk" style="margin-top:auto;">
<div class="state-title" style="color:#c53030;">💰 비용 압박 현실</div>
<ul class="state-list">
<li>연 라이선스 <strong>280만원/인</strong> — 매년 5~10% 인상</li>
<li>50인 기업 기준: 연 <strong>1.4억원</strong> 고정 지출</li>
</ul>
</div>
</div>
<div class="col-box">
<div class="box-title">기술적 우위의 허상</div>
<table class="data-table">
<thead>
<tr><th>평가 항목</th><th>AutoCAD</th><th>대안 SW</th></tr>
</thead>
<tbody>
<tr><td>2D 도면</td><td class="bg-accent">우수</td><td>동등</td></tr>
<tr><td>3D 모델링</td><td>미흡</td><td class="bg-accent">우수</td></tr>
<tr><td>토목 특화</td><td class="text-point">없음</td><td class="bg-accent">전용 기능</td></tr>
<tr><td>개방형 포맷</td><td class="text-point">DWG 독점</td><td class="bg-accent">IFC 지원</td></tr>
<tr><td>가격 경쟁력</td><td class="text-point">고가</td><td class="bg-accent">경쟁적</td></tr>
</tbody>
</table>
</div>
</div>
<div class="bottom-message">
익숙함은&nbsp;<strong>기술적 우위가 아니다</strong>&nbsp;— 객관적 비교 시 대안이 토목에 더 적합
</div>
</div>
<!-- [6. 지식재산권] -->
<div class="slide content">
<div class="header">
<h3><span class="header-num">04</span> 지식재산권: 문제점과 해결 방안</h3>
<span>데이터 주권과 성과물 소유권</span>
</div>
<div class="content-body">
<div class="col-box">
<div class="box-title">문제점: 3중 종속 구조</div>
<div class="state-box state-risk">
<div class="state-title" style="color:#c53030;">🔒 성과물 소유권 왜곡</div>
<ul class="state-list">
<li>.dwg 포맷 = Autodesk <strong>소유 포맷</strong></li>
<li>우리가 만든 도면의 포맷 소유권이 타사에 귀속</li>
</ul>
</div>
<div class="state-box state-risk">
<div class="state-title" style="color:#c53030;">⛓️ 기술 종속</div>
<ul class="state-list">
<li>DWG 읽기/쓰기에 AutoCAD <strong>필수</strong></li>
<li>라이선스 중단 시 과거 성과물 접근 불가</li>
</ul>
</div>
<div class="state-box state-risk">
<div class="state-title" style="color:#c53030;">🔓 데이터 보안</div>
<ul class="state-list">
<li>클라우드 전환 시 해외 서버 저장 리스크</li>
<li>국가 인프라 데이터의 주권 문제</li>
</ul>
</div>
</div>
<div class="col-box">
<div class="box-title">해결 방안</div>
<div class="state-box state-action">
<div class="state-title" style="color:#2b6cb0;">📂 개방형 포맷 전환</div>
<ul class="state-list">
<li><strong>IFC</strong> — 건설 산업 국제 표준</li>
<li><strong>LandXML</strong> — 토목 측량 데이터 표준</li>
<li>특정 SW 없이 성과물 열람·활용 가능</li>
</ul>
</div>
<div class="state-box state-action">
<div class="state-title" style="color:#2b6cb0;">📋 제도적 개선</div>
<ul class="state-list">
<li>공공조달 <strong>납품 포맷 다양화</strong> 의무화</li>
<li>개방형 포맷 우대 가점 제도 도입</li>
<li>국산 SW 호환 인증 체계 구축</li>
</ul>
</div>
</div>
</div>
<div class="bottom-message-blue">
<strong>개방형 포맷 전환</strong>&nbsp;<strong>제도적 뒷받침</strong>으로 데이터 주권을 확보해야 한다
</div>
</div>
<!-- [7. 대안 모색] -->
<div class="slide content">
<div class="header">
<h3><span class="header-num">05</span> 새로운 가능성: 대안을 찾아서</h3>
<span>핵심 요구사항과 대안 소프트웨어</span>
</div>
<div class="content-body">
<div class="col-box">
<div class="box-title">엔지니어 핵심 요구사항</div>
<ul class="slide-list">
<li><strong>비정형 지형 모델링</strong> + 토공량 자동 산출</li>
<li>측량·설계·시공 <strong>전주기 데이터 연계</strong></li>
<li>개방형 포맷 기반 <strong>데이터 주권 확보</strong></li>
<li>직관적 UI + 한국어 지원</li>
</ul>
<div class="box-title" style="margin-top:15px;">전환 로드맵</div>
<div class="process-flow">
<div class="process-step">현황 진단<br><span style="font-size:9pt;color:#718096;">Q1 '26</span></div>
<span class="process-arrow"></span>
<div class="process-step">대안 PoC<br><span style="font-size:9pt;color:#718096;">Q2 '26</span></div>
<span class="process-arrow"></span>
<div class="process-step">파일럿<br><span style="font-size:9pt;color:#718096;">Q3 '26</span></div>
<span class="process-arrow"></span>
<div class="process-step">전사 확산<br><span style="font-size:9pt;color:#718096;">Q4 '26</span></div>
</div>
</div>
<div class="col-box">
<div class="box-title">대안 소프트웨어 비교</div>
<table class="data-table">
<thead>
<tr><th>소프트웨어</th><th>강점</th><th>적용 분야</th></tr>
</thead>
<tbody>
<tr><td><strong>Civil 3D</strong></td><td>Autodesk 생태계 호환</td><td>도로·단지</td></tr>
<tr><td><strong>OpenRoads</strong></td><td>토목 특화 3D 설계</td><td>도로·철도</td></tr>
<tr><td><strong>국산 솔루션</strong></td><td class="bg-accent">데이터 주권 + 맞춤형</td><td>측량·GIS</td></tr>
</tbody>
</table>
<div class="state-box state-action" style="margin-top:15px;">
<div class="state-title" style="color:#2b6cb0;">🇰🇷 국산 솔루션의 전략적 중요성</div>
<ul class="state-list">
<li>국내 토목 실정에 최적화된 기능 구현</li>
<li>데이터 주권 완전 확보 (국내 서버)</li>
<li>장기적 라이선스 비용 절감</li>
</ul>
</div>
</div>
</div>
<div class="bottom-message-blue">
단계적 전환으로 리스크 최소화 —&nbsp;<strong>국산 솔루션 육성</strong>이 장기적 해법
</div>
</div>
<!-- [8. 결론] -->
<div class="slide content">
<div class="header">
<h3><span class="header-num">06</span> 결론 및 시사점</h3>
<span>전략적 선택과 실행 과제</span>
</div>
<div class="content-body">
<div class="col-box">
<div class="box-title">종합 결론</div>
<ul class="slide-list">
<li>AutoCAD 독점은 <strong>기술적 우위</strong>가 아닌 <strong>관행과 잠금 효과</strong>의 산물</li>
<li>토목 분야에서 AutoCAD는 <strong>구조적으로 부적합</strong> — 비정형 설계 한계</li>
<li>.dwg 포맷 종속은 <strong>데이터 주권</strong><strong>지식재산권</strong>을 위협</li>
<li>3D/BIM 시대 전환은 <strong>선택이 아닌 필수</strong></li>
</ul>
<div class="state-box state-action" style="margin-top:auto;">
<div class="state-title" style="color:#2b6cb0;">💡 핵심 메시지</div>
<ul class="state-list" style="font-size:12pt;">
<li><strong>독점 탈피</strong>는 비용 절감이 아닌 기술 경쟁력의 문제</li>
<li><strong>데이터 주권</strong> 확보가 국가 인프라 보호의 시작</li>
</ul>
</div>
</div>
<div class="col-box">
<div class="box-title">기대 효과</div>
<table class="data-table">
<thead>
<tr><th>영역</th><th>기대 효과</th></tr>
</thead>
<tbody>
<tr><td><strong>비용</strong></td><td>라이선스 비용 <strong style="color:#c53030;">연 30%+ 절감</strong></td></tr>
<tr><td><strong>데이터</strong></td><td>개방형 포맷으로 <strong>주권 확보</strong></td></tr>
<tr><td><strong>생산성</strong></td><td>전주기 연계로 <strong>오류 최소화</strong></td></tr>
<tr><td><strong>경쟁력</strong></td><td>3D/BIM 기반 <strong>기술 리더십</strong></td></tr>
</tbody>
</table>
<div class="box-title" style="margin-top:15px;">실행 과제</div>
<ul class="slide-list" style="font-size:12pt;">
<li>Q1: 부서별 현황 진단 및 리스크 매핑</li>
<li>Q2: 대안 SW PoC 및 벤치마크</li>
<li>Q3: 파일럿 프로젝트 실증</li>
<li>Q4: 전사 롤아웃 및 교육 체계 구축</li>
</ul>
</div>
</div>
<div class="bottom-message-blue">
<strong>지금 시작하지 않으면, 독점의 대가는 계속 커진다</strong>
</div>
</div>
</body>
</html>