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