v7:UI 보완_20260130

This commit is contained in:
2026-02-20 11:45:16 +09:00
parent 5129ee69d4
commit db6532b33c
3 changed files with 623 additions and 265 deletions

BIN
7th.zip Normal file

Binary file not shown.

170
README.md
View File

@@ -1,14 +1,14 @@
# 글벗 (Geulbeot) v6.0
# 글벗 (Geulbeot) v7.0
**HWPX 템플릿 분석·저장·관리**
**UI 고도화 — 템플릿 관리·작성 방식·문서 유형 선택 UI**
다양한 형식의 자료(PDF·HWP·이미지·Excel 등)를 입력하면, AI가 RAG 파이프라인으로 분석한 뒤
선택한 문서 유형(기획서·보고서·발표자료 등)에 맞는 표준 HTML 문서를 자동 생성합니다.
생성된 문서는 웹 편집기에서 수정하고, HTML / PDF / HWP로 출력합니다.
v6에서는 HWPX 템플릿 관리 기능을 추가했습니다.
HWPX 파일을 업로드하면 XML을 파싱하여 폰트·색상·여백·표 구조·테두리 등을 자동 분석하고,
재사용 가능한 템플릿으로 저장합니다.
v7에서는 프론트엔드 UI를 고도화했습니다.
v6에서 백엔드로만 존재하던 템플릿 관리를 화면에서 직접 조작할 수 있게 되었고,
자료 활용 방식(형식 변경·재구성·신규 작성)과 문서 유형을 시각적으로 선택하는 UI가 추가되었습니다.
---
@@ -20,17 +20,20 @@ HWPX 파일을 업로드하면 XML을 파싱하여 폰트·색상·여백·표
자료 입력 (파일/폴더)
작성 방식 선택 ─── 형식만 변경 / 내용 재구성 / 신규 작성 (v7 신규)
RAG 파이프라인 (9단계) ─── 공통 처리
문서 유형 선택
문서 유형 선택 ─── UI 리스트 (v7 신규)
├─ 기획서 (기본)
├─ 보고서 (기본)
├─ 발표자료 (기본)
└─ 사용자 등록 (확장 가능)
글벗 표준 HTML 생성 ◀── 템플릿 스타일 참조 (v6 신규)
글벗 표준 HTML 생성 ◀── 템플릿 스타일 참조 + 요소 선택 (v7 신규)
웹 편집기 (수기 편집 / AI 편집)
@@ -48,11 +51,11 @@ RAG 파이프라인 (9단계) ─── 공통 처리
- OpenAI API — RAG 임베딩, 인덱싱, 텍스트 추출
- Gemini API — 보고서 콘텐츠·HTML 생성
- **Features**:
- 자료 입력 → 9단계 RAG 파이프라인 (파일 변환 → 추출 → 도메인 분석 → 청킹 → 임베딩 → 코퍼스 → 인덱싱 → 콘텐츠 생성 → HTML 조립)
- 자료 입력 → 9단계 RAG 파이프라인
- 문서 유형별 생성: 기획서 (Claude 3단계), 보고서 (Gemini 2단계)
- AI 편집: 전체 수정 (`/refine`), 부분 수정 (`/refine-selection`)
- HWPX 템플릿 분석·저장·관리 (v6 신규)
- HWP 변환: 하이브리드 방식 — pyhwpx 기본 생성 → HWPX 스타일 주입 → 표 열 너비 수정
- HWPX 템플릿 분석·저장·관리
- HWP 변환: 하이브리드 방식 — pyhwpx → HWPX 스타일 주입 → 표 열 너비 수정
- PDF 변환: WeasyPrint 기반
### 2. Frontend (순수 JavaScript)
@@ -61,6 +64,9 @@ RAG 파이프라인 (9단계) ─── 공통 처리
- 웹 WYSIWYG 편집기 — 브라우저에서 생성된 문서 직접 수정
- 페이지 넘김·들여쓰기·정렬 등 서식 도구
- HTML / PDF / HWP 다운로드
- **작성 방식 선택 탭 (v7 신규)**: 📄 형식만 변경 / 🔄 내용의 재구성 / ✨ 문서 참고 신규 작성
- **문서 유형 선택 UI (v7 신규)**: 기획서·보고서 라디오 리스트 + 배지 스타일
- **템플릿 관리 UI (v7 신규)**: 사이드바에서 템플릿 업로드·선택·삭제, 적용할 요소 체크박스
### 3. 변환 엔진 (Converters)
@@ -68,25 +74,23 @@ RAG 파이프라인 (9단계) ─── 공통 처리
- **분량 자동 판단**: 5,000자 기준 — 긴 문서는 전체 파이프라인, 짧은 문서는 축약 파이프라인
- **HWP 변환 (하이브리드 방식)**: HTML 분석 → pyhwpx 변환 → HWPX 스타일 주입 → 표 열 너비 수정
### 4. 템플릿 관리 (v6 신규)
### 4. 템플릿 관리
- **HWPX 파싱**: 업로드된 HWPX를 압축 해제하여 header.xml + section*.xml 구조 분석
- **자동 추출 항목**:
- 폰트 정보 (이름·크기·굵기·색상)
- 문단 스타일 (정렬·줄간격·들여쓰기·번호 체계)
- 표 구조 (열 너비·행 수·셀 병합·테두리 스타일·선 종류)
- 배경 (색상·이미지 채우기)
- 테두리 (ARGB 8자리 색상 정규화, NONE 제외)
- 페이지 설정 (여백·용지 크기)
- **CSS 자동 생성**: 분석된 스타일을 CSS로 변환하여 HTML 생성 시 참조 가능
- **저장소**: `templates_store/` 디렉토리에 메타데이터(meta.json) + 원본 파일 + 분석 결과 저장
- **자동 추출**: 폰트·문단·표·배경·테두리·페이지 설정
- **CSS 자동 생성**: 분석된 스타일 → CSS 변환
- **저장소**: `templates_store/` — meta.json + 원본 + 분석 결과
- **UI 연동 (v7 신규)**: 사이드바에서 목록 조회·선택·삭제, 요소별 적용 체크박스
### 5. 주요 시나리오 (Core Scenarios)
1. **기획서 생성**: 텍스트 또는 파일을 입력하면, RAG 분석 후 Claude API가 구조 추출 → 페이지 배치 계획 → 글벗 표준 HTML 기획서를 생성. 1~N페이지 옵션 지원
2. **보고서 생성**: 폴더 경로의 자료들을 RAG 파이프라인으로 분석하고, Gemini API가 섹션별 콘텐츠 초안 → 표지·목차·간지·별첨이 포함된 다페이지 HTML 보고서를 생성
3. **AI 편집**: 생성된 문서를 웹 편집기에서 확인 후, 피드백으로 전체 또는 선택 부분을 AI가 수정
4. **템플릿 등록 (v6 신규)**: HWPX 파일을 업로드하면 XML을 파싱하여 폰트·표·테두리·색상 등을 자동 분석하고, 재사용 가능한 템플릿으로 저장. 등록된 템플릿은 조회·삭제 가능
3. **작성 방식 선택 (v7 신규)**: 업로드 자료를 어떻게 활용할지 3가지 모드 중 선택
- 📄 **형식만 변경** — 원본 내용 유지, 글벗 양식으로만 변환
- 🔄 **내용의 재구성** — 원본 기반으로 구조와 내용을 재구성 (기본값)
-**문서 참고 신규 작성** — 원본을 참고 자료로만 활용, 새로 작성
4. **템플릿 적용**: 등록된 HWPX 템플릿을 선택하고, 적용할 요소(제목 스타일·표 스타일·색상 등)를 체크박스로 선택
5. **HWP 내보내기**: pyhwpx 변환 후 HWPX 스타일 주입 + 표 열 너비 정밀 수정
### 프로세스 플로우
@@ -119,87 +123,18 @@ flowchart TD
I --> J
```
#### 문서 유형별 생성 → 편집 → 출력
```mermaid
flowchart TD
classDef decision fill:#fffde7,stroke:#f9a825,stroke-width:2px,color:#333
classDef aiClaude fill:#fff3cd,stroke:#d97706,stroke-width:2px,color:#856404
classDef aiGemini fill:#d6eaf8,stroke:#4285f4,stroke-width:2px,color:#1a4d8f
classDef editStyle fill:#fff3e0,stroke:#ef6c00,stroke-width:1.5px,color:#e65100
classDef exportStyle fill:#f3e5f5,stroke:#7b1fa2,stroke-width:1.5px,color:#4a148c
classDef startEnd fill:#1a365d,stroke:#1a365d,color:#fff,stroke-width:2px
classDef planned fill:#f5f5f5,stroke:#999,stroke-width:1px,stroke-dasharray: 5 5,color:#999
classDef newModule fill:#e0f2f1,stroke:#00695c,stroke-width:2px,color:#004d40
A(["📋 RAG 분석 결과"]):::startEnd
B{"문서 유형 선택"}:::decision
C["기획서 생성\n구조추출→배치→HTML\n⚡ Claude API"]:::aiClaude
D["보고서 생성\n콘텐츠→HTML 조립\n⚡ Gemini API"]:::aiGemini
E["발표자료 생성\n예정"]:::planned
F["사용자 등록 유형\n확장 가능"]:::planned
T["📋 템플릿 스타일 참조\ntemplates_store/\n(v6 신규)"]:::newModule
G["글벗 표준 HTML\nA4·Navy·Noto Sans KR"]:::startEnd
H{"편집 방식"}:::decision
I["웹 편집기\n수기 편집"]:::editStyle
J["AI 편집\n전체·부분 수정\n⚡ Claude API"]:::aiClaude
K{"출력 형식"}:::decision
L["HTML / PDF"]:::exportStyle
M["HWP 변환 (하이브리드)\npyhwpx→스타일주입→표주입"]:::exportStyle
N["PPT 변환\n예정"]:::planned
O(["✅ 최종 산출물"]):::startEnd
A --> B
B -->|"기획서"| C --> G
B -->|"보고서"| D --> G
B -->|"발표자료"| E -.-> G
B -->|"확장"| F -.-> G
T -.->|"스타일 참조"| G
G --> H
H -->|"수기"| I --> K
H -->|"AI"| J --> K
K -->|"웹/인쇄"| L --> O
K -->|"HWP"| M --> O
K -->|"PPT"| N -.-> O
```
#### 템플릿 분석 (v6 신규)
```mermaid
flowchart LR
classDef process fill:#e8f4fd,stroke:#1a365d,stroke-width:1.5px,color:#1a365d
classDef newModule fill:#fff3e0,stroke:#ef6c00,stroke-width:2px,color:#e65100
classDef dataStore fill:#e0f2f1,stroke:#00695c,stroke-width:1.5px,color:#004d40
classDef startEnd fill:#1a365d,stroke:#1a365d,color:#fff,stroke-width:2px
A(["📄 HWPX 업로드"]):::startEnd
B["압축 해제\nheader.xml\nsection*.xml"]:::process
C["XML 파싱\n폰트·표·테두리·색상\n페이지 설정"]:::newModule
D["CSS 자동 생성\n스타일 요약"]:::newModule
E[("📋 templates_store/\nmeta.json\n+ 분석 결과")]:::dataStore
A --> B --> C --> D --> E
```
---
## 🔄 v5 → v6 변경사항
## 🔄 v6 → v7 변경사항
| 영역 | v5 | v6 |
| 영역 | v6 | v7 |
|------|------|------|
| 템플릿 관리 | 없음 | **handlers/template/ 패키지 신규** |
| HWPX 분석 | 없음 | header.xml·section.xml 파싱 (폰트·표·테두리·배경·페이지) |
| CSS 자동 생성 | 없음 | 분석된 스타일 → CSS 변환 |
| 신규 API | — | `GET /templates` · `POST /analyze-template` · `DELETE /delete-template/<id>` |
| 저장소 | — | `templates_store/` (meta.json + 원본 + 분석 결과) |
| AI 프롬프트 | — | `analyze_template.txt` — 구조 분석 보조 |
| 작성 방식 | 없음 (무조건 재구성) | **3가지 모드 UI**: 형식 변경 / 재구성 / 신규 작성 |
| 문서 유형 선택 | 기획서·보고서 구분 없이 탭 | **문서 유형 라디오 리스트** + 배지 스타일 |
| 템플릿 관리 UI | API만 존재 (화면 없음) | **사이드바 UI**: 목록·선택·삭제 + 요소별 체크박스 |
| 템플릿 업로드 | API 직접 호출 | **모달 UI**: 파일 선택 + 이름 입력 + 업로드 |
| index.html | 2,974줄 | 3,400줄 (+426) |
| Python | 변경 없음 | 변경 없음 |
---
@@ -210,8 +145,8 @@ flowchart LR
- **Phase 3**: 출력 — HTML/PDF 다운로드, HWP 변환 (🔧 기본 구현)
- **Phase 4**: HWP/HWPX/HTML 매핑 — 스타일 분석·HWPX 생성·스타일 주입·표 주입 (🔧 기본 구현)
- **Phase 5**: 문서 유형 분석·등록 — HWPX 업로드 → AI 구조 분석 → 유형 CRUD + 확장 (예정)
- **Phase 6**: HWPX 템플릿 관리 — 파싱·스타일 추출·CSS 생성·저장·조회·삭제 (🔧 기본 구현 · 현재 버전)
- **Phase 7**: UI 고도화 — 프론트 모듈화, 데모 모드, AI 편집 개선, 도메인 선택기 (예정)
- **Phase 6**: HWPX 템플릿 관리 — 파싱·스타일 추출·CSS 생성·저장·조회·삭제 (🔧 기본 구현)
- **Phase 7**: UI 고도화 — 작성 방식 선택, 문서 유형 UI, 템플릿 관리 UI (🔧 기본 구현 · 현재 버전)
- **Phase 8**: 백엔드 재구조화 + 배포 — 패키지 정리, API 키 공통화, 로깅, Docker (예정)
---
@@ -230,8 +165,8 @@ flowchart LR
```bash
# 저장소 클론 및 설정
git clone http://[Gitea주소]/kei/geulbeot-v6.git
cd geulbeot-v6
git clone http://[Gitea주소]/kei/geulbeot-v7.git
cd geulbeot-v7
# 가상환경
python -m venv venv
@@ -265,7 +200,7 @@ python app.py
## 📂 프로젝트 구조
```
geulbeot_6th/
geulbeot_7th/
├── app.py # Flask 웹 서버 — API 라우팅
├── api_config.py # .env 환경변수 로더
@@ -273,36 +208,31 @@ geulbeot_6th/
│ ├── common.py # Claude API 호출, JSON/HTML 추출
│ ├── briefing/ # 기획서 처리 (구조추출 → 배치 → HTML)
│ ├── report/ # 보고서 처리 (RAG 파이프라인 연동)
│ └── template/ # ★ v6 신규 — 템플릿 관리
│ ├── processor.py # HWPX 파싱·분석·CSS 생성·CRUD
│ └── prompts/
│ └── analyze_template.txt # AI 구조 분석 프롬프트
│ └── template/ # 템플릿 관리 (HWPX 파싱·분석·CRUD)
├── converters/ # 변환 엔진
│ ├── pipeline/ # 9단계 RAG 파이프라인
│ │ ├── router.py # 분량 판단 (5,000자 기준)
│ │ └── step1 ~ step9 # 변환→추출→분석→청킹→임베딩→코퍼스→인덱싱→콘텐츠→HTML
│ ├── style_analyzer.py # HTML 요소 역할 분류
│ ├── hwpx_generator.py # HWPX 파일 직접 생성
│ ├── hwp_style_mapping.py # 역할 → HWP 스타일 매핑
│ ├── hwpx_style_injector.py # HWPX 커스텀 스타일 주입
│ ├── hwpx_table_injector.py # HWPX 표 열 너비 정밀 수정
│ ├── html_to_hwp.py # 보고서 → HWP 변환 (하이브리드 워크플로우)
│ ├── html_to_hwp.py # 보고서 → HWP 변환
│ └── html_to_hwp_briefing.py # 기획서 → HWP 변환
├── templates_store/ # ★ v6 신규 — 등록된 템플릿 저장소
├── templates_store/ # 등록된 템플릿 저장소
├── static/
│ ├── js/editor.js # 웹 WYSIWYG 편집기
│ └── css/editor.css # 편집기 스타일
├── templates/
│ ├── index.html # 메인 UI
│ ├── index.html # ★ v7 고도화 — 작성 방식·문서 유형·템플릿 UI
│ └── hwp_guide.html # HWP 변환 가이드
├── .env / .env.sample # API 키 관리
├── .gitignore
├── requirements.txt
├── Procfile # 배포 설정 (Gunicorn)
├── Procfile
└── README.md
```
@@ -326,7 +256,8 @@ geulbeot_6th/
- API 키 분산: 파이프라인 각 step에 개별 정의 (공통화 미완)
- HWP 변환: Windows + pyhwpx + 한글 프로그램 필수
- 문서 유형: 기획서·보고서만 구현, 발표자료·사용자 등록 유형 미구현
- 템플릿 → 문서 생성 연동: 아직 미연 (분석·저장만 가능, 생성 시 자동 적용은 예정)
- 작성 방식: UI만 구현, 백엔드 로직 미연 (모드별 프롬프트 분기 예정)
- 템플릿 → 문서 생성 연동: 아직 미연결 (선택·체크는 가능, 생성 시 자동 적용은 예정)
- 레거시 잔존: prompts/ 디렉토리
---
@@ -335,9 +266,9 @@ geulbeot_6th/
| 영역 | 줄 수 |
|------|-------|
| Python 전체 | 11,406 (+624) |
| 프론트엔드 (JS + CSS + HTML) | 3,859 |
| **합계** | **~15,300** |
| Python 전체 | 11,500 |
| 프론트엔드 (JS + CSS + HTML) | 4,904 (+1,045) |
| **합계** | **~16,400** |
---
@@ -350,7 +281,8 @@ geulbeot_6th/
| v3 | 9단계 RAG 파이프라인 + HWP 변환 |
| v4 | 코드 모듈화 (handlers 패키지) + 스타일 분석기·HWPX 생성기 |
| v5 | HWPX 스타일 주입 + 표 열 너비 정밀 변환 |
| **v6** | **HWPX 템플릿 분석·저장·관리** |
| v6 | HWPX 템플릿 분석·저장·관리 |
| **v7** | **UI 고도화 — 작성 방식·문서 유형·템플릿 관리 UI** |
---

