📦 Initialize Geulbeot structure and merge Prompts & test projects

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

View File

@@ -0,0 +1,26 @@
{
"id": "briefing",
"name": "기획서",
"icon": "📋",
"description": "1~2페이지 분량의 임원 보고용 문서",
"features": [
{"icon": "📌", "text": "헤더 + 제목 블록"},
{"icon": "💡", "text": "핵심 요약 (Lead Box)"},
{"icon": "📊", "text": "본문 섹션 + 첨부"}
],
"thumbnailType": "briefing",
"enabled": true,
"isDefault": true,
"order": 1,
"options": {
"pageConfig": {
"type": "radio-with-input",
"choices": [
{"value": "body-only", "label": "(본문) 1p"},
{"value": "body-attach", "label": "(본문) 1p + (첨부)", "hasInput": true, "inputSuffix": "p", "inputDefault": 1, "inputMin": 1, "inputMax": 10, "default": true}
]
}
},
"createdAt": "2024-01-01T00:00:00Z",
"updatedAt": "2025-01-30T00:00:00Z"
}

View File

@@ -0,0 +1,27 @@
{
"id": "presentation",
"name": "발표자료",
"icon": "📊",
"description": "슬라이드 형식의 프레젠테이션",
"features": [
{"icon": "🎯", "text": "슬라이드 레이아웃"},
{"icon": "📈", "text": "차트/다이어그램"},
{"icon": "🎨", "text": "비주얼 중심 구성"}
],
"thumbnailType": "ppt",
"enabled": false,
"isDefault": true,
"order": 3,
"badge": "준비중",
"options": {
"slideCount": [
{"value": "auto", "label": "자동 (내용 기반)", "default": true},
{"value": "5", "label": "5장 이내"},
{"value": "10", "label": "10장 이내"},
{"value": "20", "label": "20장 이내"}
]
},
"generateFlow": "draft-first",
"createdAt": "2024-01-01T00:00:00Z",
"updatedAt": "2025-01-30T00:00:00Z"
}

View File

@@ -0,0 +1,26 @@
{
"id": "report",
"name": "보고서",
"icon": "📄",
"description": "다페이지 분량의 상세 보고서",
"features": [
{"icon": "📘", "text": "표지 (선택)"},
{"icon": "📑", "text": "목차 자동 생성"},
{"icon": "📝", "text": "챕터별 내지"}
],
"thumbnailType": "report",
"enabled": true,
"isDefault": true,
"order": 2,
"options": {
"components": [
{"id": "cover", "label": "표지", "icon": "📘", "default": true},
{"id": "toc", "label": "목차", "icon": "📑", "default": true},
{"id": "divider", "label": "간지", "icon": "📄", "default": false},
{"id": "content", "label": "내지 (필수)", "icon": "📝", "default": true, "required": true}
]
},
"generateFlow": "draft-first",
"createdAt": "2024-01-01T00:00:00Z",
"updatedAt": "2025-01-30T00:00:00Z"
}

View File

@@ -0,0 +1,302 @@
# A4 HTML 문서 레이아웃 가이드
> 이 가이드는 글벗 doc_template_analyzer가 HWPX에서 추출한 구조를
> A4 규격 HTML template.html로 변환할 때 참조하는 레이아웃 규격입니다.
>
> ★ 이 파일의 값은 코드에 하드코딩하지 않습니다.
> ★ doc_template_analyzer._build_css(), _build_full_html() 등에서 이 파일을 읽어 적용합니다.
> ★ 색상, 폰트 등 스타일은 HWPX에서 추출한 값을 우선 사용하고, 없으면 이 가이드의 기본값을 사용합니다.
---
## 1. 페이지 규격 (Page Dimensions)
```yaml
page:
width: 210mm # A4 가로
height: 297mm # A4 세로
background: white
boxSizing: border-box
margins:
top: 20mm # 상단 여백 (머릿말 + 본문 시작)
bottom: 20mm # 하단 여백 (꼬릿말 + 본문 끝)
left: 20mm # 좌측 여백
right: 20mm # 우측 여백
# 본문 가용 높이 = 297mm - 20mm(상) - 20mm(하) = 257mm ≈ 970px
bodyMaxHeight: 970px
```
## 2. HTML 골격 구조 (Page Structure)
각 페이지는 `.sheet` 클래스로 감싸며, 내부에 header/body/footer를 absolute로 배치합니다.
```html
<!-- 페이지 단위 -->
<div class="sheet">
<!-- 머릿말: 상단 여백(20mm) 안, 위에서 10mm 지점 -->
<div class="page-header">
<!-- HWPX 머릿말 테이블 내용 -->
</div>
<!-- 본문: 상하좌우 20mm 안쪽 -->
<div class="body-content">
<!-- 섹션 제목, 표, 개조식 등 -->
</div>
<!-- 꼬릿말: 하단 여백(20mm) 안, 아래에서 10mm 지점 -->
<div class="page-footer">
<!-- HWPX 꼬릿말 테이블 내용 -->
</div>
</div>
```
## 3. 핵심 CSS 레이아웃 (Layout CSS)
### 3.1 용지 (.sheet)
```css
.sheet {
width: 210mm;
height: 297mm;
background: white;
margin: 20px auto; /* 화면 미리보기용 */
position: relative;
overflow: hidden;
box-sizing: border-box;
box-shadow: 0 0 15px rgba(0,0,0,0.1);
}
```
### 3.2 인쇄 대응
```css
@media print {
.sheet { margin: 0; break-after: page; box-shadow: none; }
body { background: white; }
}
```
### 3.3 머릿말 (.page-header)
```css
.page-header {
position: absolute;
top: 10mm; /* 상단 여백(20mm)의 중간 */
left: 20mm;
right: 20mm;
font-size: 9pt;
padding-bottom: 5px;
}
```
- 머릿말이 **테이블 형태**인 경우: `<table>` 사용, 테두리 없음
- HWPX에서 추출한 열 수와 셀 내용을 placeholder로 배치
- 다중행 셀은 `<br>`로 줄바꿈
### 3.4 꼬릿말 (.page-footer)
```css
.page-footer {
position: absolute;
bottom: 10mm; /* 하단 여백(20mm)의 중간 */
left: 20mm;
right: 20mm;
font-size: 9pt;
color: #555;
border-top: 1px solid #eee;
padding-top: 5px;
}
```
- 꼬릿말이 **테이블 형태**인 경우: `<table>` 사용, 테두리 없음
- 2열 이상일 때 `display: flex; justify-content: space-between` 패턴도 가능
- 페이지 번호는 별도 `<span class="pg-num">` 으로
### 3.5 본문 영역 (.body-content)
```css
.body-content {
position: absolute;
top: 20mm;
left: 20mm;
right: 20mm;
bottom: 20mm; /* 또는 auto + JS 제어 */
}
```
## 4. 타이포그래피 기본값 (Typography Defaults)
> HWPX에서 폰트/크기를 추출했으면 그 값을 사용합니다.
> 추출 실패 시 아래 기본값을 적용합니다.
```yaml
typography:
fontFamily: "'Noto Sans KR', sans-serif"
fontImport: "https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700;900&display=swap"
body:
fontSize: 12pt
lineHeight: 1.6
textAlign: justify
wordBreak: keep-all # 한글 단어 중간 끊김 방지
heading:
h1: { fontSize: 20pt, fontWeight: 900 }
h2: { fontSize: 18pt, fontWeight: 700 }
h3: { fontSize: 14pt, fontWeight: 700 }
headerFooter:
fontSize: 9pt
table:
fontSize: 9.5pt
thFontSize: 9pt
```
## 5. 표 스타일 기본값 (Table Defaults)
```yaml
table:
width: "100%"
borderCollapse: collapse
tableLayout: fixed # colgroup 비율 적용 시 fixed 필수
borderTop: "2px solid" # 상단 굵은 선 (색상은 HWPX 추출)
th:
fontWeight: 900
textAlign: center
verticalAlign: middle
whiteSpace: nowrap # 헤더 셀은 한 줄 유지
wordBreak: keep-all
padding: "6px 5px"
td:
textAlign: center
verticalAlign: middle
wordBreak: keep-all
wordWrap: break-word
padding: "6px 5px"
border: "1px solid #ddd"
```
## 6. 머릿말/꼬릿말 테이블 (Header/Footer Table)
머릿말/꼬릿말이 HWPX에서 테이블로 구성된 경우:
```yaml
headerFooterTable:
border: none # 테두리 없음
width: "100%"
fontSize: 9pt
# 열 역할 패턴 (HWPX에서 추출)
# 보통 3열: [소속정보 | 빈칸/로고 | 작성자/날짜]
# 또는 2열: [제목 | 페이지번호]
cellStyle:
padding: "2px 5px"
verticalAlign: middle
border: none
```
## 7. 개조식 (Bullet Style)
```yaml
bulletList:
marker: "·" # 한국 문서 기본 불릿
className: "bullet-list"
css: |
.bullet-list {
list-style: none;
padding-left: 15px;
margin: 5px 0;
}
.bullet-list li::before {
content: "· ";
font-weight: bold;
}
.bullet-list li {
margin-bottom: 3px;
line-height: 1.5;
}
```
## 8. 색상 (Color Scheme)
> HWPX에서 추출한 색상을 CSS 변수로 주입합니다.
> 추출 실패 시 아래 기본값을 사용합니다.
```yaml
colors:
# Navy 계열 (기본)
primary: "#1a365d"
accent: "#2c5282"
lightBg: "#EBF4FF"
# 문서별 오버라이드 (HWPX 추출값)
# doc_template_analyzer가 HWPX의 글자색/배경색을 분석하여
# 이 값을 덮어씁니다.
css: |
:root {
--primary: #1a365d;
--accent: #2c5282;
--light-bg: #EBF4FF;
--bg: #f5f5f5;
}
```
## 9. 페이지 분할 규칙 (Page Break Rules)
```yaml
pageBreak:
# H1(대제목)에서만 강제 페이지 분할
h1Break: true
# H2/H3이 페이지 하단에 홀로 남지 않도록
orphanControl: true
orphanMinSpace: 90px # 이 공간 미만이면 다음 페이지로
# 표/그림은 분할하지 않음
atomicBlocks:
- table
- figure
- ".highlight-box"
# break-inside: avoid 적용 대상
avoidBreakInside:
- table
- figure
- ".atomic-block"
```
## 10. 배경 (Preview Background)
```yaml
preview:
bodyBackground: "#525659" # 회색 배경 위에 흰색 용지
# 인쇄 시 배경 제거 (@media print)
```
---
## ★ 사용 방법 (How doc_template_analyzer uses this guide)
1. `doc_template_analyzer._build_full_html()` 호출 시:
- 이 가이드 파일을 읽음
- HWPX에서 추출한 스타일(색상, 폰트, 크기)이 있으면 오버라이드
- 없으면 가이드 기본값 사용
2. CSS 생성 순서:
```
가이드 기본값 → HWPX 추출 스타일 오버라이드 → CSS 변수로 통합
```
3. HTML 구조:
```
가이드의 골격(.sheet > .page-header + .body-content + .page-footer)
+ HWPX에서 추출한 placeholder 배치
= template.html
```
4. 색상 결정:
```
HWPX headerTextColor → --primary
HWPX headerBgColor → --light-bg
없으면 → 가이드 기본값(Navy 계열)
```