View File

@@ -303,6 +303,103 @@
animation: spin 0.8s linear infinite;
}
/* 템플릿 리스트 */
.template-list {
display: flex;
flex-direction: column;
gap: 4px;
margin-bottom: 10px;
}
.template-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
background: var(--ui-panel);
border: 1px solid var(--ui-border);
border-radius: 6px;
cursor: pointer;
transition: all 0.15s;
}
.template-item:hover {
background: var(--ui-hover);
border-color: var(--ui-dim);
}
.template-item.selected {
border-color: var(--ui-accent);
background: rgba(0, 200, 83, 0.1);
}
.template-item input[type="radio"] {
accent-color: var(--ui-accent);
}
.template-item .label {
flex: 1;
font-size: 13px;
font-weight: 500;
}
.template-item .delete-btn {
opacity: 0;
background: transparent;
border: none;
color: var(--ui-dim);
cursor: pointer;
font-size: 14px;
}
.template-item:hover .delete-btn {
opacity: 1;
}
.template-item .delete-btn:hover {
color: var(--ui-error);
}
/* 템플릿 요소 체크박스 */
.template-elements {
margin-top: 12px;
padding: 12px;
background: var(--ui-bg);
border: 1px solid var(--ui-border);
border-radius: 6px;
}
.elements-title {
font-size: 10px;
font-weight: 600;
color: var(--ui-dim);
margin-bottom: 8px;
text-transform: uppercase;
}
.elements-list {
display: flex;
flex-direction: column;
gap: 6px;
}
.element-checkbox {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: var(--ui-text);
cursor: pointer;
}
.element-checkbox input[type="checkbox"] {
accent-color: var(--ui-accent);
}
.element-checkbox .element-icon {
font-size: 14px;
}
/* 사용자 템플릿 프리뷰 */
.user-template-preview {
display: none;
@@ -494,7 +591,7 @@
flex-direction: column;
gap: 15px;
}
.section-title {
font-size: 11px;
font-weight: 700;
@@ -503,7 +600,7 @@
letter-spacing: 0.5px;
margin-bottom: 8px;
}
/* 참고 파일 확인 */
.file-check-box {
background: var(--ui-panel);
@@ -572,7 +669,72 @@
border-color: var(--ui-accent);
color: var(--ui-accent);
}
/* 작성 방식 선택 (가로 탭) */
.write-mode-box {
background: var(--ui-panel);
border: 1px solid var(--ui-border);
border-radius: 8px;
padding: 10px;
}
.write-mode-tabs {
display: flex;
gap: 4px;
}
.write-mode-tab {
flex: 1;
padding: 10px 8px;
border: 1px solid var(--ui-border);
border-radius: 6px;
background: transparent;
cursor: pointer;
transition: all 0.15s;
text-align: center;
}
.write-mode-tab:hover {
background: var(--ui-hover);
border-color: var(--ui-dim);
}
.write-mode-tab.selected {
background: rgba(0, 200, 83, 0.15);
border-color: var(--ui-accent);
}
.write-mode-tab input[type="radio"] {
display: none;
}
.write-mode-icon {
font-size: 16px;
margin-bottom: 4px;
}
.write-mode-label {
font-size: 11px;
font-weight: 600;
color: var(--ui-text);
}
.write-mode-tab.selected .write-mode-label {
color: var(--ui-accent);
}
.write-mode-notice {
margin-top: 8px;
padding: 6px 8px;
background: rgba(255, 152, 0, 0.1);
border-radius: 4px;
font-size: 10px;
color: var(--ui-warning);
display: flex;
align-items: center;
gap: 5px;
}
/* 진행 상태 */
.step-list {
display: flex;
@@ -756,6 +918,45 @@
align-items: center;
gap: 12px;
}
/* 목차 확인 액션바 */
.toc-action-bar {
background: var(--ui-panel);
border-top: 1px solid var(--ui-border);
padding: 12px 20px;
display: none;
align-items: center;
justify-content: flex-end;
gap: 12px;
}
.toc-action-bar.show { display: flex; }
.toc-action-btn {
padding: 10px 24px;
border-radius: 6px;
border: none;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all 0.15s;
}
.toc-action-btn.primary {
background: var(--ui-accent);
color: #003300;
}
.toc-action-btn.primary:hover {
background: #00e676;
}
.toc-action-btn.secondary {
background: var(--ui-hover);
color: var(--ui-text);
border: 1px solid var(--ui-border);
}
.toc-action-btn.secondary:hover {
border-color: var(--ui-accent);
color: var(--ui-accent);
}
.feedback-bar.show { display: flex; }
.feedback-input {
@@ -1437,14 +1638,14 @@
<!-- 좌측 사이드바: 입력 -->
<div class="sidebar">
<div class="sidebar-header">
<div class="sidebar-title">입력</div>
<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>
<span>외부 링크</span>
</button>
<button class="sidebar-btn" onclick="openHtmlModal()">
<span class="icon">📋</span>
@@ -1456,7 +1657,7 @@
<div class="sidebar-content">
<!-- 참고 파일 확인 -->
<div>
<div class="section-title">참고 파일 확인</div>
<div class="section-title">업로드 파일 검토</div>
<div class="file-check-box">
<div class="file-path empty" id="folderPathDisplay">폴더 경로가 설정되지 않음</div>
<div class="file-check-row">
@@ -1488,12 +1689,35 @@
</div>
</div>
</div>
<!-- 초안 생성 버튼 -->
<button class="sidebar-generate-btn" id="generateBtnSide" onclick="generate()" disabled>
🚀 초안 생성하기
</button>
<!-- 작성 방식 -->
<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>
@@ -1560,7 +1784,13 @@
</div>
</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을 첨부로 이동해줘)">
@@ -1577,9 +1807,8 @@
<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">
@@ -1587,65 +1816,13 @@
<div class="doc-type-item selected" data-type="briefing" onclick="selectDocType('briefing')">
<input type="radio" name="docType" checked>
<span class="label">📋 기획서</span>
<!-- 플로팅 프리뷰 -->
<div class="doc-type-preview">
<div class="preview-thumbnail 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-body" style="width:80%"></div>
<div class="page-body" style="width:90%"></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" style="width:85%"></div>
<div class="page-body"></div>
<div class="page-body" style="width:75%"></div>
<div class="page-bottom"></div>
</div>
</div>
<div class="preview-title">기획서 (보고자료)</div>
<div class="preview-desc">임원보고용 정형화된 1~2페이지 문서</div>
<div class="preview-features">
<div class="preview-feature"><span class="icon">📄</span> 1p 본문만 / 1p+1p첨부 / 1p+np첨부</div>
<div class="preview-feature"><span class="icon">🎨</span> Navy 양식 (A4 인쇄 최적화)</div>
<div class="preview-feature"><span class="icon">✍️</span> 개조식 자동 변환</div>
</div>
</div>
<!-- 프리뷰 생략 -->
</div>
<!-- 보고서 -->
<div class="doc-type-item" data-type="report" onclick="selectDocType('report')">
<input type="radio" name="docType">
<span class="label">📄 보고서</span>
<div class="doc-type-preview">
<div class="preview-thumbnail report">
<div class="line h1"></div>
<div class="line body"></div>
<div class="line body" style="width:90%"></div>
<div class="line body" style="width:85%"></div>
<div class="line h2"></div>
<div class="line body"></div>
<div class="line body" style="width:92%"></div>
<div class="line h2"></div>
<div class="line body" style="width:88%"></div>
</div>
<div class="preview-title">보고서 (HWP)</div>
<div class="preview-desc">RAG 기반 장문 보고서 → HWPX 출력</div>
<div class="preview-features">
<div class="preview-feature"><span class="icon">🏷️</span> AI 스타일 자동 태깅</div>
<div class="preview-feature"><span class="icon">📝</span> 대제목/중제목/소제목/본문</div>
<div class="preview-feature"><span class="icon"></span> 한글에서 스타일 일괄 변경</div>
</div>
</div>
<div class="doc-type-item" data-type="report" onclick="selectDocType('report')">
<input type="radio" name="docType">
<span class="label">📄 보고서</span>
</div>
<!-- 발표자료 -->
@@ -1653,73 +1830,35 @@
<input type="radio" name="docType" disabled>
<span class="label">📊 발표자료</span>
<span class="badge">준비중</span>
<div class="doc-type-preview">
<div class="preview-thumbnail ppt">
<div class="slide">
<div class="slide-title">제목</div>
<div class="slide-body"></div>
<div class="slide-body"></div>
</div>
<div class="slide">
<div class="slide-title">본문</div>
<div class="slide-body"></div>
<div class="slide-body"></div>
<div class="slide-body"></div>
</div>
<div class="slide">
<div class="slide-title">결론</div>
<div class="slide-body"></div>
</div>
</div>
<div class="preview-title">발표자료 (PPT)</div>
<div class="preview-desc">프레젠테이션 형식 슬라이드</div>
<div class="preview-features">
<div class="preview-feature"><span class="icon">📊</span> 슬라이드 자동 구성</div>
<div class="preview-feature"><span class="icon">🎯</span> 핵심 내용 추출</div>
<div class="preview-feature"><span class="icon">🖼️</span> 도식화 자동 생성</div>
</div>
</div>
</div>
<!-- 사용자 추가 문서 유형 (동적) -->
<div id="customDocTypesArea"></div>
</div>
<div id="userTemplatesArea" style="display: none;">
<div class="template-divider"></div>
<div class="user-templates-section" id="userTemplatesList">
<!-- 동적으로 추가됨 -->
</div>
</div>
<!-- 템플릿 추가 버튼 -->
<button class="add-template-btn" onclick="openTemplateModal()">+ 템플릿 추가</button>
<!-- 문서 유형 추가 버튼 -->
<button class="add-template-btn" onclick="openDocTypeModal()">+ 문서 유형 추가</button>
</div>
<!-- 기획서 옵션 -->
<!-- ===== 문서 유형별 옵션 (기존 유지) ===== -->
<div id="briefingOptions">
<!-- 페이지 구성 -->
<div class="option-section">
<div class="option-title">페이지 구성</div>
<div class="option-group">
<div class="option-item" onclick="selectPageOption('1')">
<input type="radio" name="pages" value="1" id="page1">
<label for="page1"> (본문) 1p</label>
<label for="page1">(본문) 1p</label>
</div>
<div class="option-item selected" onclick="selectPageOption('2')">
<input type="radio" name="pages" value="2" id="page2" checked>
<label for="page2"> (본문) 1p + (첨부) 1p</label>
<label for="page2">(본문) 1p + (첨부) 1p</label>
</div>
<div class="option-item" onclick="selectPageOption('n')">
<input type="radio" name="pages" value="n" id="pageN">
<label for="pageN"> (본문) 1p + (첨부) np</label>
<label for="pageN">(본문) 1p + (첨부) np</label>
</div>
</div>
</div>
<!-- 요청사항 -->
<div class="option-section">
<div class="option-title">요청사항</div>
<textarea class="request-textarea" id="instructionInput" placeholder="예: 세무 리스크 중심으로 작성해줘&#10;예: 표를 더 상세하게 만들어줘"></textarea>
</div>
</div>
<!-- 보고서 옵션 -->
@@ -1746,33 +1885,55 @@
</div>
</div>
</div>
<!-- 템플릿 옵션 (요청사항만) -->
<div id="templateOptions" style="display:none;">
<div class="option-section">
<div class="option-title">요청사항</div>
<textarea class="request-textarea" id="templateInstructionInput" placeholder="예: 요약을 상세하게 작성해줘&#10;예: 특정 섹션 강조"></textarea>
</div>
</div>
<!-- 요청사항 -->
<div class="option-section">
<div class="option-title">요청사항</div>
<textarea class="request-textarea" id="reportInstructionInput" placeholder="예: 요약을 상세하게 작성해줘&#10;예: 표지에 로고 추가"></textarea>
</div>
</div>
<div id="templateOptions" style="display:none;">
<div class="option-section">
<div class="option-title">요청사항</div>
<textarea class="request-textarea" id="templateInstructionInput"
placeholder="예: 요약을 상세하게 작성해줘&#10;예: 특정 섹션 강조"></textarea>
</div>
</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>
<!-- 선택된 템플릿 옵션 (AI 분석 결과) -->
<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>
@@ -1837,9 +1998,14 @@
let generatedHTML = '';
let currentDocType = 'briefing';
let currentPageOption = '2';
let currentWriteMode = 'restructure'; // format, restructure, new
let currentZoom = 100;
let folderPath = '';
let referenceLinks = [];
let customDocTypes = []; // [{id, name, structure, created_at}, ...]
let currentTemplate = 'default';
let templateElements = {}; // {templateId: {cover: true, divider: false, ...}}
// ===== AI 부분 수정 관련 =====
let selectedText = '';
@@ -2069,6 +2235,121 @@
document.getElementById('briefingOptions').style.display = isBriefing ? 'block' : 'none';
document.getElementById('reportOptions').style.display = isReport ? 'block' : 'none';
document.getElementById('templateOptions').style.display = isTemplate ? 'block' : 'none';
const generateBtnText = document.getElementById('generateBtnText');
if (type === 'report' || type === 'presentation') {
generateBtnText.textContent = '📋 목차 확인하기';
} else {
generateBtnText.textContent = '🚀 생성하기';
}
}
// ===== 문서 유형 추가 모달 =====
function openDocTypeModal() {
document.getElementById('docTypeModal').classList.add('active');
}
function closeDocTypeModal() {
document.getElementById('docTypeModal').classList.remove('active');
}
// ===== 템플릿 선택 =====
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';
}
console.log('템플릿 선택:', templateId);
}
// ===== 템플릿 요소 표시 (AI 분석 결과) =====
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;
console.log('템플릿 요소 변경:', 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('');
}
// ===== 작성 방식 선택 =====
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');
}
console.log('작성 방식 선택:', mode);
}
// ===== HWP 추출 =====
@@ -2351,7 +2632,6 @@
// 생성 버튼 활성화 (HTML 입력이 있거나, 폴더/링크가 있으면)
const canGenerate = hasHtml || hasFolder || hasLinks;
document.getElementById('generateBtnSide').disabled = !canGenerate;
document.getElementById('generateBtn').disabled = !canGenerate;
// Step 0 상태 업데이트
@@ -2503,11 +2783,118 @@
async function generate() {
if (currentDocType === 'briefing') {
await generateBriefing();
} else if (currentDocType === 'report') {
await generateReport();
} else if (currentDocType === 'report' || currentDocType === 'presentation') {
await generateDraft(); // ← 목차 확인 플로우
} else if (currentDocType.startsWith('template_')) {
// ⭐ 사용자 템플릿: 보고서와 동일하게 처리
await generateBriefing(); // 템플릿은 바로 생성
}
}
// ===== 초안/목차 생성 (보고서/발표자료용) =====
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 {
// Step 1~7 실행
for (let i = 1; i <= 7; i++) {
updateStep(i, 'running');
await new Promise(r => setTimeout(r, 500));
updateStep(i, 'done');
}
// 목차 테이블 표시
document.getElementById('placeholder').style.display = 'none';
const frame = document.getElementById('previewFrame');
frame.classList.remove('active');
let tocContainer = document.getElementById('tocContainer');
if (!tocContainer) {
tocContainer = document.createElement('div');
tocContainer.id = 'tocContainer';
document.getElementById('a4Preview').appendChild(tocContainer);
}
const tableContent = document.getElementById('embedded-table-context');
if (tableContent) {
tocContainer.innerHTML = tableContent.innerHTML;
tocContainer.style.display = 'block';
}
// 액션바 표시
document.getElementById('tocActionBar').classList.add('show');
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 editToc() {
const tocContainer = document.getElementById('tocContainer');
if (tocContainer) {
tocContainer.contentEditable = true;
tocContainer.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');
setStatus('최종 문서 생성 중...', true);
// 목차 컨테이너 숨기기
const tocContainer = document.getElementById('tocContainer');
if (tocContainer) {
tocContainer.style.display = 'none';
tocContainer.contentEditable = false;
}
try {
// Step 8~9 실행
for (let i = 8; i <= 9; i++) {
updateStep(i, 'running');
await new Promise(r => setTimeout(r, 800));
updateStep(i, 'done');
}
// TODO: 실제 API 호출 (generateReport 로직 활용)
// 지금은 generateReport() 호출
await generateReport();
} catch (error) {
alert('문서 생성 오류: ' + error.message);
setStatus('오류 발생', false);
} finally {
btn.disabled = false;
btn.textContent = '✅ 승인 & 생성하기';
}
}
@@ -2522,10 +2909,8 @@
const btn = document.getElementById('generateBtn');
const btnText = document.getElementById('generateBtnText');
const spinner = document.getElementById('generateSpinner');
const sideBtn = document.getElementById('generateBtnSide');
btn.disabled = true;
sideBtn.disabled = true;
btnText.textContent = '생성 중...';
spinner.style.display = 'block';
resetSteps();
@@ -2588,7 +2973,6 @@
}
} finally {
btn.disabled = false;
sideBtn.disabled = false;
btnText.textContent = '🚀 생성하기';
spinner.style.display = 'none';
}
@@ -2971,5 +3355,47 @@
</div>
</div>
<div id="embedded-table-context" style="display:none;">
<!-- 목차 테이블 HTML (나중에 첨부파일 내용으로 대체) -->
<div style="padding: 15mm; font-family: 'Noto Sans KR', sans-serif;">
<div style="text-align:center; margin-bottom:20px;">
<div style="font-size:18pt; font-weight:800; color:#006400;">목차 확인</div>
<div style="font-size:10pt; color:#666; margin-top:5px;">체크박스로 포함/제외를 선택하세요</div>
</div>
<!-- 실제 목차 테이블은 API 응답으로 동적 생성 -->
<p style="color:#888; text-align:center;">목차 데이터 로딩 중...</p>
</div>
</div>
<!-- 문서 유형 추가 모달 -->
<div class="template-modal" id="docTypeModal">
<div class="template-modal-content">
<div class="template-modal-header">
<div class="template-modal-title">📁 문서 유형 추가</div>
<button class="template-modal-close" onclick="closeDocTypeModal()"></button>
</div>
<div class="template-input-group">
<label class="template-input-label">유형 이름</label>
<input type="text" class="template-name-input" id="docTypeNameInput"
placeholder="예: 제안서, 기술보고서, 회의록">
</div>
<div class="template-input-group">
<label class="template-input-label">샘플 문서 (선택)</label>
<div class="template-dropzone" id="docTypeDropzone">
<div class="template-dropzone-icon">📄</div>
<div class="template-dropzone-text">샘플 문서를 업로드하면 AI가 구조를 분석합니다</div>
<div class="template-dropzone-hint">HWPX, HWP, PDF, DOCX 지원</div>
</div>
</div>
<button class="template-submit-btn" id="docTypeSubmitBtn" onclick="submitDocType()">
<span id="docTypeSpinner" class="spinner"></span>
<span id="docTypeSubmitText">✨ 분석 및 추가</span>
</button>
</div>
</div>
</body>
</html>