View File

@@ -0,0 +1,116 @@
{
"_comment": "A4 HTML 문서 레이아웃 기본값 - hwp_html_guide.md 참조. HWPX 추출값이 있으면 오버라이드됨",
"page": {
"width": "210mm",
"height": "297mm",
"background": "white"
},
"margins": {
"top": "20mm",
"bottom": "20mm",
"left": "20mm",
"right": "20mm"
},
"headerPosition": {
"top": "10mm",
"left": "20mm",
"right": "20mm"
},
"footerPosition": {
"bottom": "10mm",
"left": "20mm",
"right": "20mm"
},
"bodyContent": {
"top": "20mm",
"left": "20mm",
"right": "20mm",
"bottom": "20mm"
},
"bodyMaxHeight": "970px",
"preview": {
"bodyBackground": "#f5f5f5",
"sheetMargin": "20px auto",
"sheetShadow": "0 0 15px rgba(0,0,0,0.1)"
},
"typography": {
"fontFamily": "'Noto Sans KR', sans-serif",
"fontImport": "https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700;900&display=swap",
"body": {
"fontSize": "10pt",
"lineHeight": "1.6",
"textAlign": "justify",
"wordBreak": "keep-all"
},
"heading": {
"h1": { "fontSize": "20pt", "fontWeight": "900" },
"h2": { "fontSize": "16pt", "fontWeight": "700" },
"h3": { "fontSize": "13pt", "fontWeight": "700" }
},
"headerFooter": {
"fontSize": "9pt"
}
},
"colors": {
"primary": "#1a365d",
"accent": "#2c5282",
"lightBg": "#EBF4FF",
"text": "#000",
"headerText": "#000",
"footerText": "#555",
"footerBorder": "#eee",
"tableBorderTop": "#1a365d",
"tableBorder": "#ddd",
"tableHeaderBg": "#EBF4FF"
},
"table": {
"width": "100%",
"borderCollapse": "collapse",
"tableLayout": "fixed",
"fontSize": "9.5pt",
"th": {
"fontSize": "9pt",
"fontWeight": "900",
"textAlign": "center",
"verticalAlign": "middle",
"whiteSpace": "nowrap",
"padding": "6px 5px"
},
"td": {
"textAlign": "center",
"verticalAlign": "middle",
"wordBreak": "keep-all",
"padding": "6px 5px"
}
},
"headerFooterTable": {
"border": "none",
"width": "100%",
"fontSize": "9pt",
"cellPadding": "2px 5px"
},
"bulletList": {
"marker": "·",
"className": "bullet-list",
"paddingLeft": "15px",
"itemMargin": "3px 0"
},
"pageBreak": {
"h1Break": true,
"orphanControl": true,
"orphanMinSpace": "90px"
}
}

View File

@@ -0,0 +1,782 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>글벗 - AI 문서 자동화 시스템</title>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700;900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/static/css/editor.css">
<link rel="stylesheet" href="/static/css/main.css">
<style>
/* 목차 애니메이션 */
.toc-display-area {
padding: 20px 32px;
overflow-y: auto;
max-height: calc(100vh - 200px);
}
.toc-anim-item {
padding: 14px 18px;
background: var(--ui-surface, #2a2f38);
border: 1px solid var(--ui-border, #363a43);
border-left: 3px solid var(--ui-accent, #4fc3f7);
border-radius: 8px;
margin-bottom: 10px;
}
.toc-anim-num {
font-size: 12px;
color: var(--ui-accent, #4fc3f7);
font-weight: 600;
margin-bottom: 4px;
}
.toc-anim-title {
font-size: 14px;
font-weight: 600;
color: var(--ui-text, #e8eaed);
margin-bottom: 6px;
}
.toc-anim-guide {
font-size: 12px;
color: var(--ui-dim, #9aa0a6);
line-height: 1.6;
}
.toc-anim-keywords {
display: flex;
gap: 6px;
flex-wrap: wrap;
margin-top: 8px;
}
.toc-anim-keyword {
font-size: 11px;
padding: 2px 8px;
background: rgba(79, 195, 247, 0.1);
color: var(--ui-accent, #4fc3f7);
border-radius: 4px;
}
.toc-anim-contents {
margin-top: 8px;
padding: 8px 10px;
background: rgba(255,255,255,0.03);
border-radius: 4px;
border: 1px solid rgba(79, 195, 247, 0.08);
}
.toc-anim-content-item {
font-size: 11px;
color: var(--ui-dim, #9aa0a6);
line-height: 1.7;
padding-left: 4px;
}
.toc-loading {
display: flex;
gap: 6px;
justify-content: center;
padding: 24px;
}
.toc-loading-dot {
width: 8px;
height: 8px;
background: var(--ui-accent, #4fc3f7);
border-radius: 50%;
animation: tocDotPulse 1.2s infinite;
}
.toc-loading-dot:nth-child(2) { animation-delay: 0.2s; }
.toc-loading-dot:nth-child(3) { animation-delay: 0.4s; }
@keyframes tocDotPulse {
0%, 60%, 100% { opacity: 0.3; transform: scale(0.8); }
30% { opacity: 1; transform: scale(1); }
}
/* 생성 진행률 */
.gen-progress-area {
padding: 16px 32px;
}
.gen-progress-header {
font-size: 13px;
font-weight: 600;
color: var(--ui-text, #e8eaed);
margin-bottom: 10px;
}
.gen-progress-bar-wrap {
height: 4px;
background: var(--ui-border, #363a43);
border-radius: 2px;
overflow: hidden;
}
.gen-progress-bar {
height: 100%;
background: linear-gradient(90deg, var(--ui-accent, #4fc3f7), #66bb6a);
border-radius: 2px;
transition: width 0.5s ease;
}
.gen-progress-text {
font-size: 12px;
color: var(--ui-dim, #9aa0a6);
margin-top: 8px;
}
/* AI 수정 버튼 활성 상태 */
#aiEditToolbarBtn.has-selection {
background: rgba(79, 195, 247, 0.15);
color: var(--ui-accent, #4fc3f7);
border-color: var(--ui-accent, #4fc3f7);
}
/* ===== 도메인 선택 ===== */
.domain-select-box {
background: var(--ui-surface, #2a2f38);
border: 1px solid var(--ui-border, #363a43);
border-radius: 8px; padding: 10px 12px; cursor: pointer;
}
.domain-select-box:hover { border-color: var(--ui-accent, #4fc3f7); }
.domain-display-text { font-size: 12px; color: var(--ui-text); margin-top: 6px; }
.domain-display-text.auto { color: var(--ui-dim, #9aa0a6); font-style: italic; }
.domain-display-text.selected { color: var(--ui-accent, #4fc3f7); }
#domainModal {
position: fixed; top:0; left:0; right:0; bottom:0;
background: rgba(0,0,0,0.65); display: none;
align-items: center; justify-content: center; z-index: 10000;
}
.domain-modal-content {
background: var(--ui-bg, #1e2228); border: 1px solid var(--ui-border);
border-radius: 14px; width: 560px; max-height: 80vh;
display: flex; flex-direction: column; box-shadow: 0 20px 60px rgba(0,0,0,0.4);
}
.domain-modal-header { display: flex; align-items: center; justify-content: space-between; padding: 18px 24px; border-bottom: 1px solid var(--ui-border); }
.domain-modal-title { font-size: 16px; font-weight: 700; color: var(--ui-text); }
.domain-modal-close { background: none; border: none; color: var(--ui-dim); font-size: 20px; cursor: pointer; }
.domain-modal-body { padding: 16px 24px; overflow-y: auto; flex: 1; }
.domain-modal-desc { font-size: 13px; color: var(--ui-dim); margin-bottom: 16px; line-height: 1.6; }
.domain-category { margin-bottom: 10px; }
.domain-cat-label {
display: flex; align-items: center; gap: 10px;
padding: 12px 14px; background: var(--ui-surface, #2a2f38);
border: 1px solid var(--ui-border); border-radius: 8px; cursor: pointer;
}
.domain-cat-label:hover { border-color: var(--ui-accent); }
.domain-cat-label.checked { background: rgba(79,195,247,0.08); border-color: var(--ui-accent); }
.domain-cat-label input[type="checkbox"] { width: 16px; height: 16px; accent-color: var(--ui-accent); }
.domain-cat-icon { font-size: 20px; }
.domain-cat-text { flex: 1; }
.domain-cat-name { display: block; font-size: 14px; font-weight: 600; color: var(--ui-text); }
.domain-cat-desc { display: block; font-size: 11px; color: var(--ui-dim); margin-top: 2px; }
.domain-cat-expand { font-size: 10px; color: var(--ui-dim); transition: transform 0.2s; }
.domain-cat-expand.open { transform: rotate(180deg); }
.domain-sub-panel { padding: 10px 14px 10px 40px; border-left: 2px solid var(--ui-accent); margin-left: 20px; margin-top: 4px; }
.domain-sub-group { margin-bottom: 10px; }
.domain-sub-group-label { font-size: 11px; font-weight: 600; color: var(--ui-dim); margin-bottom: 6px; }
.domain-sub-chips { display: flex; flex-wrap: wrap; gap: 6px; }
.domain-chip {
display: inline-flex; align-items: center; gap: 4px;
padding: 5px 12px; background: var(--ui-surface);
border: 1px solid var(--ui-border); border-radius: 20px;
font-size: 12px; color: var(--ui-text); cursor: pointer;
}
.domain-chip:hover { border-color: var(--ui-accent); }
.domain-chip.selected { background: rgba(79,195,247,0.15); border-color: var(--ui-accent); color: var(--ui-accent); }
.domain-chip input[type="checkbox"] { display: none; }
.domain-modal-footer { padding: 14px 24px; border-top: 1px solid var(--ui-border); display: flex; align-items: center; justify-content: space-between; }
.domain-summary-text { font-size: 12px; color: var(--ui-accent); flex: 1; }
.domain-summary-text.empty { color: var(--ui-dim); font-style: italic; }
.domain-modal-actions { display: flex; gap: 8px; }
.domain-btn { padding: 8px 18px; border-radius: 6px; font-size: 13px; font-weight: 600; cursor: pointer; border: 1px solid var(--ui-border); background: var(--ui-surface); color: var(--ui-text); }
.domain-btn.primary { background: var(--ui-accent); color: #000; border-color: var(--ui-accent); }
</style>
</head>
<body>
<!-- 상단 툴바 -->
<div class="toolbar">
<div class="toolbar-logo">📝 글벗</div>
<div class="toolbar-spacer"></div>
<button class="toolbar-btn" id="editModeBtn" onclick="toggleEditMode()">✏️ 편집하기</button>
<button class="toolbar-btn" id="aiEditToolbarBtn" onclick="triggerAiEdit()" title="AI 수정 (텍스트를 먼저 선택하세요)">🤖 AI 수정</button>
<div class="toolbar-divider"></div>
<select class="zoom-select" id="zoomSelect" onchange="setZoom(this.value)">
<option value="50">50%</option>
<option value="75">75%</option>
<option value="100" selected>100%</option>
<option value="125">125%</option>
<option value="150">150%</option>
</select>
<div class="toolbar-divider"></div>
<button class="toolbar-btn" onclick="exportHwp()">📄 HWP 추출</button>
<button class="toolbar-btn" onclick="saveHtml()">💾 HTML 저장</button>
<button class="toolbar-btn" onclick="saveHtml()">📊 PPT 저장</button>
<button class="toolbar-btn" onclick="printDoc()">🖨️ PDF/인쇄</button>
</div>
<!-- 메인 앱 -->
<div class="app">
<!-- 좌측 사이드바: 입력 -->
<div class="sidebar">
<div class="sidebar-header">
<div class="sidebar-title">문서 업로드</div>
<button class="sidebar-btn" onclick="openFolderModal()">
<span class="icon">📁</span>
<span>폴더 위치</span>
</button>
<button class="sidebar-btn" onclick="openLinkModal()">
<span class="icon">🔗</span>
<span>외부 링크</span>
</button>
<button class="sidebar-btn" onclick="openHtmlModal()">
<span class="icon">📋</span>
<span>HTML 붙여넣기</span>
</button>
</div>
<div class="sidebar-content">
<!-- 참고 파일 확인 -->
<div>
<div class="section-title">업로드 파일 검토</div>
<div class="file-check-box">
<div class="file-path empty" id="folderPathDisplay">폴더 경로가 설정되지 않음</div>
<div class="file-check-row">
<span class="file-check-label">전체 파일</span>
<span class="file-check-value" id="totalCount">0개</span>
</div>
<div class="file-check-row">
<span class="file-check-label">확인 (변환 가능)</span>
<span class="file-check-value ok" id="okCount">0개 ✓</span>
</div>
<div class="file-check-row">
<span class="file-check-label">미확인</span>
<span class="file-check-value warn" id="unknownCount" onclick="toggleUnknownFiles()">0개</span>
</div>
<div class="unknown-files" id="unknownFilesBox">
<div id="unknownFilesList"></div>
<button class="open-folder-btn" onclick="openFolder()">📂 폴더 열기</button>
</div>
<div class="file-check-row" style="border-top: 1px solid var(--ui-border); margin-top: 8px; padding-top: 8px;">
<span class="file-check-label">참고 링크</span>
<span class="file-check-value" id="linkCount">0개</span>
</div>
<div class="file-check-row">
<span class="file-check-label">HTML 입력</span>
<span class="file-check-value" id="htmlInputStatus">없음</span>
</div>
</div>
</div>
<!-- 도메인 지식 선택 -->
<div id="domainSection" style="display:none;">
<div class="section-title">도메인 지식 선택</div>
<div class="domain-select-box" onclick="openDomainModal()">
<div style="display:flex; justify-content:space-between; font-size:12px; color:var(--ui-dim);">
<span>📚 업로드 자료의 성격을 선택해주세요</span>
<span></span>
</div>
<div class="domain-display-text auto" id="domainDisplayText">자동 분석 (AI 판단)</div>
</div>
</div>
<!-- 작성 방식 -->
<div>
<div class="section-title">업로드 자료 활용 범위</div>
<div class="write-mode-box">
<div class="write-mode-tabs">
<label class="write-mode-tab" onclick="selectWriteMode('format')">
<input type="radio" name="writeMode" value="format">
<div class="write-mode-icon">📄</div>
<div class="write-mode-label">형식만 <br>변경</div>
</label>
<label class="write-mode-tab selected" onclick="selectWriteMode('restructure')">
<input type="radio" name="writeMode" value="restructure" checked>
<div class="write-mode-icon">🔄</div>
<div class="write-mode-label">내용의<br>재구성</div>
</label>
<label class="write-mode-tab" onclick="selectWriteMode('new')">
<input type="radio" name="writeMode" value="new">
<div class="write-mode-icon"></div>
<div class="write-mode-label">문서 참고 <br> 신규 작성</div>
</label>
</div>
<div class="write-mode-notice">
<span>⚠️</span>
<span>문서 유형에 적합하지 않은 콘텐츠 업로드시,<br> 자동 재구성 및 신규 콘텐츠 작성 </span>
</div>
</div>
</div>
<!-- 진행 상태 -->
<div>
<div class="section-title">진행 상태</div>
<div class="step-list" id="stepList">
<div class="step-item pending" data-step="0">
<span class="status"></span>
<span>참고 파일 확인</span>
</div>
<div class="step-divider"></div>
<div class="step-item pending" data-step="1">
<span class="status"></span>
<span>Step 1: 파일 변환</span>
</div>
<div class="step-item pending" data-step="2">
<span class="status"></span>
<span>Step 2: 텍스트 추출</span>
</div>
<div class="step-item pending" data-step="3">
<span class="status"></span>
<span>Step 3: 도메인 분석</span>
</div>
<div class="step-item pending" data-step="4">
<span class="status"></span>
<span>Step 4: 청킹/요약</span>
</div>
<div class="step-item pending" data-step="5">
<span class="status"></span>
<span>Step 5: RAG 구축</span>
</div>
<div class="step-item pending" data-step="6">
<span class="status"></span>
<span>Step 6: Corpus 생성</span>
</div>
<div class="step-item pending" data-step="7">
<span class="status"></span>
<span>Step 7: 목차 구성</span>
</div>
<div class="step-divider"></div>
<div class="step-item pending" data-step="8">
<span class="status"></span>
<span>Step 8: 콘텐츠 생성</span>
</div>
<div class="step-item pending" data-step="9">
<span class="status"></span>
<span>Step 9: HTML 변환</span>
</div>
</div>
</div>
</div>
</div>
<!-- 가운데 뷰어 -->
<div class="main">
<div class="viewer" id="viewer">
<div class="a4-wrapper" id="a4Wrapper">
<div class="a4-preview" id="a4Preview">
<iframe id="previewFrame" class="preview-iframe"></iframe>
<div class="placeholder" id="placeholder">
<div class="icon">📄</div>
<div class="text">HTML을 입력하고 생성하세요</div>
<div class="sub-text">좌측에서 HTML 붙여넣기 또는 파일 업로드</div>
</div>
</div>
</div>
</div>
<!-- 목차 애니메이션 표시 영역 -->
<div class="toc-display-area" id="tocDisplayArea" style="display:none;"></div>
<!-- 생성 진행률 -->
<div class="gen-progress-area" id="genProgressArea" style="display:none;">
<div class="gen-progress-header">📝 문서 생성 중...</div>
<div class="gen-progress-bar-wrap">
<div class="gen-progress-bar" id="genProgressBar" style="width:0%"></div>
</div>
<div class="gen-progress-text" id="genProgressText">준비 중...</div>
</div>
<!-- 목차 확인 액션바 -->
<div class="toc-action-bar" id="tocActionBar">
<button class="toc-action-btn secondary" onclick="editToc()">✏️ 편집</button>
<button class="toc-action-btn primary" id="approveBtn" onclick="approveToc()">✅ 승인 & 생성하기</button>
</div>
<!-- 하단 피드백 바 -->
<div class="feedback-bar" id="feedbackBar">
<input type="text" class="feedback-input" id="feedbackInput" placeholder="수정 요청사항을 입력하세요... (예: 5 Step을 첨부로 이동해줘)">
<button class="feedback-btn primary" id="feedbackBtn" onclick="submitFeedback()">
<span id="feedbackBtnText">🔄 수정 반영</span>
<span id="feedbackSpinner" class="spinner" style="display:none;"></span>
</button>
<button class="feedback-btn secondary" onclick="regenerate()">🗑️ 다시 생성</button>
</div>
</div>
<!-- 우측 패널: 옵션 -->
<div class="right-panel">
<div class="panel-header">
<span class="panel-title">문서 설정</span>
</div>
<div class="panel-body">
<!-- ===== 문서 유형 (동적 로드) ===== -->
<div class="option-section">
<div class="option-title">문서 유형</div>
<div class="doc-type-list loading" id="docTypeList">
<!-- API에서 동적으로 로드 -->
</div>
<!-- 문서 유형 추가 버튼 -->
<button class="add-template-btn" onclick="openDocTypeModal()">+ 문서 유형 추가</button>
</div>
<!-- ===== 문서 유형별 옵션 (동적) ===== -->
<div id="docTypeOptionsContainer">
<!-- selectDocType() 호출 시 동적으로 렌더링 -->
</div>
<!-- ===== 템플릿 선택 ===== -->
<div class="option-section">
<div class="option-title">템플릿</div>
<div class="template-list">
<div class="template-item selected" data-template="default" onclick="selectTemplate('default')">
<input type="radio" name="template" checked>
<span class="label">📄 기본 템플릿</span>
</div>
<div id="userTemplatesListNew"></div>
</div>
<button class="add-template-btn" onclick="openTemplateModal()">+ 템플릿 추가</button>
<div id="templateElementOptions" class="template-elements" style="display:none;">
<div class="elements-title">적용 요소</div>
<div class="elements-list"></div>
</div>
</div>
<!-- ===== 요청사항 ===== -->
<div class="option-section">
<div class="option-title">요청사항</div>
<textarea class="request-textarea" id="globalInstructionInput"
placeholder="예: 표를 더 상세하게&#10;예: 전문 용어 풀어서 설명"></textarea>
</div>
<!-- ===== 생성 버튼 ===== -->
<button class="generate-btn" id="generateBtn" onclick="generate()" disabled>
<span id="generateBtnText">🚀 생성하기</span>
<div class="loading-spinner" id="generateSpinner" style="display:none;"></div>
</button>
</div>
</div>
</div>
<!-- 상태바 -->
<div class="status-bar">
<div class="status-left">
<span class="status-dot" id="statusDot"></span>
<span id="statusMessage">준비됨</span>
</div>
<div id="statusRight">글벗 Light v2.1</div>
</div>
<!-- HTML 입력 모달 -->
<div class="modal-overlay" id="htmlModal">
<div class="modal">
<div class="modal-header">📋 HTML 붙여넣기</div>
<div class="modal-body">
<textarea class="modal-textarea" id="htmlContent" placeholder="HTML 코드를 붙여넣으세요..."></textarea>
</div>
<div class="modal-footer">
<button class="modal-btn" onclick="closeHtmlModal()">취소</button>
<button class="modal-btn primary" onclick="submitHtml()">확인</button>
</div>
</div>
</div>
<!-- 폴더 모달 -->
<div class="modal-overlay" id="folderModal">
<div class="modal">
<div class="modal-header">📁 폴더 위치 입력</div>
<div class="modal-body">
<input type="text" id="folderPath" class="modal-input" placeholder="C:\Users\...\Documents\참고자료" style="width:100%; padding:12px; border-radius:6px; border:1px solid var(--ui-border); background:var(--ui-bg); color:var(--ui-text); font-size:13px;">
<p style="margin-top:10px; font-size:11px; color:var(--ui-dim);">* 로컬 폴더 경로를 입력하세요. (Engine 실행 필요)</p>
</div>
<div class="modal-footer">
<button class="modal-btn" onclick="closeFolderModal()">취소</button>
<button class="modal-btn primary" onclick="submitFolder()">확인</button>
</div>
</div>
</div>
<!-- 참고 링크 모달 -->
<div class="modal-overlay" id="linkModal">
<div class="modal">
<div class="modal-header">🔗 참고 링크 입력</div>
<div class="modal-body">
<div id="linkInputList">
<input type="text" class="link-input" placeholder="https://..." 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;">
</div>
<button onclick="addLinkInput()" style="padding:8px 12px; border-radius:4px; border:1px dashed var(--ui-border); background:transparent; color:var(--ui-dim); font-size:12px; cursor:pointer; width:100%;">+ 링크 추가</button>
</div>
<div class="modal-footer">
<button class="modal-btn" onclick="closeLinkModal()">취소</button>
<button class="modal-btn primary" onclick="submitLinks()">확인</button>
</div>
</div>
</div>
<!-- AI 부분 수정 팝업 -->
<div class="ai-edit-popup" id="aiEditPopup">
<button class="ai-edit-close" onclick="closeAiEditPopup()"></button>
<div class="ai-edit-header">🤖 AI로 수정하기</div>
<div class="ai-edit-selected">선택된 텍스트:</div>
<div class="ai-edit-selected-text" id="aiEditSelectedText"></div>
<textarea class="ai-edit-input" id="aiEditInput" rows="3" placeholder="예: 한 줄로 요약해줘&#10;예: 표 형태로 만들어줘"></textarea>
<button class="ai-edit-btn" onclick="submitAiEdit()">✨ 수정하기</button>
</div>
<!-- 도메인 선택 모달 -->
<div id="domainModal">
<div class="domain-modal-content">
<div class="domain-modal-header">
<div class="domain-modal-title">📚 도메인 지식 선택</div>
<button class="domain-modal-close" onclick="closeDomainModal()"></button>
</div>
<div class="domain-modal-body">
<div class="domain-modal-desc">
업로드 자료의 분야를 선택하면 해당 전문 지식이 AI에 전달됩니다.<br>
선택하지 않으면 AI가 자동으로 분야를 판단합니다.
</div>
<div id="domainCategoryList"></div>
</div>
<div class="domain-modal-footer">
<div class="domain-summary-text empty" id="domainSummaryText">선택된 도메인이 없습니다.</div>
<div class="domain-modal-actions">
<button class="domain-btn" onclick="clearAllDomains()">초기화</button>
<button class="domain-btn primary" onclick="submitDomainSelection()">확인</button>
</div>
</div>
</div>
</div>
<!-- 템플릿 추가 모달 -->
<div class="template-modal" id="templateModal">
<div class="template-modal-content">
<div class="template-modal-header">
<div class="template-modal-title">📁 템플릿 추가</div>
<button class="template-modal-close" onclick="closeTemplateModal()"></button>
</div>
<div class="template-input-group">
<label class="template-input-label">템플릿 이름</label>
<input type="text" class="template-name-input" id="templateNameInput"
placeholder="예: 걸포4지구 교통영향평가">
</div>
<div class="template-input-group">
<label class="template-input-label">템플릿 파일</label>
<div class="template-dropzone" id="templateDropzone" onclick="document.getElementById('templateFileInput').click()">
<div class="template-dropzone-icon">📄</div>
<div class="template-dropzone-text">파일을 드래그하거나 클릭하여 선택</div>
<div class="template-dropzone-hint">HWPX, HWP, PDF 지원</div>
</div>
<input type="file" id="templateFileInput" accept=".hwpx,.hwp,.pdf" style="display:none" onchange="handleTemplateFile(this)">
<div class="template-dropzone-file" id="templateFileInfo">
<span class="filename" id="templateFileName"></span>
<span class="remove" onclick="removeTemplateFile()"></span>
</div>
</div>
<button class="template-submit-btn" id="templateSubmitBtn" onclick="submitTemplate()" disabled>
<span class="spinner" id="templateSpinner"></span>
<span id="templateSubmitText">✨ 분석 및 추가</span>
</button>
</div>
</div>
<!-- 문서 유형 추가 모달 -->
<div class="modal-overlay" id="addDocTypeModal">
<div class="modal" style="min-width: 500px; max-width: 550px;">
<div class="modal-header">
<span id="addDocTypeModalTitle">📄 문서 유형 추가</span>
</div>
<!-- Step 1: 입력 -->
<div id="docTypeStep1" class="modal-body">
<div style="margin-bottom: 16px;">
<label style="font-size: 12px; font-weight: 600; color: var(--ui-dim); display: block; margin-bottom: 8px;">문서 유형 이름</label>
<input type="text" id="newDocTypeName" placeholder="예: 제안서, 회의록..." style="width:100%; padding:10px; border-radius:6px; border:1px solid var(--ui-border); background:var(--ui-bg); color:var(--ui-text); font-size:13px;">
</div>
<div style="margin-bottom: 16px;">
<label style="font-size: 12px; font-weight: 600; color: var(--ui-dim); display: block; margin-bottom: 8px;">설명 (선택)</label>
<input type="text" id="newDocTypeDesc" placeholder="문서 유형에 대한 간단한 설명" style="width:100%; padding:10px; border-radius:6px; border:1px solid var(--ui-border); background:var(--ui-bg); color:var(--ui-text); font-size:13px;">
</div>
<div style="margin-bottom: 16px;">
<label style="font-size: 12px; font-weight: 600; color: var(--ui-dim); display: block; margin-bottom: 8px;">샘플 문서 (HWPX 권장)</label>
<input type="file" id="newDocTypeFile" accept=".hwpx,.hwp,.pdf" 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;">
<p style="margin-top:8px; font-size:11px; color:var(--ui-dim);">샘플 문서를 분석하여 스타일과 구조를 추출합니다</p>
</div>
</div>
<!-- Step 2: 분석 진행 -->
<div id="docTypeStep2" class="modal-body" style="display:none;">
<div class="analysis-progress">
<div id="analysisSteps"></div>
<div class="progress-bar-container">
<div id="analysisProgressBar" class="progress-bar" style="width: 0%"></div>
</div>
<p id="analysisProgressText" style="text-align: center; margin-top: 12px; font-size: 12px; color: var(--ui-dim);">준비 중...</p>
</div>
</div>
<!-- Step 3: 결과 확인 -->
<div id="docTypeStep3" class="modal-body" style="display:none;">
<div class="analysis-result">
<h4 style="margin-bottom: 16px; color: var(--ui-accent);">📋 분석 완료</h4>
<div id="analysisResultSummary" class="result-summary"></div>
<div id="analysisResultToc" class="result-toc" style="margin-top: 16px;"></div>
</div>
</div>
<div class="modal-footer" id="docTypeModalFooter">
<button class="modal-btn" onclick="closeAddDocTypeModal()">취소</button>
<button class="modal-btn primary" id="docTypeActionBtn" onclick="startDocTypeAnalysis()">분석 시작</button>
</div>
</div>
</div>
<script>
// ===== 상태 변수 =====
let inputContent = '';
var generatedHTML = '';
var currentDocType = 'briefing';
let currentWriteMode = 'restructure';
let currentZoom = 100;
let folderPath = '';
let referenceLinks = [];
let currentTemplate = 'default';
let templateElements = {};
let analysisResult = null;
// ===== 문서 유형 데이터 =====
let docTypes = []; // 전체 문서 유형 (default + user)
let selectedDocTypeFile = null;
// ===== 템플릿 관련 =====
let userTemplates = [];
let selectedTemplateFile = null;
// ===== AI 부분 수정 =====
let selectedText = '';
let selectedRange = null;
// ===== 활용 범위 선택 =====
function selectWriteMode(mode) {
currentWriteMode = mode;
document.querySelectorAll('.write-mode-tab').forEach(tab => {
const radio = tab.querySelector('input[type="radio"]');
if (radio && radio.value === mode) {
tab.classList.add('selected');
radio.checked = true;
} else {
tab.classList.remove('selected');
}
});
updateDomainSectionVisibility();
}
function updateDomainSectionVisibility() {
const section = document.getElementById('domainSection');
if (!section) return;
const hasFolder = folderPath && folderPath.trim() !== '';
const hasLinks = referenceLinks && referenceLinks.length > 0;
section.style.display = (hasFolder || hasLinks) ? 'block' : 'none';
}
// ===== 썸네일 템플릿 =====
const thumbnailTemplates = {
briefing: `
<div class="page">
<div class="page-header"></div>
<div class="page-title"></div>
<div class="page-divider"></div>
<div class="page-lead"></div>
<div class="page-body"></div>
<div class="page-bottom"></div>
</div>
<div class="page">
<div class="page-header"></div>
<div class="page-attach">첨부</div>
<div class="page-body"></div>
<div class="page-body"></div>
<div class="page-bottom"></div>
</div>
`,
report: `
<div class="line h1"></div>
<div class="line body"></div>
<div class="line body" style="width:90%"></div>
<div class="line h2"></div>
<div class="line body" style="width:85%"></div>
`,
ppt: `
<div class="slide"><div class="slide-title">TITLE</div><div class="slide-body"></div></div>
<div class="slide"><div class="slide-title">CONTENT</div><div class="slide-body"></div></div>
`,
custom: `
<div class="line h1"></div>
<div class="line body"></div>
<div class="line h2"></div>
<div class="line body"></div>
`
};
// ===== 초기화 =====
document.addEventListener('DOMContentLoaded', function() {
setStatus('로딩 중...', false);
if (typeof loadDocTypes === 'function') loadDocTypes();
if (typeof loadUserTemplates === 'function') loadUserTemplates();
// 드롭존 이벤트
const dropzones = ['templateDropzone', 'docTypeDropzone'];
dropzones.forEach(id => {
const dropzone = document.getElementById(id);
if (dropzone) {
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
dropzone.addEventListener(eventName, (e) => {
e.preventDefault();
e.stopPropagation();
});
});
['dragenter', 'dragover'].forEach(eventName => {
dropzone.addEventListener(eventName, () => dropzone.classList.add('dragover'));
});
['dragleave', 'drop'].forEach(eventName => {
dropzone.addEventListener(eventName, () => dropzone.classList.remove('dragover'));
});
}
});
// 템플릿 입력 이벤트
const templateNameInput = document.getElementById('templateNameInput');
if (templateNameInput) {
if (typeof updateTemplateSubmitBtn === 'function') {
templateNameInput.addEventListener('input', updateTemplateSubmitBtn);
}
}
setStatus('준비됨', true);
});
// Enter 키로 피드백 제출
document.getElementById('feedbackInput').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
submitFeedback();
}
});
</script>
<script src="/static/js/ui.js"></script>
<script src="/static/js/doc_type.js"></script>
<script src="/static/js/template.js"></script>
<script src="/static/js/modals.js"></script>
<script src="/static/js/generator.js"></script>
<script src="/static/js/demo_mode.js"></script>
<script src="/static/js/export.js"></script>
<script src="/static/js/ai_edit.js"></script>
<script src="/static/js/editor.js"></script>
<script src="/static/js/domain_selector.js"></script>
</body>
</html>