diff --git a/7th.zip b/7th.zip deleted file mode 100644 index ef8cd9f..0000000 Binary files a/7th.zip and /dev/null differ diff --git a/README.md b/README.md index 69a8688..6dca87e 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,15 @@ -# 글벗 (Geulbeot) v7.0 +# 글벗 (Geulbeot) v8.0 -**UI 고도화 — 템플릿 관리·작성 방식·문서 유형 선택 UI** +**문서 유형 분석·등록 + HWPX 추출 도구 12종 + 템플릿 고도화** 다양한 형식의 자료(PDF·HWP·이미지·Excel 등)를 입력하면, AI가 RAG 파이프라인으로 분석한 뒤 선택한 문서 유형(기획서·보고서·발표자료 등)에 맞는 표준 HTML 문서를 자동 생성합니다. 생성된 문서는 웹 편집기에서 수정하고, HTML / PDF / HWP로 출력합니다. -v7에서는 프론트엔드 UI를 고도화했습니다. -v6에서 백엔드로만 존재하던 템플릿 관리를 화면에서 직접 조작할 수 있게 되었고, -자료 활용 방식(형식 변경·재구성·신규 작성)과 문서 유형을 시각적으로 선택하는 UI가 추가되었습니다. +v8에서는 **문서 유형 분석·등록 시스템**을 구축했습니다. +HWPX 파일을 업로드하면 12종의 추출 도구가 XML을 코드 기반으로 파싱하고, +시맨틱 매퍼가 요소 의미를 판별한 뒤, 스타일 생성기가 CSS를 산출하여 +사용자 정의 문서 유형으로 등록합니다. 등록된 유형은 기획서·보고서와 동일하게 문서 생성에 사용됩니다. --- @@ -20,20 +21,20 @@ v6에서 백엔드로만 존재하던 템플릿 관리를 화면에서 직접 자료 입력 (파일/폴더) │ ▼ -작성 방식 선택 ─── 형식만 변경 / 내용 재구성 / 신규 작성 (v7 신규) +작성 방식 선택 ─── 형식만 변경 / 내용 재구성 / 신규 작성 │ ▼ RAG 파이프라인 (9단계) ─── 공통 처리 │ ▼ -문서 유형 선택 ─── UI 리스트 (v7 신규) +문서 유형 선택 ├─ 기획서 (기본) ├─ 보고서 (기본) ├─ 발표자료 (기본) - └─ 사용자 등록 (확장 가능) + └─ 사용자 등록 (v8 — HWPX 분석 → 자동 등록) │ ▼ -글벗 표준 HTML 생성 ◀── 템플릿 스타일 참조 + 요소 선택 (v7 신규) +글벗 표준 HTML 생성 ◀── 템플릿 스타일 + 시맨틱 맵 참조 │ ▼ 웹 편집기 (수기 편집 / AI 편집) @@ -47,51 +48,76 @@ RAG 파이프라인 (9단계) ─── 공통 처리 - **Language**: Python 3.13 - **Web Framework**: Flask 3.0 — 웹 서버 엔진, API 라우팅 - **AI**: - - Claude API (Anthropic) — 기획서 생성, AI 편집 + - Claude API (Anthropic) — 기획서 생성, AI 편집, 문서 유형 맥락 분석 - OpenAI API — RAG 임베딩, 인덱싱, 텍스트 추출 - Gemini API — 보고서 콘텐츠·HTML 생성 - **Features**: - 자료 입력 → 9단계 RAG 파이프라인 - - 문서 유형별 생성: 기획서 (Claude 3단계), 보고서 (Gemini 2단계) + - 문서 유형별 생성: 기획서 (Claude), 보고서 (Gemini), 사용자 정의 유형 (v8 신규) - AI 편집: 전체 수정 (`/refine`), 부분 수정 (`/refine-selection`) - - HWPX 템플릿 분석·저장·관리 - - HWP 변환: 하이브리드 방식 — pyhwpx → HWPX 스타일 주입 → 표 열 너비 수정 + - 문서 유형 분석·등록 (v8 신규): HWPX 업로드 → 12종 도구 추출 → 시맨틱 매핑 → 스타일 생성 → 유형 CRUD + - HWPX 템플릿 관리: 추출·저장·교체·삭제 + - HWP 변환: 하이브리드 방식 - PDF 변환: WeasyPrint 기반 ### 2. Frontend (순수 JavaScript) - **Features**: - - 웹 WYSIWYG 편집기 — 브라우저에서 생성된 문서 직접 수정 - - 페이지 넘김·들여쓰기·정렬 등 서식 도구 + - 웹 WYSIWYG 편집기 — 생성된 문서 직접 수정 + - 작성 방식 선택 탭: 형식만 변경 / 내용 재구성 / 신규 작성 + - 문서 유형 선택 UI: 기본 3종 + 사용자 등록 유형 동적 표시 + - 템플릿 관리 UI: 사이드바 목록·선택·삭제, 요소별 체크박스 - HTML / PDF / HWP 다운로드 - - **작성 방식 선택 탭 (v7 신규)**: 📄 형식만 변경 / 🔄 내용의 재구성 / ✨ 문서 참고 신규 작성 - - **문서 유형 선택 UI (v7 신규)**: 기획서·보고서 라디오 리스트 + 배지 스타일 - - **템플릿 관리 UI (v7 신규)**: 사이드바에서 템플릿 업로드·선택·삭제, 적용할 요소 체크박스 ### 3. 변환 엔진 (Converters) - **RAG 파이프라인**: 9단계 — 파일 형식 통일 → 텍스트·이미지 추출 → 도메인 분석 → 의미 단위 청킹 → RAG 임베딩 → 코퍼스 구축 → FAISS 인덱싱 → 콘텐츠 생성 → HTML 조립 -- **분량 자동 판단**: 5,000자 기준 — 긴 문서는 전체 파이프라인, 짧은 문서는 축약 파이프라인 -- **HWP 변환 (하이브리드 방식)**: HTML 분석 → pyhwpx 변환 → HWPX 스타일 주입 → 표 열 너비 수정 +- **분량 자동 판단**: 5,000자 기준 +- **HWP 변환 (하이브리드)**: HTML 분석 → pyhwpx 변환 → HWPX 스타일 주입 → 표 열 너비 수정 -### 4. 템플릿 관리 +### 4. HWPX 추출 도구 12종 (v8 신규) -- **HWPX 파싱**: 업로드된 HWPX를 압축 해제하여 header.xml + section*.xml 구조 분석 -- **자동 추출**: 폰트·문단·표·배경·테두리·페이지 설정 -- **CSS 자동 생성**: 분석된 스타일 → CSS 변환 -- **저장소**: `templates_store/` — meta.json + 원본 + 분석 결과 -- **UI 연동 (v7 신규)**: 사이드바에서 목록 조회·선택·삭제, 요소별 적용 체크박스 +HWPX XML에서 특정 항목을 **코드 기반**으로 추출하는 모듈 패키지 (`handlers/tools/`): -### 5. 주요 시나리오 (Core Scenarios) +| 도구 | 대상 | 추출 내용 | +|------|------|----------| +| page_setup | §7 용지/여백 | pagePr, margin, 용지 크기 | +| font | §3 글꼴 | fontface → 폰트명·유형 매핑 | +| char_style | §4 글자 모양 | charPr 28개 속성 전체 | +| para_style | §5 문단 모양 | paraPr 23개 속성 전체 | +| border_fill | §2 테두리/배경 | borderFill, 색상·선 종류 | +| table | §6 표 | tbl, tc, 병합·너비·셀 구조 | +| header_footer | §8 머리말/꼬리말 | headerFooter 영역 | +| section | §9 구역 정의 | secPr, 다단, 페이지 속성 | +| style_def | 스타일 정의 | styles 목록 (charPr + paraPr 조합) | +| numbering | 번호매기기 | 글머리표·번호 체계 | +| image | 이미지 | 그리기 객체, 크기·위치 | +| content_order | 본문 순서 | section*.xml 문단·표·이미지 순서 | -1. **기획서 생성**: 텍스트 또는 파일을 입력하면, RAG 분석 후 Claude API가 구조 추출 → 페이지 배치 계획 → 글벗 표준 HTML 기획서를 생성. 1~N페이지 옵션 지원 -2. **보고서 생성**: 폴더 경로의 자료들을 RAG 파이프라인으로 분석하고, Gemini API가 섹션별 콘텐츠 초안 → 표지·목차·간지·별첨이 포함된 다페이지 HTML 보고서를 생성 -3. **작성 방식 선택 (v7 신규)**: 업로드 자료를 어떻게 활용할지 3가지 모드 중 선택 - - 📄 **형식만 변경** — 원본 내용 유지, 글벗 양식으로만 변환 - - 🔄 **내용의 재구성** — 원본 기반으로 구조와 내용을 재구성 (기본값) - - ✨ **문서 참고 신규 작성** — 원본을 참고 자료로만 활용, 새로 작성 -4. **템플릿 적용**: 등록된 HWPX 템플릿을 선택하고, 적용할 요소(제목 스타일·표 스타일·색상 등)를 체크박스로 선택 -5. **HWP 내보내기**: pyhwpx 변환 후 HWPX 스타일 주입 + 표 열 너비 정밀 수정 +- 추출 실패 시 `None` 반환 (디폴트값 절대 생성 안 함) +- 모든 단위 변환은 `hwpx_utils` 사용 (hwpunit→mm, charsize→pt) +- `hwpx_domain_guide.md` 기준 준수 + +### 5. 문서 유형 분석·등록 (v8 신규) + +HWPX 업로드 → 자동 분석 → 사용자 정의 문서 유형 등록: + +1. **DocTemplateAnalyzer**: HWPX 압축 해제 → 12종 도구로 코드 기반 추출 +2. **SemanticMapper**: 추출 결과에서 요소 의미 판별 (헤더표/푸터표/제목블록/데이터표/섹션) +3. **StyleGenerator**: 추출값 → CSS 생성 (charPr→클래스, paraPr→클래스, 폰트 매핑, 줄간격) +4. **ContentAnalyzer**: template_info + semantic_map → content_prompt.json (placeholder 의미·유형·작성 패턴) +5. **DocTypeAnalyzer**: AI로 맥락(목적/문서유형)과 구조 가이드(섹션별 작성법) 분석 — 레이아웃은 코드 추출 +6. **TemplateManager**: 템플릿 CRUD (생성·조회·삭제·교체), template.html 조립 +7. **CustomDocType**: 등록된 유형으로 실제 문서 생성 — template.html에 사용자 콘텐츠 채움 + +### 6. 주요 시나리오 (Core Scenarios) + +1. **기획서 생성**: RAG 분석 후 Claude API가 구조 추출 → 배치 → 글벗 표준 HTML 생성 +2. **보고서 생성**: RAG 파이프라인 → Gemini API가 다페이지 HTML 보고서 생성 +3. **사용자 정의 문서 생성 (v8 신규)**: 등록된 유형의 template.html + content_prompt.json을 기반으로, 사용자 입력 내용을 정리·재구성하여 문서 생성 +4. **문서 유형 등록 (v8 신규)**: HWPX 업로드 → 12종 도구 추출 → 시맨틱 매핑 → CSS 생성 → config.json + template.html + semantic_map.json + style.json 자동 저장 +5. **AI 편집**: 웹 편집기에서 전체·부분 수정 +6. **HWP 내보내기**: 하이브리드 변환 ### 프로세스 플로우 @@ -123,30 +149,125 @@ flowchart TD I --> J ``` +#### 전체 워크플로우 (v8 시점) + +```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 + classDef uiNew fill:#e8eaf6,stroke:#3949ab,stroke-width:2px,color:#1a237e + + A(["📂 자료 입력"]):::startEnd + + W{"작성 방식 선택"}:::uiNew + W1["📄 형식만 변경"]:::uiNew + W2["🔄 내용 재구성"]:::uiNew + W3["✨ 신규 작성"]:::uiNew + + R["RAG 파이프라인\n9단계 공통 처리"]:::startEnd + + B{"문서 유형 선택"}:::decision + + C["기획서 생성\n⚡ Claude API"]:::aiClaude + D["보고서 생성\n⚡ Gemini API"]:::aiGemini + E["발표자료\n예정"]:::planned + U["사용자 정의 유형\ntemplate.html 기반\n(v8 신규)"]:::newModule + + T["📋 템플릿 + 시맨틱 맵\nstyle.json\nsemantic_map.json\ncontent_prompt.json"]:::newModule + + G["글벗 표준 HTML"]:::startEnd + + H{"편집 방식"}:::decision + I["웹 편집기\n수기 편집"]:::editStyle + J["AI 편집\n전체·부분 수정\n⚡ Claude API"]:::aiClaude + + K{"출력 형식"}:::decision + L["HTML / PDF"]:::exportStyle + M["HWP 변환\n하이브리드"]:::exportStyle + N["PPT\n예정"]:::planned + O(["✅ 최종 산출물"]):::startEnd + + A --> W + W --> W1 & W2 & W3 + W1 & W2 & W3 --> R + R --> B + + B -->|"기획서"| C --> G + B -->|"보고서"| D --> G + B -->|"발표자료"| E -.-> G + B -->|"사용자 유형"| U --> G + + T -.->|"스타일·구조 참조"| U + + G --> H + H -->|"수기"| I --> K + H -->|"AI"| J --> K + K -->|"웹/인쇄"| L --> O + K -->|"HWP"| M --> O + K -->|"PPT"| N -.-> O +``` + +#### 문서 유형 등록 (v8 신규) + +```mermaid +flowchart TD + classDef process fill:#e8f4fd,stroke:#1a365d,stroke-width:1.5px,color:#1a365d + classDef newModule fill:#fff3e0,stroke:#ef6c00,stroke-width:2px,color:#e65100 + classDef aiNode fill:#d4edda,stroke:#10a37f,stroke-width:2px,color:#155724 + 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["DocTemplateAnalyzer\n12종 tools 코드 추출"]:::newModule + C["SemanticMapper\n요소 의미 판별\n헤더표/푸터표/제목블록/데이터표"]:::newModule + D["StyleGenerator\n추출값 → CSS 생성\ncharPr·paraPr·폰트 매핑"]:::newModule + E["ContentAnalyzer\nplaceholder 의미·유형\ncontent_prompt.json"]:::newModule + F["DocTypeAnalyzer\n⚡ AI 맥락·구조 분석\nconfig.json"]:::aiNode + G["TemplateManager\ntemplate.html 조립"]:::newModule + + H[("📋 templates/user/\ntemplates/{tpl_id}/\ndoc_types/{type_id}/")]:::dataStore + + A --> B --> C --> D --> E + B --> F + C & D & E & F --> G --> H +``` + --- -## 🔄 v6 → v7 변경사항 +## 🔄 v7 → v8 변경사항 -| 영역 | v6 | v7 | +| 영역 | v7 | v8 | |------|------|------| -| 작성 방식 | 없음 (무조건 재구성) | **3가지 모드 UI**: 형식 변경 / 재구성 / 신규 작성 | -| 문서 유형 선택 | 기획서·보고서 구분 없이 탭 | **문서 유형 라디오 리스트** + 배지 스타일 | -| 템플릿 관리 UI | API만 존재 (화면 없음) | **사이드바 UI**: 목록·선택·삭제 + 요소별 체크박스 | -| 템플릿 업로드 | API 직접 호출 | **모달 UI**: 파일 선택 + 이름 입력 + 업로드 | -| index.html | 2,974줄 | 3,400줄 (+426) | -| Python | 변경 없음 | 변경 없음 | +| 문서 유형 등록 | 없음 | **HWPX → 자동 분석 → 유형 CRUD** (doc_type_analyzer + custom_doc_type) | +| HWPX 추출 | template/processor.py 단일 | **handlers/tools/ 12종 모듈** (폰트·문단·표·테두리·이미지 등) | +| 시맨틱 매핑 | 없음 | **semantic_mapper** — 요소 의미 판별 (헤더/푸터/제목/데이터표) | +| 스타일 생성 | CSS 자동 생성 (기초) | **style_generator v2.1** — charPr 28개·paraPr 23개 전체 CSS 클래스 | +| 콘텐츠 분석 | 없음 | **content_analyzer** — placeholder 의미·유형·작성 패턴 추출 | +| 템플릿 관리 | 분석·저장·CRUD | **template_manager v5.2** — content_order 기반 본문 조립, 독립 저장 구조 | +| 도메인 지식 | 없음 | **domain/hwpx/** — hwpx_utils + hwpx_domain_guide.md | +| 기본 문서유형 | 하드코딩 | **config.json 3종** (briefing·report·presentation) | +| 사용자 유형 | 없음 | **templates/user/** 디렉토리 (doc_types + templates 분리) | +| 신규 API | — | `/api/doc-types` CRUD, `/api/templates` CRUD, `/api/doc-types/analyze-stream` | +| app.py | 354줄 | 682줄 (+328) | +| Python 전체 | 11,500줄 | 18,917줄 (+7,417) | --- ## 🗺 상태 및 로드맵 (Status & Roadmap) - **Phase 1**: RAG 파이프라인 — 9단계 파이프라인, 도메인 분석, 분량 자동 판단 (🔧 기본 구현) -- **Phase 2**: 문서 생성 — 기획서·보고서 AI 생성 + 글벗 표준 HTML 양식 (🔧 기본 구현) +- **Phase 2**: 문서 생성 — 기획서·보고서·사용자 정의 유형 AI 생성 (🔧 기본 구현) - **Phase 3**: 출력 — HTML/PDF 다운로드, HWP 변환 (🔧 기본 구현) - **Phase 4**: HWP/HWPX/HTML 매핑 — 스타일 분석·HWPX 생성·스타일 주입·표 주입 (🔧 기본 구현) -- **Phase 5**: 문서 유형 분석·등록 — HWPX 업로드 → AI 구조 분석 → 유형 CRUD + 확장 (예정) -- **Phase 6**: HWPX 템플릿 관리 — 파싱·스타일 추출·CSS 생성·저장·조회·삭제 (🔧 기본 구현) -- **Phase 7**: UI 고도화 — 작성 방식 선택, 문서 유형 UI, 템플릿 관리 UI (🔧 기본 구현 · 현재 버전) +- **Phase 5**: 문서 유형 분석·등록 — HWPX → 12종 도구 추출 → 시맨틱 매핑 → 유형 CRUD (🔧 기본 구현 · 현재 버전) +- **Phase 6**: HWPX 템플릿 관리 — template_manager v5.2, content_order 기반 조립, 독립 저장 (🔧 기본 구현 · 현재 버전) +- **Phase 7**: UI 고도화 — 작성 방식·문서 유형·템플릿 관리 UI (🔧 기본 구현) - **Phase 8**: 백엔드 재구조화 + 배포 — 패키지 정리, API 키 공통화, 로깅, Docker (예정) --- @@ -156,7 +277,7 @@ flowchart TD ### 사전 요구사항 - Python 3.10+ -- Claude API 키 (Anthropic) — 기획서 생성, AI 편집 +- Claude API 키 (Anthropic) — 기획서 생성, AI 편집, 문서 유형 분석 - OpenAI API 키 — RAG 파이프라인 - Gemini API 키 — 보고서 콘텐츠·HTML 생성 - pyhwpx — HWP 변환 시 (Windows + 한글 프로그램 필수) @@ -164,9 +285,9 @@ flowchart TD ### 환경 설정 ```bash -# 저장소 클론 및 설정 -git clone http://[Gitea주소]/kei/geulbeot-v7.git -cd geulbeot-v7 +# 저장소 클론 +git clone http://[Gitea주소]/kei/geulbeot-v8.git +cd geulbeot-v8 # 가상환경 python -m venv venv @@ -183,7 +304,7 @@ cp .env.sample .env ### .env 작성 ```env -CLAUDE_API_KEY=sk-ant-your-key-here # 기획서 생성, AI 편집 +CLAUDE_API_KEY=sk-ant-your-key-here # 기획서 생성, AI 편집, 유형 분석 GPT_API_KEY=sk-proj-your-key-here # RAG 파이프라인 GEMINI_API_KEY=AIzaSy-your-key-here # 보고서 콘텐츠 생성 ``` @@ -200,36 +321,70 @@ python app.py ## 📂 프로젝트 구조 ``` -geulbeot_7th/ -├── app.py # Flask 웹 서버 — API 라우팅 -├── api_config.py # .env 환경변수 로더 +geulbeot_8th/ +├── app.py # Flask 웹 서버 — API 라우팅 (682줄) +├── api_config.py # .env 환경변수 로더 │ -├── handlers/ # 비즈니스 로직 -│ ├── common.py # Claude API 호출, JSON/HTML 추출 -│ ├── briefing/ # 기획서 처리 (구조추출 → 배치 → HTML) -│ ├── report/ # 보고서 처리 (RAG 파이프라인 연동) -│ └── template/ # 템플릿 관리 (HWPX 파싱·분석·CRUD) +├── domain/ # ★ v8 신규 — 도메인 지식 +│ └── hwpx/ +│ ├── hwpx_domain_guide.md # HWPX 명세서 (§1~§11) +│ └── hwpx_utils.py # 단위 변환 (hwpunit→mm, charsize→pt) │ -├── converters/ # 변환 엔진 -│ ├── pipeline/ # 9단계 RAG 파이프라인 -│ ├── 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_briefing.py # 기획서 → HWP 변환 +├── handlers/ # 비즈니스 로직 +│ ├── common.py # Claude API 호출, JSON/HTML 추출 +│ ├── briefing/ # 기획서 처리 +│ ├── report/ # 보고서 처리 +│ ├── template/ # 템플릿 기본 관리 +│ │ +│ ├── doc_type_analyzer.py # ★ v8 — 문서 유형 AI 분석 (맥락·구조) +│ ├── doc_template_analyzer.py # ★ v8 — HWPX → 12종 도구 추출 오케스트레이터 +│ ├── semantic_mapper.py # ★ v8 — 요소 의미 판별 (헤더/푸터/제목/데이터표) +│ ├── style_generator.py # ★ v8 — 추출값 → CSS 클래스 생성 +│ ├── content_analyzer.py # ★ v8 — placeholder 의미·유형·작성 패턴 +│ ├── template_manager.py # ★ v8 — 템플릿 CRUD + template.html 조립 +│ ├── custom_doc_type.py # ★ v8 — 사용자 정의 유형 문서 생성 +│ │ +│ └── tools/ # ★ v8 — HWPX 추출 도구 12종 +│ ├── page_setup.py # §7 용지/여백 +│ ├── font.py # §3 글꼴 +│ ├── char_style.py # §4 글자 모양 (charPr 28개) +│ ├── para_style.py # §5 문단 모양 (paraPr 23개) +│ ├── border_fill.py # §2 테두리/배경 +│ ├── table.py # §6 표 (병합·너비·셀) +│ ├── header_footer.py # §8 머리말/꼬리말 +│ ├── section.py # §9 구역 정의 +│ ├── style_def.py # 스타일 정의 +│ ├── numbering.py # 번호매기기/글머리표 +│ ├── image.py # 이미지/그리기 객체 +│ └── content_order.py # 본문 콘텐츠 순서 │ -├── templates_store/ # 등록된 템플릿 저장소 +├── converters/ # 변환 엔진 +│ ├── pipeline/ # 9단계 RAG 파이프라인 +│ ├── 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_briefing.py # 기획서 → HWP 변환 +│ +├── templates/ # 문서 유형 + UI +│ ├── default/doc_types/ # 기본 유형 설정 +│ │ ├── briefing/config.json # 기획서 +│ │ ├── report/config.json # 보고서 +│ │ └── presentation/config.json # 발표자료 +│ ├── user/ # ★ v8 — 사용자 등록 데이터 +│ │ ├── doc_types/{type_id}/ # config.json + content_prompt.json +│ │ └── templates/{tpl_id}/ # meta.json + style.json + semantic_map.json + template.html +│ ├── hwp_guide.md +│ ├── hwp_html_defaults.json +│ └── index.html # 메인 UI │ ├── static/ -│ ├── js/editor.js # 웹 WYSIWYG 편집기 -│ └── css/editor.css # 편집기 스타일 -├── templates/ -│ ├── index.html # ★ v7 고도화 — 작성 방식·문서 유형·템플릿 UI -│ └── hwp_guide.html # HWP 변환 가이드 +│ ├── js/editor.js # 웹 WYSIWYG 편집기 +│ └── css/editor.css # 편집기 스타일 │ -├── .env / .env.sample # API 키 관리 +├── .env / .env.sample ├── .gitignore ├── requirements.txt ├── Procfile @@ -255,10 +410,9 @@ geulbeot_7th/ - 로컬 경로 하드코딩: `D:\for python\...` 잔존 (router.py, app.py) - API 키 분산: 파이프라인 각 step에 개별 정의 (공통화 미완) - HWP 변환: Windows + pyhwpx + 한글 프로그램 필수 -- 문서 유형: 기획서·보고서만 구현, 발표자료·사용자 등록 유형 미구현 -- 작성 방식: UI만 구현, 백엔드 로직 미연동 (모드별 프롬프트 분기 예정) -- 템플릿 → 문서 생성 연동: 아직 미연결 (선택·체크는 가능, 생성 시 자동 적용은 예정) -- 레거시 잔존: prompts/ 디렉토리 +- 발표자료: config.json만 존재, 실제 생성 미구현 +- 사용자 유형 생성: template.html 기반 채움만 (AI 창작 아닌 정리·재구성) +- 레거시 잔존: prompts/ 디렉토리, templates/hwp_guide.html → .md 전환 중 --- @@ -266,9 +420,9 @@ geulbeot_7th/ | 영역 | 줄 수 | |------|-------| -| Python 전체 | 11,500 | -| 프론트엔드 (JS + CSS + HTML) | 4,904 (+1,045) | -| **합계** | **~16,400** | +| Python 전체 | 18,917 (+7,417) | +| 프론트엔드 (JS + CSS + HTML) | 5,269 (+365) | +| **합계** | **~24,200** | --- @@ -282,7 +436,8 @@ geulbeot_7th/ | v4 | 코드 모듈화 (handlers 패키지) + 스타일 분석기·HWPX 생성기 | | v5 | HWPX 스타일 주입 + 표 열 너비 정밀 변환 | | v6 | HWPX 템플릿 분석·저장·관리 | -| **v7** | **UI 고도화 — 작성 방식·문서 유형·템플릿 관리 UI** | +| v7 | UI 고도화 — 작성 방식·문서 유형·템플릿 관리 UI | +| **v8** | **문서 유형 분석·등록 + HWPX 추출 도구 12종 + 템플릿 고도화** | --- diff --git a/app.py b/app.py index 178a54d..0ff38e4 100644 --- a/app.py +++ b/app.py @@ -7,41 +7,135 @@ Flask 라우팅 + 공통 기능 import os import io import tempfile +import json +import shutil from datetime import datetime from flask import Flask, render_template, request, jsonify, Response, session, send_file -from handlers.template import TemplateProcessor - +import queue +import threading +from handlers.template_manager import TemplateManager +from pathlib import Path # 문서 유형별 프로세서 +from handlers.template import TemplateProcessor from handlers.briefing import BriefingProcessor from handlers.report import ReportProcessor +from handlers.custom_doc_type import CustomDocTypeProcessor +from handlers.doc_type_analyzer import DocTypeAnalyzer + app = Flask(__name__) app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB max app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'geulbeot-light-secret-key-v2') # processors 딕셔너리에 추가 +template_mgr = TemplateManager() processors = { 'briefing': BriefingProcessor(), 'report': ReportProcessor(), - 'template': TemplateProcessor() # 추가 + 'template': TemplateProcessor(), + 'custom': CustomDocTypeProcessor() } +DOC_TYPES_DEFAULT = Path('templates/default/doc_types') +DOC_TYPES_USER = Path('templates/user/doc_types') # ============== 메인 페이지 ============== - @app.route('/') def index(): """메인 페이지""" return render_template('index.html') +@app.route('/api/doc-types', methods=['GET']) +def get_doc_types(): + """문서 유형 목록 조회""" + try: + doc_types = [] + + # default 폴더 스캔 + if DOC_TYPES_DEFAULT.exists(): + for folder in DOC_TYPES_DEFAULT.iterdir(): + if folder.is_dir(): + config_file = folder / 'config.json' + if config_file.exists(): + with open(config_file, 'r', encoding='utf-8') as f: + doc_types.append(json.load(f)) + + # user 폴더 스캔 + if DOC_TYPES_USER.exists(): + for folder in DOC_TYPES_USER.iterdir(): + if folder.is_dir(): + config_file = folder / 'config.json' + if config_file.exists(): + with open(config_file, 'r', encoding='utf-8') as f: + doc_types.append(json.load(f)) + + # order → isDefault 순 정렬 + doc_types.sort(key=lambda x: (x.get('order', 999), not x.get('isDefault', False))) + + return jsonify(doc_types) + + except Exception as e: + import traceback + return jsonify({'error': str(e), 'trace': traceback.format_exc()}), 500 + + +@app.route('/api/doc-types', methods=['POST']) +def add_doc_type(): + """문서 유형 추가 (분석 결과 저장)""" + try: + data = request.get_json() + + if not data: + return jsonify({'error': 'JSON 데이터가 필요합니다'}), 400 + + # user 폴더 생성 + DOC_TYPES_USER.mkdir(parents=True, exist_ok=True) + + type_id = data.get('id') + if not type_id: + import time + type_id = f"user_{int(time.time())}" + data['id'] = type_id + + folder_path = DOC_TYPES_USER / type_id + folder_path.mkdir(parents=True, exist_ok=True) + + # config.json 저장 + with open(folder_path / 'config.json', 'w', encoding='utf-8') as f: + json.dump(data, f, ensure_ascii=False, indent=2) + + return jsonify(data) + + except Exception as e: + import traceback + return jsonify({'error': str(e), 'trace': traceback.format_exc()}), 500 + + +@app.route('/api/doc-types/', methods=['DELETE']) +def delete_doc_type(type_id): + """문서 유형 삭제""" + try: + folder_path = DOC_TYPES_USER / type_id + + if not folder_path.exists(): + return jsonify({'error': '문서 유형을 찾을 수 없습니다'}), 404 + + shutil.rmtree(folder_path) + return jsonify({'success': True, 'deleted': type_id}) + + except Exception as e: + import traceback + return jsonify({'error': str(e), 'trace': traceback.format_exc()}), 500 + + # ============== 생성 API ============== @app.route('/generate', methods=['POST']) def generate(): - """기획서 생성 API""" + """문서 생성 API""" try: content = "" if 'file' in request.files and request.files['file'].filename: @@ -50,13 +144,22 @@ def generate(): elif 'content' in request.form: content = request.form.get('content', '') - options = { - 'page_option': request.form.get('page_option', '1'), - 'department': request.form.get('department', '총괄기획실'), - 'instruction': request.form.get('instruction', '') - } + doc_type = request.form.get('doc_type', 'briefing') - result = processors['briefing'].generate(content, options) + if doc_type.startswith('user_'): + options = { + 'instruction': request.form.get('instruction', '') + } + result = processors['custom'].generate(content, doc_type, options) + else: + options = { + 'page_option': request.form.get('page_option', '1'), + 'department': request.form.get('department', ''), + 'instruction': request.form.get('instruction', '') + } + + processor = processors.get(doc_type, processors['briefing']) + result = processor.generate(content, options) if 'error' in result: return jsonify(result), 400 if 'trace' not in result else 500 @@ -257,6 +360,40 @@ def export_hwp(): return jsonify({'error': str(e)}), 500 +# 기존 add_doc_type 대체 또는 수정 +@app.route('/api/doc-types/analyze', methods=['POST']) +def analyze_doc_type(): + """문서 유형 분석 API""" + if 'file' not in request.files: + return jsonify({"error": "파일이 필요합니다"}), 400 + + file = request.files['file'] + doc_name = request.form.get('name', '새 문서 유형') + + # 임시 저장 + import tempfile + temp_path = os.path.join(tempfile.gettempdir(), file.filename) + file.save(temp_path) + + try: + analyzer = DocTypeAnalyzer() + result = analyzer.analyze(temp_path, doc_name) + + return jsonify({ + "success": True, + "config": result["config"], + "summary": { + "pageCount": result["structure"]["pageCount"], + "sections": len(result["toc"]), + "style": result["style"] + } + }) + except Exception as e: + return jsonify({"error": str(e)}), 500 + finally: + os.remove(temp_path) + + @app.route('/analyze-styles', methods=['POST']) def analyze_styles(): """HTML 스타일 분석 미리보기""" @@ -300,15 +437,24 @@ def analyze_styles(): def get_templates(): """저장된 템플릿 목록 조회""" try: - result = processors['template'].get_list() - return jsonify(result) + templates = template_mgr.list_templates() + return jsonify(templates) + except Exception as e: + return jsonify({'error': str(e)}), 500 + +@app.route('/api/templates', methods=['GET']) +def get_templates_api(): + """템플릿 목록 조회 (API 경로)""" + try: + templates = template_mgr.list_templates() + return jsonify(templates) except Exception as e: return jsonify({'error': str(e)}), 500 @app.route('/analyze-template', methods=['POST']) def analyze_template(): - """템플릿 분석 및 저장""" + """템플릿 추출 및 저장 (doc_template_analyzer → template_manager)""" try: if 'file' not in request.files: return jsonify({'error': '파일이 없습니다'}), 400 @@ -322,29 +468,211 @@ def analyze_template(): if not file.filename: return jsonify({'error': '파일을 선택해주세요'}), 400 - result = processors['template'].analyze(file, name) - - if 'error' in result: - return jsonify(result), 400 - - return jsonify(result) + # 임시 저장 → HWPX 파싱 → 템플릿 추출 + temp_dir = tempfile.gettempdir() + temp_path = os.path.join(temp_dir, file.filename) + file.save(temp_path) + try: + # v3 파서 재사용 (HWPX → parsed dict) + from handlers.doc_type_analyzer import DocTypeAnalyzer + parser = DocTypeAnalyzer() + parsed = parser._parse_hwpx(temp_path) + + # template_manager로 추출+저장 + result = template_mgr.extract_and_save( + parsed, name, + source_file=file.filename + ) + + return jsonify(result) + finally: + try: + os.remove(temp_path) + except: + pass + except Exception as e: import traceback return jsonify({'error': str(e), 'trace': traceback.format_exc()}), 500 +# ============== 문서 유형 분석 SSE API ============== + +@app.route('/api/doc-types/analyze-stream', methods=['POST']) +def analyze_doc_type_stream(): + """ + 문서 유형 분석 (SSE 스트리밍) + 실시간으로 각 단계의 진행 상황을 전달 + """ + import tempfile + + # 파일 및 데이터 검증 + if 'file' not in request.files: + return jsonify({'error': '파일이 없습니다'}), 400 + + file = request.files['file'] + name = request.form.get('name', '').strip() + description = request.form.get('description', '').strip() + + if not name: + return jsonify({'error': '문서 유형 이름을 입력해주세요'}), 400 + + if not file.filename: + return jsonify({'error': '파일을 선택해주세요'}), 400 + + # 임시 파일 저장 + temp_dir = tempfile.gettempdir() + temp_path = os.path.join(temp_dir, file.filename) + file.save(temp_path) + + # 메시지 큐 생성 + message_queue = queue.Queue() + analysis_result = {"data": None, "error": None} + + def progress_callback(step_id, status, message): + """진행 상황 콜백 - 메시지 큐에 추가""" + message_queue.put({ + "type": "progress", + "step": step_id, + "status": status, + "message": message + }) + + def run_analysis(): + """분석 실행 (별도 스레드)""" + try: + + analyzer = DocTypeAnalyzer(progress_callback=progress_callback) + result = analyzer.analyze(temp_path, name, description) + + # 저장 + save_path = analyzer.save_doc_type(result["config"], result.get("template", "") ) + + analysis_result["data"] = { + "success": True, + "config": result["config"], + "layout": result.get("layout", {}), + "context": result.get("context", {}), + "structure": result.get("structure", {}), + "template_generated": bool(result.get("template_id") or result.get("template")), + "template_id": result.get("template_id"), # ★ 추가 + "saved_path": save_path + } + + except Exception as e: + import traceback + analysis_result["error"] = { + "message": str(e), + "trace": traceback.format_exc() + } + finally: + # 완료 신호 + message_queue.put({"type": "complete"}) + # 임시 파일 삭제 + try: + os.remove(temp_path) + except: + pass + + def generate_events(): + """SSE 이벤트 생성기""" + # 분석 시작 + analysis_thread = threading.Thread(target=run_analysis) + analysis_thread.start() + + # 이벤트 스트리밍 + while True: + try: + msg = message_queue.get(timeout=60) # 60초 타임아웃 + + if msg["type"] == "complete": + # 분석 완료 + if analysis_result["error"]: + yield f"data: {json.dumps({'type': 'error', 'error': analysis_result['error']}, ensure_ascii=False)}\n\n" + else: + yield f"data: {json.dumps({'type': 'result', 'data': analysis_result['data']}, ensure_ascii=False)}\n\n" + break + else: + # 진행 상황 + yield f"data: {json.dumps(msg, ensure_ascii=False)}\n\n" + + except queue.Empty: + # 타임아웃 + yield f"data: {json.dumps({'type': 'error', 'error': {'message': '분석 시간 초과'}}, ensure_ascii=False)}\n\n" + break + + return Response( + generate_events(), + mimetype='text/event-stream', + headers={ + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive', + 'X-Accel-Buffering': 'no' + } + ) @app.route('/delete-template/', methods=['DELETE']) def delete_template(template_id): - """템플릿 삭제""" + """템플릿 삭제 (레거시 호환)""" try: - result = processors['template'].delete(template_id) - + result = template_mgr.delete_template(template_id) if 'error' in result: return jsonify(result), 400 - return jsonify(result) + except Exception as e: + return jsonify({'error': str(e)}), 500 + + +@app.route('/api/templates/', methods=['GET']) +def get_template(tpl_id): + """특정 템플릿 조회""" + try: + result = template_mgr.load_template(tpl_id) + if 'error' in result: + return jsonify(result), 404 + return jsonify(result) + except Exception as e: + return jsonify({'error': str(e)}), 500 + + +@app.route('/api/templates/', methods=['DELETE']) +def delete_template_new(tpl_id): + """템플릿 삭제""" + try: + result = template_mgr.delete_template(tpl_id) + if 'error' in result: + return jsonify(result), 400 + return jsonify(result) + except Exception as e: + return jsonify({'error': str(e)}), 500 + + +@app.route('/api/doc-types//template', methods=['PUT']) +def change_doc_type_template(type_id): + """문서 유형의 템플릿 교체""" + try: + data = request.get_json() + new_tpl_id = data.get('template_id') + if not new_tpl_id: + return jsonify({'error': 'template_id가 필요합니다'}), 400 + + result = template_mgr.change_template(type_id, new_tpl_id) + if 'error' in result: + return jsonify(result), 400 + return jsonify(result) + except Exception as e: + return jsonify({'error': str(e)}), 500 + + +@app.route('/api/doc-types//template', methods=['GET']) +def get_doc_type_template(type_id): + """문서 유형에 연결된 템플릿 조회""" + try: + result = template_mgr.get_template_for_doctype(type_id) + if 'error' in result: + return jsonify(result), 404 + return jsonify(result) except Exception as e: return jsonify({'error': str(e)}), 500 diff --git a/converters/pipeline/step1_convert.py b/converters/pipeline/step1_convert.py index a3b57b6..d15f2dc 100644 --- a/converters/pipeline/step1_convert.py +++ b/converters/pipeline/step1_convert.py @@ -776,8 +776,8 @@ class SurveyingFileConverter: if __name__ == "__main__": # 경로 설정 - SOURCE_DIR = r"D:\for python\테스트 중(측량)\측량_GIS_드론 관련 자료들" - OUTPUT_DIR = r"D:\for python\테스트 중(측량)\추출" + SOURCE_DIR = r"D:\for python\geulbeot-light\geulbeot-light\00.test\hwpx\in" + OUTPUT_DIR = r"D:\for python\geulbeot-light\geulbeot-light\00.test\hwpx\out" # 변환기 실행 converter = SurveyingFileConverter(SOURCE_DIR, OUTPUT_DIR) diff --git a/converters/pipeline/step2_extract.py b/converters/pipeline/step2_extract.py index be4d6d6..9e9554f 100644 --- a/converters/pipeline/step2_extract.py +++ b/converters/pipeline/step2_extract.py @@ -27,8 +27,8 @@ except ImportError: print("[INFO] pytesseract 미설치 - 텍스트 잘림 필터 비활성화") # ===== 경로 설정 ===== -BASE_DIR = Path(r"D:\for python\survey_test\extract") # PDF 원본 위치 -OUTPUT_BASE = Path(r"D:\for python\survey_test\process") # 출력 위치 +BASE_DIR = Path(r"D:\for python\geulbeot-light\geulbeot-light\00.test\hwpx\out") # PDF 원본 위치 +OUTPUT_BASE = Path(r"D:\for python\geulbeot-light\geulbeot-light\00.test\hwpx\out\out") # 출력 위치 CAPTION_PATTERN = re.compile( r'^\s*(?:[<\[\(\{]\s*)?(그림|figure|fig)\s*\.?\s*(?:[<\[\(\{]\s*)?0*\d+(?:\s*[-–]\s*\d+)?', diff --git a/converters/pipeline/step3_domain.py b/converters/pipeline/step3_domain.py index e01a87a..29a5547 100644 --- a/converters/pipeline/step3_domain.py +++ b/converters/pipeline/step3_domain.py @@ -29,8 +29,8 @@ from api_config import API_KEYS pytesseract.pytesseract.tesseract_cmd = r"C:\Program Files\Tesseract-OCR\tesseract.exe" # ===== 경로 설정 ===== -DATA_ROOT = Path(r"D:\for python\survey_test\extract") -OUTPUT_ROOT = Path(r"D:\for python\survey_test\output") +DATA_ROOT = Path(r"D:\for python\geulbeot-light\geulbeot-light\00.test\hwpx\out") +OUTPUT_ROOT = Path(r"D:\for python\geulbeot-light\geulbeot-light\00.test\hwpx\out\out") # 출력 위치 CONTEXT_DIR = OUTPUT_ROOT / "context" LOG_DIR = OUTPUT_ROOT / "logs" diff --git a/converters/pipeline/step4_chunk.py b/converters/pipeline/step4_chunk.py index 9680692..b1309cf 100644 --- a/converters/pipeline/step4_chunk.py +++ b/converters/pipeline/step4_chunk.py @@ -26,8 +26,8 @@ from openai import OpenAI from api_config import API_KEYS # ===== 경로 ===== -DATA_ROOT = Path(r"D:\for python\survey_test\process") -OUTPUT_ROOT = Path(r"D:\for python\survey_test\output") +DATA_ROOT = Path(r"D:\for python\geulbeot-light\geulbeot-light\00.test\hwpx\out") +OUTPUT_ROOT = Path(r"D:\for python\geulbeot-light\geulbeot-light\00.test\hwpx\out\out") # 출력 위치 TEXT_DIR = OUTPUT_ROOT / "text" JSON_DIR = OUTPUT_ROOT / "json" diff --git a/converters/pipeline/step5_rag.py b/converters/pipeline/step5_rag.py index 30ef48e..0525082 100644 --- a/converters/pipeline/step5_rag.py +++ b/converters/pipeline/step5_rag.py @@ -20,8 +20,8 @@ from openai import OpenAI from api_config import API_KEYS # ===== 경로 설정 ===== -DATA_ROOT = Path(r"D:\for python\survey_test\process") -OUTPUT_ROOT = Path(r"D:\for python\survey_test\output") +DATA_ROOT = Path(r"D:\for python\geulbeot-light\geulbeot-light\00.test\hwpx\out") +OUTPUT_ROOT = Path(r"D:\for python\geulbeot-light\geulbeot-light\00.test\hwpx\out\out") # 출력 위치 RAG_DIR = OUTPUT_ROOT / "rag" LOG_DIR = OUTPUT_ROOT / "logs" diff --git a/converters/pipeline/step6_corpus.py b/converters/pipeline/step6_corpus.py index d3e33d0..4a3cb3e 100644 --- a/converters/pipeline/step6_corpus.py +++ b/converters/pipeline/step6_corpus.py @@ -23,8 +23,8 @@ from openai import OpenAI from api_config import API_KEYS # ===== 경로 설정 ===== -DATA_ROOT = Path(r"D:\for python\survey_test\process") -OUTPUT_ROOT = Path(r"D:\for python\survey_test\output") +DATA_ROOT = Path(r"D:\for python\geulbeot-light\geulbeot-light\00.test\hwpx\out") +OUTPUT_ROOT = Path(r"D:\for python\geulbeot-light\geulbeot-light\00.test\hwpx\out\out") # 출력 위치 RAG_DIR = OUTPUT_ROOT / "rag" CONTEXT_DIR = OUTPUT_ROOT / "context" LOG_DIR = OUTPUT_ROOT / "logs" diff --git a/converters/pipeline/step7_index.py b/converters/pipeline/step7_index.py index 3180719..4f40baf 100644 --- a/converters/pipeline/step7_index.py +++ b/converters/pipeline/step7_index.py @@ -22,8 +22,8 @@ from openai import OpenAI from api_config import API_KEYS # ===== 경로 설정 ===== -DATA_ROOT = Path(r"D:\for python\survey_test\process") -OUTPUT_ROOT = Path(r"D:\for python\survey_test\output") +DATA_ROOT = Path(r"D:\for python\geulbeot-light\geulbeot-light\00.test\hwpx\out") +OUTPUT_ROOT = Path(r"D:\for python\geulbeot-light\geulbeot-light\00.test\hwpx\out\out") # 출력 위치 CONTEXT_DIR = OUTPUT_ROOT / "context" LOG_DIR = OUTPUT_ROOT / "logs" diff --git a/converters/pipeline/step8_content.py b/converters/pipeline/step8_content.py index 5f66190..4330251 100644 --- a/converters/pipeline/step8_content.py +++ b/converters/pipeline/step8_content.py @@ -55,8 +55,8 @@ GEMINI_MODEL = "gemini-3-pro-preview" gemini_client = genai.Client(api_key=GEMINI_API_KEY) # ===== 경로 설정 ===== -DATA_ROOT = Path(r"D:\for python\survey_test\process") -OUTPUT_ROOT = Path(r"D:\for python\survey_test\output") +DATA_ROOT = Path(r"D:\for python\geulbeot-light\geulbeot-light\00.test\hwpx\out") +OUTPUT_ROOT = Path(r"D:\for python\geulbeot-light\geulbeot-light\00.test\hwpx\out\out") # 출력 위치 CONTEXT_DIR = OUTPUT_ROOT / "context" LOG_DIR = OUTPUT_ROOT / "logs" RAG_DIR = OUTPUT_ROOT / "rag" diff --git a/converters/pipeline/step9_html.py b/converters/pipeline/step9_html.py index 3ee7365..9e20780 100644 --- a/converters/pipeline/step9_html.py +++ b/converters/pipeline/step9_html.py @@ -25,7 +25,7 @@ from typing import List, Dict, Any, Tuple, Optional from dataclasses import dataclass, field # ===== 경로 설정 ===== -OUTPUT_ROOT = Path(r"D:\for python\survey_test\output") +OUTPUT_ROOT = Path(r"D:\for python\geulbeot-light\geulbeot-light\00.test\hwpx\out\out") # 출력 위치 GEN_DIR = OUTPUT_ROOT / "generated" ASSETS_DIR = GEN_DIR / "assets" LOG_DIR = OUTPUT_ROOT / "logs" diff --git a/domain/__init__.py b/domain/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/domain/hwpx/__init__.py b/domain/hwpx/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/domain/hwpx/hwpx_domain_guide.md b/domain/hwpx/hwpx_domain_guide.md new file mode 100644 index 0000000..da48039 --- /dev/null +++ b/domain/hwpx/hwpx_domain_guide.md @@ -0,0 +1,769 @@ +# HWP/HWPX ↔ HTML/CSS 도메인 가이드 + +> **목적**: HWPX에서 문서 유형·스타일·템플릿을 추출하거나, HTML → HWPX → HWP 변환 시 +> 하드코딩 없이 이 가이드를 참조하여 정확한 매핑을 수행한다. +> **출처**: 한글과컴퓨터 공식 "글 문서 파일 구조 5.0" (revision 1.3, 2018-11-08) +> **범위**: HWP 5.0 바이너리 스펙의 개념 체계 + HWPX XML 태그 + HTML/CSS 매핑 + +--- + +## 0. 문서 형식 관계 + +``` +HWP (바이너리) HWPX (XML) HTML/CSS +───────────────── ───────────────────── ───────────────── +Compound File ZIP Archive 단일 HTML 파일 +├─ FileHeader ├─ META-INF/ ├─ +├─ DocInfo │ └─ manifest.xml │ ├─ +│ (글꼴, 스타일, ├─ Contents/ │ └─ + + +
+ +{header_html} + +{body_html} + +{footer_html} + +
+ +""" + return html + + # ── 보조 메서드들 ── + def _build_header_html(self, header_info: dict | None) -> str: + """header tools 추출값 → HTML + placeholder""" + if not header_info or not header_info.get("exists"): + return "" + + html = '
\n' + + if header_info.get("type") == "table" and header_info.get("table"): + tbl = header_info["table"] + rows = tbl.get("rows", []) + col_pcts = tbl.get("colWidths_pct", []) + + # ★ 추가: colWidths_pct 없으면 셀 width_hu에서 계산 + if not col_pcts and rows: + widths = [c.get("width_hu", 0) for c in rows[0]] + total = sum(widths) + if total > 0: + col_pcts = [round(w / total * 100) for w in widths] + + html += '\n' + if col_pcts: + html += '\n' + for pct in col_pcts: + html += f' \n' + html += '\n' + + for r_idx, row in enumerate(rows): + html += '\n' + for c_idx, cell in enumerate(row): + lines = cell.get("lines", []) + cell_text = cell.get("text", "").strip() # ★ 추가 + ph_name = f"HEADER_R{r_idx+1}_C{c_idx+1}" + + # ★ 수정: 텍스트 없는 셀은 비움 + if not cell_text and not lines: + content = "" + elif len(lines) > 1: + # 멀티라인 셀 → 각 라인별 placeholder + line_phs = [] + for l_idx in range(len(lines)): + line_phs.append(f"{{{{{ph_name}_LINE_{l_idx+1}}}}}") + content = "
".join(line_phs) + else: + content = f"{{{{{ph_name}}}}}" + + # colSpan/rowSpan + attrs = "" + bf_ref = cell.get("borderFillIDRef") + if bf_ref: + attrs += f' class="bf-{bf_ref}"' + if cell.get("colSpan", 1) > 1: + attrs += f' colspan="{cell["colSpan"]}"' + if cell.get("rowSpan", 1) > 1: + attrs += f' rowspan="{cell["rowSpan"]}"' + + html += f' {content}\n' + html += '\n' + + html += '
\n' + else: + # 텍스트형 헤더 + texts = header_info.get("texts", []) + for i in range(max(len(texts), 1)): + html += f'
{{{{{f"HEADER_TEXT_{i+1}"}}}}}
\n' + + html += '
' + return html + + def _build_footer_html(self, footer_info: dict | None) -> str: + """footer tools 추출값 → HTML + placeholder""" + if not footer_info or not footer_info.get("exists"): + return "" + + html = '' + return html + + def _build_body_html(self, template_info: dict, parsed: dict, + semantic_map: dict = None) -> str: + """본문 영역 HTML 생성. + + ★ v5.2: content_order가 있으면 원본 순서 그대로 조립. + content_order 없으면 기존 섹션+표 방식 (하위 호환). + """ + content_order = template_info.get("content_order") + + if content_order and self._has_paragraph_content(content_order): + return self._build_body_from_content_order( + template_info, content_order, semantic_map + ) + else: + return self._build_body_legacy( + template_info, parsed, semantic_map + ) + + # ── content_order 기반 본문 생성 (v5.2+) ── + + def _has_paragraph_content(self, content_order: list) -> bool: + """content_order에 문단이 있는지 (표만 있으면 legacy 사용)""" + return any( + c['type'] == 'paragraph' for c in content_order + ) + + def _build_body_from_content_order(self, template_info: dict, + content_order: list, + semantic_map: dict = None) -> str: + """content_order 기반 — 원본 문서 순서 그대로 HTML 조립. + + 콘텐츠 유형별 처리: + paragraph →

{{CONTENT_n}}

+ table → data-table placeholder (title_table 제외) + image →
{{IMAGE_n}}
+ empty → 생략 (연속 빈 문단 의미 없음) + """ + import re + + tables = template_info.get("tables", []) + + # semantic_map에서 title/body 인덱스 + title_table_idx = None + body_table_indices = [] + if semantic_map: + title_table_idx = semantic_map.get("title_table") + body_table_indices = semantic_map.get("body_tables", []) + else: + body_table_indices = [t["index"] for t in tables] + + # ★ v5.3: content_order table_idx → tables 리스트 매핑 + # content_order.table_idx = section body에서 만난 표 순번 (0-based) + # tables 리스트 = HWPX 전체 표 (header/footer 포함) + # → header/footer 제외한 "본문 가시 표" 리스트로 매핑해야 정확함 + header_footer_indices = set() + if semantic_map: + for idx_key, role_info in semantic_map.get("table_roles", {}).items(): + role = role_info.get("role", "") + if role in ("header_table", "footer_table"): + try: + header_footer_indices.add(int(idx_key)) + except (ValueError, TypeError): + pass + + body_visible_tables = [ + t for t in tables + if t["index"] not in header_footer_indices + ] + + body_parts = [] + + # ── 제목 블록 (title_table이 있으면) ── + if title_table_idx is not None: + title_tbl = next( + (t for t in tables if t["index"] == title_table_idx), None + ) + if title_tbl: + body_parts.append( + self._build_title_block_html(title_tbl) + ) + + # ── content_order 순회 ── + para_num = 0 # 문단 placeholder 번호 + tbl_num = 0 # 데이터 표 번호 (1-based) + img_num = 0 # 이미지 placeholder 번호 + in_section = False + section_num = 0 + + # 섹션 제목 패턴 + sec_patterns = [ + re.compile(r'^\d+\.\s+\S'), + re.compile(r'^[ⅠⅡⅢⅣⅤⅥⅦⅧⅨⅩ]\.\s*\S'), + re.compile(r'^제\s*\d+\s*[장절항]\s*\S'), + ] + + def _is_section_title(text: str) -> bool: + return any(p.match(text) for p in sec_patterns) + + for item in content_order: + itype = item['type'] + + # ── 빈 문단: 생략 ── + if itype == 'empty': + continue + + # ── 표: title_table은 이미 처리, body_table만 ── + # table_idx = content_order.py가 부여한 등장순서 0-based + # ★ v5.3: body_visible_tables로 매핑 (header/footer 표 제외) + if itype == 'table': + t_idx = item.get('table_idx', 0) + # body_visible_tables에서 해당 인덱스의 표 가져오기 + if t_idx < len(body_visible_tables): + tbl_data = body_visible_tables[t_idx] + if tbl_data["index"] == title_table_idx: + continue # title_table 건너뛰기 + if tbl_data["index"] not in body_table_indices: + continue # body 데이터 표가 아니면 건너뛰기 + + tbl_num += 1 + col_cnt = item.get('colCnt', '3') + try: + col_cnt = int(col_cnt) + except (ValueError, TypeError): + col_cnt = 3 + + # semantic_map에서 col_headers 가져오기 + _roles = semantic_map.get("table_roles", {}) if semantic_map else {} + if t_idx < len(body_visible_tables): + tbl_data = body_visible_tables[t_idx] + tbl_role = _roles.get(tbl_data["index"], + _roles.get(str(tbl_data["index"]), {})) + col_headers = tbl_role.get("col_headers", []) + actual_col_cnt = len(col_headers) if col_headers else col_cnt + + rows = tbl_data.get("rows", []) + header_row_data = rows[0] if rows else None + col_pcts = tbl_data.get("colWidths_pct", []) + else: + actual_col_cnt = col_cnt + header_row_data = None + col_pcts = [] + + body_parts.append( + self._build_table_placeholder( + tbl_num, actual_col_cnt, col_pcts, + header_row=header_row_data + ) + ) + continue + + # ── 이미지 ── + if itype == 'image': + img_num += 1 + ppr = item.get('paraPrIDRef', '0') + caption = item.get('text', '') + ref = item.get('binaryItemIDRef', '') + + img_html = f'
\n' + img_html += f' {{{{IMAGE_{img_num}}}}}\n' + if caption: + img_html += f'

{{{{IMAGE_{img_num}_CAPTION}}}}

\n' + img_html += '
' + body_parts.append(img_html) + continue + + # ── 문단 ── + if itype == 'paragraph': + text = item.get('text', '') + ppr = item.get('paraPrIDRef', '0') + cpr = item.get('charPrIDRef', '0') + + # 섹션 제목 감지 + if _is_section_title(text): + # 이전 섹션 닫기 + if in_section: + body_parts.append('\n') + + section_num += 1 + in_section = True + body_parts.append( + f'
\n' + f'

' + f'{{{{SECTION_{section_num}_TITLE}}}}

' + ) + continue + + # 일반 문단 + para_num += 1 + + # runs가 여러 개면 다중 span + runs = item.get('runs', []) + if len(runs) > 1: + spans = [] + for r_idx, run in enumerate(runs): + r_cpr = run.get('charPrIDRef', cpr) + spans.append( + f'' + f'{{{{PARA_{para_num}_RUN_{r_idx+1}}}}}' + ) + inner = ''.join(spans) + else: + inner = ( + f'' + f'{{{{PARA_{para_num}}}}}' + ) + + body_parts.append( + f'

{inner}

' + ) + + # 마지막 섹션 닫기 + if in_section: + body_parts.append('
\n') + + return "\n\n".join(body_parts) + + def _build_title_block_html(self, title_tbl: dict) -> str: + """제목표 → title-block HTML (기존 로직 분리)""" + rows = title_tbl.get("rows", []) + col_pcts = title_tbl.get("colWidths_pct", []) + + html = '
\n\n' + + if col_pcts: + html += '\n' + for pct in col_pcts: + html += f' \n' + html += '\n' + + for r_idx, row in enumerate(rows): + html += '\n' + for c_idx, cell in enumerate(row): + attrs = "" + bf_ref = cell.get("borderFillIDRef") + if bf_ref: + attrs += f' class="bf-{bf_ref}"' + cs = cell.get("colSpan", 1) + if cs > 1: + attrs += f' colspan="{cs}"' + rs = cell.get("rowSpan", 1) + if rs > 1: + attrs += f' rowspan="{rs}"' + + cell_text = cell.get("text", "").strip() + if cell_text: + ph_name = f"TITLE_R{r_idx+1}_C{c_idx+1}" + html += f' {{{{{ph_name}}}}}\n' + else: + html += f' \n' + html += '\n' + + html += '
\n
\n' + return html + + # ── 기존 섹션+표 방식 (하위 호환) ── + + def _build_body_legacy(self, template_info: dict, parsed: dict, + semantic_map: dict = None) -> str: + """content_order 없을 때 — 기존 v5.1 방식 유지""" + body_parts = [] + tables = template_info.get("tables", []) + + # ── semantic_map이 있으면 활용 ── + if semantic_map: + body_table_indices = semantic_map.get("body_tables", []) + title_idx = semantic_map.get("title_table") + else: + # semantic_map 없으면 전체 표 사용 (하위 호환) + body_table_indices = [t["index"] for t in tables] + title_idx = None + + # ── 제목 블록 ── + if title_idx is not None: + title_tbl = next((t for t in tables if t["index"] == title_idx), None) + if title_tbl: + body_parts.append(self._build_title_block_html(title_tbl)) + + # ── 본문 데이터 표만 필터링 ── + body_tables = [t for t in tables if t["index"] in body_table_indices] + + # ── 섹션 감지 ── + section_titles = self._detect_section_titles(parsed) + + if not section_titles and not body_tables: + # 구조 정보 부족 → 기본 1섹션 + body_parts.append( + '
\n' + '
{{SECTION_1_TITLE}}
\n' + '
{{SECTION_1_CONTENT}}
\n' + '
' + ) + else: + sec_count = max(len(section_titles), 1) + tbl_idx = 0 + + for s in range(sec_count): + s_num = s + 1 + body_parts.append( + f'
\n' + f'
{{{{SECTION_{s_num}_TITLE}}}}
\n' + f'
{{{{SECTION_{s_num}_CONTENT}}}}
\n' + ) + + # 이 섹션에 표 배분 + if tbl_idx < len(body_tables): + t = body_tables[tbl_idx] + col_cnt = t.get("colCnt", 3) + + # semantic_map에서 실제 col_headers 가져오기 + _roles = semantic_map.get("table_roles", {}) if semantic_map else {} + tbl_role = _roles.get(t["index"], _roles.get(str(t["index"]), {})) + col_headers = tbl_role.get("col_headers", []) + actual_col_cnt = len(col_headers) if col_headers else col_cnt + + # 헤더행 셀 데이터 (bf_id 포함) + rows = t.get("rows", []) + header_row_data = rows[0] if rows else None + + body_parts.append( + self._build_table_placeholder( + tbl_idx + 1, actual_col_cnt, + t.get("colWidths_pct", []), + header_row=header_row_data # ★ 헤더행 전달 + ) + ) + tbl_idx += 1 + + body_parts.append('
\n') + + # 남은 표 + while tbl_idx < len(body_tables): + t = body_tables[tbl_idx] + col_cnt = t.get("colCnt", 3) + _roles = semantic_map.get("table_roles", {}) if semantic_map else {} + tbl_role = _roles.get(t["index"], _roles.get(str(t["index"]), {})) + col_headers = tbl_role.get("col_headers", []) + actual_col_cnt = len(col_headers) if col_headers else col_cnt + rows = t.get("rows", []) + header_row_data = rows[0] if rows else None + body_parts.append( + self._build_table_placeholder( + tbl_idx + 1, actual_col_cnt, + t.get("colWidths_pct", []), + header_row=header_row_data + ) + ) + tbl_idx += 1 + + return "\n".join(body_parts) + + def _build_table_placeholder(self, tbl_num: int, col_cnt: int, + col_pcts: list = None, + header_row: list = None) -> str: + """표 1개의 placeholder HTML 생성 + + Args: + tbl_num: 표 번호 (1-based) + col_cnt: 열 수 + col_pcts: 열 너비 % 리스트 + header_row: 헤더행 셀 리스트 [{bf_id, colSpan, ...}, ...] + """ + # colgroup + colgroup = "" + num_cols = len(col_pcts) if col_pcts else col_cnt + if num_cols > 0: + colgroup = "\n" + if col_pcts and len(col_pcts) == num_cols: + for pct in col_pcts: + colgroup += f' \n' + else: + for _ in range(num_cols): + colgroup += " \n" + colgroup += "\n" + + # 헤더 행 — ★ bf_id가 있으면 class 적용 + header_cells = [] + if header_row: + for c, cell in enumerate(header_row): + bf_id = cell.get("borderFillIDRef") + cs = cell.get("colSpan", 1) + + attrs = "" + if bf_id: + attrs += f' class="bf-{bf_id}"' + if cs > 1: + attrs += f' colspan="{cs}"' + + header_cells.append( + f' {{{{TABLE_{tbl_num}_H_C{c+1}}}}}' + ) + else: + # fallback: bf 없는 경우 + for c in range(col_cnt): + header_cells.append( + f' {{{{TABLE_{tbl_num}_H_C{c+1}}}}}' + ) + + header_row_html = "\n".join(header_cells) + + return ( + f'\n' + f'{colgroup}' + f'\n' + f' \n{header_row_html}\n \n' + f'\n' + f'\n' + f' {{{{TABLE_{tbl_num}_BODY}}}}\n' + f'\n' + f'
' + ) + + def _detect_section_titles(self, parsed: dict) -> list: + """parsed 텍스트에서 섹션 제목 패턴 탐색""" + import re + titles = [] + + # parsed에서 텍스트 추출 + paragraphs = parsed.get("paragraphs", []) + if not paragraphs: + # raw_xml에서 태그 텍스트 추출 시도 + section_xml = "" + raw_xml = parsed.get("raw_xml", {}) + for key, val in raw_xml.items(): + if "section" in key.lower(): + section_xml = val if isinstance(val, str) else "" + break + if not section_xml: + section_xml = parsed.get("section_xml", "") + + if section_xml: + t_matches = re.findall(r'([^<]+)', section_xml) + paragraphs = [t.strip() for t in t_matches if t.strip()] + + # 섹션 제목 패턴 + patterns = [ + r'^(\d+)\.\s+\S', # "1. 제목" + r'^[ⅠⅡⅢⅣⅤⅥⅦⅧⅨⅩ]\.\s*\S', # "Ⅰ. 제목" + r'^제\s*\d+\s*[장절항]\s*\S', # "제1장 제목" + ] + + for text in paragraphs: + if isinstance(text, dict): + text = text.get("text", "") + text = str(text).strip() + if not text: + continue + for pat in patterns: + if re.match(pat, text): + titles.append(text) + break + + return titles + + def _extract_colors(self, template_info: dict) -> dict: + """template_info에서 색상 정보 추출""" + colors = {"background": [], "border": [], "text": []} + + bf = template_info.get("border_fills", {}) + for fill_id, fill_data in bf.items(): + # ★ background 키 사용 (bg → background) + bg = fill_data.get("background", fill_data.get("bg", "")) + if bg and bg.lower() not in ("", "none", "transparent") \ + and bg not in colors["background"]: + colors["background"].append(bg) + + # ★ css dict에서 border 색상 추출 + css_dict = fill_data.get("css", {}) + for prop, val in css_dict.items(): + if "border" in prop and val and val != "none": + # "0.12mm solid #999999" → "#999999" + parts = val.split() + if len(parts) >= 3: + c = parts[-1] + if c.startswith("#") and c not in colors["border"]: + colors["border"].append(c) + + # fallback: 직접 side 키 (top/bottom/left/right) + for side_key in ("top", "bottom", "left", "right"): + side = fill_data.get(side_key, {}) + if isinstance(side, dict): + c = side.get("color", "") + if c and c not in colors["border"]: + colors["border"].append(c) + + return colors + + def _summarize_features(self, template_info: dict, + semantic_map: dict = None) -> list: + """template_info에서 특징 요약""" + features = [] + + header = template_info.get("header", {}) + footer = template_info.get("footer", {}) + tables = template_info.get("tables", []) + + # 폰트 (fonts 구조: {"HANGUL": [{"face": "맑은 고딕"}], ...}) + fonts = template_info.get("fonts", {}) + hangul = fonts.get("HANGUL", []) + if hangul and isinstance(hangul, list) and len(hangul) > 0: + features.append(f"폰트: {hangul[0].get('face', '?')}") + + # 머릿말 (header.table.colCnt) + if header.get("exists"): + col_cnt = header.get("table", {}).get("colCnt", "?") + features.append(f"머릿말: {col_cnt}열") + + # 꼬릿말 (footer.table.colCnt) + if footer.get("exists"): + col_cnt = footer.get("table", {}).get("colCnt", "?") + features.append(f"꼬릿말: {col_cnt}열") + + # 표 — semantic_map이 있으면 데이터 표만 + if semantic_map and semantic_map.get("body_tables"): + for idx in semantic_map["body_tables"]: + t = next((tb for tb in tables if tb["index"] == idx), None) + if t: + features.append( + f"표: {t.get('rowCnt', '?')}x{t.get('colCnt', '?')}" + ) + elif tables: + t = tables[0] + features.append(f"표: {t.get('rowCnt', '?')}x{t.get('colCnt', '?')}") + + return features \ No newline at end of file diff --git a/handlers/tools/__init__.py b/handlers/tools/__init__.py new file mode 100644 index 0000000..14b8b13 --- /dev/null +++ b/handlers/tools/__init__.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +""" +HWPX 템플릿 추출 도구 모음 + +각 모듈은 HWPX XML에서 특정 항목을 코드 기반으로 추출한다. +- 추출 실패 시 None 반환 (디폴트값 절대 생성 안 함) +- 모든 단위 변환은 hwpx_utils 사용 +- hwpx_domain_guide.md 기준 준수 + +모듈 목록: + page_setup : §7 용지/여백 (pagePr + margin) + font : §3 글꼴 (fontface → font) + char_style : §4 글자 모양 (charPr) + para_style : §5 문단 모양 (paraPr) + border_fill : §2 테두리/배경 (borderFill) + table : §6 표 (tbl, tc) + header_footer: §8 머리말/꼬리말 (headerFooter) + section : §9 구역 정의 (secPr) + style_def : 스타일 정의 (styles) + numbering : 번호매기기/글머리표 + image : 이미지/그리기 객체 + content_order: 본문 콘텐츠 순서 (section*.xml) +""" + +from . import page_setup +from . import font +from . import char_style +from . import para_style +from . import border_fill +from . import table +from . import header_footer +from . import section +from . import style_def +from . import numbering +from . import image +from . import content_order + +__all__ = [ + "page_setup", + "font", + "char_style", + "para_style", + "border_fill", + "table", + "header_footer", + "section", + "style_def", + "numbering", + "image", + "content_order" +] \ No newline at end of file diff --git a/handlers/tools/border_fill.py b/handlers/tools/border_fill.py new file mode 100644 index 0000000..1f72936 --- /dev/null +++ b/handlers/tools/border_fill.py @@ -0,0 +1,127 @@ +# -*- coding: utf-8 -*- +""" +§2 테두리/배경(BorderFill) 추출 + +HWPX 실제 태그 (header.xml): + + + + + + + + + + + +디폴트값 생성 안 함. +""" + +import re + +from domain.hwpx.hwpx_utils import BORDER_TYPE_TO_CSS, hwpx_border_to_css + + +def extract(raw_xml: dict, parsed: dict = None) -> dict | None: + """§2 borderFill 전체 추출 → id별 dict. + + Returns: + { + 3: { + "id": 3, + "left": {"type": "SOLID", "width": "0.12 mm", "color": "#000000"}, + "right": {"type": "SOLID", "width": "0.12 mm", "color": "#000000"}, + "top": {"type": "SOLID", "width": "0.12 mm", "color": "#000000"}, + "bottom": {"type": "SOLID", "width": "0.12 mm", "color": "#000000"}, + "diagonal": {"type": "SOLID", "width": "0.1 mm", "color": "#000000"}, + "background": "#EDEDED", # fillBrush faceColor + "css": { # 편의: 미리 변환된 CSS + "border-left": "0.12mm solid #000000", + ... + "background-color": "#EDEDED", + } + }, + ... + } + 또는 추출 실패 시 None + """ + header_xml = _get_header_xml(raw_xml, parsed) + if not header_xml: + return None + + blocks = re.findall( + r']*)>(.*?)', + header_xml, re.DOTALL + ) + + if not blocks: + return None + + result = {} + for attrs_str, inner in blocks: + id_m = re.search(r'\bid="(\d+)"', attrs_str) + if not id_m: + continue + bf_id = int(id_m.group(1)) + + item = {"id": bf_id} + + # 4방향 + diagonal + for side, tag in [ + ("left", "leftBorder"), + ("right", "rightBorder"), + ("top", "topBorder"), + ("bottom", "bottomBorder"), + ("diagonal", "diagonal"), + ]: + # 태그 전체를 먼저 찾고, 속성을 개별 추출 (순서 무관) + tag_m = re.search(rf'', inner) + if tag_m: + tag_attrs = tag_m.group(1) + t = re.search(r'\btype="([^"]+)"', tag_attrs) + w = re.search(r'\bwidth="([^"]+)"', tag_attrs) + c = re.search(r'\bcolor="([^"]+)"', tag_attrs) + item[side] = { + "type": t.group(1) if t else "NONE", + "width": w.group(1).replace(" ", "") if w else "0.12mm", + "color": c.group(1) if c else "#000000", + } + + # 배경 (fillBrush > winBrush faceColor) + bg_m = re.search( + r']*\bfaceColor="([^"]+)"', inner + ) + if bg_m: + face = bg_m.group(1) + if face and face.lower() != "none": + item["background"] = face + + # CSS 편의 변환 + css = {} + for side in ["left", "right", "top", "bottom"]: + border_data = item.get(side) + if border_data: + css[f"border-{side}"] = hwpx_border_to_css(border_data) + else: + css[f"border-{side}"] = "none" + # border_data가 없으면 CSS에도 넣지 않음 + + if "background" in item: + css["background-color"] = item["background"] + + if css: + item["css"] = css + + result[bf_id] = item + + return result if result else None + + +def _get_header_xml(raw_xml: dict, parsed: dict = None) -> str | None: + if parsed and parsed.get("header_xml"): + return parsed["header_xml"] + if isinstance(raw_xml, dict): + for name, content in raw_xml.items(): + if "header" in name.lower() and isinstance(content, str): + return content + return raw_xml if isinstance(raw_xml, str) else None \ No newline at end of file diff --git a/handlers/tools/char_style.py b/handlers/tools/char_style.py new file mode 100644 index 0000000..52b9c9f --- /dev/null +++ b/handlers/tools/char_style.py @@ -0,0 +1,133 @@ +# -*- coding: utf-8 -*- +""" +§4 글자 모양(CharShape) 추출 + +HWPX 실제 태그 (header.xml): + + + + + + + + + + + + +디폴트값 생성 안 함. +""" + +import re + +from domain.hwpx.hwpx_utils import charsize_to_pt + + +def extract(raw_xml: dict, parsed: dict = None) -> list | None: + """§4 charPr 전체 목록 추출. + + Returns: + [ + { + "id": 0, + "height_pt": 10.0, + "textColor": "#000000", + "bold": False, + "italic": False, + "underline": "NONE", + "strikeout": "NONE", + "fontRef": {"hangul": 7, "latin": 6, ...}, + "ratio": {"hangul": 100, "latin": 100, ...}, + "spacing": {"hangul": 0, "latin": 0, ...}, + "borderFillIDRef": 2, + }, + ... + ] + """ + header_xml = _get_header_xml(raw_xml, parsed) + if not header_xml: + return None + + # charPr 블록 추출 (self-closing이 아닌 블록) + blocks = re.findall( + r']*)>(.*?)', + header_xml, re.DOTALL + ) + + if not blocks: + return None + + result = [] + for attrs_str, inner in blocks: + item = {} + + # 속성 파싱 + id_m = re.search(r'\bid="(\d+)"', attrs_str) + if id_m: + item["id"] = int(id_m.group(1)) + + height_m = re.search(r'\bheight="(\d+)"', attrs_str) + if height_m: + item["height_pt"] = charsize_to_pt(int(height_m.group(1))) + + color_m = re.search(r'\btextColor="([^"]+)"', attrs_str) + if color_m: + item["textColor"] = color_m.group(1) + + shade_m = re.search(r'\bshadeColor="([^"]+)"', attrs_str) + if shade_m and shade_m.group(1) != "none": + item["shadeColor"] = shade_m.group(1) + + bf_m = re.search(r'\bborderFillIDRef="(\d+)"', attrs_str) + if bf_m: + item["borderFillIDRef"] = int(bf_m.group(1)) + + # bold / italic (태그 존재 여부로 판단) + item["bold"] = bool(re.search(r'', inner)) + item["italic"] = bool(re.search(r'', inner)) + + # fontRef + fr = re.search(r'', inner) + if fr: + item["fontRef"] = _parse_lang_attrs(fr.group(1)) + + # ratio + ra = re.search(r'', inner) + if ra: + item["ratio"] = _parse_lang_attrs(ra.group(1)) + + # spacing + sp = re.search(r'', inner) + if sp: + item["spacing"] = _parse_lang_attrs(sp.group(1)) + + # underline + ul = re.search(r']*\btype="([^"]+)"', inner) + if ul: + item["underline"] = ul.group(1) + + # strikeout + so = re.search(r']*\bshape="([^"]+)"', inner) + if so: + item["strikeout"] = so.group(1) + + result.append(item) + + return result if result else None + + +def _parse_lang_attrs(attrs_str: str) -> dict: + """hangul="7" latin="6" ... → {"hangul": 7, "latin": 6, ...}""" + pairs = re.findall(r'(\w+)="(-?\d+)"', attrs_str) + return {k: int(v) for k, v in pairs} + + +def _get_header_xml(raw_xml: dict, parsed: dict = None) -> str | None: + if parsed and parsed.get("header_xml"): + return parsed["header_xml"] + if isinstance(raw_xml, dict): + for name, content in raw_xml.items(): + if "header" in name.lower() and isinstance(content, str): + return content + return raw_xml if isinstance(raw_xml, str) else None \ No newline at end of file diff --git a/handlers/tools/content_order.py b/handlers/tools/content_order.py new file mode 100644 index 0000000..eca9e40 --- /dev/null +++ b/handlers/tools/content_order.py @@ -0,0 +1,529 @@ +# -*- coding: utf-8 -*- +""" +content_order.py — HWPX section*.xml 본문 콘텐츠 순서 추출 + +기존 12개 tool이 header.xml의 "정의(definition)"를 추출하는 반면, +이 tool은 section0.xml의 "본문(content)" 순서를 추출한다. + +추출 결과는 template_manager._build_body_html()이 +원본 순서 그대로 HTML을 조립하는 데 사용된다. + +콘텐츠 유형: + - paragraph : 일반 텍스트 문단 + - table : 표 () + - image : 이미지 () + - empty : 빈 문단 (줄바꿈 역할) + +참조: hwpx_domain_guide.md §6(표), §7(본문 구조) +""" + +import re +import logging + +logger = logging.getLogger(__name__) + +# ================================================================ +# 네임스페이스 +# ================================================================ +# HWPX는 여러 네임스페이스를 사용한다. +# section*.xml: hp: (본문), ha: (속성) +# header.xml: hh: (헤더 정의) +# 실제 파일에서 네임스페이스 URI가 다를 수 있으므로 로컬명 기반 탐색도 병행한다. + +DEFAULT_NS = { + 'hp': 'http://www.hancom.co.kr/hwpml/2011/paragraph', + 'ha': 'http://www.hancom.co.kr/hwpml/2011/attributes', + 'hh': 'http://www.hancom.co.kr/hwpml/2011/head', + 'hc': 'http://www.hancom.co.kr/hwpml/2011/core', +} + + +# ================================================================ +# 공개 API +# ================================================================ + +def extract(raw_xml, parsed, ns=None): + """section*.xml에서 본문 콘텐츠 순서를 추출한다. + + Args: + raw_xml (dict): 원본 XML 문자열 딕셔너리. + raw_xml.get("section0") 등으로 section XML에 접근. + parsed (dict): processor.py가 HWPX를 파싱한 전체 결과 dict. + parsed.get("section_xml") 등으로 parsed Element에 접근. + ns (dict, optional): 네임스페이스 매핑. None이면 자동 감지. + + Returns: + list[dict]: 콘텐츠 순서 리스트. 각 항목은 다음 키를 포함: + - type: "paragraph" | "table" | "image" | "empty" + - index: 전체 순서 내 인덱스 (0부터) + - paraPrIDRef: 문단모양 참조 ID (str or None) + - styleIDRef: 스타일 참조 ID (str or None) + + type별 추가 키 (아래 참조) + 추출 실패 시 None 반환 (analyzer가 결과에서 제외함). + """ + # ── section XML 찾기 ── + # raw_xml dict에서 section 원본 문자열 추출 + section_raw = None + if isinstance(raw_xml, dict): + # 키 이름은 프로젝트마다 다를 수 있음: section0, section_xml 등 + for key in ['section0', 'section_xml', 'section0.xml']: + if key in raw_xml: + section_raw = raw_xml[key] + break + # 못 찾으면 "section"으로 시작하는 첫 번째 키 + if section_raw is None: + for key, val in raw_xml.items(): + if key.startswith('section') and isinstance(val, str): + section_raw = val + break + elif isinstance(raw_xml, str): + section_raw = raw_xml + + # parsed dict에서 section Element 또는 문자열 추출 + section_parsed = None + if isinstance(parsed, dict): + for key in ['section_xml', 'section0', 'section_parsed', 'section0_parsed']: + val = parsed.get(key) + if val is None: + continue + if isinstance(val, str): + # 문자열이면 section_raw로 활용 (table.py와 동일) + if section_raw is None: + section_raw = val + elif not isinstance(val, dict): + # Element 객체로 추정 + section_parsed = val + break + # fallback: raw_xml 문자열을 직접 파싱 + if section_parsed is None and section_raw: + import xml.etree.ElementTree as ET + try: + section_parsed = ET.fromstring(section_raw) + except ET.ParseError: + logger.warning("section XML 파싱 실패") + return None + else: + # parsed 자체가 Element일 수 있음 (직접 호출 시) + section_parsed = parsed + + if section_parsed is None: + logger.warning("section XML을 찾을 수 없음 — content_order 추출 생략") + return None + + if ns is None: + ns = _detect_namespaces(section_raw or '', section_parsed) + + # 엘리먼트 수집 — secPr 내부는 제외 + paragraphs = _collect_body_paragraphs(section_parsed, ns) + + content_order = [] + table_idx = 0 + image_idx = 0 + + for p_elem in paragraphs: + para_pr_id = _get_attr(p_elem, 'paraPrIDRef') + style_id = _get_attr(p_elem, 'styleIDRef') + + base = { + 'index': len(content_order), + 'paraPrIDRef': para_pr_id, + 'styleIDRef': style_id, + } + + # ── (1) 표 확인 ── + tbl = _find_element(p_elem, 'tbl', ns) + if tbl is not None: + tbl_info = _extract_table_info(tbl, ns) + content_order.append({ + **base, + 'type': 'table', + 'table_idx': table_idx, + **tbl_info, + }) + table_idx += 1 + continue + + # ── (2) 이미지 확인 ── + pic = _find_element(p_elem, 'pic', ns) + if pic is not None: + img_info = _extract_image_info(pic, p_elem, ns) + content_order.append({ + **base, + 'type': 'image', + 'image_idx': image_idx, + **img_info, + }) + image_idx += 1 + continue + + # ── (3) 텍스트 문단 / 빈 문단 ── + text = _collect_text(p_elem, ns) + runs_info = _extract_runs_info(p_elem, ns) + + if not text.strip(): + content_order.append({ + **base, + 'type': 'empty', + }) + else: + content_order.append({ + **base, + 'type': 'paragraph', + 'text': text, + 'charPrIDRef': runs_info.get('first_charPrIDRef'), + 'runs': runs_info.get('runs', []), + }) + + logger.info( + "content_order 추출 완료: %d items " + "(paragraphs=%d, tables=%d, images=%d, empty=%d)", + len(content_order), + sum(1 for c in content_order if c['type'] == 'paragraph'), + table_idx, + image_idx, + sum(1 for c in content_order if c['type'] == 'empty'), + ) + + return content_order + + +# ================================================================ +# 본문 수집 — secPr 내부 제외 +# ================================================================ + +def _collect_body_paragraphs(root, ns): + """ 직계 만 수집한다. + + secPr, headerFooter 내부의 는 본문이 아니므로 제외. + subList 내부(셀 안 문단)도 제외 — 표는 통째로 하나의 항목. + """ + paragraphs = [] + + # 방법 1: sec 직계 자식 중 p 태그만 + sec = _find_element(root, 'sec', ns) + if sec is None: + # 루트 자체가 sec일 수 있음 + sec = root + + for child in sec: + tag = _local_tag(child) + if tag == 'p': + paragraphs.append(child) + + # 직계 자식에서 못 찾았으면 fallback: 전체 탐색 (but secPr/subList 제외) + if not paragraphs: + paragraphs = _collect_paragraphs_fallback(root, ns) + + return paragraphs + + +def _collect_paragraphs_fallback(root, ns): + """fallback: 전체에서 를 찾되, secPr/headerFooter/subList 내부는 제외""" + skip_tags = {'secPr', 'headerFooter', 'subList', 'tc'} + result = [] + + def _walk(elem, skip=False): + if skip: + return + tag = _local_tag(elem) + if tag in skip_tags: + return + if tag == 'p': + # 부모가 sec이거나 루트 직계인 경우만 + result.append(elem) + return # p 내부의 하위 p는 수집하지 않음 + for child in elem: + _walk(child) + + _walk(root) + return result + + +# ================================================================ +# 표 정보 추출 +# ================================================================ + +def _extract_table_info(tbl, ns): + """ 에서 기본 메타 정보 추출""" + info = { + 'rowCnt': _get_attr(tbl, 'rowCnt'), + 'colCnt': _get_attr(tbl, 'colCnt'), + 'borderFillIDRef': _get_attr(tbl, 'borderFillIDRef'), + } + + # 열 너비 + col_sz = _find_element(tbl, 'colSz', ns) + if col_sz is not None: + width_list_elem = _find_element(col_sz, 'widthList', ns) + if width_list_elem is not None and width_list_elem.text: + info['colWidths'] = width_list_elem.text.strip().split() + + return info + + +# ================================================================ +# 이미지 정보 추출 +# ================================================================ + +def _extract_image_info(pic, p_elem, ns): + """ 에서 이미지 참조 정보 추출""" + info = { + 'binaryItemIDRef': None, + 'text': '', # 이미지와 같은 문단에 있는 텍스트 (캡션 등) + } + + # img 태그에서 binaryItemIDRef + img = _find_element(pic, 'img', ns) + if img is not None: + info['binaryItemIDRef'] = _get_attr(img, 'binaryItemIDRef') + + # imgRect에서 크기 정보 + img_rect = _find_element(pic, 'imgRect', ns) + if img_rect is not None: + info['imgRect'] = { + 'x': _get_attr(img_rect, 'x'), + 'y': _get_attr(img_rect, 'y'), + 'w': _get_attr(img_rect, 'w'), + 'h': _get_attr(img_rect, 'h'), + } + + # 같은 문단 내 텍스트 (pic 바깥의 run들) + info['text'] = _collect_text_outside(p_elem, pic, ns) + + return info + + +# ================================================================ +# 텍스트 수집 +# ================================================================ + +def _collect_text(p_elem, ns): + """ 내 모든 텍스트를 순서대로 합침 + + 주의: t.tail은 XML 들여쓰기 공백이므로 수집하지 않는다. + HWPX에서 실제 텍스트는 항상 ... 안에 있다. + """ + parts = [] + for t in _find_all_elements(p_elem, 't', ns): + if t.text: + parts.append(t.text) + return ''.join(parts) + + +def _collect_text_outside(p_elem, exclude_elem, ns): + """p_elem 내에서 exclude_elem(예: pic) 바깥의 텍스트만 수집""" + parts = [] + + def _walk(elem): + if elem is exclude_elem: + return + tag = _local_tag(elem) + if tag == 't' and elem.text: + parts.append(elem.text) + for child in elem: + _walk(child) + + _walk(p_elem) + return ''.join(parts) + + +# ================================================================ +# Run 정보 추출 +# ================================================================ + +def _extract_runs_info(p_elem, ns): + """ 들의 charPrIDRef와 텍스트 추출 + + Returns: + { + 'first_charPrIDRef': str or None, + 'runs': [ + {'charPrIDRef': '8', 'text': '1. SamanPro...'}, + {'charPrIDRef': '24', 'text': '포장설계...'}, + ] + } + """ + runs = [] + first_char_pr = None + + for run_elem in _find_direct_runs(p_elem, ns): + char_pr = _get_attr(run_elem, 'charPrIDRef') + if first_char_pr is None and char_pr is not None: + first_char_pr = char_pr + + text_parts = [] + for t in _find_all_elements(run_elem, 't', ns): + if t.text: + text_parts.append(t.text) + + if text_parts: + runs.append({ + 'charPrIDRef': char_pr, + 'text': ''.join(text_parts), + }) + + return { + 'first_charPrIDRef': first_char_pr, + 'runs': runs, + } + + +def _find_direct_runs(p_elem, ns): + """ 직계 만 찾음 (subList 내부 제외)""" + results = [] + for child in p_elem: + tag = _local_tag(child) + if tag == 'run': + results.append(child) + return results + + +# ================================================================ +# 네임스페이스 감지 +# ================================================================ + +def _detect_namespaces(raw_xml, parsed): + """XML에서 실제 사용된 네임스페이스 URI를 감지한다. + + HWPX 버전에 따라 네임스페이스 URI가 다를 수 있다: + - 2011 버전: http://www.hancom.co.kr/hwpml/2011/paragraph + - 2016 버전: http://www.hancom.co.kr/hwpml/2016/paragraph (일부) + """ + ns = dict(DEFAULT_NS) + + if raw_xml: + # xmlns:hp="..." 패턴으로 실제 URI 추출 + for prefix in ['hp', 'ha', 'hh', 'hc']: + pattern = rf'xmlns:{prefix}="([^"]+)"' + match = re.search(pattern, raw_xml) + if match: + ns[prefix] = match.group(1) + + return ns + + +# ================================================================ +# XML 유틸리티 — 네임스페이스 불가지론적 탐색 +# ================================================================ + +def _local_tag(elem): + """'{namespace}localname' → 'localname'""" + tag = elem.tag + if '}' in tag: + return tag.split('}', 1)[1] + return tag + + +def _get_attr(elem, attr_name): + """속성값 가져오기. 네임스페이스 유무 모두 시도.""" + # 직접 속성명 + val = elem.get(attr_name) + if val is not None: + return val + + # 네임스페이스 접두사가 붙은 속성 시도 + for full_attr in elem.attrib: + if full_attr.endswith(attr_name): + return elem.attrib[full_attr] + + return None + + +def _find_element(parent, local_name, ns): + """자식 중 로컬명이 일치하는 첫 번째 엘리먼트를 찾는다. + + 네임스페이스 prefix 시도 후, 실패하면 로컬명 직접 비교. + """ + # 1차: 네임스페이스 prefix로 탐색 + for prefix in ['hp', 'hh', 'hc', 'ha']: + uri = ns.get(prefix, '') + found = parent.find(f'{{{uri}}}{local_name}') + if found is not None: + return found + + # 2차: 직계 자식 로컬명 비교 + for child in parent: + if _local_tag(child) == local_name: + return child + + # 3차: 재귀 탐색 (1단계만) + for child in parent: + for grandchild in child: + if _local_tag(grandchild) == local_name: + return grandchild + + return None + + +def _find_all_elements(parent, local_name, ns): + """하위 전체에서 로컬명이 일치하는 모든 엘리먼트를 찾는다.""" + results = [] + + def _walk(elem): + if _local_tag(elem) == local_name: + results.append(elem) + for child in elem: + _walk(child) + + _walk(parent) + return results + + +# ================================================================ +# 편의 함수 +# ================================================================ + +def summarize(content_order): + """content_order 리스트를 사람이 읽기 쉬운 요약으로 변환""" + lines = [] + for item in content_order: + idx = item['index'] + t = item['type'] + + if t == 'paragraph': + text_preview = item['text'][:50] + if len(item['text']) > 50: + text_preview += '...' + lines.append( + f"[{idx:3d}] P paraPr={item['paraPrIDRef']:<4s} " + f"charPr={item.get('charPrIDRef', '-'):<4s} " + f"\"{text_preview}\"" + ) + elif t == 'table': + lines.append( + f"[{idx:3d}] T table_idx={item['table_idx']} " + f"({item.get('rowCnt', '?')}×{item.get('colCnt', '?')})" + ) + elif t == 'image': + ref = item.get('binaryItemIDRef', '?') + caption = item.get('text', '')[:30] + lines.append( + f"[{idx:3d}] I image_idx={item['image_idx']} " + f"ref={ref} \"{caption}\"" + ) + elif t == 'empty': + lines.append(f"[{idx:3d}] _ (empty)") + + return '\n'.join(lines) + + +def get_stats(content_order): + """content_order 통계 반환""" + type_map = { + 'paragraph': 'paragraphs', + 'table': 'tables', + 'image': 'images', + 'empty': 'empty', + } + stats = { + 'total': len(content_order), + 'paragraphs': 0, + 'tables': 0, + 'images': 0, + 'empty': 0, + } + for item in content_order: + key = type_map.get(item['type']) + if key: + stats[key] += 1 + return stats \ No newline at end of file diff --git a/handlers/tools/font.py b/handlers/tools/font.py new file mode 100644 index 0000000..a4ea867 --- /dev/null +++ b/handlers/tools/font.py @@ -0,0 +1,82 @@ +# -*- coding: utf-8 -*- +""" +§3 글꼴(FaceName) 추출 + +HWPX 실제 태그 (header.xml): + + + + + + + + +디폴트값 생성 안 함. 추출 실패 시 None 반환. +""" + +import re + + +def extract(raw_xml: dict, parsed: dict = None) -> dict | None: + """§3 fontface에서 언어별 글꼴 정의 추출. + + Returns: + { + "HANGUL": [{"id": 0, "face": "돋움", "type": "TTF"}, ...], + "LATIN": [{"id": 0, "face": "돋움", "type": "TTF"}, ...], + "HANJA": [...], + ... + } + 또는 추출 실패 시 None + """ + header_xml = _get_header_xml(raw_xml, parsed) + if not header_xml: + return None + + result = {} + + # fontface 블록을 lang별로 추출 + fontface_blocks = re.findall( + r']*\blang="([^"]+)"[^>]*>(.*?)', + header_xml, re.DOTALL + ) + + if not fontface_blocks: + return None + + for lang, block_content in fontface_blocks: + fonts = [] + font_matches = re.finditer( + r']*' + r'\bid="(\d+)"[^>]*' + r'\bface="([^"]+)"[^>]*' + r'\btype="([^"]+)"', + block_content + ) + for fm in font_matches: + fonts.append({ + "id": int(fm.group(1)), + "face": fm.group(2), + "type": fm.group(3), + }) + + if fonts: + result[lang] = fonts + + return result if result else None + + +def _get_header_xml(raw_xml: dict, parsed: dict = None) -> str | None: + """header.xml 문자열을 가져온다.""" + if parsed and parsed.get("header_xml"): + return parsed["header_xml"] + + if isinstance(raw_xml, dict): + for name, content in raw_xml.items(): + if "header" in name.lower() and isinstance(content, str): + return content + + if isinstance(raw_xml, str): + return raw_xml + + return None \ No newline at end of file diff --git a/handlers/tools/header_footer.py b/handlers/tools/header_footer.py new file mode 100644 index 0000000..7dc9b30 --- /dev/null +++ b/handlers/tools/header_footer.py @@ -0,0 +1,200 @@ +# -*- coding: utf-8 -*- +""" +§8 머리말/꼬리말(HeaderFooter) 추출 + +HWPX 실제 태그 (section0.xml): + + + + + 머리말/꼬리말 안에 표가 있는 경우: + - 표의 셀에 다중행 텍스트가 포함될 수 있음 + - 각 셀의 colSpan, rowSpan, width, borderFillIDRef 등 추출 필요 + +secPr 내 속성: + + +디폴트값 생성 안 함. +""" + +import re + +from domain.hwpx.hwpx_utils import hwpunit_to_mm + + +def extract_header(raw_xml: dict, parsed: dict = None) -> dict | None: + """머리말 구조 추출. + + Returns: + { + "exists": True, + "type": "table" | "text", + "hidden": False, + "table": { ... } | None, # 표가 있는 경우 + "texts": ["부서명", ...], + } + """ + return _extract_hf(raw_xml, parsed, "header") + + +def extract_footer(raw_xml: dict, parsed: dict = None) -> dict | None: + """꼬리말 구조 추출.""" + return _extract_hf(raw_xml, parsed, "footer") + + +def _extract_hf(raw_xml: dict, parsed: dict, hf_type: str) -> dict | None: + """header 또는 footer 추출 공통 로직""" + # 1) parsed에서 직접 제공된 header/footer XML + hf_xml = None + if parsed: + key = f"page_{hf_type}_xml" + hf_xml = parsed.get(key, "") + + # 2) section XML에서 headerFooter 블록 탐색 + section_xml = _get_section_xml(raw_xml, parsed) + + if not hf_xml and section_xml: + # headerFooter 태그에서 header/footer 구분 + hf_blocks = re.findall( + r']*)>(.*?)', + section_xml, re.DOTALL + ) + for attrs, inner in hf_blocks: + # type 속성으로 구분 (HEADER / FOOTER) + type_m = re.search(r'\btype="([^"]+)"', attrs) + if type_m: + if type_m.group(1).upper() == hf_type.upper(): + hf_xml = inner + break + + if not hf_xml or not hf_xml.strip(): + return None # 해당 머리말/꼬리말 없음 + + result = {"exists": True} + + # hidden 여부 + if section_xml: + hide_key = f"hideFirst{'Header' if hf_type == 'header' else 'Footer'}" + hide_m = re.search(rf'\b{hide_key}="(\d+)"', section_xml) + if hide_m: + result["hidden"] = bool(int(hide_m.group(1))) + + # 텍스트 추출 + texts = re.findall(r'([^<]*)', hf_xml) + clean_texts = [t.strip() for t in texts if t.strip()] + if clean_texts: + result["texts"] = clean_texts + + # 표 존재 여부 + tbl_match = re.search( + r']*)>(.*?)', + hf_xml, re.DOTALL + ) + if tbl_match: + result["type"] = "table" + result["table"] = _parse_hf_table(tbl_match.group(1), tbl_match.group(2)) + else: + result["type"] = "text" + + return result + + +def _parse_hf_table(tbl_attrs: str, tbl_inner: str) -> dict: + """머리말/꼬리말 내 표 파싱""" + table = {} + + # rowCnt, colCnt + for attr in ["rowCnt", "colCnt"]: + m = re.search(rf'\b{attr}="(\d+)"', tbl_attrs) + if m: + table[attr] = int(m.group(1)) + + # 열 너비 + wl = re.search(r'([^<]+)', tbl_inner) + if wl: + try: + widths = [int(w) for w in wl.group(1).strip().split()] + table["colWidths_hu"] = widths + total = sum(widths) or 1 + table["colWidths_pct"] = [round(w / total * 100) for w in widths] + except ValueError: + pass + + # 행/셀 + rows = [] + tr_blocks = re.findall(r']*>(.*?)', tbl_inner, re.DOTALL) + for tr in tr_blocks: + cells = [] + tc_blocks = re.finditer( + r']*)>(.*?)', tr, re.DOTALL + ) + for tc in tc_blocks: + cell = _parse_hf_cell(tc.group(1), tc.group(2)) + cells.append(cell) + rows.append(cells) + + if rows: + table["rows"] = rows + + return table + + +def _parse_hf_cell(tc_attrs: str, tc_inner: str) -> dict: + """머리말/꼬리말 셀 파싱""" + cell = {} + + # borderFillIDRef + bf = re.search(r'\bborderFillIDRef="(\d+)"', tc_attrs) + if bf: + cell["borderFillIDRef"] = int(bf.group(1)) + + # cellAddr + addr = re.search( + r']*\bcolAddr="(\d+)"[^>]*\browAddr="(\d+)"', + tc_inner + ) + if addr: + cell["colAddr"] = int(addr.group(1)) + cell["rowAddr"] = int(addr.group(2)) + + # cellSpan + span = re.search(r'', tc_inner) + if span: + cs = re.search(r'\bcolSpan="(\d+)"', span.group(1)) + rs = re.search(r'\browSpan="(\d+)"', span.group(1)) + if cs: + cell["colSpan"] = int(cs.group(1)) + if rs: + cell["rowSpan"] = int(rs.group(1)) + + # cellSz + sz = re.search(r'', tc_inner) + if sz: + w = re.search(r'\bwidth="(\d+)"', sz.group(1)) + if w: + cell["width_hu"] = int(w.group(1)) + + # 셀 텍스트 (다중행) + paras = re.findall(r']*>(.*?)', tc_inner, re.DOTALL) + lines = [] + for p in paras: + p_texts = re.findall(r'([^<]*)', p) + line = " ".join(t.strip() for t in p_texts if t.strip()) + if line: + lines.append(line) + + if lines: + cell["text"] = " ".join(lines) + cell["lines"] = lines + + return cell + + +def _get_section_xml(raw_xml: dict, parsed: dict = None) -> str | None: + if parsed and parsed.get("section_xml"): + return parsed["section_xml"] + if isinstance(raw_xml, dict): + for name, content in raw_xml.items(): + if "section" in name.lower() and isinstance(content, str): + return content + return raw_xml if isinstance(raw_xml, str) else None \ No newline at end of file diff --git a/handlers/tools/image.py b/handlers/tools/image.py new file mode 100644 index 0000000..d989ccb --- /dev/null +++ b/handlers/tools/image.py @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- +""" +이미지/그리기 객체(ShapeObject) 추출 + +HWPX 실제 태그 (section0.xml): + + + + + + + + + + + + 또는 그리기 객체: + + + ... + + +디폴트값 생성 안 함. +""" + +import re + +from domain.hwpx.hwpx_utils import hwpunit_to_mm + + +def extract(raw_xml: dict, parsed: dict = None) -> list | None: + """이미지/그리기 객체 추출. + + Returns: + [ + { + "type": "image", + "binaryItemRef": "image1.JPG", + "width_hu": 28346, "height_hu": 14173, + "width_mm": 100.0, "height_mm": 50.0, + "offset": {"x": 0, "y": 0}, + }, + ... + ] + """ + section_xml = _get_section_xml(raw_xml, parsed) + if not section_xml: + return None + + result = [] + + # 블록 + pic_blocks = re.finditer( + r']*)>(.*?)', + section_xml, re.DOTALL + ) + for pm in pic_blocks: + pic_inner = pm.group(2) + item = {"type": "image"} + + # binaryItemRef + img = re.search(r']*\bbinaryItemIDRef="([^"]+)"', pic_inner) + if img: + item["binaryItemRef"] = img.group(1) + + # curSz (현재 크기) + csz = re.search( + r']*\bwidth="(\d+)"[^>]*\bheight="(\d+)"', + pic_inner + ) + if csz: + w, h = int(csz.group(1)), int(csz.group(2)) + item["width_hu"] = w + item["height_hu"] = h + item["width_mm"] = round(hwpunit_to_mm(w), 1) + item["height_mm"] = round(hwpunit_to_mm(h), 1) + + # offset + off = re.search( + r']*\bx="(-?\d+)"[^>]*\by="(-?\d+)"', + pic_inner + ) + if off: + item["offset"] = {"x": int(off.group(1)), "y": int(off.group(2))} + + result.append(item) + + return result if result else None + + +def _get_section_xml(raw_xml: dict, parsed: dict = None) -> str | None: + if parsed and parsed.get("section_xml"): + return parsed["section_xml"] + if isinstance(raw_xml, dict): + for name, content in raw_xml.items(): + if "section" in name.lower() and isinstance(content, str): + return content + return raw_xml if isinstance(raw_xml, str) else None \ No newline at end of file diff --git a/handlers/tools/numbering.py b/handlers/tools/numbering.py new file mode 100644 index 0000000..b6e048d --- /dev/null +++ b/handlers/tools/numbering.py @@ -0,0 +1,136 @@ +# -*- coding: utf-8 -*- +""" +번호매기기(Numbering) / 글머리표(Bullet) 추출 + +HWPX 실제 태그 (header.xml): + + ^1. + ^2. + + + + + + +디폴트값 생성 안 함. +""" + +import re + + +def extract(raw_xml: dict, parsed: dict = None) -> dict | None: + """번호매기기 + 글머리표 정의 추출. + + Returns: + { + "numberings": [ + { + "id": 1, "start": 0, + "levels": [ + {"level": 1, "numFormat": "DIGIT", "pattern": "^1.", + "align": "LEFT"}, + {"level": 2, "numFormat": "HANGUL_SYLLABLE", "pattern": "^2."}, + ... + ] + } + ], + "bullets": [ + {"id": 1, "char": "-", "useImage": False} + ] + } + """ + header_xml = _get_header_xml(raw_xml, parsed) + if not header_xml: + return None + + result = {} + + # ── 번호매기기 ── + numbering_blocks = re.findall( + r']*)>(.*?)', + header_xml, re.DOTALL + ) + if numbering_blocks: + nums = [] + for attrs, inner in numbering_blocks: + num = {} + id_m = re.search(r'\bid="(\d+)"', attrs) + if id_m: + num["id"] = int(id_m.group(1)) + start_m = re.search(r'\bstart="(\d+)"', attrs) + if start_m: + num["start"] = int(start_m.group(1)) + + # paraHead 레벨들 + levels = [] + heads = re.finditer( + r']*)>([^<]*)', + inner + ) + for h in heads: + h_attrs = h.group(1) + h_pattern = h.group(2).strip() + level = {} + + lv = re.search(r'\blevel="(\d+)"', h_attrs) + if lv: + level["level"] = int(lv.group(1)) + + fmt = re.search(r'\bnumFormat="([^"]+)"', h_attrs) + if fmt: + level["numFormat"] = fmt.group(1) + + al = re.search(r'\balign="([^"]+)"', h_attrs) + if al: + level["align"] = al.group(1) + + if h_pattern: + level["pattern"] = h_pattern + + if level: + levels.append(level) + + if levels: + num["levels"] = levels + nums.append(num) + + if nums: + result["numberings"] = nums + + # ── 글머리표 ── + bullet_blocks = re.findall( + r']*)>(.*?)', + header_xml, re.DOTALL + ) + if bullet_blocks: + bullets = [] + for attrs, inner in bullet_blocks: + bullet = {} + id_m = re.search(r'\bid="(\d+)"', attrs) + if id_m: + bullet["id"] = int(id_m.group(1)) + char_m = re.search(r'\bchar="([^"]*)"', attrs) + if char_m: + bullet["char"] = char_m.group(1) + img_m = re.search(r'\buseImage="(\d+)"', attrs) + if img_m: + bullet["useImage"] = bool(int(img_m.group(1))) + bullets.append(bullet) + + if bullets: + result["bullets"] = bullets + + return result if result else None + + +def _get_header_xml(raw_xml: dict, parsed: dict = None) -> str | None: + if parsed and parsed.get("header_xml"): + return parsed["header_xml"] + if isinstance(raw_xml, dict): + for name, content in raw_xml.items(): + if "header" in name.lower() and isinstance(content, str): + return content + return raw_xml if isinstance(raw_xml, str) else None \ No newline at end of file diff --git a/handlers/tools/page_setup.py b/handlers/tools/page_setup.py new file mode 100644 index 0000000..b31994a --- /dev/null +++ b/handlers/tools/page_setup.py @@ -0,0 +1,110 @@ +# -*- coding: utf-8 -*- +""" +§7 용지 설정 추출 (pagePr + margin) + +HWPX 실제 태그: + + + +디폴트값 생성 안 함. 추출 실패 시 None 반환. +""" + +import re + +from domain.hwpx.hwpx_utils import hwpunit_to_mm, mm_format, detect_paper_size + + +def extract(raw_xml: dict, parsed: dict = None) -> dict | None: + """§7 pagePr + margin에서 용지/여백 정보 추출. + + Returns: + { + "paper": {"name": "A4", "width_mm": 210.0, "height_mm": 297.0, + "landscape": True/False}, + "margins": {"top": "10.0mm", "bottom": "10.0mm", + "left": "20.0mm", "right": "20.0mm", + "header": "15.0mm", "footer": "15.0mm", + "gutter": "0.0mm"} + } + 또는 추출 실패 시 None + """ + section_xml = _get_section_xml(raw_xml, parsed) + if not section_xml: + return None + + result = {} + + # ── 용지 크기 ───────────────────────────────── + page_match = re.search( + r']*' + r'\bwidth="(\d+)"[^>]*' + r'\bheight="(\d+)"', + section_xml + ) + if not page_match: + # 속성 순서가 다를 수 있음 + page_match = re.search( + r']*' + r'\bheight="(\d+)"[^>]*' + r'\bwidth="(\d+)"', + section_xml + ) + if page_match: + h_hu, w_hu = int(page_match.group(1)), int(page_match.group(2)) + else: + return None + else: + w_hu, h_hu = int(page_match.group(1)), int(page_match.group(2)) + + landscape_match = re.search( + r']*\blandscape="([^"]+)"', section_xml + ) + is_landscape = False + if landscape_match: + is_landscape = landscape_match.group(1) == "WIDELY" + + paper_name = detect_paper_size(w_hu, h_hu) + + result["paper"] = { + "name": paper_name, + "width_mm": round(hwpunit_to_mm(w_hu), 1), + "height_mm": round(hwpunit_to_mm(h_hu), 1), + "landscape": is_landscape, + } + + # ── 여백 ────────────────────────────────────── + margin_match = re.search(r'', section_xml) + if not margin_match: + return result # 용지 크기는 있으나 여백은 없을 수 있음 + + attrs_str = margin_match.group(1) + margins = {} + for key in ["top", "bottom", "left", "right", "header", "footer", "gutter"]: + m = re.search(rf'\b{key}="(\d+)"', attrs_str) + if m: + margins[key] = mm_format(int(m.group(1))) + + if margins: + result["margins"] = margins + + return result + + +def _get_section_xml(raw_xml: dict, parsed: dict = None) -> str | None: + """section XML 문자열을 가져온다.""" + # parsed에서 직접 제공 + if parsed and parsed.get("section_xml"): + return parsed["section_xml"] + + # raw_xml dict에서 section 파일 찾기 + if isinstance(raw_xml, dict): + for name, content in raw_xml.items(): + if "section" in name.lower() and isinstance(content, str): + return content + + # raw_xml이 문자열이면 그대로 + if isinstance(raw_xml, str): + return raw_xml + + return None \ No newline at end of file diff --git a/handlers/tools/para_style.py b/handlers/tools/para_style.py new file mode 100644 index 0000000..2b6dd3a --- /dev/null +++ b/handlers/tools/para_style.py @@ -0,0 +1,185 @@ +# -*- coding: utf-8 -*- +""" +§5 문단 모양(ParaShape) 추출 + +HWPX 실제 태그 (header.xml): + + + + + + + + + + + + + + + + + +디폴트값 생성 안 함. +""" + +import re + +from domain.hwpx.hwpx_utils import hwpunit_to_mm + + +def extract(raw_xml: dict, parsed: dict = None) -> list | None: + """§5 paraPr 전체 목록 추출. + + Returns: + [ + { + "id": 0, + "align": "JUSTIFY", + "verticalAlign": "BASELINE", + "heading": {"type": "NONE", "idRef": 0, "level": 0}, + "breakSetting": { + "widowOrphan": False, "keepWithNext": False, + "keepLines": False, "pageBreakBefore": False, + "lineWrap": "BREAK", + "breakLatinWord": "KEEP_WORD", + "breakNonLatinWord": "KEEP_WORD" + }, + "margin": { + "indent_hu": -1310, "left_hu": 0, "right_hu": 0, + "before_hu": 0, "after_hu": 0, + }, + "lineSpacing": {"type": "PERCENT", "value": 130}, + "borderFillIDRef": 2, + "tabPrIDRef": 1, + }, + ... + ] + """ + header_xml = _get_header_xml(raw_xml, parsed) + if not header_xml: + return None + + blocks = re.findall( + r']*)>(.*?)', + header_xml, re.DOTALL + ) + + if not blocks: + return None + + result = [] + for attrs_str, inner in blocks: + item = {} + + # id + id_m = re.search(r'\bid="(\d+)"', attrs_str) + if id_m: + item["id"] = int(id_m.group(1)) + + # tabPrIDRef + tab_m = re.search(r'\btabPrIDRef="(\d+)"', attrs_str) + if tab_m: + item["tabPrIDRef"] = int(tab_m.group(1)) + + # align + al = re.search(r']*\bhorizontal="([^"]+)"', inner) + if al: + item["align"] = al.group(1) + + val = re.search(r']*\bvertical="([^"]+)"', inner) + if val: + item["verticalAlign"] = val.group(1) + + # heading + hd = re.search( + r']*\btype="([^"]+)"[^>]*' + r'\bidRef="(\d+)"[^>]*\blevel="(\d+)"', inner + ) + if hd: + item["heading"] = { + "type": hd.group(1), + "idRef": int(hd.group(2)), + "level": int(hd.group(3)), + } + + # breakSetting + bs = re.search(r'', inner) + if bs: + bstr = bs.group(1) + item["breakSetting"] = { + "widowOrphan": _bool_attr(bstr, "widowOrphan"), + "keepWithNext": _bool_attr(bstr, "keepWithNext"), + "keepLines": _bool_attr(bstr, "keepLines"), + "pageBreakBefore": _bool_attr(bstr, "pageBreakBefore"), + "lineWrap": _str_attr(bstr, "lineWrap"), + "breakLatinWord": _str_attr(bstr, "breakLatinWord"), + "breakNonLatinWord": _str_attr(bstr, "breakNonLatinWord"), + } + + # margin (hp:case 블록 내 첫 번째 사용 — HwpUnitChar case 우선) + case_block = re.search( + r']*required-namespace="[^"]*HwpUnitChar[^"]*"[^>]*>' + r'(.*?)', + inner, re.DOTALL + ) + margin_src = case_block.group(1) if case_block else inner + + margin = {} + for tag, key in [ + ("intent", "indent_hu"), + ("left", "left_hu"), + ("right", "right_hu"), + ("prev", "before_hu"), + ("next", "after_hu"), + ]: + m = re.search( + rf']*\bvalue="(-?\d+)"', margin_src + ) + if m: + margin[key] = int(m.group(1)) + + if margin: + item["margin"] = margin + + # lineSpacing + ls = re.search( + r']*\btype="([^"]+)"[^>]*\bvalue="(\d+)"', + margin_src + ) + if ls: + item["lineSpacing"] = { + "type": ls.group(1), + "value": int(ls.group(2)), + } + + # borderFillIDRef + bf = re.search(r']*\bborderFillIDRef="(\d+)"', inner) + if bf: + item["borderFillIDRef"] = int(bf.group(1)) + + result.append(item) + + return result if result else None + + +def _bool_attr(s: str, name: str) -> bool | None: + m = re.search(rf'\b{name}="(\d+)"', s) + return bool(int(m.group(1))) if m else None + + +def _str_attr(s: str, name: str) -> str | None: + m = re.search(rf'\b{name}="([^"]+)"', s) + return m.group(1) if m else None + + +def _get_header_xml(raw_xml: dict, parsed: dict = None) -> str | None: + if parsed and parsed.get("header_xml"): + return parsed["header_xml"] + if isinstance(raw_xml, dict): + for name, content in raw_xml.items(): + if "header" in name.lower() and isinstance(content, str): + return content + return raw_xml if isinstance(raw_xml, str) else None \ No newline at end of file diff --git a/handlers/tools/section.py b/handlers/tools/section.py new file mode 100644 index 0000000..c93e2b0 --- /dev/null +++ b/handlers/tools/section.py @@ -0,0 +1,120 @@ +# -*- coding: utf-8 -*- +""" +§9 구역 정의(Section) 추출 + +HWPX 실제 태그 (section0.xml): + + + + + + + + + +디폴트값 생성 안 함. +""" + +import re + + +def extract(raw_xml: dict, parsed: dict = None) -> dict | None: + """§9 구역 속성 추출. + + Returns: + { + "textDirection": "HORIZONTAL", + "hideFirstHeader": False, + "hideFirstFooter": False, + "pageNum": {"pos": "BOTTOM_CENTER", "formatType": "DIGIT", + "sideChar": "-"}, + "startNum": {"page": 0}, + "colDef": None, + } + """ + section_xml = _get_section_xml(raw_xml, parsed) + if not section_xml: + return None + + sec_match = re.search( + r']*)>(.*?)', + section_xml, re.DOTALL + ) + if not sec_match: + return None + + attrs_str = sec_match.group(1) + inner = sec_match.group(2) + + result = {} + + # textDirection + td = re.search(r'\btextDirection="([^"]+)"', attrs_str) + if td: + result["textDirection"] = td.group(1) + + # visibility + vis = re.search(r'', inner) + if vis: + v = vis.group(1) + for attr in ["hideFirstHeader", "hideFirstFooter", + "hideFirstMasterPage", "hideFirstPageNum", + "hideFirstEmptyLine"]: + m = re.search(rf'\b{attr}="(\d+)"', v) + if m: + result[attr] = bool(int(m.group(1))) + + # startNum + sn = re.search(r'', inner) + if sn: + sns = sn.group(1) + start = {} + pso = re.search(r'\bpageStartsOn="([^"]+)"', sns) + if pso: + start["pageStartsOn"] = pso.group(1) + pg = re.search(r'\bpage="(\d+)"', sns) + if pg: + start["page"] = int(pg.group(1)) + if start: + result["startNum"] = start + + # pageNum + pn = re.search(r'', inner) + if pn: + pns = pn.group(1) + pagenum = {} + for attr in ["pos", "formatType", "sideChar"]: + m = re.search(rf'\b{attr}="([^"]*)"', pns) + if m: + pagenum[attr] = m.group(1) + if pagenum: + result["pageNum"] = pagenum + + # colDef (단 설정) + cd = re.search(r']*)>(.*?)', inner, re.DOTALL) + if cd: + cds = cd.group(1) + coldef = {} + cnt = re.search(r'\bcount="(\d+)"', cds) + if cnt: + coldef["count"] = int(cnt.group(1)) + layout = re.search(r'\blayout="([^"]+)"', cds) + if layout: + coldef["layout"] = layout.group(1) + if coldef: + result["colDef"] = coldef + + return result if result else None + + +def _get_section_xml(raw_xml: dict, parsed: dict = None) -> str | None: + if parsed and parsed.get("section_xml"): + return parsed["section_xml"] + if isinstance(raw_xml, dict): + for name, content in raw_xml.items(): + if "section" in name.lower() and isinstance(content, str): + return content + return raw_xml if isinstance(raw_xml, str) else None \ No newline at end of file diff --git a/handlers/tools/style_def.py b/handlers/tools/style_def.py new file mode 100644 index 0000000..f055bdd --- /dev/null +++ b/handlers/tools/style_def.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +""" +스타일 정의(Style) 추출 + +HWPX 실제 태그 (header.xml): + + + + + +charPrIDRef → charPr(글자모양), paraPrIDRef → paraPr(문단모양) 연결. +디폴트값 생성 안 함. +""" + +import re + + +def extract(raw_xml: dict, parsed: dict = None) -> list | None: + """스타일 정의 추출. + + Returns: + [ + { + "id": 0, "type": "PARA", + "name": "바탕글", "engName": "Normal", + "paraPrIDRef": 3, "charPrIDRef": 0, + "nextStyleIDRef": 0, + }, + ... + ] + """ + header_xml = _get_header_xml(raw_xml, parsed) + if not header_xml: + return None + + styles = re.findall(r'', header_xml) + if not styles: + return None + + result = [] + for s in styles: + item = {} + for attr in ["id", "paraPrIDRef", "charPrIDRef", "nextStyleIDRef"]: + m = re.search(rf'\b{attr}="(\d+)"', s) + if m: + item[attr] = int(m.group(1)) + + for attr in ["type", "name", "engName"]: + m = re.search(rf'\b{attr}="([^"]*)"', s) + if m: + item[attr] = m.group(1) + + result.append(item) + + return result if result else None + + +def _get_header_xml(raw_xml: dict, parsed: dict = None) -> str | None: + if parsed and parsed.get("header_xml"): + return parsed["header_xml"] + if isinstance(raw_xml, dict): + for name, content in raw_xml.items(): + if "header" in name.lower() and isinstance(content, str): + return content + return raw_xml if isinstance(raw_xml, str) else None \ No newline at end of file diff --git a/handlers/tools/table.py b/handlers/tools/table.py new file mode 100644 index 0000000..d1f160c --- /dev/null +++ b/handlers/tools/table.py @@ -0,0 +1,328 @@ +# -*- coding: utf-8 -*- +""" +§6 표(Table) 구조 추출 + +HWPX 실제 태그 (section0.xml): + + 8504 8504 8504 + 또는 열 수에 맞는 hp:colSz 형태 + + + + + + + + 셀 텍스트 + + + + + +디폴트값 생성 안 함. +""" + +import re + +from domain.hwpx.hwpx_utils import hwpunit_to_mm + + +def extract(raw_xml: dict, parsed: dict = None) -> list | None: + """§6 모든 표 추출. + + Returns: + [ + { + "index": 0, + "rowCnt": 5, "colCnt": 3, + "repeatHeader": True, + "pageBreak": "CELL", + "colWidths_hu": [8504, 8504, 8504], + "colWidths_pct": [33, 34, 33], + "rows": [ + [ # row 0 + { + "colAddr": 0, "rowAddr": 0, + "colSpan": 2, "rowSpan": 1, + "width_hu": 17008, "height_hu": 2400, + "borderFillIDRef": 5, + "cellMargin": {"left": 510, "right": 510, + "top": 142, "bottom": 142}, + "text": "셀 텍스트", + "lines": ["셀 텍스트"], + }, + ... + ], + ... + ], + }, + ... + ] + """ + section_xml = _get_section_xml(raw_xml, parsed) + if not section_xml: + return None + + # tbl 블록 전체 추출 + tbl_blocks = _find_tbl_blocks(section_xml) + if not tbl_blocks: + return None + + result = [] + for idx, (tbl_attrs, tbl_inner) in enumerate(tbl_blocks): + tbl = {"index": idx} + + # 표 속성 + for attr in ["rowCnt", "colCnt"]: + m = re.search(rf'\b{attr}="(\d+)"', tbl_attrs) + if m: + tbl[attr] = int(m.group(1)) + + rh = re.search(r'\brepeatHeader="(\d+)"', tbl_attrs) + if rh: + tbl["repeatHeader"] = bool(int(rh.group(1))) + + pb = re.search(r'\bpageBreak="([^"]+)"', tbl_attrs) + if pb: + tbl["pageBreak"] = pb.group(1) + + # 행/셀 (열 너비보다 먼저 — 첫 행에서 열 너비 추출 가능) + rows = _extract_rows(tbl_inner) + if rows: + tbl["rows"] = rows + + # 열 너비 + col_widths = _extract_col_widths(tbl_inner) + if not col_widths and rows: + # colSz 없으면 행 데이터에서 추출 (colspan 고려) + col_cnt = tbl.get("colCnt", 0) + col_widths = _col_widths_from_rows(rows, col_cnt) + if not col_widths: + col_widths = _col_widths_from_first_row(rows[0]) + if col_widths: + tbl["colWidths_hu"] = col_widths + total = sum(col_widths) or 1 + tbl["colWidths_pct"] = [round(w / total * 100) for w in col_widths] + + result.append(tbl) + + return result if result else None + + +def _find_tbl_blocks(xml: str) -> list: + """중첩 표를 고려하여 최상위 tbl 블록 추출""" + blocks = [] + start = 0 + while True: + # ]*)>', xml[start:]) + if not m: + break + + attrs = m.group(1) + tag_start = start + m.start() + content_start = start + m.end() + + # 중첩 카운트로 닫는 태그 찾기 + depth = 1 + pos = content_start + while depth > 0 and pos < len(xml): + open_m = re.search(r'', xml[pos:]) + + if close_m is None: + break + + if open_m and open_m.start() < close_m.start(): + depth += 1 + pos += open_m.end() + else: + depth -= 1 + if depth == 0: + inner = xml[content_start:pos + close_m.start()] + blocks.append((attrs, inner)) + pos += close_m.end() + + start = pos + + return blocks + + +def _extract_col_widths(tbl_inner: str) -> list | None: + """열 너비 HWPUNIT 추출""" + # 패턴 1: 8504 8504 8504 + wl = re.search(r'([^<]+)', tbl_inner) + if wl: + try: + return [int(w) for w in wl.group(1).strip().split()] + except ValueError: + pass + + # 패턴 2: 개별 colSz 태그 + cols = re.findall(r']*\bwidth="(\d+)"', tbl_inner) + if cols: + return [int(c) for c in cols] + + return None + + +def _extract_rows(tbl_inner: str) -> list: + """tr/tc 파싱하여 2D 셀 배열 반환""" + rows = [] + + tr_blocks = re.findall( + r']*>(.*?)', tbl_inner, re.DOTALL + ) + + for tr_inner in tr_blocks: + cells = [] + tc_blocks = re.finditer( + r']*)>(.*?)', tr_inner, re.DOTALL + ) + + for tc_match in tc_blocks: + tc_attrs = tc_match.group(1) + tc_inner = tc_match.group(2) + cell = _parse_cell(tc_attrs, tc_inner) + cells.append(cell) + + rows.append(cells) + + return rows + + +def _parse_cell(tc_attrs: str, tc_inner: str) -> dict: + """개별 셀 파싱""" + cell = {} + + # borderFillIDRef on tc tag + bf = re.search(r'\bborderFillIDRef="(\d+)"', tc_attrs) + if bf: + cell["borderFillIDRef"] = int(bf.group(1)) + + # header flag + hd = re.search(r'\bheader="(\d+)"', tc_attrs) + if hd: + cell["isHeader"] = bool(int(hd.group(1))) + + # cellAddr + addr = re.search( + r']*\bcolAddr="(\d+)"[^>]*\browAddr="(\d+)"', + tc_inner + ) + if addr: + cell["colAddr"] = int(addr.group(1)) + cell["rowAddr"] = int(addr.group(2)) + + # cellSpan + span = re.search(r'', tc_inner) + if span: + cs = re.search(r'\bcolSpan="(\d+)"', span.group(1)) + rs = re.search(r'\browSpan="(\d+)"', span.group(1)) + if cs: + cell["colSpan"] = int(cs.group(1)) + if rs: + cell["rowSpan"] = int(rs.group(1)) + + # cellSz + sz = re.search(r'', tc_inner) + if sz: + w = re.search(r'\bwidth="(\d+)"', sz.group(1)) + h = re.search(r'\bheight="(\d+)"', sz.group(1)) + if w: + cell["width_hu"] = int(w.group(1)) + if h: + cell["height_hu"] = int(h.group(1)) + + # cellMargin + cm = re.search(r'', tc_inner) + if cm: + margin = {} + for side in ["left", "right", "top", "bottom"]: + m = re.search(rf'\b{side}="(\d+)"', cm.group(1)) + if m: + margin[side] = int(m.group(1)) + if margin: + cell["cellMargin"] = margin + + # 셀 텍스트 + texts = re.findall(r'([^<]*)', tc_inner) + all_text = " ".join(t.strip() for t in texts if t.strip()) + if all_text: + cell["text"] = all_text + + # ★ v2: 셀 내 run의 charPrIDRef 추출 (스타일 연결용) + run_cprs = re.findall(r']*\bcharPrIDRef="(\d+)"', tc_inner) + if run_cprs: + cell["charPrIDRefs"] = [int(c) for c in run_cprs] + cell["primaryCharPrIDRef"] = int(run_cprs[0]) + + # ★ v2: 셀 내 p의 paraPrIDRef, styleIDRef 추출 + para_pprs = re.findall(r']*\bparaPrIDRef="(\d+)"', tc_inner) + if para_pprs: + cell["paraPrIDRefs"] = [int(p) for p in para_pprs] + cell["primaryParaPrIDRef"] = int(para_pprs[0]) + + para_stys = re.findall(r']*\bstyleIDRef="(\d+)"', tc_inner) + if para_stys: + cell["styleIDRefs"] = [int(s) for s in para_stys] + + # 다중행 (p 태그 기준) + paras = re.findall(r']*>(.*?)', tc_inner, re.DOTALL) + lines = [] + for p in paras: + p_texts = re.findall(r'([^<]*)', p) + line = " ".join(t.strip() for t in p_texts if t.strip()) + if line: + lines.append(line) + if lines: + cell["lines"] = lines + + return cell + + +def _col_widths_from_first_row(first_row: list) -> list | None: + """첫 행 셀의 width_hu에서 열 너비 추출 (colSz 없을 때 대체)""" + widths = [] + for cell in first_row: + w = cell.get("width_hu") + if w: + widths.append(w) + return widths if widths else None + + +def _col_widths_from_rows(rows: list, col_cnt: int) -> list | None: + """★ v2: 모든 행을 순회하여 colspan=1인 행에서 정확한 열 너비 추출. + + 첫 행에 colspan이 있으면 열 너비가 부정확하므로, + 모든 열이 colspan=1인 행을 찾아 사용. + """ + if not rows or not col_cnt: + return None + + # colspan=1인 셀만 있는 행 찾기 (모든 열 존재) + for row in rows: + # 이 행의 모든 셀이 colspan=1이고, 셀 수 == col_cnt인지 + all_single = all(cell.get("colSpan", 1) == 1 for cell in row) + if all_single and len(row) == col_cnt: + widths = [] + for cell in sorted(row, key=lambda c: c.get("colAddr", 0)): + w = cell.get("width_hu") + if w: + widths.append(w) + if len(widths) == col_cnt: + return widths + + # 못 찾으면 첫 행 폴백 + return _col_widths_from_first_row(rows[0]) if rows else None + + +def _get_section_xml(raw_xml: dict, parsed: dict = None) -> str | None: + if parsed and parsed.get("section_xml"): + return parsed["section_xml"] + if isinstance(raw_xml, dict): + for name, content in raw_xml.items(): + if "section" in name.lower() and isinstance(content, str): + return content + return raw_xml if isinstance(raw_xml, str) else None \ No newline at end of file diff --git a/templates/default/doc_types/briefing/config.json b/templates/default/doc_types/briefing/config.json new file mode 100644 index 0000000..20369db --- /dev/null +++ b/templates/default/doc_types/briefing/config.json @@ -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" +} \ No newline at end of file diff --git a/templates/default/doc_types/presentation/config.json b/templates/default/doc_types/presentation/config.json new file mode 100644 index 0000000..3b8c5db --- /dev/null +++ b/templates/default/doc_types/presentation/config.json @@ -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" +} \ No newline at end of file diff --git a/templates/default/doc_types/report/config.json b/templates/default/doc_types/report/config.json new file mode 100644 index 0000000..578026e --- /dev/null +++ b/templates/default/doc_types/report/config.json @@ -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" +} \ No newline at end of file diff --git a/templates/hwp_guide.html b/templates/hwp_guide.html deleted file mode 100644 index 3aa587e..0000000 --- a/templates/hwp_guide.html +++ /dev/null @@ -1,343 +0,0 @@ - - - - - - HWP 변환 가이드 - 글벗 Light - - - - - - -
-
-
-
- ← 메인으로 -

HWP 변환 가이드

-
-
-
-
- -
- -
-

⚠️ HWP 변환 요구사항

-
    -
  • • Windows 운영체제
  • -
  • • 한글 프로그램 (한컴오피스) 설치
  • -
  • • Python 3.8 이상
  • -
-
- - -
-

1. 필요 라이브러리 설치

-
pip install pyhwpx beautifulsoup4
-
- - -
-

2. 사용 방법

-
    -
  1. 글벗 Light에서 HTML 파일을 다운로드합니다.
  2. -
  3. 아래 Python 스크립트를 다운로드합니다.
  4. -
  5. 스크립트 내 경로를 수정합니다.
  6. -
  7. 스크립트를 실행합니다.
  8. -
-
- - -
-
-

3. HWP 변환 스크립트

- -
-
# -*- coding: utf-8 -*-
-"""
-글벗 Light - HTML → HWP 변환기
-Windows + 한글 프로그램 필요
-"""
-
-from pyhwpx import Hwp
-from bs4 import BeautifulSoup
-import os
-
-
-class HtmlToHwpConverter:
-    def __init__(self, visible=True):
-        self.hwp = Hwp(visible=visible)
-        self.colors = {}
-    
-    def _init_colors(self):
-        self.colors = {
-            'primary-navy': self.hwp.RGBColor(26, 54, 93),
-            'secondary-navy': self.hwp.RGBColor(44, 82, 130),
-            'dark-gray': self.hwp.RGBColor(45, 55, 72),
-            'medium-gray': self.hwp.RGBColor(74, 85, 104),
-            'bg-light': self.hwp.RGBColor(247, 250, 252),
-            'white': self.hwp.RGBColor(255, 255, 255),
-            'black': self.hwp.RGBColor(0, 0, 0),
-        }
-    
-    def _mm(self, mm):
-        return self.hwp.MiliToHwpUnit(mm)
-    
-    def _font(self, size=10, color='black', bold=False):
-        self.hwp.set_font(
-            FaceName='맑은 고딕',
-            Height=size,
-            Bold=bold,
-            TextColor=self.colors.get(color, self.colors['black'])
-        )
-    
-    def _align(self, align):
-        actions = {'left': 'ParagraphShapeAlignLeft', 'center': 'ParagraphShapeAlignCenter', 'right': 'ParagraphShapeAlignRight'}
-        if align in actions:
-            self.hwp.HAction.Run(actions[align])
-    
-    def _para(self, text='', size=10, color='black', bold=False, align='left'):
-        self._align(align)
-        self._font(size, color, bold)
-        if text:
-            self.hwp.insert_text(text)
-        self.hwp.BreakPara()
-    
-    def _exit_table(self):
-        self.hwp.HAction.Run("Cancel")
-        self.hwp.HAction.Run("CloseEx")
-        self.hwp.HAction.Run("MoveDocEnd")
-        self.hwp.BreakPara()
-    
-    def _set_cell_bg(self, color_name):
-        self.hwp.HAction.GetDefault("CellBorderFill", self.hwp.HParameterSet.HCellBorderFill.HSet)
-        pset = self.hwp.HParameterSet.HCellBorderFill
-        pset.FillAttr.type = self.hwp.BrushType("NullBrush|WinBrush")
-        pset.FillAttr.WinBrushFaceStyle = self.hwp.HatchStyle("None")
-        pset.FillAttr.WinBrushHatchColor = self.hwp.RGBColor(0, 0, 0)
-        pset.FillAttr.WinBrushFaceColor = self.colors.get(color_name, self.colors['white'])
-        pset.FillAttr.WindowsBrush = 1
-        self.hwp.HAction.Execute("CellBorderFill", pset.HSet)
-    
-    def _create_header(self, left_text, right_text):
-        try:
-            self.hwp.HAction.GetDefault("HeaderFooter", self.hwp.HParameterSet.HHeaderFooter.HSet)
-            self.hwp.HParameterSet.HHeaderFooter.HSet.SetItem("HeaderFooterStyle", 0)
-            self.hwp.HParameterSet.HHeaderFooter.HSet.SetItem("HeaderFooterCtrlType", 0)
-            self.hwp.HAction.Execute("HeaderFooter", self.hwp.HParameterSet.HHeaderFooter.HSet)
-            self._font(9, 'medium-gray')
-            self.hwp.insert_text(left_text)
-            self.hwp.insert_text("\t" * 12)
-            self.hwp.insert_text(right_text)
-            self.hwp.HAction.Run("CloseEx")
-        except Exception as e:
-            print(f"머리말 생성 실패: {e}")
-    
-    def _create_footer(self, text):
-        try:
-            self.hwp.HAction.GetDefault("HeaderFooter", self.hwp.HParameterSet.HHeaderFooter.HSet)
-            self.hwp.HParameterSet.HHeaderFooter.HSet.SetItem("HeaderFooterStyle", 0)
-            self.hwp.HParameterSet.HHeaderFooter.HSet.SetItem("HeaderFooterCtrlType", 1)
-            self.hwp.HAction.Execute("HeaderFooter", self.hwp.HParameterSet.HHeaderFooter.HSet)
-            self._align('center')
-            self._font(8.5, 'medium-gray')
-            self.hwp.insert_text(text)
-            self.hwp.HAction.Run("CloseEx")
-        except Exception as e:
-            print(f"꼬리말 생성 실패: {e}")
-    
-    def _convert_lead_box(self, elem):
-        content = elem.find("div")
-        if not content:
-            return
-        text = ' '.join(content.get_text().split())
-        self.hwp.create_table(1, 1, treat_as_char=True)
-        self._set_cell_bg('bg-light')
-        self._font(11.5, 'dark-gray', False)
-        self.hwp.insert_text(text)
-        self._exit_table()
-    
-    def _convert_bottom_box(self, elem):
-        left = elem.find(class_="bottom-left")
-        right = elem.find(class_="bottom-right")
-        if not left or not right:
-            return
-        left_text = ' '.join(left.get_text().split())
-        right_text = right.get_text(strip=True)
-        
-        self.hwp.create_table(1, 2, treat_as_char=True)
-        self._set_cell_bg('primary-navy')
-        self._font(10.5, 'white', True)
-        self._align('center')
-        self.hwp.insert_text(left_text)
-        self.hwp.HAction.Run("MoveRight")
-        self._set_cell_bg('bg-light')
-        self._font(10.5, 'primary-navy', True)
-        self._align('center')
-        self.hwp.insert_text(right_text)
-        self._exit_table()
-    
-    def _convert_section(self, section):
-        title = section.find(class_="section-title")
-        if title:
-            self._para("■ " + title.get_text(strip=True), 12, 'primary-navy', True)
-        
-        ul = section.find("ul")
-        if ul:
-            for li in ul.find_all("li", recursive=False):
-                keyword = li.find(class_="keyword")
-                if keyword:
-                    kw_text = keyword.get_text(strip=True)
-                    full = li.get_text(strip=True)
-                    rest = full.replace(kw_text, '', 1).strip()
-                    self._font(10.5, 'primary-navy', True)
-                    self.hwp.insert_text("  • " + kw_text + " ")
-                    self._font(10.5, 'dark-gray', False)
-                    self.hwp.insert_text(rest)
-                    self.hwp.BreakPara()
-                else:
-                    self._para("  • " + li.get_text(strip=True), 10.5, 'dark-gray')
-        self._para()
-    
-    def _convert_sheet(self, sheet, is_first_page=False):
-        if is_first_page:
-            header = sheet.find(class_="page-header")
-            if header:
-                left = header.find(class_="header-left")
-                right = header.find(class_="header-right")
-                left_text = left.get_text(strip=True) if left else ""
-                right_text = right.get_text(strip=True) if right else ""
-                if left_text or right_text:
-                    self._create_header(left_text, right_text)
-            
-            footer = sheet.find(class_="page-footer")
-            if footer:
-                self._create_footer(footer.get_text(strip=True))
-        
-        title = sheet.find(class_="header-title")
-        if title:
-            title_text = title.get_text(strip=True)
-            if '[첨부]' in title_text:
-                self._para(title_text, 15, 'primary-navy', True, 'left')
-            else:
-                self._para(title_text, 23, 'primary-navy', True, 'center')
-            self._font(10, 'secondary-navy')
-            self._align('center')
-            self.hwp.insert_text("━" * 45)
-            self.hwp.BreakPara()
-        
-        self._para()
-        
-        lead_box = sheet.find(class_="lead-box")
-        if lead_box:
-            self._convert_lead_box(lead_box)
-            self._para()
-        
-        for section in sheet.find_all(class_="section"):
-            self._convert_section(section)
-        
-        bottom_box = sheet.find(class_="bottom-box")
-        if bottom_box:
-            self._para()
-            self._convert_bottom_box(bottom_box)
-    
-    def convert(self, html_path, output_path):
-        print(f"[입력] {html_path}")
-        
-        with open(html_path, 'r', encoding='utf-8') as f:
-            soup = BeautifulSoup(f.read(), 'html.parser')
-        
-        self.hwp.FileNew()
-        self._init_colors()
-        
-        # 페이지 설정
-        try:
-            self.hwp.HAction.GetDefault("PageSetup", self.hwp.HParameterSet.HSecDef.HSet)
-            sec = self.hwp.HParameterSet.HSecDef
-            sec.PageDef.LeftMargin = self._mm(20)
-            sec.PageDef.RightMargin = self._mm(20)
-            sec.PageDef.TopMargin = self._mm(20)
-            sec.PageDef.BottomMargin = self._mm(20)
-            sec.PageDef.HeaderLen = self._mm(10)
-            sec.PageDef.FooterLen = self._mm(10)
-            self.hwp.HAction.Execute("PageSetup", sec.HSet)
-        except Exception as e:
-            print(f"페이지 설정 실패: {e}")
-        
-        sheets = soup.find_all(class_="sheet")
-        total = len(sheets)
-        print(f"[변환] 총 {total} 페이지")
-        
-        for i, sheet in enumerate(sheets, 1):
-            print(f"[{i}/{total}] 페이지 처리 중...")
-            self._convert_sheet(sheet, is_first_page=(i == 1))
-            if i < total:
-                self.hwp.HAction.Run("BreakPage")
-        
-        self.hwp.SaveAs(output_path)
-        print(f"✅ 저장 완료: {output_path}")
-    
-    def close(self):
-        try:
-            self.hwp.Quit()
-        except:
-            pass
-
-
-def main():
-    # ====================================
-    # 경로 설정 (본인 환경에 맞게 수정)
-    # ====================================
-    html_path = r"C:\Users\User\Downloads\report.html"
-    output_path = r"C:\Users\User\Downloads\report.hwp"
-    
-    print("=" * 50)
-    print("글벗 Light - HTML → HWP 변환기")
-    print("=" * 50)
-    
-    try:
-        converter = HtmlToHwpConverter(visible=True)
-        converter.convert(html_path, output_path)
-        print("\n✅ 변환 완료!")
-        input("Enter를 누르면 HWP가 닫힙니다...")
-        converter.close()
-    except FileNotFoundError:
-        print(f"\n[에러] 파일을 찾을 수 없습니다: {html_path}")
-    except Exception as e:
-        print(f"\n[에러] {e}")
-        import traceback
-        traceback.print_exc()
-
-
-if __name__ == "__main__":
-    main()
-
- - -
-

4. 경로 수정

-

스크립트 하단의 main() 함수에서 경로를 수정하세요:

-
html_path = r"C:\다운로드경로\report.html"
-output_path = r"C:\저장경로\report.hwp"
-
-
- - - - diff --git a/templates/hwp_guide.md b/templates/hwp_guide.md new file mode 100644 index 0000000..da7aafa --- /dev/null +++ b/templates/hwp_guide.md @@ -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 + +
+ + + + +
+ +
+ + + +
+``` + +## 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; +} +``` +- 머릿말이 **테이블 형태**인 경우: `` 사용, 테두리 없음 +- HWPX에서 추출한 열 수와 셀 내용을 placeholder로 배치 +- 다중행 셀은 `
`로 줄바꿈 + +### 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; +} +``` +- 꼬릿말이 **테이블 형태**인 경우: `
` 사용, 테두리 없음 +- 2열 이상일 때 `display: flex; justify-content: space-between` 패턴도 가능 +- 페이지 번호는 별도 `` 으로 + +### 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 계열) + ``` \ No newline at end of file diff --git a/templates/hwp_html_defaults.json b/templates/hwp_html_defaults.json new file mode 100644 index 0000000..34b5243 --- /dev/null +++ b/templates/hwp_html_defaults.json @@ -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" + } +} \ No newline at end of file diff --git a/templates/index.html b/templates/index.html index 150d13a..d805e73 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1029,7 +1029,7 @@ display: flex; flex-direction: column; gap: 4px; - margin-bottom: 20px; + margin-bottom: 10px; } .doc-type-item { @@ -1078,6 +1078,31 @@ color: #000; font-weight: 600; } + + .doc-type-item .delete-btn { + width: 20px; + height: 20px; + border: none; + background: transparent; + color: var(--ui-dim); + font-size: 14px; + cursor: pointer; + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + transition: all 0.15s; + } + + .doc-type-item:hover .delete-btn { + opacity: 1; + } + + .doc-type-item .delete-btn:hover { + background: var(--ui-error); + color: white; + } /* 플로팅 프리뷰 팝업 - fixed로 변경 */ .doc-type-preview { @@ -1236,6 +1261,37 @@ background: rgba(255,255,255,0.3); margin-bottom: 2px; } + + /* custom 프리뷰 */ + .preview-thumbnail.custom { + flex-direction: column; + justify-content: flex-start; + padding: 15px; + } + .preview-thumbnail.custom .line { + width: 100%; + height: 3px; + background: #ddd; + margin-bottom: 4px; + border-radius: 1px; + } + .preview-thumbnail.custom .line.h1 { + background: #1a365d; + height: 5px; + width: 60%; + margin-bottom: 8px; + } + .preview-thumbnail.custom .line.h2 { + background: #2c5282; + height: 4px; + width: 45%; + margin-top: 6px; + margin-bottom: 6px; + } + .preview-thumbnail.custom .line.body { + background: #cbd5e0; + width: 90%; + } /* 프리뷰 정보 */ .preview-title { @@ -1328,7 +1384,8 @@ background: rgba(0, 200, 83, 0.1); } - .option-item input[type="radio"] { + .option-item input[type="radio"], + .option-item input[type="checkbox"] { accent-color: var(--ui-accent); } @@ -1337,6 +1394,42 @@ cursor: pointer; flex: 1; } + + /* 페이지 수 입력 필드 */ + .page-input { + width: 45px; + padding: 4px 6px; + border: 1px solid var(--ui-border); + border-radius: 4px; + background: var(--ui-bg); + color: var(--ui-text); + font-size: 12px; + text-align: center; + margin-left: 4px; + } + + .page-input:focus { + outline: none; + border-color: var(--ui-accent); + } + + .page-input:disabled { + opacity: 0.5; + cursor: not-allowed; + } + + .page-input-suffix { + font-size: 12px; + color: var(--ui-dim); + margin-left: 2px; + } + + /* 스피너 숨기기 (Chrome) */ + .page-input::-webkit-inner-spin-button, + .page-input::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; + } /* 요청사항 */ .request-textarea { @@ -1527,7 +1620,7 @@ display: none; position: fixed; top: 80px; - right: 300px; /* 우측 패널 옆 */ + right: 300px; width: 320px; background: var(--ui-panel); border: 1px solid var(--ui-accent); @@ -1601,10 +1694,145 @@ cursor: pointer; font-size: 16px; } - - - + /* 문서 유형별 옵션 컨테이너 */ + #docTypeOptionsContainer { + margin-bottom: 20px; + } + + /* 사용자 문서 유형 구분선 */ + .doc-type-divider { + height: 1px; + background: var(--ui-border); + margin: 10px 0; + position: relative; + } + + .doc-type-divider::after { + content: '사용자 추가'; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: var(--ui-nav); + padding: 0 8px; + font-size: 9px; + color: var(--ui-dim); + text-transform: uppercase; + letter-spacing: 0.5px; + } + + /* 로딩 상태 */ + .doc-type-list.loading { + min-height: 100px; + display: flex; + align-items: center; + justify-content: center; + } + + .doc-type-list.loading::after { + content: '로딩 중...'; + color: var(--ui-dim); + font-size: 12px; + } + + .analysis-progress { + padding: 20px 0; + } + + .analysis-step { + display: flex; + align-items: center; + padding: 8px 12px; + margin-bottom: 4px; + border-radius: 6px; + background: var(--ui-bg); + } + + .analysis-step.running { + background: var(--accent-light); + } + + .analysis-step.done { + opacity: 0.7; + } + + .analysis-step .step-icon { + width: 24px; + text-align: center; + } + + .analysis-step .step-name { + flex: 1; + margin-left: 8px; + } + + .analysis-step .step-status { + font-size: 12px; + color: var(--ui-text-muted); + } + + .progress-bar-container { + height: 8px; + background: var(--ui-border); + border-radius: 4px; + margin-top: 16px; + overflow: hidden; + } + + .progress-bar { + height: 100%; + background: var(--accent-primary); + transition: width 0.3s ease; + } + + /* 분석 결과 UI */ + .analysis-result h4 { + margin-bottom: 16px; + } + + .result-summary { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 12px; + margin-bottom: 20px; + } + + .summary-item { + padding: 12px; + background: var(--ui-bg); + border-radius: 6px; + text-align: center; + } + + .toc-list { + list-style: none; + padding: 0; + } + + .toc-list li { + padding: 6px 12px; + border-left: 2px solid var(--ui-border); + margin-bottom: 2px; + } + + .toc-level-2 { padding-left: 28px; } + .toc-level-3 { padding-left: 44px; } + .step-icon.spinning { + display: inline-block; + animation: spin 1s linear infinite; + } + + .analysis-step .step-status { + font-size: 10px; + color: var(--ui-dim); + margin-left: auto; + max-width: 150px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + @@ -1651,7 +1879,6 @@ 📋 HTML 붙여넣기 - 반복)", + "example": "", + "location": "table_1" + } + }, + "table_guide": { + "1": { + "col_headers": [ + "구분", + "내용", + "비고" + ], + "col_count": 3, + "row_count": 5, + "merge_pattern": { + "col_0": "col_span", + "col_3": "row_group" + }, + "bullet_chars": [ + "- ", + "· " + ], + "example_rows": [ + [ + "소개", + "개요", + "- 현황 및 문제점 : 인적 오류와 추가적 리소스(인력, 시간) 투입 · 동일한 원천데이터로 산출물 형식만 달라짐 (제안서, 보고서 등) ...", + "1p" + ], + [ + "글벗 소개", + "- 글벗 기능 소개 · (Input) 로컬, 링크, HTML 구조 · (Process) 목차 구성 및 문서 작성 / (Edit) 편집기 ·..." + ], + [ + "시연", + "글벗 시연", + "- (기능 1) (Input) 업로드한 문서 기반 목차 정리 / 작성 - (기능 2) (Process) 웹 편집기 - (기능 3) (Exp...", + "글벗 & Visual Studio" + ] + ], + "col_types": [ + { + "col": 0, + "type": "category", + "header": "구분" + }, + { + "col": 1, + "type": "content", + "header": "내용" + }, + { + "col": 2, + "type": "note", + "header": "비고" + } + ], + "row_bf_pattern": [ + { + "col": 0, + "bf_class": "bf-12", + "colSpan": 1, + "rowSpan": 2 + }, + { + "col": 1, + "bf_class": "bf-8", + "colSpan": 1, + "rowSpan": 1 + }, + { + "col": 2, + "bf_class": "bf-7", + "colSpan": 1, + "rowSpan": 1 + }, + { + "col": 3, + "bf_class": "bf-19", + "colSpan": 1, + "rowSpan": 2 + } + ] + } + }, + "writing_guide": { + "bullet_styles": [ + "- ", + "· " + ], + "numbering_patterns": [ + [ + "^1.", + "^2.", + "^3)" + ] + ], + "avg_line_length": 16, + "font_primary": "돋움", + "font_size_body": "10.0pt" + } +} \ No newline at end of file diff --git a/templates/user/doc_types/user_1770301063/config.json b/templates/user/doc_types/user_1770301063/config.json new file mode 100644 index 0000000..e765733 --- /dev/null +++ b/templates/user/doc_types/user_1770301063/config.json @@ -0,0 +1,184 @@ +{ + "id": "user_1770301063", + "name": "55", + "icon": "📄", + "description": "55", + "features": [ + { + "icon": "📋", + "text": "평가보고서" + }, + { + "icon": "🎯", + "text": "완성된 제품/솔루션의 현황을..." + }, + { + "icon": "👥", + "text": "개발팀, 관리자, 의사결정권자" + }, + { + "icon": "📄", + "text": "약 2p" + } + ], + "thumbnailType": "custom", + "enabled": true, + "isDefault": false, + "order": 100, + "template_id": "tpl_1770301063", + "context": { + "documentDefinition": "개발된 솔루션을 검토하고 개선방향을 제시하기 위한 평가보고서", + "documentType": "평가보고서", + "purpose": "완성된 제품/솔루션의 현황을 정리하고, 장단점을 분석하여 향후 개선방향을 제시", + "perspective": "객관적 평가와 건설적 개선안 도출 관점으로 재구성", + "audience": "개발팀, 관리자, 의사결정권자", + "tone": "분석형/제안형" + }, + "layout": { + "hasHeader": false, + "headerLayout": { + "structure": "없음" + }, + "hasFooter": false, + "footerLayout": { + "structure": "없음" + }, + "titleBlock": { + "type": "없음" + }, + "sections": [ + { + "name": "1. (스마트설계팀) SamanPro(V3.0)", + "hasBulletIcon": false, + "hasTable": false, + "tableIndex": null + }, + { + "name": "내용 요약", + "hasBulletIcon": true, + "hasTable": false, + "tableIndex": null + }, + { + "name": "주요 기능", + "hasBulletIcon": true, + "hasTable": false, + "tableIndex": null + }, + { + "name": "관련 의견", + "hasBulletIcon": true, + "hasTable": false, + "tableIndex": null + }, + { + "name": "장점", + "hasBulletIcon": true, + "hasTable": false, + "tableIndex": null + }, + { + "name": "확인 필요 지점", + "hasBulletIcon": true, + "hasTable": false, + "tableIndex": null + }, + { + "name": "개선 방향 제안", + "hasBulletIcon": true, + "hasTable": false, + "tableIndex": null + } + ], + "overallStyle": { + "writingStyle": "혼합", + "bulletType": "·", + "tableUsage": "보통" + } + }, + "structure": { + "sectionGuides": [ + { + "name": "1. (스마트설계팀) SamanPro(V3.0)", + "role": "평가 대상 솔루션의 제목과 개발팀 정보를 명시하는 헤더 섹션", + "writingStyle": "제목식", + "contentGuide": "순번, 개발팀명(괄호), 솔루션명, 버전 정보를 포함한 간결한 제목 형식", + "hasTable": false + }, + { + "name": "내용 요약", + "role": "솔루션의 핵심 목적과 AI 활용 방식을 간략히 개괄하는 섹션", + "writingStyle": "서술식", + "contentGuide": "솔루션의 주요 목적, AI 기술 활용 방법, 전체적인 접근 방식을 2-3문장으로 요약", + "hasTable": false + }, + { + "name": "주요 기능", + "role": "솔루션에서 제공하는 구체적인 기능들을 카테고리별로 나열하는 섹션", + "writingStyle": "개조식", + "contentGuide": "기능 카테고리별로 구분하여 나열하며, 각 기능의 세부 사항과 활용 방법을 불릿 포인트로 정리. 부가 설명은 각주나 괄호로 표기", + "hasTable": false + }, + { + "name": "관련 의견", + "role": "솔루션에 대한 전반적인 평가와 특징을 제시하는 섹션", + "writingStyle": "개조식", + "hasTable": true, + "contentGuide": "솔루션의 핵심 특징과 접근 방식을 평가자 관점에서 서술. 표를 통해 구체적 내용을 보완", + "tableStructure": { + "columns": 3, + "columnDefs": [ + { + "name": "좌측 여백", + "role": "여백 또는 표시자", + "style": "빈 칸" + }, + { + "name": "주요 내용", + "role": "평가 의견의 핵심 내용", + "style": "서술식 문장" + }, + { + "name": "우측 여백", + "role": "여백 또는 추가 정보", + "style": "빈 칸 또는 보조 정보" + } + ], + "rowGuide": "평가자의 관점에서 본 솔루션의 특징과 의의를 중앙 열에 기술" + } + }, + { + "name": "장점", + "role": "솔루션의 긍정적인 측면들을 구체적으로 나열하는 섹션", + "writingStyle": "개조식", + "contentGuide": "솔루션의 우수한 점들을 불릿 포인트로 나열하며, 정량적 성과나 구체적 사례를 포함하여 설득력 있게 작성", + "hasTable": false + }, + { + "name": "확인 필요 지점", + "role": "솔루션 운영 시 검토가 필요한 사항들을 질문 형태로 제시하는 섹션", + "writingStyle": "개조식", + "contentGuide": "운영상 고려해야 할 이슈들을 질문 형태로 제기하고, 화살표(→)를 사용하여 해결 방향이나 고려사항을 제안", + "hasTable": false + }, + { + "name": "개선 방향 제안", + "role": "솔루션의 향후 발전 방향과 개선 사항들을 구체적으로 제안하는 섹션", + "writingStyle": "개조식", + "contentGuide": "개선 영역별로 구분하여 제안사항을 나열하며, 번호나 괄호를 사용하여 세부 항목을 정리. 예시나 구체적 방안을 포함하여 실행 가능한 제안으로 작성", + "hasTable": false + } + ], + "writingPrinciples": [ + "기술적 세부사항과 업무 프로세스를 정확히 이해하고 전문적으로 평가", + "장점과 개선점을 균형있게 제시하여 객관적 평가 유지", + "구체적 사례와 정량적 데이터를 활용하여 설득력 있는 평가 작성", + "실무진이 실제 적용할 수 있는 구체적이고 실행 가능한 개선 방안 제시", + "기술 발전과 업무 효율성 측면에서 솔루션의 가치를 다각도로 분석" + ], + "pageEstimate": 2 + }, + "options": {}, + "createdAt": "2026-02-05T23:17:43Z", + "updatedAt": "2026-02-05T23:17:43Z" +} \ No newline at end of file diff --git a/templates/user/doc_types/user_1770301063/content_prompt.json b/templates/user/doc_types/user_1770301063/content_prompt.json new file mode 100644 index 0000000..0c83dba --- /dev/null +++ b/templates/user/doc_types/user_1770301063/content_prompt.json @@ -0,0 +1,295 @@ +{ + "version": "1.0", + "document": { + "paper": "A4", + "layout": "landscape", + "margins": { + "top": "10.0mm", + "bottom": "10.0mm", + "left": "20.0mm", + "right": "20.0mm", + "header": "15.0mm", + "footer": "15.0mm", + "gutter": "0.0mm" + }, + "purpose_hint": "", + "audience_hint": "", + "tone_hint": "" + }, + "placeholders": { + "SECTION_1_TITLE": { + "type": "section_title", + "pattern": "섹션 제목", + "example": "1. (스마트설계팀) SamanPro(V3.0)", + "location": "body" + }, + "IMAGE_1": { + "type": "image", + "pattern": "이미지", + "example_ref": "image9", + "location": "body" + }, + "IMAGE_1_CAPTION": { + "type": "image_caption", + "pattern": "이미지 캡션", + "example": " 내용 요약", + "location": "body" + }, + "IMAGE_2": { + "type": "image", + "pattern": "이미지", + "example_ref": "image10", + "location": "body" + }, + "IMAGE_2_CAPTION": { + "type": "image_caption", + "pattern": "이미지 캡션", + "example": " 반복적 도로 설계 계산 작업과 행정의 자동화를 위한 통합 설계 플랫폼 구축", + "location": "body" + }, + "PARA_1": { + "type": "text", + "pattern": "자유 텍스트", + "example": "AI는 앱 개발 과정에서 코드 생성과 에러 해결을 위한 방식으로 활용", + "location": "body" + }, + "IMAGE_3": { + "type": "image", + "pattern": "이미지", + "example_ref": "image10", + "location": "body" + }, + "IMAGE_3_CAPTION": { + "type": "image_caption", + "pattern": "이미지 캡션", + "example": " 주요 기능", + "location": "body" + }, + "PARA_2": { + "type": "text", + "pattern": "자유 텍스트", + "example": "계산, 외부 데이터 확인(API) 및 제안서 작성 시 활용할 수 있는 프롬프트 등", + "location": "body" + }, + "PARA_3": { + "type": "text", + "pattern": "자유 텍스트", + "example": " · 포장설계, 동결심도, 수리계산, 확폭계산, 편경사계산 등 설계 관련 계산 기능과 착수 일자와 과업기간 입력 시, 중공일자 표출되는 준공계 보조 계산 기능 등", + "location": "body" + }, + "PARA_4": { + "type": "text", + "pattern": "자유 텍스트", + "example": " · 한국은행 API를 활용하여 (연도별/분기별) 건설투자 GDP 제공", + "location": "body" + }, + "PARA_5": { + "type": "text", + "pattern": "자유 텍스트", + "example": " · 제안서 작성 시 AI에게 입력할 발주처(도로공사, 국토관리청, 지자체)별 기초 프롬프트*", + "location": "body" + }, + "PARA_6": { + "type": "text", + "pattern": "자유 텍스트", + "example": " ※ 프롬프트 구성 : 역학과 목표, 입력 정의, 산출물 요구사항, 작업절차 단계, 출력 형식 등", + "location": "body" + }, + "PARA_7_RUN_1": { + "type": "text", + "pattern": "자유 텍스트", + "example": "야근일자 계산기, 모니터 끄기, 자동종료 및 재시작, 계산기, pc 클리너 기능", + "location": "body", + "run_index": 1 + }, + "PARA_7_RUN_2": { + "type": "text", + "pattern": "자유 텍스트", + "example": "*", + "location": "body", + "run_index": 2 + }, + "PARA_8": { + "type": "text", + "pattern": "자유 텍스트", + "example": " ※ GUI 기반의 앱으로 시간에 맞추어 자동종료, 재시작, PC 클린 등 수행", + "location": "body" + }, + "IMAGE_4": { + "type": "image", + "pattern": "이미지", + "example_ref": "image9", + "location": "body" + }, + "IMAGE_4_CAPTION": { + "type": "image_caption", + "pattern": "이미지 캡션", + "example": " 관련 의견", + "location": "body" + }, + "PARA_9": { + "type": "text", + "pattern": "자유 텍스트", + "example": "※ 도로배수시설 설계 및 유지관리지침, 설계유량(합리식(Rational formula), 흐름해석(Manning 공식) 등 반영", + "location": "body" + }, + "IMAGE_5": { + "type": "image", + "pattern": "이미지", + "example_ref": "image10", + "location": "body" + }, + "IMAGE_5_CAPTION": { + "type": "image_caption", + "pattern": "이미지 캡션", + "example": " 장점", + "location": "body" + }, + "PARA_10": { + "type": "text", + "pattern": "자유 텍스트", + "example": "비개발자가 AI를 통해 개발 및 사내에 공유 (경영진 92회 포함 총 712회 사용 등)", + "location": "body" + }, + "PARA_11": { + "type": "text", + "pattern": "자유 텍스트", + "example": "(제안서 프롬프트) 질문-수집-생성 파이프라인까지 체계적으로 구축", + "location": "body" + }, + "IMAGE_6": { + "type": "image", + "pattern": "이미지", + "example_ref": "image10", + "location": "body" + }, + "IMAGE_6_CAPTION": { + "type": "image_caption", + "pattern": "이미지 캡션", + "example": " 확인 필요 지점 ", + "location": "body" + }, + "PARA_12": { + "type": "text", + "pattern": "자유 텍스트", + "example": "지침 개정 시, 계산 로직 또는 기준값 등을 사람이 확인, 반영하는 것?", + "location": "body" + }, + "PARA_13": { + "type": "text", + "pattern": "자유 텍스트", + "example": " → 개정 반영 표준화 또는 파이프라인 등을 통하여 운영 체계를 구축하는 것에 대한 고려", + "location": "body" + }, + "IMAGE_7": { + "type": "image", + "pattern": "이미지", + "example_ref": "image10", + "location": "body" + }, + "IMAGE_7_CAPTION": { + "type": "image_caption", + "pattern": "이미지 캡션", + "example": " 개선 방향 제안", + "location": "body" + }, + "PARA_14": { + "type": "text", + "pattern": "자유 텍스트", + "example": "(제안서 프롬프트) ① 상용 AI 모델의 업데이트 상황에 따른 품질 변동, ② 특정 모델에 최적화, ③ 단일 프롬프트에 모든 단계를 포함하여 중간 결과물의 유실될 가능성(단계를 ", + "location": "body" + }, + "PARA_15": { + "type": "slogan", + "pattern": "회사 슬로건/비전", + "example": "(수리 계산 기준 표출) 기준과 버전 사항들도 함께 계산기 내에서 표출될 필요", + "location": "body" + }, + "PARA_16": { + "type": "text", + "pattern": "자유 텍스트", + "example": " (예) 수리계산(Box/Pipe) : 도로배수시설 설계 및 유지관리지침(2025) 반영 ", + "location": "body" + }, + "PARA_17": { + "type": "text", + "pattern": "자유 텍스트", + "example": "(계산 결과 출력) 기준, 입력 변수, 산식, 출력 결과값 등이 바로 활용될 수 있도록 한글(HWP), 엑셀이나 특정 템플릿 등으로 출력을 고려", + "location": "body" + }, + "PARA_18": { + "type": "text", + "pattern": "자유 텍스트", + "example": "(향후 로드맵) AI 기반 설계 검토와 BIM 연동 등은 지금 기술 대비 난이도가 크게 상승(AI API 적용, 파이프라인 구축 등), 단계별 검증과 구체적 마일스톤 수립이 필요", + "location": "body" + }, + "TABLE_1_BODY": { + "type": "table_body", + "pattern": "표 데이터 행들 (HTML 반복)", + "example": "", + "location": "table_1" + } + }, + "table_guide": { + "1": { + "col_headers": [], + "col_count": 3, + "row_count": 3, + "merge_pattern": {}, + "bullet_chars": [], + "example_rows": [ + [ + "", + "", + "" + ], + [ + "", + "지침 기반의 정형 계산 * 과 행정 보조를 GUI 앱으로 통합해 반복 업무를 자동화한 실무 도구", + "" + ], + [ + "", + "", + "" + ] + ], + "col_types": [], + "row_bf_pattern": [ + { + "col": 0, + "bf_class": "bf-4", + "colSpan": 1, + "rowSpan": 1 + }, + { + "col": 1, + "bf_class": "bf-5", + "colSpan": 1, + "rowSpan": 1 + }, + { + "col": 2, + "bf_class": "bf-6", + "colSpan": 1, + "rowSpan": 1 + } + ] + } + }, + "writing_guide": { + "bullet_styles": [ + "- " + ], + "numbering_patterns": [ + [ + "^1.", + "^2.", + "^3)" + ] + ], + "avg_line_length": 57, + "font_primary": "돋움", + "font_size_body": "10.0pt" + } +} \ No newline at end of file diff --git a/templates/user/templates/tpl_1770300969/meta.json b/templates/user/templates/tpl_1770300969/meta.json new file mode 100644 index 0000000..7a480b9 --- /dev/null +++ b/templates/user/templates/tpl_1770300969/meta.json @@ -0,0 +1,15 @@ +{ + "id": "tpl_1770300969", + "name": "3 양식", + "original_file": "sample.hwpx", + "file_type": ".hwpx", + "description": "3에서 추출한 문서 양식", + "features": [ + "폰트: 돋움", + "머릿말: 3열", + "꼬릿말: 3열", + "표: 5x4" + ], + "created_at": "2026-02-05T23:16:09Z", + "source": "doc_template_analyzer" +} \ No newline at end of file diff --git a/templates/user/templates/tpl_1770300969/semantic_map.json b/templates/user/templates/tpl_1770300969/semantic_map.json new file mode 100644 index 0000000..ec0ff4d --- /dev/null +++ b/templates/user/templates/tpl_1770300969/semantic_map.json @@ -0,0 +1,222 @@ +{ + "version": "1.0", + "table_roles": { + "0": { + "role": "footer_table", + "match_source": "footer", + "matched_texts": [ + "기술 로 사람 과 자연 이 함께하는 세상을 만들어 갑니다." + ] + }, + "1": { + "role": "header_table", + "match_source": "header", + "matched_texts": [ + "2025. 2. 5(목)", + "총괄기획실 기술기획팀" + ] + }, + "2": { + "role": "title_block", + "title_text": "AI 업무 활용 적용 사례 발표 계획(안)" + }, + "3": { + "role": "data_table", + "header_row": 0, + "col_headers": [ + "구분", + "내용", + "비고" + ], + "row_count": 5, + "col_count": 4 + } + }, + "body_tables": [ + 3 + ], + "title_table": 2, + "sections": [], + "style_mappings": { + "char_pr": {}, + "border_fill": { + "1": { + "css_class": "bf-1", + "bg": "", + "borders": {} + }, + "2": { + "css_class": "bf-2", + "bg": "", + "borders": {} + }, + "3": { + "css_class": "bf-3", + "bg": "", + "borders": { + "border-left": "0.12mm solid #000000", + "border-right": "0.12mm solid #000000", + "border-top": "0.12mm solid #000000", + "border-bottom": "0.12mm solid #000000" + } + }, + "4": { + "css_class": "bf-4", + "bg": "", + "borders": { + "border-bottom": "0.7mm solid #3057B9" + } + }, + "5": { + "css_class": "bf-5", + "bg": "", + "borders": {} + }, + "6": { + "css_class": "bf-6", + "bg": "", + "borders": { + "border-left": "0.12mm solid #999999", + "border-right": "0.12mm solid #999999", + "border-top": "0.12mm solid #BBBBBB", + "border-bottom": "0.3mm solid #BBBBBB" + } + }, + "7": { + "css_class": "bf-7", + "bg": "", + "borders": { + "border-left": "0.12mm solid #999999", + "border-right": "0.12mm solid #999999", + "border-top": "0.12mm solid #BBBBBB", + "border-bottom": "0.12mm solid #BBBBBB" + } + }, + "8": { + "css_class": "bf-8", + "bg": "#EDEDED", + "borders": { + "border-left": "0.12mm solid #999999", + "border-right": "0.12mm solid #999999", + "border-top": "0.12mm solid #BBBBBB", + "border-bottom": "0.12mm solid #BBBBBB" + } + }, + "9": { + "css_class": "bf-9", + "bg": "#EDEDED", + "borders": { + "border-left": "0.12mm solid #999999", + "border-right": "0.12mm solid #999999", + "border-top": "0.12mm solid #BBBBBB", + "border-bottom": "0.3mm solid #BBBBBB" + } + }, + "10": { + "css_class": "bf-10", + "bg": "#DCDCDC", + "borders": { + "border-right": "0.12mm solid #999999", + "border-top": "0.3mm solid #BBBBBB", + "border-bottom": "0.12mm solid #BBBBBB" + } + }, + "11": { + "css_class": "bf-11", + "bg": "#EDEDED", + "borders": { + "border-right": "0.12mm solid #999999", + "border-top": "0.5mm solid #BBBBBB", + "border-bottom": "0.3mm solid #BBBBBB" + } + }, + "12": { + "css_class": "bf-12", + "bg": "#EDEDED", + "borders": { + "border-right": "0.12mm solid #999999", + "border-top": "0.12mm solid #BBBBBB", + "border-bottom": "0.5mm solid #BBBBBB" + } + }, + "13": { + "css_class": "bf-13", + "bg": "#DCDCDC", + "borders": { + "border-left": "0.12mm solid #999999", + "border-right": "0.12mm solid #999999", + "border-top": "0.3mm solid #BBBBBB", + "border-bottom": "0.12mm solid #BBBBBB" + } + }, + "14": { + "css_class": "bf-14", + "bg": "#DCDCDC", + "borders": { + "border-left": "0.12mm solid #999999", + "border-top": "0.3mm solid #BBBBBB", + "border-bottom": "0.12mm solid #BBBBBB" + } + }, + "15": { + "css_class": "bf-15", + "bg": "#EDEDED", + "borders": { + "border-left": "0.12mm solid #999999", + "border-right": "0.12mm solid #999999", + "border-top": "0.5mm solid #BBBBBB", + "border-bottom": "0.12mm solid #BBBBBB" + } + }, + "16": { + "css_class": "bf-16", + "bg": "#EDEDED", + "borders": { + "border-left": "0.12mm solid #999999", + "border-right": "0.12mm solid #999999", + "border-top": "0.12mm solid #BBBBBB", + "border-bottom": "0.5mm solid #BBBBBB" + } + }, + "17": { + "css_class": "bf-17", + "bg": "", + "borders": { + "border-left": "0.12mm solid #999999", + "border-right": "0.12mm solid #999999", + "border-top": "0.5mm solid #BBBBBB", + "border-bottom": "0.12mm solid #BBBBBB" + } + }, + "18": { + "css_class": "bf-18", + "bg": "", + "borders": { + "border-left": "0.12mm solid #999999", + "border-right": "0.12mm solid #999999", + "border-top": "0.12mm solid #BBBBBB", + "border-bottom": "0.5mm solid #BBBBBB" + } + }, + "19": { + "css_class": "bf-19", + "bg": "", + "borders": { + "border-left": "0.12mm solid #999999", + "border-top": "0.12mm solid #BBBBBB", + "border-bottom": "0.5mm solid #BBBBBB" + } + }, + "20": { + "css_class": "bf-20", + "bg": "", + "borders": { + "border-left": "0.12mm solid #999999", + "border-top": "0.5mm solid #BBBBBB", + "border-bottom": "0.3mm solid #BBBBBB" + } + } + }, + "para_pr": {} + } +} \ No newline at end of file diff --git a/templates/user/templates/tpl_1770300969/style.json b/templates/user/templates/tpl_1770300969/style.json new file mode 100644 index 0000000..5928bae --- /dev/null +++ b/templates/user/templates/tpl_1770300969/style.json @@ -0,0 +1,4688 @@ +{ + "version": "v4", + "source": "doc_template_analyzer", + "template_info": { + "page": { + "paper": { + "name": "A4", + "width_mm": 210.0, + "height_mm": 297.0, + "landscape": true + }, + "margins": { + "top": "10.0mm", + "bottom": "10.0mm", + "left": "20.0mm", + "right": "20.0mm", + "header": "15.0mm", + "footer": "15.0mm", + "gutter": "0.0mm" + } + }, + "fonts": { + "HANGUL": [ + { + "id": 0, + "face": "돋움", + "type": "TTF" + }, + { + "id": 1, + "face": "맑은 고딕", + "type": "TTF" + }, + { + "id": 2, + "face": "휴먼고딕", + "type": "TTF" + }, + { + "id": 3, + "face": "휴먼명조", + "type": "TTF" + }, + { + "id": 4, + "face": "HY헤드라인M", + "type": "TTF" + }, + { + "id": 5, + "face": "한양견명조", + "type": "HFT" + }, + { + "id": 6, + "face": "한양중고딕", + "type": "HFT" + }, + { + "id": 7, + "face": "나눔명조", + "type": "TTF" + }, + { + "id": 8, + "face": "-윤고딕130", + "type": "TTF" + } + ], + "LATIN": [ + { + "id": 0, + "face": "돋움", + "type": "TTF" + }, + { + "id": 1, + "face": "맑은 고딕", + "type": "TTF" + }, + { + "id": 2, + "face": "휴먼고딕", + "type": "TTF" + }, + { + "id": 3, + "face": "휴먼명조", + "type": "TTF" + }, + { + "id": 4, + "face": "HY헤드라인M", + "type": "TTF" + }, + { + "id": 5, + "face": "한양중고딕", + "type": "HFT" + }, + { + "id": 6, + "face": "나눔명조", + "type": "TTF" + }, + { + "id": 7, + "face": "-윤고딕130", + "type": "TTF" + }, + { + "id": 8, + "face": "한양견명조", + "type": "HFT" + } + ], + "HANJA": [ + { + "id": 0, + "face": "맑은 고딕", + "type": "TTF" + }, + { + "id": 1, + "face": "한컴바탕", + "type": "TTF" + }, + { + "id": 2, + "face": "휴먼고딕", + "type": "TTF" + }, + { + "id": 3, + "face": "휴먼명조", + "type": "TTF" + }, + { + "id": 4, + "face": "HY헤드라인M", + "type": "TTF" + }, + { + "id": 5, + "face": "한양중고딕", + "type": "HFT" + }, + { + "id": 6, + "face": "나눔명조", + "type": "TTF" + }, + { + "id": 7, + "face": "-윤고딕130", + "type": "TTF" + }, + { + "id": 8, + "face": "신명 견명조", + "type": "HFT" + } + ], + "JAPANESE": [ + { + "id": 0, + "face": "맑은 고딕", + "type": "TTF" + }, + { + "id": 1, + "face": "한컴바탕", + "type": "TTF" + }, + { + "id": 2, + "face": "휴먼고딕", + "type": "TTF" + }, + { + "id": 3, + "face": "휴먼명조", + "type": "TTF" + }, + { + "id": 4, + "face": "HY헤드라인M", + "type": "TTF" + }, + { + "id": 5, + "face": "한양중고딕", + "type": "HFT" + }, + { + "id": 6, + "face": "나눔명조", + "type": "TTF" + }, + { + "id": 7, + "face": "-윤고딕130", + "type": "TTF" + }, + { + "id": 8, + "face": "신명 견명조", + "type": "HFT" + } + ], + "OTHER": [ + { + "id": 0, + "face": "맑은 고딕", + "type": "TTF" + }, + { + "id": 1, + "face": "한컴바탕", + "type": "TTF" + }, + { + "id": 2, + "face": "휴먼고딕", + "type": "TTF" + }, + { + "id": 3, + "face": "휴먼명조", + "type": "TTF" + }, + { + "id": 4, + "face": "HY헤드라인M", + "type": "TTF" + }, + { + "id": 5, + "face": "나눔명조", + "type": "TTF" + }, + { + "id": 6, + "face": "-윤고딕130", + "type": "TTF" + }, + { + "id": 7, + "face": "한양신명조", + "type": "HFT" + } + ], + "SYMBOL": [ + { + "id": 0, + "face": "맑은 고딕", + "type": "TTF" + }, + { + "id": 1, + "face": "한컴바탕", + "type": "TTF" + }, + { + "id": 2, + "face": "휴먼고딕", + "type": "TTF" + }, + { + "id": 3, + "face": "휴먼명조", + "type": "TTF" + }, + { + "id": 4, + "face": "HY헤드라인M", + "type": "TTF" + }, + { + "id": 5, + "face": "한양중고딕", + "type": "HFT" + }, + { + "id": 6, + "face": "나눔명조", + "type": "TTF" + }, + { + "id": 7, + "face": "-윤고딕330", + "type": "TTF" + }, + { + "id": 8, + "face": "신명 견명조", + "type": "HFT" + } + ], + "USER": [ + { + "id": 0, + "face": "맑은 고딕", + "type": "TTF" + }, + { + "id": 1, + "face": "한컴바탕", + "type": "TTF" + }, + { + "id": 2, + "face": "휴먼고딕", + "type": "TTF" + }, + { + "id": 3, + "face": "휴먼명조", + "type": "TTF" + }, + { + "id": 4, + "face": "HY헤드라인M", + "type": "TTF" + }, + { + "id": 5, + "face": "나눔명조", + "type": "TTF" + }, + { + "id": 6, + "face": "-윤고딕130", + "type": "TTF" + }, + { + "id": 7, + "face": "명조", + "type": "HFT" + } + ] + }, + "char_styles": [ + { + "id": 0, + "height_pt": 10.0, + "textColor": "#000000", + "borderFillIDRef": 2, + "bold": false, + "italic": false, + "fontRef": { + "hangul": 7, + "latin": 6, + "hanja": 6, + "japanese": 6, + "other": 5, + "symbol": 6, + "user": 5 + }, + "ratio": { + "hangul": 100, + "latin": 100, + "hanja": 100, + "japanese": 100, + "other": 100, + "symbol": 100, + "user": 100 + }, + "spacing": { + "hangul": 0, + "latin": 0, + "hanja": 0, + "japanese": 0, + "other": 0, + "symbol": 0, + "user": 0 + }, + "underline": "NONE", + "strikeout": "3D" + }, + { + "id": 1, + "height_pt": 9.0, + "textColor": "#000000", + "borderFillIDRef": 2, + "bold": false, + "italic": false, + "fontRef": { + "hangul": 1, + "latin": 1, + "hanja": 0, + "japanese": 0, + "other": 0, + "symbol": 0, + "user": 0 + }, + "ratio": { + "hangul": 95, + "latin": 95, + "hanja": 95, + "japanese": 95, + "other": 95, + "symbol": 95, + "user": 95 + }, + "spacing": { + "hangul": -5, + "latin": -5, + "hanja": -5, + "japanese": -5, + "other": -5, + "symbol": -5, + "user": -5 + }, + "underline": "NONE", + "strikeout": "3D" + }, + { + "id": 2, + "height_pt": 8.0, + "textColor": "#000000", + "borderFillIDRef": 2, + "bold": false, + "italic": false, + "fontRef": { + "hangul": 1, + "latin": 1, + "hanja": 0, + "japanese": 0, + "other": 0, + "symbol": 0, + "user": 0 + }, + "ratio": { + "hangul": 100, + "latin": 100, + "hanja": 100, + "japanese": 100, + "other": 100, + "symbol": 100, + "user": 100 + }, + "spacing": { + "hangul": 0, + "latin": 0, + "hanja": 0, + "japanese": 0, + "other": 0, + "symbol": 0, + "user": 0 + }, + "underline": "NONE", + "strikeout": "3D" + }, + { + "id": 3, + "height_pt": 9.0, + "textColor": "#000000", + "borderFillIDRef": 2, + "bold": false, + "italic": false, + "fontRef": { + "hangul": 1, + "latin": 1, + "hanja": 0, + "japanese": 0, + "other": 0, + "symbol": 0, + "user": 0 + }, + "ratio": { + "hangul": 100, + "latin": 100, + "hanja": 100, + "japanese": 100, + "other": 100, + "symbol": 100, + "user": 100 + }, + "spacing": { + "hangul": 0, + "latin": 0, + "hanja": 0, + "japanese": 0, + "other": 0, + "symbol": 0, + "user": 0 + }, + "underline": "NONE", + "strikeout": "3D" + }, + { + "id": 4, + "height_pt": 15.0, + "textColor": "#000000", + "borderFillIDRef": 2, + "bold": false, + "italic": false, + "fontRef": { + "hangul": 4, + "latin": 4, + "hanja": 4, + "japanese": 4, + "other": 4, + "symbol": 4, + "user": 4 + }, + "ratio": { + "hangul": 100, + "latin": 100, + "hanja": 100, + "japanese": 100, + "other": 100, + "symbol": 100, + "user": 100 + }, + "spacing": { + "hangul": 0, + "latin": 0, + "hanja": 0, + "japanese": 0, + "other": 0, + "symbol": 0, + "user": 0 + }, + "underline": "NONE", + "strikeout": "3D" + }, + { + "id": 5, + "height_pt": 8.0, + "textColor": "#000000", + "borderFillIDRef": 2, + "bold": true, + "italic": false, + "fontRef": { + "hangul": 6, + "latin": 5, + "hanja": 5, + "japanese": 5, + "other": 5, + "symbol": 5, + "user": 5 + }, + "ratio": { + "hangul": 100, + "latin": 100, + "hanja": 100, + "japanese": 100, + "other": 100, + "symbol": 100, + "user": 100 + }, + "spacing": { + "hangul": 0, + "latin": 0, + "hanja": 0, + "japanese": 0, + "other": 0, + "symbol": 0, + "user": 0 + }, + "underline": "NONE", + "strikeout": "3D" + }, + { + "id": 6, + "height_pt": 10.0, + "textColor": "#000000", + "borderFillIDRef": 2, + "bold": false, + "italic": false, + "fontRef": { + "hangul": 4, + "latin": 4, + "hanja": 4, + "japanese": 4, + "other": 4, + "symbol": 4, + "user": 4 + }, + "ratio": { + "hangul": 100, + "latin": 100, + "hanja": 100, + "japanese": 100, + "other": 100, + "symbol": 100, + "user": 100 + }, + "spacing": { + "hangul": 0, + "latin": 0, + "hanja": 0, + "japanese": 0, + "other": 0, + "symbol": 0, + "user": 0 + }, + "underline": "NONE", + "strikeout": "3D" + }, + { + "id": 7, + "height_pt": 8.0, + "textColor": "#000000", + "borderFillIDRef": 2, + "bold": true, + "italic": false, + "fontRef": { + "hangul": 2, + "latin": 2, + "hanja": 2, + "japanese": 2, + "other": 2, + "symbol": 2, + "user": 2 + }, + "ratio": { + "hangul": 100, + "latin": 100, + "hanja": 100, + "japanese": 100, + "other": 100, + "symbol": 100, + "user": 100 + }, + "spacing": { + "hangul": 0, + "latin": 0, + "hanja": 0, + "japanese": 0, + "other": 0, + "symbol": 0, + "user": 0 + }, + "underline": "NONE", + "strikeout": "3D" + }, + { + "id": 8, + "height_pt": 8.0, + "textColor": "#000000", + "borderFillIDRef": 2, + "bold": false, + "italic": false, + "fontRef": { + "hangul": 2, + "latin": 2, + "hanja": 2, + "japanese": 2, + "other": 2, + "symbol": 2, + "user": 2 + }, + "ratio": { + "hangul": 100, + "latin": 100, + "hanja": 100, + "japanese": 100, + "other": 100, + "symbol": 100, + "user": 100 + }, + "spacing": { + "hangul": 0, + "latin": 0, + "hanja": 0, + "japanese": 0, + "other": 0, + "symbol": 0, + "user": 0 + }, + "underline": "NONE", + "strikeout": "3D" + }, + { + "id": 9, + "height_pt": 8.0, + "textColor": "#0000FF", + "borderFillIDRef": 2, + "bold": true, + "italic": false, + "fontRef": { + "hangul": 2, + "latin": 2, + "hanja": 2, + "japanese": 2, + "other": 2, + "symbol": 2, + "user": 2 + }, + "ratio": { + "hangul": 100, + "latin": 100, + "hanja": 100, + "japanese": 100, + "other": 100, + "symbol": 100, + "user": 100 + }, + "spacing": { + "hangul": 0, + "latin": 0, + "hanja": 0, + "japanese": 0, + "other": 0, + "symbol": 0, + "user": 0 + }, + "underline": "NONE", + "strikeout": "3D" + }, + { + "id": 10, + "height_pt": 8.0, + "textColor": "#FF0000", + "borderFillIDRef": 2, + "bold": true, + "italic": false, + "fontRef": { + "hangul": 2, + "latin": 2, + "hanja": 2, + "japanese": 2, + "other": 2, + "symbol": 2, + "user": 2 + }, + "ratio": { + "hangul": 100, + "latin": 100, + "hanja": 100, + "japanese": 100, + "other": 100, + "symbol": 100, + "user": 100 + }, + "spacing": { + "hangul": 0, + "latin": 0, + "hanja": 0, + "japanese": 0, + "other": 0, + "symbol": 0, + "user": 0 + }, + "underline": "NONE", + "strikeout": "3D" + }, + { + "id": 11, + "height_pt": 8.0, + "textColor": "#008000", + "borderFillIDRef": 2, + "bold": true, + "italic": false, + "fontRef": { + "hangul": 2, + "latin": 2, + "hanja": 2, + "japanese": 2, + "other": 2, + "symbol": 2, + "user": 2 + }, + "ratio": { + "hangul": 100, + "latin": 100, + "hanja": 100, + "japanese": 100, + "other": 100, + "symbol": 100, + "user": 100 + }, + "spacing": { + "hangul": 0, + "latin": 0, + "hanja": 0, + "japanese": 0, + "other": 0, + "symbol": 0, + "user": 0 + }, + "underline": "NONE", + "strikeout": "3D" + }, + { + "id": 12, + "height_pt": 8.0, + "textColor": "#000000", + "borderFillIDRef": 2, + "bold": false, + "italic": false, + "fontRef": { + "hangul": 2, + "latin": 2, + "hanja": 2, + "japanese": 2, + "other": 2, + "symbol": 2, + "user": 2 + }, + "ratio": { + "hangul": 90, + "latin": 90, + "hanja": 90, + "japanese": 90, + "other": 90, + "symbol": 90, + "user": 90 + }, + "spacing": { + "hangul": -5, + "latin": -5, + "hanja": -5, + "japanese": -5, + "other": -5, + "symbol": -5, + "user": -5 + }, + "underline": "NONE", + "strikeout": "3D" + }, + { + "id": 13, + "height_pt": 9.0, + "textColor": "#000000", + "borderFillIDRef": 2, + "bold": false, + "italic": false, + "fontRef": { + "hangul": 7, + "latin": 6, + "hanja": 6, + "japanese": 6, + "other": 5, + "symbol": 6, + "user": 5 + }, + "ratio": { + "hangul": 100, + "latin": 100, + "hanja": 100, + "japanese": 100, + "other": 100, + "symbol": 100, + "user": 100 + }, + "spacing": { + "hangul": 0, + "latin": 0, + "hanja": 0, + "japanese": 0, + "other": 0, + "symbol": 0, + "user": 0 + }, + "underline": "NONE", + "strikeout": "3D" + }, + { + "id": 14, + "height_pt": 10.0, + "textColor": "#000000", + "borderFillIDRef": 2, + "bold": false, + "italic": false, + "fontRef": { + "hangul": 7, + "latin": 6, + "hanja": 6, + "japanese": 6, + "other": 5, + "symbol": 6, + "user": 5 + }, + "ratio": { + "hangul": 100, + "latin": 100, + "hanja": 100, + "japanese": 100, + "other": 100, + "symbol": 100, + "user": 100 + }, + "spacing": { + "hangul": 0, + "latin": 0, + "hanja": 0, + "japanese": 0, + "other": 0, + "symbol": 0, + "user": 0 + }, + "underline": "NONE", + "strikeout": "NONE" + }, + { + "id": 15, + "height_pt": 15.0, + "textColor": "#000000", + "borderFillIDRef": 2, + "bold": false, + "italic": false, + "fontRef": { + "hangul": 4, + "latin": 4, + "hanja": 4, + "japanese": 4, + "other": 4, + "symbol": 4, + "user": 4 + }, + "ratio": { + "hangul": 100, + "latin": 100, + "hanja": 100, + "japanese": 100, + "other": 100, + "symbol": 100, + "user": 100 + }, + "spacing": { + "hangul": 0, + "latin": 0, + "hanja": 0, + "japanese": 0, + "other": 0, + "symbol": 0, + "user": 0 + }, + "underline": "NONE", + "strikeout": "NONE" + }, + { + "id": 16, + "height_pt": 9.0, + "textColor": "#000000", + "borderFillIDRef": 2, + "bold": false, + "italic": false, + "fontRef": { + "hangul": 3, + "latin": 3, + "hanja": 3, + "japanese": 3, + "other": 3, + "symbol": 3, + "user": 3 + }, + "ratio": { + "hangul": 95, + "latin": 95, + "hanja": 95, + "japanese": 95, + "other": 95, + "symbol": 95, + "user": 95 + }, + "spacing": { + "hangul": -2, + "latin": -2, + "hanja": -2, + "japanese": -2, + "other": -2, + "symbol": -2, + "user": -2 + }, + "underline": "NONE", + "strikeout": "NONE" + }, + { + "id": 17, + "height_pt": 10.0, + "textColor": "#000000", + "borderFillIDRef": 2, + "bold": false, + "italic": false, + "fontRef": { + "hangul": 3, + "latin": 3, + "hanja": 3, + "japanese": 3, + "other": 3, + "symbol": 3, + "user": 3 + }, + "ratio": { + "hangul": 95, + "latin": 95, + "hanja": 95, + "japanese": 95, + "other": 95, + "symbol": 95, + "user": 95 + }, + "spacing": { + "hangul": -2, + "latin": -2, + "hanja": -2, + "japanese": -2, + "other": -2, + "symbol": -2, + "user": -2 + }, + "underline": "NONE", + "strikeout": "NONE" + }, + { + "id": 18, + "height_pt": 13.0, + "textColor": "#000000", + "borderFillIDRef": 1, + "bold": false, + "italic": false, + "fontRef": { + "hangul": 8, + "latin": 7, + "hanja": 7, + "japanese": 7, + "other": 6, + "symbol": 7, + "user": 6 + }, + "ratio": { + "hangul": 98, + "latin": 98, + "hanja": 98, + "japanese": 98, + "other": 98, + "symbol": 98, + "user": 98 + }, + "spacing": { + "hangul": -5, + "latin": -5, + "hanja": -5, + "japanese": -5, + "other": -5, + "symbol": -5, + "user": -5 + }, + "underline": "NONE", + "strikeout": "NONE" + }, + { + "id": 19, + "height_pt": 13.0, + "textColor": "#000000", + "borderFillIDRef": 2, + "bold": true, + "italic": false, + "fontRef": { + "hangul": 7, + "latin": 6, + "hanja": 6, + "japanese": 6, + "other": 5, + "symbol": 6, + "user": 5 + }, + "ratio": { + "hangul": 100, + "latin": 100, + "hanja": 100, + "japanese": 100, + "other": 100, + "symbol": 100, + "user": 100 + }, + "spacing": { + "hangul": 0, + "latin": 0, + "hanja": 0, + "japanese": 0, + "other": 0, + "symbol": 0, + "user": 0 + }, + "underline": "NONE", + "strikeout": "NONE" + }, + { + "id": 20, + "height_pt": 10.0, + "textColor": "#000000", + "borderFillIDRef": 2, + "bold": true, + "italic": false, + "fontRef": { + "hangul": 7, + "latin": 6, + "hanja": 6, + "japanese": 6, + "other": 5, + "symbol": 6, + "user": 5 + }, + "ratio": { + "hangul": 100, + "latin": 100, + "hanja": 100, + "japanese": 100, + "other": 100, + "symbol": 100, + "user": 100 + }, + "spacing": { + "hangul": 0, + "latin": 0, + "hanja": 0, + "japanese": 0, + "other": 0, + "symbol": 0, + "user": 0 + }, + "underline": "NONE", + "strikeout": "3D" + }, + { + "id": 21, + "height_pt": 11.0, + "textColor": "#000000", + "borderFillIDRef": 2, + "bold": false, + "italic": false, + "fontRef": { + "hangul": 7, + "latin": 6, + "hanja": 6, + "japanese": 6, + "other": 5, + "symbol": 6, + "user": 5 + }, + "ratio": { + "hangul": 100, + "latin": 100, + "hanja": 100, + "japanese": 100, + "other": 100, + "symbol": 100, + "user": 100 + }, + "spacing": { + "hangul": 0, + "latin": 0, + "hanja": 0, + "japanese": 0, + "other": 0, + "symbol": 0, + "user": 0 + }, + "underline": "NONE", + "strikeout": "NONE" + }, + { + "id": 22, + "height_pt": 11.0, + "textColor": "#000000", + "borderFillIDRef": 2, + "bold": false, + "italic": false, + "fontRef": { + "hangul": 3, + "latin": 3, + "hanja": 3, + "japanese": 3, + "other": 3, + "symbol": 3, + "user": 3 + }, + "ratio": { + "hangul": 95, + "latin": 95, + "hanja": 95, + "japanese": 95, + "other": 95, + "symbol": 95, + "user": 95 + }, + "spacing": { + "hangul": -2, + "latin": -2, + "hanja": -2, + "japanese": -2, + "other": -2, + "symbol": -2, + "user": -2 + }, + "underline": "NONE", + "strikeout": "NONE" + }, + { + "id": 23, + "height_pt": 10.0, + "textColor": "#000000", + "borderFillIDRef": 2, + "bold": false, + "italic": false, + "fontRef": { + "hangul": 7, + "latin": 6, + "hanja": 6, + "japanese": 6, + "other": 5, + "symbol": 6, + "user": 5 + }, + "ratio": { + "hangul": 100, + "latin": 100, + "hanja": 100, + "japanese": 100, + "other": 100, + "symbol": 100, + "user": 100 + }, + "spacing": { + "hangul": -10, + "latin": -10, + "hanja": -10, + "japanese": -10, + "other": -10, + "symbol": -10, + "user": -10 + }, + "underline": "NONE", + "strikeout": "3D" + }, + { + "id": 24, + "height_pt": 10.0, + "textColor": "#000000", + "borderFillIDRef": 2, + "bold": false, + "italic": false, + "fontRef": { + "hangul": 7, + "latin": 6, + "hanja": 6, + "japanese": 6, + "other": 5, + "symbol": 6, + "user": 5 + }, + "ratio": { + "hangul": 100, + "latin": 100, + "hanja": 100, + "japanese": 100, + "other": 100, + "symbol": 100, + "user": 100 + }, + "spacing": { + "hangul": -17, + "latin": -17, + "hanja": -17, + "japanese": -17, + "other": -17, + "symbol": -17, + "user": -17 + }, + "underline": "NONE", + "strikeout": "3D" + }, + { + "id": 25, + "height_pt": 16.0, + "textColor": "#000000", + "borderFillIDRef": 1, + "bold": false, + "italic": false, + "fontRef": { + "hangul": 5, + "latin": 8, + "hanja": 8, + "japanese": 8, + "other": 7, + "symbol": 8, + "user": 7 + }, + "ratio": { + "hangul": 100, + "latin": 100, + "hanja": 100, + "japanese": 100, + "other": 100, + "symbol": 100, + "user": 100 + }, + "spacing": { + "hangul": 0, + "latin": 0, + "hanja": 0, + "japanese": 0, + "other": 0, + "symbol": 0, + "user": 0 + }, + "underline": "NONE", + "strikeout": "NONE" + }, + { + "id": 26, + "height_pt": 11.0, + "textColor": "#000000", + "borderFillIDRef": 1, + "bold": false, + "italic": false, + "fontRef": { + "hangul": 0, + "latin": 0, + "hanja": 1, + "japanese": 1, + "other": 1, + "symbol": 1, + "user": 1 + }, + "ratio": { + "hangul": 100, + "latin": 100, + "hanja": 100, + "japanese": 100, + "other": 100, + "symbol": 100, + "user": 100 + }, + "spacing": { + "hangul": 0, + "latin": 0, + "hanja": 0, + "japanese": 0, + "other": 0, + "symbol": 0, + "user": 0 + }, + "underline": "NONE", + "strikeout": "NONE" + } + ], + "para_styles": [ + { + "id": 0, + "tabPrIDRef": 1, + "align": "JUSTIFY", + "verticalAlign": "BASELINE", + "heading": { + "type": "NONE", + "idRef": 0, + "level": 0 + }, + "breakSetting": { + "widowOrphan": false, + "keepWithNext": false, + "keepLines": false, + "pageBreakBefore": false, + "lineWrap": "BREAK", + "breakLatinWord": "KEEP_WORD", + "breakNonLatinWord": "KEEP_WORD" + }, + "margin": { + "indent_hu": -1310, + "left_hu": 0, + "right_hu": 0, + "before_hu": 0, + "after_hu": 0 + }, + "lineSpacing": { + "type": "PERCENT", + "value": 130 + }, + "borderFillIDRef": 2 + }, + { + "id": 1, + "tabPrIDRef": 0, + "align": "JUSTIFY", + "verticalAlign": "BASELINE", + "heading": { + "type": "NONE", + "idRef": 0, + "level": 0 + }, + "breakSetting": { + "widowOrphan": false, + "keepWithNext": false, + "keepLines": false, + "pageBreakBefore": false, + "lineWrap": "BREAK", + "breakLatinWord": "KEEP_WORD", + "breakNonLatinWord": "KEEP_WORD" + }, + "margin": { + "indent_hu": 0, + "left_hu": 0, + "right_hu": 0, + "before_hu": 0, + "after_hu": 0 + }, + "lineSpacing": { + "type": "PERCENT", + "value": 160 + }, + "borderFillIDRef": 2 + }, + { + "id": 2, + "tabPrIDRef": 2, + "align": "JUSTIFY", + "verticalAlign": "BASELINE", + "heading": { + "type": "NONE", + "idRef": 0, + "level": 0 + }, + "breakSetting": { + "widowOrphan": false, + "keepWithNext": false, + "keepLines": false, + "pageBreakBefore": false, + "lineWrap": "BREAK", + "breakLatinWord": "KEEP_WORD", + "breakNonLatinWord": "BREAK_WORD" + }, + "margin": { + "indent_hu": 0, + "left_hu": 0, + "right_hu": 0, + "before_hu": 0, + "after_hu": 0 + }, + "lineSpacing": { + "type": "PERCENT", + "value": 150 + }, + "borderFillIDRef": 2 + }, + { + "id": 3, + "tabPrIDRef": 0, + "align": "JUSTIFY", + "verticalAlign": "BASELINE", + "heading": { + "type": "NONE", + "idRef": 0, + "level": 0 + }, + "breakSetting": { + "widowOrphan": false, + "keepWithNext": false, + "keepLines": false, + "pageBreakBefore": false, + "lineWrap": "BREAK", + "breakLatinWord": "KEEP_WORD", + "breakNonLatinWord": "KEEP_WORD" + }, + "margin": { + "indent_hu": 0, + "left_hu": 0, + "right_hu": 0, + "before_hu": 0, + "after_hu": 0 + }, + "lineSpacing": { + "type": "PERCENT", + "value": 180 + }, + "borderFillIDRef": 2 + }, + { + "id": 4, + "tabPrIDRef": 0, + "align": "CENTER", + "verticalAlign": "BASELINE", + "heading": { + "type": "NONE", + "idRef": 0, + "level": 0 + }, + "breakSetting": { + "widowOrphan": false, + "keepWithNext": false, + "keepLines": false, + "pageBreakBefore": false, + "lineWrap": "BREAK", + "breakLatinWord": "KEEP_WORD", + "breakNonLatinWord": "BREAK_WORD" + }, + "margin": { + "indent_hu": 0, + "left_hu": 0, + "right_hu": 0, + "before_hu": 0, + "after_hu": 0 + }, + "lineSpacing": { + "type": "PERCENT", + "value": 180 + }, + "borderFillIDRef": 2 + }, + { + "id": 5, + "tabPrIDRef": 0, + "align": "JUSTIFY", + "verticalAlign": "BASELINE", + "heading": { + "type": "NONE", + "idRef": 0, + "level": 0 + }, + "breakSetting": { + "widowOrphan": false, + "keepWithNext": false, + "keepLines": false, + "pageBreakBefore": false, + "lineWrap": "BREAK", + "breakLatinWord": "KEEP_WORD", + "breakNonLatinWord": "KEEP_WORD" + }, + "margin": { + "indent_hu": 0, + "left_hu": 0, + "right_hu": 0, + "before_hu": 0, + "after_hu": 0 + }, + "lineSpacing": { + "type": "PERCENT", + "value": 110 + }, + "borderFillIDRef": 2 + }, + { + "id": 6, + "tabPrIDRef": 0, + "align": "RIGHT", + "verticalAlign": "BASELINE", + "heading": { + "type": "NONE", + "idRef": 0, + "level": 0 + }, + "breakSetting": { + "widowOrphan": false, + "keepWithNext": false, + "keepLines": false, + "pageBreakBefore": false, + "lineWrap": "BREAK", + "breakLatinWord": "KEEP_WORD", + "breakNonLatinWord": "BREAK_WORD" + }, + "margin": { + "indent_hu": 0, + "left_hu": 0, + "right_hu": 0, + "before_hu": 0, + "after_hu": 0 + }, + "lineSpacing": { + "type": "PERCENT", + "value": 110 + }, + "borderFillIDRef": 2 + }, + { + "id": 7, + "tabPrIDRef": 4, + "align": "JUSTIFY", + "verticalAlign": "BASELINE", + "heading": { + "type": "NONE", + "idRef": 0, + "level": 0 + }, + "breakSetting": { + "widowOrphan": false, + "keepWithNext": false, + "keepLines": false, + "pageBreakBefore": false, + "lineWrap": "BREAK", + "breakLatinWord": "KEEP_WORD", + "breakNonLatinWord": "KEEP_WORD" + }, + "margin": { + "indent_hu": 0, + "left_hu": 0, + "right_hu": 0, + "before_hu": 0, + "after_hu": 0 + }, + "lineSpacing": { + "type": "PERCENT", + "value": 100 + }, + "borderFillIDRef": 2 + }, + { + "id": 8, + "tabPrIDRef": 0, + "align": "DISTRIBUTE", + "verticalAlign": "BASELINE", + "heading": { + "type": "NONE", + "idRef": 0, + "level": 0 + }, + "breakSetting": { + "widowOrphan": false, + "keepWithNext": false, + "keepLines": false, + "pageBreakBefore": false, + "lineWrap": "BREAK", + "breakLatinWord": "KEEP_WORD", + "breakNonLatinWord": "KEEP_WORD" + }, + "margin": { + "indent_hu": 0, + "left_hu": 500, + "right_hu": 500, + "before_hu": 0, + "after_hu": 0 + }, + "lineSpacing": { + "type": "PERCENT", + "value": 180 + }, + "borderFillIDRef": 2 + }, + { + "id": 9, + "tabPrIDRef": 3, + "align": "JUSTIFY", + "verticalAlign": "BASELINE", + "heading": { + "type": "NONE", + "idRef": 0, + "level": 0 + }, + "breakSetting": { + "widowOrphan": false, + "keepWithNext": false, + "keepLines": false, + "pageBreakBefore": false, + "lineWrap": "BREAK", + "breakLatinWord": "KEEP_WORD", + "breakNonLatinWord": "KEEP_WORD" + }, + "margin": { + "indent_hu": 0, + "left_hu": 0, + "right_hu": 0, + "before_hu": 0, + "after_hu": 0 + }, + "lineSpacing": { + "type": "PERCENT", + "value": 180 + }, + "borderFillIDRef": 2 + }, + { + "id": 10, + "tabPrIDRef": 3, + "align": "JUSTIFY", + "verticalAlign": "BASELINE", + "heading": { + "type": "NONE", + "idRef": 0, + "level": 0 + }, + "breakSetting": { + "widowOrphan": false, + "keepWithNext": false, + "keepLines": false, + "pageBreakBefore": false, + "lineWrap": "BREAK", + "breakLatinWord": "KEEP_WORD", + "breakNonLatinWord": "KEEP_WORD" + }, + "margin": { + "indent_hu": 0, + "left_hu": 500, + "right_hu": 0, + "before_hu": 0, + "after_hu": 0 + }, + "lineSpacing": { + "type": "PERCENT", + "value": 180 + }, + "borderFillIDRef": 2 + }, + { + "id": 11, + "tabPrIDRef": 0, + "align": "LEFT", + "verticalAlign": "BASELINE", + "heading": { + "type": "NONE", + "idRef": 0, + "level": 0 + }, + "breakSetting": { + "widowOrphan": false, + "keepWithNext": false, + "keepLines": false, + "pageBreakBefore": false, + "lineWrap": "BREAK", + "breakLatinWord": "KEEP_WORD", + "breakNonLatinWord": "BREAK_WORD" + }, + "margin": { + "indent_hu": 0, + "left_hu": 0, + "right_hu": 0, + "before_hu": 0, + "after_hu": 0 + }, + "lineSpacing": { + "type": "PERCENT", + "value": 180 + }, + "borderFillIDRef": 2 + }, + { + "id": 12, + "tabPrIDRef": 3, + "align": "JUSTIFY", + "verticalAlign": "BASELINE", + "heading": { + "type": "NONE", + "idRef": 0, + "level": 0 + }, + "breakSetting": { + "widowOrphan": false, + "keepWithNext": false, + "keepLines": false, + "pageBreakBefore": false, + "lineWrap": "BREAK", + "breakLatinWord": "KEEP_WORD", + "breakNonLatinWord": "KEEP_WORD" + }, + "margin": { + "indent_hu": -1223, + "left_hu": 500, + "right_hu": 0, + "before_hu": 0, + "after_hu": 500 + }, + "lineSpacing": { + "type": "PERCENT", + "value": 170 + }, + "borderFillIDRef": 2 + }, + { + "id": 13, + "tabPrIDRef": 0, + "align": "JUSTIFY", + "verticalAlign": "BASELINE", + "heading": { + "type": "NONE", + "idRef": 0, + "level": 0 + }, + "breakSetting": { + "widowOrphan": false, + "keepWithNext": false, + "keepLines": false, + "pageBreakBefore": false, + "lineWrap": "BREAK", + "breakLatinWord": "KEEP_WORD", + "breakNonLatinWord": "BREAK_WORD" + }, + "margin": { + "indent_hu": 0, + "left_hu": 800, + "right_hu": 0, + "before_hu": 200, + "after_hu": 300 + }, + "lineSpacing": { + "type": "PERCENT", + "value": 140 + }, + "borderFillIDRef": 2 + }, + { + "id": 14, + "tabPrIDRef": 0, + "align": "JUSTIFY", + "verticalAlign": "BASELINE", + "heading": { + "type": "NONE", + "idRef": 0, + "level": 0 + }, + "breakSetting": { + "widowOrphan": false, + "keepWithNext": false, + "keepLines": false, + "pageBreakBefore": false, + "lineWrap": "BREAK", + "breakLatinWord": "KEEP_WORD", + "breakNonLatinWord": "KEEP_WORD" + }, + "margin": { + "indent_hu": 0, + "left_hu": 0, + "right_hu": 0, + "before_hu": 600, + "after_hu": 0 + }, + "lineSpacing": { + "type": "PERCENT", + "value": 155 + }, + "borderFillIDRef": 1 + }, + { + "id": 15, + "tabPrIDRef": 3, + "align": "JUSTIFY", + "verticalAlign": "BASELINE", + "heading": { + "type": "NONE", + "idRef": 0, + "level": 0 + }, + "breakSetting": { + "widowOrphan": false, + "keepWithNext": false, + "keepLines": false, + "pageBreakBefore": false, + "lineWrap": "BREAK", + "breakLatinWord": "KEEP_WORD", + "breakNonLatinWord": "KEEP_WORD" + }, + "margin": { + "indent_hu": 0, + "left_hu": 0, + "right_hu": 0, + "before_hu": 1200, + "after_hu": 500 + }, + "lineSpacing": { + "type": "PERCENT", + "value": 160 + }, + "borderFillIDRef": 2 + }, + { + "id": 16, + "tabPrIDRef": 0, + "align": "LEFT", + "verticalAlign": "BASELINE", + "heading": { + "type": "BULLET", + "idRef": 1, + "level": 0 + }, + "breakSetting": { + "widowOrphan": false, + "keepWithNext": false, + "keepLines": false, + "pageBreakBefore": false, + "lineWrap": "BREAK", + "breakLatinWord": "KEEP_WORD", + "breakNonLatinWord": "BREAK_WORD" + }, + "margin": { + "indent_hu": 0, + "left_hu": 0, + "right_hu": 0, + "before_hu": 0, + "after_hu": 0 + }, + "lineSpacing": { + "type": "PERCENT", + "value": 180 + }, + "borderFillIDRef": 2 + }, + { + "id": 17, + "tabPrIDRef": 0, + "align": "JUSTIFY", + "verticalAlign": "BASELINE", + "heading": { + "type": "NONE", + "idRef": 0, + "level": 0 + }, + "breakSetting": { + "widowOrphan": false, + "keepWithNext": false, + "keepLines": false, + "pageBreakBefore": false, + "lineWrap": "BREAK", + "breakLatinWord": "KEEP_WORD", + "breakNonLatinWord": "BREAK_WORD" + }, + "margin": { + "indent_hu": -1396, + "left_hu": 800, + "right_hu": 0, + "before_hu": 0, + "after_hu": 300 + }, + "lineSpacing": { + "type": "PERCENT", + "value": 140 + }, + "borderFillIDRef": 2 + }, + { + "id": 18, + "tabPrIDRef": 0, + "align": "JUSTIFY", + "verticalAlign": "BASELINE", + "heading": { + "type": "BULLET", + "idRef": 1, + "level": 0 + }, + "breakSetting": { + "widowOrphan": false, + "keepWithNext": false, + "keepLines": false, + "pageBreakBefore": false, + "lineWrap": "BREAK", + "breakLatinWord": "KEEP_WORD", + "breakNonLatinWord": "BREAK_WORD" + }, + "margin": { + "indent_hu": 0, + "left_hu": 800, + "right_hu": 0, + "before_hu": 500, + "after_hu": 300 + }, + "lineSpacing": { + "type": "PERCENT", + "value": 140 + }, + "borderFillIDRef": 2 + }, + { + "id": 19, + "tabPrIDRef": 3, + "align": "JUSTIFY", + "verticalAlign": "BASELINE", + "heading": { + "type": "NONE", + "idRef": 0, + "level": 0 + }, + "breakSetting": { + "widowOrphan": false, + "keepWithNext": false, + "keepLines": false, + "pageBreakBefore": false, + "lineWrap": "BREAK", + "breakLatinWord": "KEEP_WORD", + "breakNonLatinWord": "KEEP_WORD" + }, + "margin": { + "indent_hu": 0, + "left_hu": 0, + "right_hu": 0, + "before_hu": 1000, + "after_hu": 500 + }, + "lineSpacing": { + "type": "PERCENT", + "value": 160 + }, + "borderFillIDRef": 2 + }, + { + "id": 20, + "tabPrIDRef": 0, + "align": "CENTER", + "verticalAlign": "BASELINE", + "heading": { + "type": "NONE", + "idRef": 0, + "level": 0 + }, + "breakSetting": { + "widowOrphan": false, + "keepWithNext": false, + "keepLines": false, + "pageBreakBefore": false, + "lineWrap": "BREAK", + "breakLatinWord": "KEEP_WORD", + "breakNonLatinWord": "KEEP_WORD" + }, + "margin": { + "indent_hu": 0, + "left_hu": 0, + "right_hu": 0, + "before_hu": 0, + "after_hu": 0 + }, + "lineSpacing": { + "type": "PERCENT", + "value": 160 + }, + "borderFillIDRef": 1 + }, + { + "id": 21, + "tabPrIDRef": 0, + "align": "JUSTIFY", + "verticalAlign": "BASELINE", + "heading": { + "type": "NONE", + "idRef": 0, + "level": 0 + }, + "breakSetting": { + "widowOrphan": false, + "keepWithNext": false, + "keepLines": false, + "pageBreakBefore": false, + "lineWrap": "BREAK", + "breakLatinWord": "KEEP_WORD", + "breakNonLatinWord": "KEEP_WORD" + }, + "margin": { + "indent_hu": 0, + "left_hu": 0, + "right_hu": 0, + "before_hu": 0, + "after_hu": 852 + }, + "lineSpacing": { + "type": "PERCENT", + "value": 180 + }, + "borderFillIDRef": 1 + }, + { + "id": 22, + "tabPrIDRef": 0, + "align": "JUSTIFY", + "verticalAlign": "BASELINE", + "heading": { + "type": "NONE", + "idRef": 0, + "level": 0 + }, + "breakSetting": { + "widowOrphan": false, + "keepWithNext": false, + "keepLines": false, + "pageBreakBefore": false, + "lineWrap": "BREAK", + "breakLatinWord": "KEEP_WORD", + "breakNonLatinWord": "BREAK_WORD" + }, + "margin": { + "indent_hu": 0, + "left_hu": 800, + "right_hu": 0, + "before_hu": 500, + "after_hu": 300 + }, + "lineSpacing": { + "type": "PERCENT", + "value": 140 + }, + "borderFillIDRef": 2 + } + ], + "border_fills": { + "1": { + "id": 1, + "left": { + "type": "NONE", + "width": "0.1mm", + "color": "#000000" + }, + "right": { + "type": "NONE", + "width": "0.1mm", + "color": "#000000" + }, + "top": { + "type": "NONE", + "width": "0.1mm", + "color": "#000000" + }, + "bottom": { + "type": "NONE", + "width": "0.1mm", + "color": "#000000" + }, + "diagonal": { + "type": "SOLID", + "width": "0.1mm", + "color": "#000000" + }, + "css": { + "border-left": "none", + "border-right": "none", + "border-top": "none", + "border-bottom": "none" + } + }, + "2": { + "id": 2, + "left": { + "type": "NONE", + "width": "0.1mm", + "color": "#000000" + }, + "right": { + "type": "NONE", + "width": "0.1mm", + "color": "#000000" + }, + "top": { + "type": "NONE", + "width": "0.1mm", + "color": "#000000" + }, + "bottom": { + "type": "NONE", + "width": "0.1mm", + "color": "#000000" + }, + "diagonal": { + "type": "SOLID", + "width": "0.1mm", + "color": "#000000" + }, + "css": { + "border-left": "none", + "border-right": "none", + "border-top": "none", + "border-bottom": "none" + } + }, + "3": { + "id": 3, + "left": { + "type": "SOLID", + "width": "0.12mm", + "color": "#000000" + }, + "right": { + "type": "SOLID", + "width": "0.12mm", + "color": "#000000" + }, + "top": { + "type": "SOLID", + "width": "0.12mm", + "color": "#000000" + }, + "bottom": { + "type": "SOLID", + "width": "0.12mm", + "color": "#000000" + }, + "diagonal": { + "type": "SOLID", + "width": "0.1mm", + "color": "#000000" + }, + "css": { + "border-left": "0.12mm solid #000000", + "border-right": "0.12mm solid #000000", + "border-top": "0.12mm solid #000000", + "border-bottom": "0.12mm solid #000000" + } + }, + "4": { + "id": 4, + "left": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "right": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "top": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "bottom": { + "type": "SOLID", + "width": "0.7mm", + "color": "#3057B9" + }, + "diagonal": { + "type": "SOLID", + "width": "0.1mm", + "color": "#000000" + }, + "css": { + "border-left": "none", + "border-right": "none", + "border-top": "none", + "border-bottom": "0.7mm solid #3057B9" + } + }, + "5": { + "id": 5, + "left": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "right": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "top": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "bottom": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "diagonal": { + "type": "SOLID", + "width": "0.1mm", + "color": "#000000" + }, + "css": { + "border-left": "none", + "border-right": "none", + "border-top": "none", + "border-bottom": "none" + } + }, + "6": { + "id": 6, + "left": { + "type": "SOLID", + "width": "0.12mm", + "color": "#999999" + }, + "right": { + "type": "SOLID", + "width": "0.12mm", + "color": "#999999" + }, + "top": { + "type": "SOLID", + "width": "0.12mm", + "color": "#BBBBBB" + }, + "bottom": { + "type": "SOLID", + "width": "0.3mm", + "color": "#BBBBBB" + }, + "css": { + "border-left": "0.12mm solid #999999", + "border-right": "0.12mm solid #999999", + "border-top": "0.12mm solid #BBBBBB", + "border-bottom": "0.3mm solid #BBBBBB" + } + }, + "7": { + "id": 7, + "left": { + "type": "SOLID", + "width": "0.12mm", + "color": "#999999" + }, + "right": { + "type": "SOLID", + "width": "0.12mm", + "color": "#999999" + }, + "top": { + "type": "SOLID", + "width": "0.12mm", + "color": "#BBBBBB" + }, + "bottom": { + "type": "SOLID", + "width": "0.12mm", + "color": "#BBBBBB" + }, + "css": { + "border-left": "0.12mm solid #999999", + "border-right": "0.12mm solid #999999", + "border-top": "0.12mm solid #BBBBBB", + "border-bottom": "0.12mm solid #BBBBBB" + } + }, + "8": { + "id": 8, + "left": { + "type": "SOLID", + "width": "0.12mm", + "color": "#999999" + }, + "right": { + "type": "SOLID", + "width": "0.12mm", + "color": "#999999" + }, + "top": { + "type": "SOLID", + "width": "0.12mm", + "color": "#BBBBBB" + }, + "bottom": { + "type": "SOLID", + "width": "0.12mm", + "color": "#BBBBBB" + }, + "background": "#EDEDED", + "css": { + "border-left": "0.12mm solid #999999", + "border-right": "0.12mm solid #999999", + "border-top": "0.12mm solid #BBBBBB", + "border-bottom": "0.12mm solid #BBBBBB", + "background-color": "#EDEDED" + } + }, + "9": { + "id": 9, + "left": { + "type": "SOLID", + "width": "0.12mm", + "color": "#999999" + }, + "right": { + "type": "SOLID", + "width": "0.12mm", + "color": "#999999" + }, + "top": { + "type": "SOLID", + "width": "0.12mm", + "color": "#BBBBBB" + }, + "bottom": { + "type": "SOLID", + "width": "0.3mm", + "color": "#BBBBBB" + }, + "background": "#EDEDED", + "css": { + "border-left": "0.12mm solid #999999", + "border-right": "0.12mm solid #999999", + "border-top": "0.12mm solid #BBBBBB", + "border-bottom": "0.3mm solid #BBBBBB", + "background-color": "#EDEDED" + } + }, + "10": { + "id": 10, + "left": { + "type": "NONE", + "width": "0.12mm", + "color": "#999999" + }, + "right": { + "type": "SOLID", + "width": "0.12mm", + "color": "#999999" + }, + "top": { + "type": "SOLID", + "width": "0.3mm", + "color": "#BBBBBB" + }, + "bottom": { + "type": "SOLID", + "width": "0.12mm", + "color": "#BBBBBB" + }, + "background": "#DCDCDC", + "css": { + "border-left": "none", + "border-right": "0.12mm solid #999999", + "border-top": "0.3mm solid #BBBBBB", + "border-bottom": "0.12mm solid #BBBBBB", + "background-color": "#DCDCDC" + } + }, + "11": { + "id": 11, + "left": { + "type": "NONE", + "width": "0.12mm", + "color": "#999999" + }, + "right": { + "type": "SOLID", + "width": "0.12mm", + "color": "#999999" + }, + "top": { + "type": "SOLID", + "width": "0.5mm", + "color": "#BBBBBB" + }, + "bottom": { + "type": "SOLID", + "width": "0.3mm", + "color": "#BBBBBB" + }, + "background": "#EDEDED", + "css": { + "border-left": "none", + "border-right": "0.12mm solid #999999", + "border-top": "0.5mm solid #BBBBBB", + "border-bottom": "0.3mm solid #BBBBBB", + "background-color": "#EDEDED" + } + }, + "12": { + "id": 12, + "left": { + "type": "NONE", + "width": "0.12mm", + "color": "#999999" + }, + "right": { + "type": "SOLID", + "width": "0.12mm", + "color": "#999999" + }, + "top": { + "type": "SOLID", + "width": "0.12mm", + "color": "#BBBBBB" + }, + "bottom": { + "type": "SOLID", + "width": "0.5mm", + "color": "#BBBBBB" + }, + "background": "#EDEDED", + "css": { + "border-left": "none", + "border-right": "0.12mm solid #999999", + "border-top": "0.12mm solid #BBBBBB", + "border-bottom": "0.5mm solid #BBBBBB", + "background-color": "#EDEDED" + } + }, + "13": { + "id": 13, + "left": { + "type": "SOLID", + "width": "0.12mm", + "color": "#999999" + }, + "right": { + "type": "SOLID", + "width": "0.12mm", + "color": "#999999" + }, + "top": { + "type": "SOLID", + "width": "0.3mm", + "color": "#BBBBBB" + }, + "bottom": { + "type": "SOLID", + "width": "0.12mm", + "color": "#BBBBBB" + }, + "background": "#DCDCDC", + "css": { + "border-left": "0.12mm solid #999999", + "border-right": "0.12mm solid #999999", + "border-top": "0.3mm solid #BBBBBB", + "border-bottom": "0.12mm solid #BBBBBB", + "background-color": "#DCDCDC" + } + }, + "14": { + "id": 14, + "left": { + "type": "SOLID", + "width": "0.12mm", + "color": "#999999" + }, + "right": { + "type": "NONE", + "width": "0.12mm", + "color": "#999999" + }, + "top": { + "type": "SOLID", + "width": "0.3mm", + "color": "#BBBBBB" + }, + "bottom": { + "type": "SOLID", + "width": "0.12mm", + "color": "#BBBBBB" + }, + "background": "#DCDCDC", + "css": { + "border-left": "0.12mm solid #999999", + "border-right": "none", + "border-top": "0.3mm solid #BBBBBB", + "border-bottom": "0.12mm solid #BBBBBB", + "background-color": "#DCDCDC" + } + }, + "15": { + "id": 15, + "left": { + "type": "SOLID", + "width": "0.12mm", + "color": "#999999" + }, + "right": { + "type": "SOLID", + "width": "0.12mm", + "color": "#999999" + }, + "top": { + "type": "SOLID", + "width": "0.5mm", + "color": "#BBBBBB" + }, + "bottom": { + "type": "SOLID", + "width": "0.12mm", + "color": "#BBBBBB" + }, + "background": "#EDEDED", + "css": { + "border-left": "0.12mm solid #999999", + "border-right": "0.12mm solid #999999", + "border-top": "0.5mm solid #BBBBBB", + "border-bottom": "0.12mm solid #BBBBBB", + "background-color": "#EDEDED" + } + }, + "16": { + "id": 16, + "left": { + "type": "SOLID", + "width": "0.12mm", + "color": "#999999" + }, + "right": { + "type": "SOLID", + "width": "0.12mm", + "color": "#999999" + }, + "top": { + "type": "SOLID", + "width": "0.12mm", + "color": "#BBBBBB" + }, + "bottom": { + "type": "SOLID", + "width": "0.5mm", + "color": "#BBBBBB" + }, + "background": "#EDEDED", + "css": { + "border-left": "0.12mm solid #999999", + "border-right": "0.12mm solid #999999", + "border-top": "0.12mm solid #BBBBBB", + "border-bottom": "0.5mm solid #BBBBBB", + "background-color": "#EDEDED" + } + }, + "17": { + "id": 17, + "left": { + "type": "SOLID", + "width": "0.12mm", + "color": "#999999" + }, + "right": { + "type": "SOLID", + "width": "0.12mm", + "color": "#999999" + }, + "top": { + "type": "SOLID", + "width": "0.5mm", + "color": "#BBBBBB" + }, + "bottom": { + "type": "SOLID", + "width": "0.12mm", + "color": "#BBBBBB" + }, + "css": { + "border-left": "0.12mm solid #999999", + "border-right": "0.12mm solid #999999", + "border-top": "0.5mm solid #BBBBBB", + "border-bottom": "0.12mm solid #BBBBBB" + } + }, + "18": { + "id": 18, + "left": { + "type": "SOLID", + "width": "0.12mm", + "color": "#999999" + }, + "right": { + "type": "SOLID", + "width": "0.12mm", + "color": "#999999" + }, + "top": { + "type": "SOLID", + "width": "0.12mm", + "color": "#BBBBBB" + }, + "bottom": { + "type": "SOLID", + "width": "0.5mm", + "color": "#BBBBBB" + }, + "css": { + "border-left": "0.12mm solid #999999", + "border-right": "0.12mm solid #999999", + "border-top": "0.12mm solid #BBBBBB", + "border-bottom": "0.5mm solid #BBBBBB" + } + }, + "19": { + "id": 19, + "left": { + "type": "SOLID", + "width": "0.12mm", + "color": "#999999" + }, + "right": { + "type": "NONE", + "width": "0.12mm", + "color": "#BBBBBB" + }, + "top": { + "type": "SOLID", + "width": "0.12mm", + "color": "#BBBBBB" + }, + "bottom": { + "type": "SOLID", + "width": "0.5mm", + "color": "#BBBBBB" + }, + "css": { + "border-left": "0.12mm solid #999999", + "border-right": "none", + "border-top": "0.12mm solid #BBBBBB", + "border-bottom": "0.5mm solid #BBBBBB" + } + }, + "20": { + "id": 20, + "left": { + "type": "SOLID", + "width": "0.12mm", + "color": "#999999" + }, + "right": { + "type": "NONE", + "width": "0.12mm", + "color": "#BBBBBB" + }, + "top": { + "type": "SOLID", + "width": "0.5mm", + "color": "#BBBBBB" + }, + "bottom": { + "type": "SOLID", + "width": "0.3mm", + "color": "#BBBBBB" + }, + "css": { + "border-left": "0.12mm solid #999999", + "border-right": "none", + "border-top": "0.5mm solid #BBBBBB", + "border-bottom": "0.3mm solid #BBBBBB" + } + } + }, + "tables": [ + { + "index": 0, + "rowCnt": 1, + "colCnt": 3, + "repeatHeader": true, + "pageBreak": "CELL", + "rows": [ + [ + { + "borderFillIDRef": 5, + "isHeader": false, + "colAddr": 0, + "rowAddr": 0, + "colSpan": 1, + "rowSpan": 1, + "width_hu": 16723, + "height_hu": 282, + "cellMargin": { + "left": 510, + "right": 510, + "top": 141, + "bottom": 141 + }, + "text": "기술 로 사람 과 자연 이 함께하는 세상을 만들어 갑니다.", + "charPrIDRefs": [ + 9, + 8, + 10, + 8, + 11, + 8, + 8 + ], + "primaryCharPrIDRef": 9, + "paraPrIDRefs": [ + 5, + 5 + ], + "primaryParaPrIDRef": 5, + "styleIDRefs": [ + 0, + 0 + ], + "lines": [ + "기술 로 사람 과 자연 이", + "함께하는 세상을 만들어 갑니다." + ] + }, + { + "borderFillIDRef": 5, + "isHeader": false, + "colAddr": 1, + "rowAddr": 0, + "colSpan": 1, + "rowSpan": 1, + "width_hu": 2856, + "height_hu": 282, + "cellMargin": { + "left": 510, + "right": 510, + "top": 141, + "bottom": 141 + }, + "charPrIDRefs": [ + 8 + ], + "primaryCharPrIDRef": 8, + "paraPrIDRefs": [ + 3 + ], + "primaryParaPrIDRef": 3, + "styleIDRefs": [ + 0 + ] + }, + { + "borderFillIDRef": 5, + "isHeader": false, + "colAddr": 2, + "rowAddr": 0, + "colSpan": 1, + "rowSpan": 1, + "width_hu": 28043, + "height_hu": 282, + "cellMargin": { + "left": 510, + "right": 510, + "top": 141, + "bottom": 141 + }, + "charPrIDRefs": [ + 12, + 8 + ], + "primaryCharPrIDRef": 12, + "paraPrIDRefs": [ + 6, + 6 + ], + "primaryParaPrIDRef": 6, + "styleIDRefs": [ + 0, + 0 + ] + } + ] + ], + "colWidths_hu": [ + 16723, + 2856, + 28043 + ], + "colWidths_pct": [ + 35, + 6, + 59 + ] + }, + { + "index": 1, + "rowCnt": 1, + "colCnt": 3, + "repeatHeader": true, + "pageBreak": "CELL", + "rows": [ + [ + { + "borderFillIDRef": 5, + "isHeader": false, + "colAddr": 0, + "rowAddr": 0, + "colSpan": 1, + "rowSpan": 1, + "width_hu": 11912, + "height_hu": 282, + "cellMargin": { + "left": 510, + "right": 510, + "top": 141, + "bottom": 141 + }, + "text": "총괄기획실 기술기획팀", + "charPrIDRefs": [ + 7, + 8 + ], + "primaryCharPrIDRef": 7, + "paraPrIDRefs": [ + 5, + 5 + ], + "primaryParaPrIDRef": 5, + "styleIDRefs": [ + 0, + 0 + ], + "lines": [ + "총괄기획실", + "기술기획팀" + ] + }, + { + "borderFillIDRef": 5, + "isHeader": false, + "colAddr": 1, + "rowAddr": 0, + "colSpan": 1, + "rowSpan": 1, + "width_hu": 7950, + "height_hu": 282, + "cellMargin": { + "left": 510, + "right": 510, + "top": 141, + "bottom": 141 + }, + "charPrIDRefs": [ + 8 + ], + "primaryCharPrIDRef": 8, + "paraPrIDRefs": [ + 4 + ], + "primaryParaPrIDRef": 4, + "styleIDRefs": [ + 0 + ] + }, + { + "borderFillIDRef": 5, + "isHeader": false, + "colAddr": 2, + "rowAddr": 0, + "colSpan": 1, + "rowSpan": 1, + "width_hu": 27760, + "height_hu": 282, + "cellMargin": { + "left": 510, + "right": 510, + "top": 141, + "bottom": 141 + }, + "text": "2025. 2. 5(목)", + "charPrIDRefs": [ + 7 + ], + "primaryCharPrIDRef": 7, + "paraPrIDRefs": [ + 6 + ], + "primaryParaPrIDRef": 6, + "styleIDRefs": [ + 0 + ], + "lines": [ + "2025. 2. 5(목)" + ] + } + ] + ], + "colWidths_hu": [ + 11912, + 7950, + 27760 + ], + "colWidths_pct": [ + 25, + 17, + 58 + ] + }, + { + "index": 2, + "rowCnt": 1, + "colCnt": 2, + "repeatHeader": true, + "pageBreak": "CELL", + "rows": [ + [ + { + "borderFillIDRef": 4, + "isHeader": false, + "colAddr": 0, + "rowAddr": 0, + "colSpan": 1, + "rowSpan": 1, + "width_hu": 773, + "height_hu": 0, + "cellMargin": { + "left": 141, + "right": 141, + "top": 283, + "bottom": 567 + }, + "charPrIDRefs": [ + 4 + ], + "primaryCharPrIDRef": 4, + "paraPrIDRefs": [ + 3 + ], + "primaryParaPrIDRef": 3, + "styleIDRefs": [ + 0 + ] + }, + { + "borderFillIDRef": 4, + "isHeader": false, + "colAddr": 1, + "rowAddr": 0, + "colSpan": 1, + "rowSpan": 1, + "width_hu": 47185, + "height_hu": 0, + "cellMargin": { + "left": 141, + "right": 141, + "top": 283, + "bottom": 567 + }, + "text": "AI 업무 활용 적용 사례 발표 계획(안)", + "charPrIDRefs": [ + 4 + ], + "primaryCharPrIDRef": 4, + "paraPrIDRefs": [ + 3 + ], + "primaryParaPrIDRef": 3, + "styleIDRefs": [ + 0 + ], + "lines": [ + "AI 업무 활용 적용 사례 발표 계획(안)" + ] + } + ] + ], + "colWidths_hu": [ + 773, + 47185 + ], + "colWidths_pct": [ + 2, + 98 + ] + }, + { + "index": 3, + "rowCnt": 5, + "colCnt": 4, + "repeatHeader": true, + "pageBreak": "CELL", + "rows": [ + [ + { + "borderFillIDRef": 10, + "isHeader": false, + "colAddr": 0, + "rowAddr": 0, + "colSpan": 2, + "rowSpan": 1, + "width_hu": 14354, + "height_hu": 1850, + "cellMargin": { + "left": 142, + "right": 142, + "top": 425, + "bottom": 425 + }, + "text": "구분", + "charPrIDRefs": [ + 20 + ], + "primaryCharPrIDRef": 20, + "paraPrIDRefs": [ + 4 + ], + "primaryParaPrIDRef": 4, + "styleIDRefs": [ + 0 + ], + "lines": [ + "구분" + ] + }, + { + "borderFillIDRef": 13, + "isHeader": false, + "colAddr": 2, + "rowAddr": 0, + "colSpan": 1, + "rowSpan": 1, + "width_hu": 27183, + "height_hu": 1850, + "cellMargin": { + "left": 142, + "right": 142, + "top": 425, + "bottom": 425 + }, + "text": "내용", + "charPrIDRefs": [ + 20 + ], + "primaryCharPrIDRef": 20, + "paraPrIDRefs": [ + 4 + ], + "primaryParaPrIDRef": 4, + "styleIDRefs": [ + 0 + ], + "lines": [ + "내용" + ] + }, + { + "borderFillIDRef": 14, + "isHeader": false, + "colAddr": 3, + "rowAddr": 0, + "colSpan": 1, + "rowSpan": 1, + "width_hu": 5392, + "height_hu": 1850, + "cellMargin": { + "left": 142, + "right": 142, + "top": 425, + "bottom": 425 + }, + "text": "비고", + "charPrIDRefs": [ + 20 + ], + "primaryCharPrIDRef": 20, + "paraPrIDRefs": [ + 4 + ], + "primaryParaPrIDRef": 4, + "styleIDRefs": [ + 0 + ], + "lines": [ + "비고" + ] + } + ], + [ + { + "borderFillIDRef": 12, + "isHeader": false, + "colAddr": 0, + "rowAddr": 1, + "colSpan": 1, + "rowSpan": 2, + "width_hu": 5054, + "height_hu": 20930, + "cellMargin": { + "left": 142, + "right": 142, + "top": 425, + "bottom": 425 + }, + "text": "소개", + "charPrIDRefs": [ + 20 + ], + "primaryCharPrIDRef": 20, + "paraPrIDRefs": [ + 4 + ], + "primaryParaPrIDRef": 4, + "styleIDRefs": [ + 0 + ], + "lines": [ + "소개" + ] + }, + { + "borderFillIDRef": 8, + "isHeader": false, + "colAddr": 1, + "rowAddr": 1, + "colSpan": 1, + "rowSpan": 1, + "width_hu": 9300, + "height_hu": 12265, + "cellMargin": { + "left": 142, + "right": 142, + "top": 425, + "bottom": 425 + }, + "text": "개요", + "charPrIDRefs": [ + 20 + ], + "primaryCharPrIDRef": 20, + "paraPrIDRefs": [ + 4 + ], + "primaryParaPrIDRef": 4, + "styleIDRefs": [ + 0 + ], + "lines": [ + "개요" + ] + }, + { + "borderFillIDRef": 7, + "isHeader": false, + "colAddr": 2, + "rowAddr": 1, + "colSpan": 1, + "rowSpan": 1, + "width_hu": 27183, + "height_hu": 12265, + "cellMargin": { + "left": 142, + "right": 142, + "top": 425, + "bottom": 425 + }, + "text": "- 현황 및 문제점 : 인적 오류와 추가적 리소스(인력, 시간) 투입 · 동일한 원천데이터로 산출물 형식만 달라짐 (제안서, 보고서 등) · 발주처별 상이한 양식과 확장자로 재편집 - 기대효과 : 문서 작업 시간의 단축, 반복 잡업의 감소, 오류 절감", + "charPrIDRefs": [ + 24, + 24, + 24, + 24, + 24 + ], + "primaryCharPrIDRef": 24, + "paraPrIDRefs": [ + 11, + 11, + 11, + 11, + 11 + ], + "primaryParaPrIDRef": 11, + "styleIDRefs": [ + 0, + 0, + 0, + 0, + 0 + ], + "lines": [ + "- 현황 및 문제점 : 인적 오류와 추가적 리소스(인력, 시간) 투입", + "· 동일한 원천데이터로 산출물 형식만 달라짐 (제안서, 보고서 등)", + "· 발주처별 상이한 양식과 확장자로 재편집", + "- 기대효과 : 문서 작업 시간의 단축, 반복 잡업의 감소, 오류 절감" + ] + }, + { + "borderFillIDRef": 19, + "isHeader": false, + "colAddr": 3, + "rowAddr": 1, + "colSpan": 1, + "rowSpan": 2, + "width_hu": 5392, + "height_hu": 20930, + "cellMargin": { + "left": 142, + "right": 142, + "top": 425, + "bottom": 425 + }, + "text": "1p", + "charPrIDRefs": [ + 23 + ], + "primaryCharPrIDRef": 23, + "paraPrIDRefs": [ + 4 + ], + "primaryParaPrIDRef": 4, + "styleIDRefs": [ + 0 + ], + "lines": [ + "1p" + ] + } + ], + [ + { + "borderFillIDRef": 16, + "isHeader": false, + "colAddr": 1, + "rowAddr": 2, + "colSpan": 1, + "rowSpan": 1, + "width_hu": 9300, + "height_hu": 8665, + "cellMargin": { + "left": 142, + "right": 142, + "top": 425, + "bottom": 425 + }, + "text": "글벗 소개", + "charPrIDRefs": [ + 20 + ], + "primaryCharPrIDRef": 20, + "paraPrIDRefs": [ + 4 + ], + "primaryParaPrIDRef": 4, + "styleIDRefs": [ + 0 + ], + "lines": [ + "글벗 소개" + ] + }, + { + "borderFillIDRef": 18, + "isHeader": false, + "colAddr": 2, + "rowAddr": 2, + "colSpan": 1, + "rowSpan": 1, + "width_hu": 27183, + "height_hu": 8665, + "cellMargin": { + "left": 142, + "right": 142, + "top": 425, + "bottom": 425 + }, + "text": "- 글벗 기능 소개 · (Input) 로컬, 링크, HTML 구조 · (Process) 목차 구성 및 문서 작성 / (Edit) 편집기 · (Export) 인쇄, PDF, HWP", + "charPrIDRefs": [ + 24, + 24, + 24, + 24 + ], + "primaryCharPrIDRef": 24, + "paraPrIDRefs": [ + 11, + 11, + 11, + 11 + ], + "primaryParaPrIDRef": 11, + "styleIDRefs": [ + 0, + 0, + 0, + 0 + ], + "lines": [ + "- 글벗 기능 소개", + "· (Input) 로컬, 링크, HTML 구조", + "· (Process) 목차 구성 및 문서 작성 / (Edit) 편집기", + "· (Export) 인쇄, PDF, HWP" + ] + } + ], + [ + { + "borderFillIDRef": 11, + "isHeader": false, + "colAddr": 0, + "rowAddr": 3, + "colSpan": 1, + "rowSpan": 2, + "width_hu": 5054, + "height_hu": 13730, + "cellMargin": { + "left": 142, + "right": 142, + "top": 425, + "bottom": 425 + }, + "text": "시연", + "charPrIDRefs": [ + 20 + ], + "primaryCharPrIDRef": 20, + "paraPrIDRefs": [ + 4 + ], + "primaryParaPrIDRef": 4, + "styleIDRefs": [ + 0 + ], + "lines": [ + "시연" + ] + }, + { + "borderFillIDRef": 15, + "isHeader": false, + "colAddr": 1, + "rowAddr": 3, + "colSpan": 1, + "rowSpan": 1, + "width_hu": 9300, + "height_hu": 8665, + "cellMargin": { + "left": 142, + "right": 142, + "top": 425, + "bottom": 425 + }, + "text": "글벗 시연", + "charPrIDRefs": [ + 20 + ], + "primaryCharPrIDRef": 20, + "paraPrIDRefs": [ + 4 + ], + "primaryParaPrIDRef": 4, + "styleIDRefs": [ + 0 + ], + "lines": [ + "글벗 시연" + ] + }, + { + "borderFillIDRef": 17, + "isHeader": false, + "colAddr": 2, + "rowAddr": 3, + "colSpan": 1, + "rowSpan": 1, + "width_hu": 27183, + "height_hu": 8665, + "cellMargin": { + "left": 142, + "right": 142, + "top": 425, + "bottom": 425 + }, + "text": "- (기능 1) (Input) 업로드한 문서 기반 목차 정리 / 작성 - (기능 2) (Process) 웹 편집기 - (기능 3) (Export) PDF와 HWP 추출", + "charPrIDRefs": [ + 24, + 23, + 24, + 23, + 24, + 23 + ], + "primaryCharPrIDRef": 24, + "paraPrIDRefs": [ + 11, + 11, + 11 + ], + "primaryParaPrIDRef": 11, + "styleIDRefs": [ + 0, + 0, + 0 + ], + "lines": [ + "- (기능 1) (Input) 업로드한 문서 기반 목차 정리 / 작성", + "- (기능 2) (Process) 웹 편집기", + "- (기능 3) (Export) PDF와 HWP 추출" + ] + }, + { + "borderFillIDRef": 20, + "isHeader": false, + "colAddr": 3, + "rowAddr": 3, + "colSpan": 1, + "rowSpan": 2, + "width_hu": 5392, + "height_hu": 13730, + "cellMargin": { + "left": 142, + "right": 142, + "top": 425, + "bottom": 425 + }, + "text": "글벗 & Visual Studio", + "charPrIDRefs": [ + 23, + 23 + ], + "primaryCharPrIDRef": 23, + "paraPrIDRefs": [ + 4, + 4 + ], + "primaryParaPrIDRef": 4, + "styleIDRefs": [ + 0, + 0 + ], + "lines": [ + "글벗 &", + "Visual Studio" + ] + } + ], + [ + { + "borderFillIDRef": 9, + "isHeader": false, + "colAddr": 1, + "rowAddr": 4, + "colSpan": 1, + "rowSpan": 1, + "width_hu": 9300, + "height_hu": 5065, + "cellMargin": { + "left": 142, + "right": 142, + "top": 425, + "bottom": 425 + }, + "text": "글벗 만드는 과정", + "charPrIDRefs": [ + 20, + 20 + ], + "primaryCharPrIDRef": 20, + "paraPrIDRefs": [ + 4, + 4 + ], + "primaryParaPrIDRef": 4, + "styleIDRefs": [ + 0, + 0 + ], + "lines": [ + "글벗 만드는", + "과정" + ] + }, + { + "borderFillIDRef": 6, + "isHeader": false, + "colAddr": 2, + "rowAddr": 4, + "colSpan": 1, + "rowSpan": 1, + "width_hu": 27183, + "height_hu": 5065, + "cellMargin": { + "left": 142, + "right": 142, + "top": 425, + "bottom": 425 + }, + "text": "AI에게 활용할 자료 제공하기 AI를 활용하여 코딩하기", + "charPrIDRefs": [ + 24, + 24 + ], + "primaryCharPrIDRef": 24, + "paraPrIDRefs": [ + 16, + 16 + ], + "primaryParaPrIDRef": 16, + "styleIDRefs": [ + 0, + 0 + ], + "lines": [ + "AI에게 활용할 자료 제공하기", + "AI를 활용하여 코딩하기" + ] + } + ] + ], + "colWidths_hu": [ + 5054, + 9300, + 27183, + 5392 + ], + "colWidths_pct": [ + 11, + 20, + 58, + 11 + ] + } + ], + "header": { + "exists": true, + "hidden": false, + "texts": [ + "총괄기획실", + "기술기획팀", + "2025. 2. 5(목)" + ], + "type": "table", + "table": { + "rowCnt": 1, + "colCnt": 3, + "rows": [ + [ + { + "borderFillIDRef": 5, + "colAddr": 0, + "rowAddr": 0, + "colSpan": 1, + "rowSpan": 1, + "width_hu": 11912, + "text": "총괄기획실 기술기획팀", + "lines": [ + "총괄기획실", + "기술기획팀" + ] + }, + { + "borderFillIDRef": 5, + "colAddr": 1, + "rowAddr": 0, + "colSpan": 1, + "rowSpan": 1, + "width_hu": 7950 + }, + { + "borderFillIDRef": 5, + "colAddr": 2, + "rowAddr": 0, + "colSpan": 1, + "rowSpan": 1, + "width_hu": 27760, + "text": "2025. 2. 5(목)", + "lines": [ + "2025. 2. 5(목)" + ] + } + ] + ] + } + }, + "footer": { + "exists": true, + "hidden": false, + "texts": [ + "기술", + "로", + "사람", + "과", + "자연", + "이", + "함께하는 세상을 만들어 갑니다." + ], + "type": "table", + "table": { + "rowCnt": 1, + "colCnt": 3, + "rows": [ + [ + { + "borderFillIDRef": 5, + "colAddr": 0, + "rowAddr": 0, + "colSpan": 1, + "rowSpan": 1, + "width_hu": 16723, + "text": "기술 로 사람 과 자연 이 함께하는 세상을 만들어 갑니다.", + "lines": [ + "기술 로 사람 과 자연 이", + "함께하는 세상을 만들어 갑니다." + ] + }, + { + "borderFillIDRef": 5, + "colAddr": 1, + "rowAddr": 0, + "colSpan": 1, + "rowSpan": 1, + "width_hu": 2856 + }, + { + "borderFillIDRef": 5, + "colAddr": 2, + "rowAddr": 0, + "colSpan": 1, + "rowSpan": 1, + "width_hu": 28043 + } + ] + ] + } + }, + "section": { + "textDirection": "HORIZONTAL", + "hideFirstHeader": false, + "hideFirstFooter": false, + "hideFirstMasterPage": false, + "hideFirstPageNum": false, + "hideFirstEmptyLine": false, + "startNum": { + "pageStartsOn": "BOTH", + "page": 0 + } + }, + "styles": [ + { + "id": 0, + "paraPrIDRef": 3, + "charPrIDRef": 0, + "nextStyleIDRef": 0, + "type": "PARA", + "name": "바탕글", + "engName": "Normal" + }, + { + "id": 1, + "paraPrIDRef": 2, + "charPrIDRef": 3, + "nextStyleIDRef": 1, + "type": "PARA", + "name": "머리말", + "engName": "Header" + }, + { + "id": 2, + "paraPrIDRef": 1, + "charPrIDRef": 2, + "nextStyleIDRef": 2, + "type": "PARA", + "name": "쪽 번호", + "engName": "Page Number" + }, + { + "id": 3, + "paraPrIDRef": 0, + "charPrIDRef": 1, + "nextStyleIDRef": 3, + "type": "PARA", + "name": "각주", + "engName": "Footnote" + }, + { + "id": 4, + "paraPrIDRef": 0, + "charPrIDRef": 1, + "nextStyleIDRef": 4, + "type": "PARA", + "name": "미주", + "engName": "Endnote" + }, + { + "id": 5, + "paraPrIDRef": 4, + "charPrIDRef": 6, + "nextStyleIDRef": 5, + "type": "PARA", + "name": "표위", + "engName": "Memo" + }, + { + "id": 6, + "paraPrIDRef": 8, + "charPrIDRef": 0, + "nextStyleIDRef": 6, + "type": "PARA", + "name": "표옆", + "engName": "" + }, + { + "id": 7, + "paraPrIDRef": 10, + "charPrIDRef": 0, + "nextStyleIDRef": 7, + "type": "PARA", + "name": "표내용", + "engName": "" + }, + { + "id": 8, + "paraPrIDRef": 9, + "charPrIDRef": 13, + "nextStyleIDRef": 8, + "type": "PARA", + "name": "주)", + "engName": "" + }, + { + "id": 9, + "paraPrIDRef": 14, + "charPrIDRef": 18, + "nextStyleIDRef": 9, + "type": "PARA", + "name": "#큰아이콘", + "engName": "" + }, + { + "id": 10, + "paraPrIDRef": 21, + "charPrIDRef": 25, + "nextStyleIDRef": 10, + "type": "PARA", + "name": "개요1", + "engName": "" + }, + { + "id": 11, + "paraPrIDRef": 20, + "charPrIDRef": 26, + "nextStyleIDRef": 11, + "type": "PARA", + "name": "xl63", + "engName": "xl63" + } + ], + "numbering": { + "numberings": [ + { + "id": 1, + "start": 0, + "levels": [ + { + "level": 1, + "numFormat": "DIGIT", + "align": "LEFT", + "pattern": "^1." + }, + { + "level": 2, + "numFormat": "HANGUL_SYLLABLE", + "align": "LEFT", + "pattern": "^2." + }, + { + "level": 3, + "numFormat": "DIGIT", + "align": "LEFT", + "pattern": "^3)" + }, + { + "level": 4, + "numFormat": "HANGUL_SYLLABLE", + "align": "LEFT", + "pattern": "^4)" + }, + { + "level": 5, + "numFormat": "DIGIT", + "align": "LEFT", + "pattern": "(^5)" + }, + { + "level": 6, + "numFormat": "HANGUL_SYLLABLE", + "align": "LEFT", + "pattern": "(^6)" + }, + { + "level": 7, + "numFormat": "CIRCLED_DIGIT", + "align": "LEFT", + "pattern": "^7" + } + ] + } + ], + "bullets": [ + { + "id": 1, + "char": "-", + "useImage": false + } + ] + }, + "images": [ + { + "type": "image", + "width_hu": 1133, + "height_hu": 1133, + "width_mm": 4.0, + "height_mm": 4.0, + "offset": { + "x": 0, + "y": 0 + } + }, + { + "type": "image", + "width_hu": 624, + "height_hu": 624, + "width_mm": 2.2, + "height_mm": 2.2, + "offset": { + "x": 0, + "y": 0 + } + }, + { + "type": "image", + "width_hu": 1133, + "height_hu": 1133, + "width_mm": 4.0, + "height_mm": 4.0, + "offset": { + "x": 0, + "y": 0 + } + }, + { + "type": "image", + "width_hu": 624, + "height_hu": 624, + "width_mm": 2.2, + "height_mm": 2.2, + "offset": { + "x": 0, + "y": 0 + } + }, + { + "type": "image", + "width_hu": 624, + "height_hu": 624, + "width_mm": 2.2, + "height_mm": 2.2, + "offset": { + "x": 0, + "y": 0 + } + } + ], + "content_order": [ + { + "index": 0, + "paraPrIDRef": "7", + "styleIDRef": "0", + "type": "table", + "table_idx": 0, + "rowCnt": "1", + "colCnt": "2", + "borderFillIDRef": "3" + }, + { + "index": 1, + "paraPrIDRef": "17", + "styleIDRef": "0", + "type": "empty" + }, + { + "index": 2, + "paraPrIDRef": "15", + "styleIDRef": "9", + "type": "image", + "image_idx": 0, + "binaryItemIDRef": "image1", + "text": " 개요", + "imgRect": { + "x": null, + "y": null, + "w": null, + "h": null + } + }, + { + "index": 3, + "paraPrIDRef": "12", + "styleIDRef": "7", + "type": "image", + "image_idx": 1, + "binaryItemIDRef": "image2", + "text": " AI를 활용한 “업무 효율성 개선 사례”와 이를 구현한 방식에 대한 공유", + "imgRect": { + "x": null, + "y": null, + "w": null, + "h": null + } + }, + { + "index": 4, + "paraPrIDRef": "18", + "styleIDRef": "7", + "type": "paragraph", + "text": "삼안의 임원 대상 「글벗」 소개와 이를 구현한 방식에 대한 예시 시연", + "charPrIDRef": "22", + "runs": [ + { + "charPrIDRef": "22", + "text": "삼안의 임원 대상 「글벗」 소개와 이를 구현한 방식에 대한 예시 시연" + } + ] + }, + { + "index": 5, + "paraPrIDRef": "22", + "styleIDRef": "7", + "type": "empty" + }, + { + "index": 6, + "paraPrIDRef": "19", + "styleIDRef": "9", + "type": "image", + "image_idx": 2, + "binaryItemIDRef": "image1", + "text": " 발표 구성(안)", + "imgRect": { + "x": null, + "y": null, + "w": null, + "h": null + } + }, + { + "index": 7, + "paraPrIDRef": "12", + "styleIDRef": "7", + "type": "image", + "image_idx": 3, + "binaryItemIDRef": "image2", + "text": " 제목 : AI 활용 문서 업무 개선 사례 -「글벗」(사용자의 글쓰기를 돕는 친구) -", + "imgRect": { + "x": null, + "y": null, + "w": null, + "h": null + } + }, + { + "index": 8, + "paraPrIDRef": "12", + "styleIDRef": "7", + "type": "image", + "image_idx": 4, + "binaryItemIDRef": "image2", + "text": " 발표 내용 ", + "imgRect": { + "x": null, + "y": null, + "w": null, + "h": null + } + }, + { + "index": 9, + "paraPrIDRef": "13", + "styleIDRef": "0", + "type": "table", + "table_idx": 1, + "rowCnt": "5", + "colCnt": "4", + "borderFillIDRef": "3" + }, + { + "index": 10, + "paraPrIDRef": "13", + "styleIDRef": "0", + "type": "empty" + }, + { + "index": 11, + "paraPrIDRef": "22", + "styleIDRef": "7", + "type": "empty" + }, + { + "index": 12, + "paraPrIDRef": "22", + "styleIDRef": "7", + "type": "empty" + } + ] + }, + "css": "", + "fonts": {}, + "colors": { + "background": [ + "#EDEDED", + "#DCDCDC" + ], + "border": [ + "#000000", + "#3057B9", + "#999999", + "#BBBBBB" + ], + "text": [] + }, + "border_fills": { + "1": { + "id": 1, + "left": { + "type": "NONE", + "width": "0.1mm", + "color": "#000000" + }, + "right": { + "type": "NONE", + "width": "0.1mm", + "color": "#000000" + }, + "top": { + "type": "NONE", + "width": "0.1mm", + "color": "#000000" + }, + "bottom": { + "type": "NONE", + "width": "0.1mm", + "color": "#000000" + }, + "diagonal": { + "type": "SOLID", + "width": "0.1mm", + "color": "#000000" + }, + "css": { + "border-left": "none", + "border-right": "none", + "border-top": "none", + "border-bottom": "none" + } + }, + "2": { + "id": 2, + "left": { + "type": "NONE", + "width": "0.1mm", + "color": "#000000" + }, + "right": { + "type": "NONE", + "width": "0.1mm", + "color": "#000000" + }, + "top": { + "type": "NONE", + "width": "0.1mm", + "color": "#000000" + }, + "bottom": { + "type": "NONE", + "width": "0.1mm", + "color": "#000000" + }, + "diagonal": { + "type": "SOLID", + "width": "0.1mm", + "color": "#000000" + }, + "css": { + "border-left": "none", + "border-right": "none", + "border-top": "none", + "border-bottom": "none" + } + }, + "3": { + "id": 3, + "left": { + "type": "SOLID", + "width": "0.12mm", + "color": "#000000" + }, + "right": { + "type": "SOLID", + "width": "0.12mm", + "color": "#000000" + }, + "top": { + "type": "SOLID", + "width": "0.12mm", + "color": "#000000" + }, + "bottom": { + "type": "SOLID", + "width": "0.12mm", + "color": "#000000" + }, + "diagonal": { + "type": "SOLID", + "width": "0.1mm", + "color": "#000000" + }, + "css": { + "border-left": "0.12mm solid #000000", + "border-right": "0.12mm solid #000000", + "border-top": "0.12mm solid #000000", + "border-bottom": "0.12mm solid #000000" + } + }, + "4": { + "id": 4, + "left": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "right": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "top": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "bottom": { + "type": "SOLID", + "width": "0.7mm", + "color": "#3057B9" + }, + "diagonal": { + "type": "SOLID", + "width": "0.1mm", + "color": "#000000" + }, + "css": { + "border-left": "none", + "border-right": "none", + "border-top": "none", + "border-bottom": "0.7mm solid #3057B9" + } + }, + "5": { + "id": 5, + "left": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "right": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "top": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "bottom": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "diagonal": { + "type": "SOLID", + "width": "0.1mm", + "color": "#000000" + }, + "css": { + "border-left": "none", + "border-right": "none", + "border-top": "none", + "border-bottom": "none" + } + }, + "6": { + "id": 6, + "left": { + "type": "SOLID", + "width": "0.12mm", + "color": "#999999" + }, + "right": { + "type": "SOLID", + "width": "0.12mm", + "color": "#999999" + }, + "top": { + "type": "SOLID", + "width": "0.12mm", + "color": "#BBBBBB" + }, + "bottom": { + "type": "SOLID", + "width": "0.3mm", + "color": "#BBBBBB" + }, + "css": { + "border-left": "0.12mm solid #999999", + "border-right": "0.12mm solid #999999", + "border-top": "0.12mm solid #BBBBBB", + "border-bottom": "0.3mm solid #BBBBBB" + } + }, + "7": { + "id": 7, + "left": { + "type": "SOLID", + "width": "0.12mm", + "color": "#999999" + }, + "right": { + "type": "SOLID", + "width": "0.12mm", + "color": "#999999" + }, + "top": { + "type": "SOLID", + "width": "0.12mm", + "color": "#BBBBBB" + }, + "bottom": { + "type": "SOLID", + "width": "0.12mm", + "color": "#BBBBBB" + }, + "css": { + "border-left": "0.12mm solid #999999", + "border-right": "0.12mm solid #999999", + "border-top": "0.12mm solid #BBBBBB", + "border-bottom": "0.12mm solid #BBBBBB" + } + }, + "8": { + "id": 8, + "left": { + "type": "SOLID", + "width": "0.12mm", + "color": "#999999" + }, + "right": { + "type": "SOLID", + "width": "0.12mm", + "color": "#999999" + }, + "top": { + "type": "SOLID", + "width": "0.12mm", + "color": "#BBBBBB" + }, + "bottom": { + "type": "SOLID", + "width": "0.12mm", + "color": "#BBBBBB" + }, + "background": "#EDEDED", + "css": { + "border-left": "0.12mm solid #999999", + "border-right": "0.12mm solid #999999", + "border-top": "0.12mm solid #BBBBBB", + "border-bottom": "0.12mm solid #BBBBBB", + "background-color": "#EDEDED" + } + }, + "9": { + "id": 9, + "left": { + "type": "SOLID", + "width": "0.12mm", + "color": "#999999" + }, + "right": { + "type": "SOLID", + "width": "0.12mm", + "color": "#999999" + }, + "top": { + "type": "SOLID", + "width": "0.12mm", + "color": "#BBBBBB" + }, + "bottom": { + "type": "SOLID", + "width": "0.3mm", + "color": "#BBBBBB" + }, + "background": "#EDEDED", + "css": { + "border-left": "0.12mm solid #999999", + "border-right": "0.12mm solid #999999", + "border-top": "0.12mm solid #BBBBBB", + "border-bottom": "0.3mm solid #BBBBBB", + "background-color": "#EDEDED" + } + }, + "10": { + "id": 10, + "left": { + "type": "NONE", + "width": "0.12mm", + "color": "#999999" + }, + "right": { + "type": "SOLID", + "width": "0.12mm", + "color": "#999999" + }, + "top": { + "type": "SOLID", + "width": "0.3mm", + "color": "#BBBBBB" + }, + "bottom": { + "type": "SOLID", + "width": "0.12mm", + "color": "#BBBBBB" + }, + "background": "#DCDCDC", + "css": { + "border-left": "none", + "border-right": "0.12mm solid #999999", + "border-top": "0.3mm solid #BBBBBB", + "border-bottom": "0.12mm solid #BBBBBB", + "background-color": "#DCDCDC" + } + }, + "11": { + "id": 11, + "left": { + "type": "NONE", + "width": "0.12mm", + "color": "#999999" + }, + "right": { + "type": "SOLID", + "width": "0.12mm", + "color": "#999999" + }, + "top": { + "type": "SOLID", + "width": "0.5mm", + "color": "#BBBBBB" + }, + "bottom": { + "type": "SOLID", + "width": "0.3mm", + "color": "#BBBBBB" + }, + "background": "#EDEDED", + "css": { + "border-left": "none", + "border-right": "0.12mm solid #999999", + "border-top": "0.5mm solid #BBBBBB", + "border-bottom": "0.3mm solid #BBBBBB", + "background-color": "#EDEDED" + } + }, + "12": { + "id": 12, + "left": { + "type": "NONE", + "width": "0.12mm", + "color": "#999999" + }, + "right": { + "type": "SOLID", + "width": "0.12mm", + "color": "#999999" + }, + "top": { + "type": "SOLID", + "width": "0.12mm", + "color": "#BBBBBB" + }, + "bottom": { + "type": "SOLID", + "width": "0.5mm", + "color": "#BBBBBB" + }, + "background": "#EDEDED", + "css": { + "border-left": "none", + "border-right": "0.12mm solid #999999", + "border-top": "0.12mm solid #BBBBBB", + "border-bottom": "0.5mm solid #BBBBBB", + "background-color": "#EDEDED" + } + }, + "13": { + "id": 13, + "left": { + "type": "SOLID", + "width": "0.12mm", + "color": "#999999" + }, + "right": { + "type": "SOLID", + "width": "0.12mm", + "color": "#999999" + }, + "top": { + "type": "SOLID", + "width": "0.3mm", + "color": "#BBBBBB" + }, + "bottom": { + "type": "SOLID", + "width": "0.12mm", + "color": "#BBBBBB" + }, + "background": "#DCDCDC", + "css": { + "border-left": "0.12mm solid #999999", + "border-right": "0.12mm solid #999999", + "border-top": "0.3mm solid #BBBBBB", + "border-bottom": "0.12mm solid #BBBBBB", + "background-color": "#DCDCDC" + } + }, + "14": { + "id": 14, + "left": { + "type": "SOLID", + "width": "0.12mm", + "color": "#999999" + }, + "right": { + "type": "NONE", + "width": "0.12mm", + "color": "#999999" + }, + "top": { + "type": "SOLID", + "width": "0.3mm", + "color": "#BBBBBB" + }, + "bottom": { + "type": "SOLID", + "width": "0.12mm", + "color": "#BBBBBB" + }, + "background": "#DCDCDC", + "css": { + "border-left": "0.12mm solid #999999", + "border-right": "none", + "border-top": "0.3mm solid #BBBBBB", + "border-bottom": "0.12mm solid #BBBBBB", + "background-color": "#DCDCDC" + } + }, + "15": { + "id": 15, + "left": { + "type": "SOLID", + "width": "0.12mm", + "color": "#999999" + }, + "right": { + "type": "SOLID", + "width": "0.12mm", + "color": "#999999" + }, + "top": { + "type": "SOLID", + "width": "0.5mm", + "color": "#BBBBBB" + }, + "bottom": { + "type": "SOLID", + "width": "0.12mm", + "color": "#BBBBBB" + }, + "background": "#EDEDED", + "css": { + "border-left": "0.12mm solid #999999", + "border-right": "0.12mm solid #999999", + "border-top": "0.5mm solid #BBBBBB", + "border-bottom": "0.12mm solid #BBBBBB", + "background-color": "#EDEDED" + } + }, + "16": { + "id": 16, + "left": { + "type": "SOLID", + "width": "0.12mm", + "color": "#999999" + }, + "right": { + "type": "SOLID", + "width": "0.12mm", + "color": "#999999" + }, + "top": { + "type": "SOLID", + "width": "0.12mm", + "color": "#BBBBBB" + }, + "bottom": { + "type": "SOLID", + "width": "0.5mm", + "color": "#BBBBBB" + }, + "background": "#EDEDED", + "css": { + "border-left": "0.12mm solid #999999", + "border-right": "0.12mm solid #999999", + "border-top": "0.12mm solid #BBBBBB", + "border-bottom": "0.5mm solid #BBBBBB", + "background-color": "#EDEDED" + } + }, + "17": { + "id": 17, + "left": { + "type": "SOLID", + "width": "0.12mm", + "color": "#999999" + }, + "right": { + "type": "SOLID", + "width": "0.12mm", + "color": "#999999" + }, + "top": { + "type": "SOLID", + "width": "0.5mm", + "color": "#BBBBBB" + }, + "bottom": { + "type": "SOLID", + "width": "0.12mm", + "color": "#BBBBBB" + }, + "css": { + "border-left": "0.12mm solid #999999", + "border-right": "0.12mm solid #999999", + "border-top": "0.5mm solid #BBBBBB", + "border-bottom": "0.12mm solid #BBBBBB" + } + }, + "18": { + "id": 18, + "left": { + "type": "SOLID", + "width": "0.12mm", + "color": "#999999" + }, + "right": { + "type": "SOLID", + "width": "0.12mm", + "color": "#999999" + }, + "top": { + "type": "SOLID", + "width": "0.12mm", + "color": "#BBBBBB" + }, + "bottom": { + "type": "SOLID", + "width": "0.5mm", + "color": "#BBBBBB" + }, + "css": { + "border-left": "0.12mm solid #999999", + "border-right": "0.12mm solid #999999", + "border-top": "0.12mm solid #BBBBBB", + "border-bottom": "0.5mm solid #BBBBBB" + } + }, + "19": { + "id": 19, + "left": { + "type": "SOLID", + "width": "0.12mm", + "color": "#999999" + }, + "right": { + "type": "NONE", + "width": "0.12mm", + "color": "#BBBBBB" + }, + "top": { + "type": "SOLID", + "width": "0.12mm", + "color": "#BBBBBB" + }, + "bottom": { + "type": "SOLID", + "width": "0.5mm", + "color": "#BBBBBB" + }, + "css": { + "border-left": "0.12mm solid #999999", + "border-right": "none", + "border-top": "0.12mm solid #BBBBBB", + "border-bottom": "0.5mm solid #BBBBBB" + } + }, + "20": { + "id": 20, + "left": { + "type": "SOLID", + "width": "0.12mm", + "color": "#999999" + }, + "right": { + "type": "NONE", + "width": "0.12mm", + "color": "#BBBBBB" + }, + "top": { + "type": "SOLID", + "width": "0.5mm", + "color": "#BBBBBB" + }, + "bottom": { + "type": "SOLID", + "width": "0.3mm", + "color": "#BBBBBB" + }, + "css": { + "border-left": "0.12mm solid #999999", + "border-right": "none", + "border-top": "0.5mm solid #BBBBBB", + "border-bottom": "0.3mm solid #BBBBBB" + } + } + }, + "tables": [], + "style_summary": {} +} \ No newline at end of file diff --git a/templates/user/templates/tpl_1770300969/template.html b/templates/user/templates/tpl_1770300969/template.html new file mode 100644 index 0000000..d7ef470 --- /dev/null +++ b/templates/user/templates/tpl_1770300969/template.html @@ -0,0 +1,590 @@ + + + + +Template + + + +
+ +
+
++ + + + + + + + + +
{{HEADER_R1_C1_LINE_1}}
{{HEADER_R1_C1_LINE_2}}
{{HEADER_R1_C3}}
+ + +
+ ++ + + + + + + +
{{TITLE_R1_C2}}
+
+ + +
+ {{IMAGE_1}} +

{{IMAGE_1_CAPTION}}

+
+ +
+ {{IMAGE_2}} +

{{IMAGE_2_CAPTION}}

+
+ +

{{PARA_1}}

+ +
+ {{IMAGE_3}} +

{{IMAGE_3_CAPTION}}

+
+ +
+ {{IMAGE_4}} +

{{IMAGE_4_CAPTION}}

+
+ +
+ {{IMAGE_5}} +

{{IMAGE_5_CAPTION}}

+
+ + ++ + + + + + + + + + + + + + {{TABLE_1_BODY}} + +
{{TABLE_1_H_C1}}{{TABLE_1_H_C2}}{{TABLE_1_H_C3}}
+ + + + + + \ No newline at end of file diff --git a/templates/user/templates/tpl_1770301063/meta.json b/templates/user/templates/tpl_1770301063/meta.json new file mode 100644 index 0000000..122d962 --- /dev/null +++ b/templates/user/templates/tpl_1770301063/meta.json @@ -0,0 +1,13 @@ +{ + "id": "tpl_1770301063", + "name": "55 양식", + "original_file": "발표자료복사본.hwpx", + "file_type": ".hwpx", + "description": "55에서 추출한 문서 양식", + "features": [ + "폰트: 돋움", + "표: 3x3" + ], + "created_at": "2026-02-05T23:17:43Z", + "source": "doc_template_analyzer" +} \ No newline at end of file diff --git a/templates/user/templates/tpl_1770301063/semantic_map.json b/templates/user/templates/tpl_1770301063/semantic_map.json new file mode 100644 index 0000000..b14ba03 --- /dev/null +++ b/templates/user/templates/tpl_1770301063/semantic_map.json @@ -0,0 +1,94 @@ +{ + "version": "1.0", + "table_roles": { + "0": { + "role": "data_table", + "header_row": null, + "col_headers": [], + "row_count": 3, + "col_count": 3 + } + }, + "body_tables": [ + 0 + ], + "title_table": null, + "sections": [ + { + "index": 1, + "title": "1. (스마트설계팀) SamanPro(V3.0)", + "pattern_type": "numbered" + } + ], + "style_mappings": { + "char_pr": {}, + "border_fill": { + "1": { + "css_class": "bf-1", + "bg": "", + "borders": {} + }, + "2": { + "css_class": "bf-2", + "bg": "", + "borders": {} + }, + "3": { + "css_class": "bf-3", + "bg": "", + "borders": { + "border-left": "0.12mm solid #000000", + "border-right": "0.12mm solid #000000", + "border-top": "0.12mm solid #000000", + "border-bottom": "0.12mm solid #000000" + } + }, + "4": { + "css_class": "bf-4", + "bg": "", + "borders": {} + }, + "5": { + "css_class": "bf-5", + "bg": "", + "borders": {} + }, + "6": { + "css_class": "bf-6", + "bg": "", + "borders": {} + }, + "7": { + "css_class": "bf-7", + "bg": "", + "borders": {} + }, + "8": { + "css_class": "bf-8", + "bg": "#F3F3F3", + "borders": {} + }, + "9": { + "css_class": "bf-9", + "bg": "", + "borders": {} + }, + "10": { + "css_class": "bf-10", + "bg": "", + "borders": {} + }, + "11": { + "css_class": "bf-11", + "bg": "", + "borders": {} + }, + "12": { + "css_class": "bf-12", + "bg": "", + "borders": {} + } + }, + "para_pr": {} + } +} \ No newline at end of file diff --git a/templates/user/templates/tpl_1770301063/style.json b/templates/user/templates/tpl_1770301063/style.json new file mode 100644 index 0000000..7535138 --- /dev/null +++ b/templates/user/templates/tpl_1770301063/style.json @@ -0,0 +1,3355 @@ +{ + "version": "v4", + "source": "doc_template_analyzer", + "template_info": { + "page": { + "paper": { + "name": "A4", + "width_mm": 210.0, + "height_mm": 297.0, + "landscape": true + }, + "margins": { + "top": "10.0mm", + "bottom": "10.0mm", + "left": "20.0mm", + "right": "20.0mm", + "header": "15.0mm", + "footer": "15.0mm", + "gutter": "0.0mm" + } + }, + "fonts": { + "HANGUL": [ + { + "id": 0, + "face": "돋움", + "type": "TTF" + }, + { + "id": 1, + "face": "맑은 고딕", + "type": "TTF" + }, + { + "id": 2, + "face": "HY헤드라인M", + "type": "TTF" + }, + { + "id": 3, + "face": "한양견명조", + "type": "HFT" + }, + { + "id": 4, + "face": "나눔명조", + "type": "TTF" + }, + { + "id": 5, + "face": "-윤고딕130", + "type": "TTF" + } + ], + "LATIN": [ + { + "id": 0, + "face": "돋움", + "type": "TTF" + }, + { + "id": 1, + "face": "맑은 고딕", + "type": "TTF" + }, + { + "id": 2, + "face": "HY헤드라인M", + "type": "TTF" + }, + { + "id": 3, + "face": "한양견명조", + "type": "HFT" + }, + { + "id": 4, + "face": "나눔명조", + "type": "TTF" + }, + { + "id": 5, + "face": "-윤고딕130", + "type": "TTF" + } + ], + "HANJA": [ + { + "id": 0, + "face": "맑은 고딕", + "type": "TTF" + }, + { + "id": 1, + "face": "한컴바탕", + "type": "TTF" + }, + { + "id": 2, + "face": "HY헤드라인M", + "type": "TTF" + }, + { + "id": 3, + "face": "신명 견명조", + "type": "HFT" + }, + { + "id": 4, + "face": "나눔명조", + "type": "TTF" + }, + { + "id": 5, + "face": "-윤고딕130", + "type": "TTF" + } + ], + "JAPANESE": [ + { + "id": 0, + "face": "맑은 고딕", + "type": "TTF" + }, + { + "id": 1, + "face": "한컴바탕", + "type": "TTF" + }, + { + "id": 2, + "face": "HY헤드라인M", + "type": "TTF" + }, + { + "id": 3, + "face": "신명 견명조", + "type": "HFT" + }, + { + "id": 4, + "face": "나눔명조", + "type": "TTF" + }, + { + "id": 5, + "face": "-윤고딕130", + "type": "TTF" + } + ], + "OTHER": [ + { + "id": 0, + "face": "맑은 고딕", + "type": "TTF" + }, + { + "id": 1, + "face": "한컴바탕", + "type": "TTF" + }, + { + "id": 2, + "face": "HY헤드라인M", + "type": "TTF" + }, + { + "id": 3, + "face": "한양신명조", + "type": "HFT" + }, + { + "id": 4, + "face": "나눔명조", + "type": "TTF" + }, + { + "id": 5, + "face": "-윤고딕130", + "type": "TTF" + } + ], + "SYMBOL": [ + { + "id": 0, + "face": "맑은 고딕", + "type": "TTF" + }, + { + "id": 1, + "face": "한컴바탕", + "type": "TTF" + }, + { + "id": 2, + "face": "HY헤드라인M", + "type": "TTF" + }, + { + "id": 3, + "face": "신명 견명조", + "type": "HFT" + }, + { + "id": 4, + "face": "나눔명조", + "type": "TTF" + }, + { + "id": 5, + "face": "-윤고딕330", + "type": "TTF" + } + ], + "USER": [ + { + "id": 0, + "face": "맑은 고딕", + "type": "TTF" + }, + { + "id": 1, + "face": "한컴바탕", + "type": "TTF" + }, + { + "id": 2, + "face": "HY헤드라인M", + "type": "TTF" + }, + { + "id": 3, + "face": "명조", + "type": "HFT" + }, + { + "id": 4, + "face": "나눔명조", + "type": "TTF" + }, + { + "id": 5, + "face": "-윤고딕130", + "type": "TTF" + } + ] + }, + "char_styles": [ + { + "id": 0, + "height_pt": 10.0, + "textColor": "#000000", + "borderFillIDRef": 2, + "bold": false, + "italic": false, + "fontRef": { + "hangul": 4, + "latin": 4, + "hanja": 4, + "japanese": 4, + "other": 4, + "symbol": 4, + "user": 4 + }, + "ratio": { + "hangul": 100, + "latin": 100, + "hanja": 100, + "japanese": 100, + "other": 100, + "symbol": 100, + "user": 100 + }, + "spacing": { + "hangul": 0, + "latin": 0, + "hanja": 0, + "japanese": 0, + "other": 0, + "symbol": 0, + "user": 0 + }, + "underline": "NONE", + "strikeout": "3D" + }, + { + "id": 1, + "height_pt": 9.0, + "textColor": "#000000", + "borderFillIDRef": 2, + "bold": false, + "italic": false, + "fontRef": { + "hangul": 1, + "latin": 1, + "hanja": 0, + "japanese": 0, + "other": 0, + "symbol": 0, + "user": 0 + }, + "ratio": { + "hangul": 95, + "latin": 95, + "hanja": 95, + "japanese": 95, + "other": 95, + "symbol": 95, + "user": 95 + }, + "spacing": { + "hangul": -5, + "latin": -5, + "hanja": -5, + "japanese": -5, + "other": -5, + "symbol": -5, + "user": -5 + }, + "underline": "NONE", + "strikeout": "3D" + }, + { + "id": 2, + "height_pt": 8.0, + "textColor": "#000000", + "borderFillIDRef": 2, + "bold": false, + "italic": false, + "fontRef": { + "hangul": 1, + "latin": 1, + "hanja": 0, + "japanese": 0, + "other": 0, + "symbol": 0, + "user": 0 + }, + "ratio": { + "hangul": 100, + "latin": 100, + "hanja": 100, + "japanese": 100, + "other": 100, + "symbol": 100, + "user": 100 + }, + "spacing": { + "hangul": 0, + "latin": 0, + "hanja": 0, + "japanese": 0, + "other": 0, + "symbol": 0, + "user": 0 + }, + "underline": "NONE", + "strikeout": "3D" + }, + { + "id": 3, + "height_pt": 9.0, + "textColor": "#000000", + "borderFillIDRef": 2, + "bold": false, + "italic": false, + "fontRef": { + "hangul": 1, + "latin": 1, + "hanja": 0, + "japanese": 0, + "other": 0, + "symbol": 0, + "user": 0 + }, + "ratio": { + "hangul": 100, + "latin": 100, + "hanja": 100, + "japanese": 100, + "other": 100, + "symbol": 100, + "user": 100 + }, + "spacing": { + "hangul": 0, + "latin": 0, + "hanja": 0, + "japanese": 0, + "other": 0, + "symbol": 0, + "user": 0 + }, + "underline": "NONE", + "strikeout": "3D" + }, + { + "id": 4, + "height_pt": 10.0, + "textColor": "#000000", + "borderFillIDRef": 2, + "bold": false, + "italic": false, + "fontRef": { + "hangul": 2, + "latin": 2, + "hanja": 2, + "japanese": 2, + "other": 2, + "symbol": 2, + "user": 2 + }, + "ratio": { + "hangul": 100, + "latin": 100, + "hanja": 100, + "japanese": 100, + "other": 100, + "symbol": 100, + "user": 100 + }, + "spacing": { + "hangul": 0, + "latin": 0, + "hanja": 0, + "japanese": 0, + "other": 0, + "symbol": 0, + "user": 0 + }, + "underline": "NONE", + "strikeout": "3D" + }, + { + "id": 5, + "height_pt": 9.0, + "textColor": "#000000", + "borderFillIDRef": 2, + "bold": false, + "italic": false, + "fontRef": { + "hangul": 4, + "latin": 4, + "hanja": 4, + "japanese": 4, + "other": 4, + "symbol": 4, + "user": 4 + }, + "ratio": { + "hangul": 100, + "latin": 100, + "hanja": 100, + "japanese": 100, + "other": 100, + "symbol": 100, + "user": 100 + }, + "spacing": { + "hangul": 0, + "latin": 0, + "hanja": 0, + "japanese": 0, + "other": 0, + "symbol": 0, + "user": 0 + }, + "underline": "NONE", + "strikeout": "3D" + }, + { + "id": 6, + "height_pt": 13.0, + "textColor": "#000000", + "borderFillIDRef": 1, + "bold": false, + "italic": false, + "fontRef": { + "hangul": 5, + "latin": 5, + "hanja": 5, + "japanese": 5, + "other": 5, + "symbol": 5, + "user": 5 + }, + "ratio": { + "hangul": 98, + "latin": 98, + "hanja": 98, + "japanese": 98, + "other": 98, + "symbol": 98, + "user": 98 + }, + "spacing": { + "hangul": -5, + "latin": -5, + "hanja": -5, + "japanese": -5, + "other": -5, + "symbol": -5, + "user": -5 + }, + "underline": "NONE", + "strikeout": "NONE" + }, + { + "id": 7, + "height_pt": 16.0, + "textColor": "#000000", + "borderFillIDRef": 1, + "bold": false, + "italic": false, + "fontRef": { + "hangul": 3, + "latin": 3, + "hanja": 3, + "japanese": 3, + "other": 3, + "symbol": 3, + "user": 3 + }, + "ratio": { + "hangul": 100, + "latin": 100, + "hanja": 100, + "japanese": 100, + "other": 100, + "symbol": 100, + "user": 100 + }, + "spacing": { + "hangul": 0, + "latin": 0, + "hanja": 0, + "japanese": 0, + "other": 0, + "symbol": 0, + "user": 0 + }, + "underline": "NONE", + "strikeout": "NONE" + }, + { + "id": 8, + "height_pt": 11.0, + "textColor": "#000000", + "borderFillIDRef": 1, + "bold": false, + "italic": false, + "fontRef": { + "hangul": 0, + "latin": 0, + "hanja": 1, + "japanese": 1, + "other": 1, + "symbol": 1, + "user": 1 + }, + "ratio": { + "hangul": 100, + "latin": 100, + "hanja": 100, + "japanese": 100, + "other": 100, + "symbol": 100, + "user": 100 + }, + "spacing": { + "hangul": 0, + "latin": 0, + "hanja": 0, + "japanese": 0, + "other": 0, + "symbol": 0, + "user": 0 + }, + "underline": "NONE", + "strikeout": "NONE" + }, + { + "id": 9, + "height_pt": 1.0, + "textColor": "#000000", + "borderFillIDRef": 2, + "bold": true, + "italic": false, + "fontRef": { + "hangul": 4, + "latin": 4, + "hanja": 4, + "japanese": 4, + "other": 4, + "symbol": 4, + "user": 4 + }, + "ratio": { + "hangul": 100, + "latin": 100, + "hanja": 100, + "japanese": 100, + "other": 100, + "symbol": 100, + "user": 100 + }, + "spacing": { + "hangul": 0, + "latin": 0, + "hanja": 0, + "japanese": 0, + "other": 0, + "symbol": 0, + "user": 0 + }, + "underline": "NONE", + "strikeout": "3D" + }, + { + "id": 10, + "height_pt": 11.0, + "textColor": "#000000", + "borderFillIDRef": 2, + "bold": false, + "italic": false, + "fontRef": { + "hangul": 4, + "latin": 4, + "hanja": 4, + "japanese": 4, + "other": 4, + "symbol": 4, + "user": 4 + }, + "ratio": { + "hangul": 100, + "latin": 100, + "hanja": 100, + "japanese": 100, + "other": 100, + "symbol": 100, + "user": 100 + }, + "spacing": { + "hangul": -5, + "latin": -5, + "hanja": -5, + "japanese": -5, + "other": -5, + "symbol": -5, + "user": -5 + }, + "underline": "NONE", + "strikeout": "NONE" + }, + { + "id": 11, + "height_pt": 11.0, + "textColor": "#000000", + "borderFillIDRef": 2, + "bold": false, + "italic": false, + "fontRef": { + "hangul": 4, + "latin": 4, + "hanja": 4, + "japanese": 4, + "other": 4, + "symbol": 4, + "user": 4 + }, + "ratio": { + "hangul": 100, + "latin": 100, + "hanja": 100, + "japanese": 100, + "other": 100, + "symbol": 100, + "user": 100 + }, + "spacing": { + "hangul": 0, + "latin": 0, + "hanja": 0, + "japanese": 0, + "other": 0, + "symbol": 0, + "user": 0 + }, + "underline": "NONE", + "strikeout": "NONE" + }, + { + "id": 12, + "height_pt": 13.0, + "textColor": "#000000", + "borderFillIDRef": 2, + "bold": true, + "italic": false, + "fontRef": { + "hangul": 1, + "latin": 1, + "hanja": 0, + "japanese": 0, + "other": 0, + "symbol": 0, + "user": 0 + }, + "ratio": { + "hangul": 100, + "latin": 100, + "hanja": 100, + "japanese": 100, + "other": 100, + "symbol": 100, + "user": 100 + }, + "spacing": { + "hangul": 0, + "latin": 0, + "hanja": 0, + "japanese": 0, + "other": 0, + "symbol": 0, + "user": 0 + }, + "underline": "NONE", + "strikeout": "NONE" + }, + { + "id": 13, + "height_pt": 11.0, + "textColor": "#000000", + "borderFillIDRef": 2, + "bold": false, + "italic": false, + "fontRef": { + "hangul": 1, + "latin": 1, + "hanja": 0, + "japanese": 0, + "other": 0, + "symbol": 0, + "user": 0 + }, + "ratio": { + "hangul": 100, + "latin": 100, + "hanja": 100, + "japanese": 100, + "other": 100, + "symbol": 100, + "user": 100 + }, + "spacing": { + "hangul": 0, + "latin": 0, + "hanja": 0, + "japanese": 0, + "other": 0, + "symbol": 0, + "user": 0 + }, + "underline": "NONE", + "strikeout": "NONE" + }, + { + "id": 14, + "height_pt": 11.0, + "textColor": "#000000", + "borderFillIDRef": 2, + "bold": true, + "italic": false, + "fontRef": { + "hangul": 1, + "latin": 1, + "hanja": 0, + "japanese": 0, + "other": 0, + "symbol": 0, + "user": 0 + }, + "ratio": { + "hangul": 100, + "latin": 100, + "hanja": 100, + "japanese": 100, + "other": 100, + "symbol": 100, + "user": 100 + }, + "spacing": { + "hangul": 0, + "latin": 0, + "hanja": 0, + "japanese": 0, + "other": 0, + "symbol": 0, + "user": 0 + }, + "underline": "NONE", + "strikeout": "NONE" + }, + { + "id": 15, + "height_pt": 9.0, + "textColor": "#000000", + "borderFillIDRef": 2, + "bold": false, + "italic": false, + "fontRef": { + "hangul": 1, + "latin": 1, + "hanja": 0, + "japanese": 0, + "other": 0, + "symbol": 0, + "user": 0 + }, + "ratio": { + "hangul": 95, + "latin": 95, + "hanja": 95, + "japanese": 95, + "other": 95, + "symbol": 95, + "user": 95 + }, + "spacing": { + "hangul": -2, + "latin": -2, + "hanja": -2, + "japanese": -2, + "other": -2, + "symbol": -2, + "user": -2 + }, + "underline": "NONE", + "strikeout": "NONE" + }, + { + "id": 16, + "height_pt": 11.0, + "textColor": "#000000", + "borderFillIDRef": 2, + "bold": false, + "italic": false, + "fontRef": { + "hangul": 1, + "latin": 1, + "hanja": 0, + "japanese": 0, + "other": 0, + "symbol": 0, + "user": 0 + }, + "ratio": { + "hangul": 95, + "latin": 95, + "hanja": 95, + "japanese": 95, + "other": 95, + "symbol": 95, + "user": 95 + }, + "spacing": { + "hangul": -2, + "latin": -2, + "hanja": -2, + "japanese": -2, + "other": -2, + "symbol": -2, + "user": -2 + }, + "underline": "NONE", + "strikeout": "NONE" + }, + { + "id": 17, + "height_pt": 11.0, + "textColor": "#000000", + "borderFillIDRef": 2, + "bold": false, + "italic": false, + "fontRef": { + "hangul": 1, + "latin": 1, + "hanja": 0, + "japanese": 0, + "other": 0, + "symbol": 0, + "user": 0 + }, + "ratio": { + "hangul": 95, + "latin": 95, + "hanja": 95, + "japanese": 95, + "other": 95, + "symbol": 95, + "user": 95 + }, + "spacing": { + "hangul": -2, + "latin": -2, + "hanja": -2, + "japanese": -2, + "other": -2, + "symbol": -2, + "user": -2 + }, + "underline": "NONE", + "strikeout": "NONE" + }, + { + "id": 18, + "height_pt": 11.0, + "textColor": "#000000", + "borderFillIDRef": 2, + "bold": false, + "italic": false, + "fontRef": { + "hangul": 1, + "latin": 1, + "hanja": 0, + "japanese": 0, + "other": 0, + "symbol": 0, + "user": 0 + }, + "ratio": { + "hangul": 100, + "latin": 100, + "hanja": 100, + "japanese": 100, + "other": 100, + "symbol": 100, + "user": 100 + }, + "spacing": { + "hangul": -4, + "latin": -4, + "hanja": -4, + "japanese": -4, + "other": -4, + "symbol": -4, + "user": -4 + }, + "underline": "NONE", + "strikeout": "NONE" + } + ], + "para_styles": [ + { + "id": 0, + "tabPrIDRef": 1, + "align": "JUSTIFY", + "verticalAlign": "BASELINE", + "heading": { + "type": "NONE", + "idRef": 0, + "level": 0 + }, + "breakSetting": { + "widowOrphan": false, + "keepWithNext": false, + "keepLines": false, + "pageBreakBefore": false, + "lineWrap": "BREAK", + "breakLatinWord": "KEEP_WORD", + "breakNonLatinWord": "KEEP_WORD" + }, + "margin": { + "indent_hu": -1310, + "left_hu": 0, + "right_hu": 0, + "before_hu": 0, + "after_hu": 0 + }, + "lineSpacing": { + "type": "PERCENT", + "value": 130 + }, + "borderFillIDRef": 2 + }, + { + "id": 1, + "tabPrIDRef": 0, + "align": "JUSTIFY", + "verticalAlign": "BASELINE", + "heading": { + "type": "NONE", + "idRef": 0, + "level": 0 + }, + "breakSetting": { + "widowOrphan": false, + "keepWithNext": false, + "keepLines": false, + "pageBreakBefore": false, + "lineWrap": "BREAK", + "breakLatinWord": "KEEP_WORD", + "breakNonLatinWord": "KEEP_WORD" + }, + "margin": { + "indent_hu": 0, + "left_hu": 0, + "right_hu": 0, + "before_hu": 0, + "after_hu": 0 + }, + "lineSpacing": { + "type": "PERCENT", + "value": 160 + }, + "borderFillIDRef": 2 + }, + { + "id": 2, + "tabPrIDRef": 2, + "align": "JUSTIFY", + "verticalAlign": "BASELINE", + "heading": { + "type": "NONE", + "idRef": 0, + "level": 0 + }, + "breakSetting": { + "widowOrphan": false, + "keepWithNext": false, + "keepLines": false, + "pageBreakBefore": false, + "lineWrap": "BREAK", + "breakLatinWord": "KEEP_WORD", + "breakNonLatinWord": "BREAK_WORD" + }, + "margin": { + "indent_hu": 0, + "left_hu": 0, + "right_hu": 0, + "before_hu": 0, + "after_hu": 0 + }, + "lineSpacing": { + "type": "PERCENT", + "value": 150 + }, + "borderFillIDRef": 2 + }, + { + "id": 3, + "tabPrIDRef": 0, + "align": "JUSTIFY", + "verticalAlign": "BASELINE", + "heading": { + "type": "NONE", + "idRef": 0, + "level": 0 + }, + "breakSetting": { + "widowOrphan": false, + "keepWithNext": false, + "keepLines": false, + "pageBreakBefore": false, + "lineWrap": "BREAK", + "breakLatinWord": "KEEP_WORD", + "breakNonLatinWord": "KEEP_WORD" + }, + "margin": { + "indent_hu": 0, + "left_hu": 0, + "right_hu": 0, + "before_hu": 0, + "after_hu": 0 + }, + "lineSpacing": { + "type": "PERCENT", + "value": 180 + }, + "borderFillIDRef": 2 + }, + { + "id": 4, + "tabPrIDRef": 0, + "align": "CENTER", + "verticalAlign": "BASELINE", + "heading": { + "type": "NONE", + "idRef": 0, + "level": 0 + }, + "breakSetting": { + "widowOrphan": false, + "keepWithNext": false, + "keepLines": false, + "pageBreakBefore": false, + "lineWrap": "BREAK", + "breakLatinWord": "KEEP_WORD", + "breakNonLatinWord": "BREAK_WORD" + }, + "margin": { + "indent_hu": 0, + "left_hu": 0, + "right_hu": 0, + "before_hu": 0, + "after_hu": 0 + }, + "lineSpacing": { + "type": "PERCENT", + "value": 180 + }, + "borderFillIDRef": 2 + }, + { + "id": 5, + "tabPrIDRef": 0, + "align": "DISTRIBUTE", + "verticalAlign": "BASELINE", + "heading": { + "type": "NONE", + "idRef": 0, + "level": 0 + }, + "breakSetting": { + "widowOrphan": false, + "keepWithNext": false, + "keepLines": false, + "pageBreakBefore": false, + "lineWrap": "BREAK", + "breakLatinWord": "KEEP_WORD", + "breakNonLatinWord": "KEEP_WORD" + }, + "margin": { + "indent_hu": 0, + "left_hu": 500, + "right_hu": 500, + "before_hu": 0, + "after_hu": 0 + }, + "lineSpacing": { + "type": "PERCENT", + "value": 180 + }, + "borderFillIDRef": 2 + }, + { + "id": 6, + "tabPrIDRef": 3, + "align": "JUSTIFY", + "verticalAlign": "BASELINE", + "heading": { + "type": "NONE", + "idRef": 0, + "level": 0 + }, + "breakSetting": { + "widowOrphan": false, + "keepWithNext": false, + "keepLines": false, + "pageBreakBefore": false, + "lineWrap": "BREAK", + "breakLatinWord": "KEEP_WORD", + "breakNonLatinWord": "KEEP_WORD" + }, + "margin": { + "indent_hu": 0, + "left_hu": 0, + "right_hu": 0, + "before_hu": 0, + "after_hu": 0 + }, + "lineSpacing": { + "type": "PERCENT", + "value": 180 + }, + "borderFillIDRef": 2 + }, + { + "id": 7, + "tabPrIDRef": 3, + "align": "JUSTIFY", + "verticalAlign": "BASELINE", + "heading": { + "type": "NONE", + "idRef": 0, + "level": 0 + }, + "breakSetting": { + "widowOrphan": false, + "keepWithNext": false, + "keepLines": false, + "pageBreakBefore": false, + "lineWrap": "BREAK", + "breakLatinWord": "KEEP_WORD", + "breakNonLatinWord": "KEEP_WORD" + }, + "margin": { + "indent_hu": 0, + "left_hu": 500, + "right_hu": 0, + "before_hu": 0, + "after_hu": 0 + }, + "lineSpacing": { + "type": "PERCENT", + "value": 180 + }, + "borderFillIDRef": 2 + }, + { + "id": 8, + "tabPrIDRef": 3, + "align": "JUSTIFY", + "verticalAlign": "BASELINE", + "heading": { + "type": "NONE", + "idRef": 0, + "level": 0 + }, + "breakSetting": { + "widowOrphan": false, + "keepWithNext": false, + "keepLines": false, + "pageBreakBefore": false, + "lineWrap": "BREAK", + "breakLatinWord": "KEEP_WORD", + "breakNonLatinWord": "KEEP_WORD" + }, + "margin": { + "indent_hu": -1223, + "left_hu": 500, + "right_hu": 0, + "before_hu": 0, + "after_hu": 500 + }, + "lineSpacing": { + "type": "PERCENT", + "value": 170 + }, + "borderFillIDRef": 2 + }, + { + "id": 9, + "tabPrIDRef": 0, + "align": "JUSTIFY", + "verticalAlign": "BASELINE", + "heading": { + "type": "NONE", + "idRef": 0, + "level": 0 + }, + "breakSetting": { + "widowOrphan": false, + "keepWithNext": false, + "keepLines": false, + "pageBreakBefore": false, + "lineWrap": "BREAK", + "breakLatinWord": "KEEP_WORD", + "breakNonLatinWord": "KEEP_WORD" + }, + "margin": { + "indent_hu": 0, + "left_hu": 0, + "right_hu": 0, + "before_hu": 600, + "after_hu": 0 + }, + "lineSpacing": { + "type": "PERCENT", + "value": 155 + }, + "borderFillIDRef": 1 + }, + { + "id": 10, + "tabPrIDRef": 0, + "align": "JUSTIFY", + "verticalAlign": "BASELINE", + "heading": { + "type": "NONE", + "idRef": 0, + "level": 0 + }, + "breakSetting": { + "widowOrphan": false, + "keepWithNext": false, + "keepLines": false, + "pageBreakBefore": false, + "lineWrap": "BREAK", + "breakLatinWord": "KEEP_WORD", + "breakNonLatinWord": "BREAK_WORD" + }, + "margin": { + "indent_hu": -1396, + "left_hu": 800, + "right_hu": 0, + "before_hu": 0, + "after_hu": 300 + }, + "lineSpacing": { + "type": "PERCENT", + "value": 140 + }, + "borderFillIDRef": 2 + }, + { + "id": 11, + "tabPrIDRef": 0, + "align": "JUSTIFY", + "verticalAlign": "BASELINE", + "heading": { + "type": "BULLET", + "idRef": 1, + "level": 0 + }, + "breakSetting": { + "widowOrphan": false, + "keepWithNext": false, + "keepLines": false, + "pageBreakBefore": false, + "lineWrap": "BREAK", + "breakLatinWord": "KEEP_WORD", + "breakNonLatinWord": "BREAK_WORD" + }, + "margin": { + "indent_hu": 0, + "left_hu": 800, + "right_hu": 0, + "before_hu": 500, + "after_hu": 300 + }, + "lineSpacing": { + "type": "PERCENT", + "value": 140 + }, + "borderFillIDRef": 2 + }, + { + "id": 12, + "tabPrIDRef": 3, + "align": "JUSTIFY", + "verticalAlign": "BASELINE", + "heading": { + "type": "NONE", + "idRef": 0, + "level": 0 + }, + "breakSetting": { + "widowOrphan": false, + "keepWithNext": false, + "keepLines": false, + "pageBreakBefore": false, + "lineWrap": "BREAK", + "breakLatinWord": "KEEP_WORD", + "breakNonLatinWord": "KEEP_WORD" + }, + "margin": { + "indent_hu": 0, + "left_hu": 0, + "right_hu": 0, + "before_hu": 1000, + "after_hu": 500 + }, + "lineSpacing": { + "type": "PERCENT", + "value": 160 + }, + "borderFillIDRef": 2 + }, + { + "id": 13, + "tabPrIDRef": 0, + "align": "CENTER", + "verticalAlign": "BASELINE", + "heading": { + "type": "NONE", + "idRef": 0, + "level": 0 + }, + "breakSetting": { + "widowOrphan": false, + "keepWithNext": false, + "keepLines": false, + "pageBreakBefore": false, + "lineWrap": "BREAK", + "breakLatinWord": "KEEP_WORD", + "breakNonLatinWord": "KEEP_WORD" + }, + "margin": { + "indent_hu": 0, + "left_hu": 0, + "right_hu": 0, + "before_hu": 0, + "after_hu": 0 + }, + "lineSpacing": { + "type": "PERCENT", + "value": 160 + }, + "borderFillIDRef": 1 + }, + { + "id": 14, + "tabPrIDRef": 0, + "align": "JUSTIFY", + "verticalAlign": "BASELINE", + "heading": { + "type": "NONE", + "idRef": 0, + "level": 0 + }, + "breakSetting": { + "widowOrphan": false, + "keepWithNext": false, + "keepLines": false, + "pageBreakBefore": false, + "lineWrap": "BREAK", + "breakLatinWord": "KEEP_WORD", + "breakNonLatinWord": "KEEP_WORD" + }, + "margin": { + "indent_hu": 0, + "left_hu": 0, + "right_hu": 0, + "before_hu": 0, + "after_hu": 852 + }, + "lineSpacing": { + "type": "PERCENT", + "value": 180 + }, + "borderFillIDRef": 1 + }, + { + "id": 15, + "tabPrIDRef": 0, + "align": "JUSTIFY", + "verticalAlign": "BASELINE", + "heading": { + "type": "BULLET", + "idRef": 1, + "level": 0 + }, + "breakSetting": { + "widowOrphan": false, + "keepWithNext": false, + "keepLines": false, + "pageBreakBefore": false, + "lineWrap": "BREAK", + "breakLatinWord": "KEEP_WORD", + "breakNonLatinWord": "BREAK_WORD" + }, + "margin": { + "indent_hu": 0, + "left_hu": 800, + "right_hu": 0, + "before_hu": 0, + "after_hu": 500 + }, + "lineSpacing": { + "type": "PERCENT", + "value": 140 + }, + "borderFillIDRef": 2 + }, + { + "id": 16, + "tabPrIDRef": 3, + "align": "CENTER", + "verticalAlign": "BASELINE", + "heading": { + "type": "NONE", + "idRef": 0, + "level": 0 + }, + "breakSetting": { + "widowOrphan": false, + "keepWithNext": false, + "keepLines": false, + "pageBreakBefore": false, + "lineWrap": "BREAK", + "breakLatinWord": "KEEP_WORD", + "breakNonLatinWord": "BREAK_WORD" + }, + "margin": { + "indent_hu": -1223, + "left_hu": 500, + "right_hu": 0, + "before_hu": 0, + "after_hu": 500 + }, + "lineSpacing": { + "type": "PERCENT", + "value": 170 + }, + "borderFillIDRef": 2 + }, + { + "id": 17, + "tabPrIDRef": 3, + "align": "JUSTIFY", + "verticalAlign": "BASELINE", + "heading": { + "type": "NONE", + "idRef": 0, + "level": 0 + }, + "breakSetting": { + "widowOrphan": false, + "keepWithNext": false, + "keepLines": false, + "pageBreakBefore": false, + "lineWrap": "BREAK", + "breakLatinWord": "KEEP_WORD", + "breakNonLatinWord": "KEEP_WORD" + }, + "margin": { + "indent_hu": -1223, + "left_hu": 500, + "right_hu": 0, + "before_hu": 500, + "after_hu": 500 + }, + "lineSpacing": { + "type": "PERCENT", + "value": 170 + }, + "borderFillIDRef": 2 + }, + { + "id": 18, + "tabPrIDRef": 0, + "align": "JUSTIFY", + "verticalAlign": "BASELINE", + "heading": { + "type": "NONE", + "idRef": 0, + "level": 0 + }, + "breakSetting": { + "widowOrphan": false, + "keepWithNext": false, + "keepLines": false, + "pageBreakBefore": false, + "lineWrap": "BREAK", + "breakLatinWord": "KEEP_WORD", + "breakNonLatinWord": "BREAK_WORD" + }, + "margin": { + "indent_hu": 0, + "left_hu": 800, + "right_hu": 0, + "before_hu": 0, + "after_hu": 500 + }, + "lineSpacing": { + "type": "PERCENT", + "value": 140 + }, + "borderFillIDRef": 2 + }, + { + "id": 19, + "tabPrIDRef": 0, + "align": "JUSTIFY", + "verticalAlign": "BASELINE", + "heading": { + "type": "NONE", + "idRef": 0, + "level": 0 + }, + "breakSetting": { + "widowOrphan": false, + "keepWithNext": false, + "keepLines": false, + "pageBreakBefore": false, + "lineWrap": "BREAK", + "breakLatinWord": "KEEP_WORD", + "breakNonLatinWord": "BREAK_WORD" + }, + "margin": { + "indent_hu": -1724, + "left_hu": 800, + "right_hu": 0, + "before_hu": 0, + "after_hu": 300 + }, + "lineSpacing": { + "type": "PERCENT", + "value": 160 + }, + "borderFillIDRef": 2 + }, + { + "id": 20, + "tabPrIDRef": 0, + "align": "JUSTIFY", + "verticalAlign": "BASELINE", + "heading": { + "type": "NONE", + "idRef": 0, + "level": 0 + }, + "breakSetting": { + "widowOrphan": false, + "keepWithNext": false, + "keepLines": false, + "pageBreakBefore": false, + "lineWrap": "BREAK", + "breakLatinWord": "KEEP_WORD", + "breakNonLatinWord": "BREAK_WORD" + }, + "margin": { + "indent_hu": -2247, + "left_hu": 800, + "right_hu": 0, + "before_hu": 0, + "after_hu": 300 + }, + "lineSpacing": { + "type": "PERCENT", + "value": 160 + }, + "borderFillIDRef": 2 + }, + { + "id": 21, + "tabPrIDRef": 3, + "align": "JUSTIFY", + "verticalAlign": "BASELINE", + "heading": { + "type": "NONE", + "idRef": 0, + "level": 0 + }, + "breakSetting": { + "widowOrphan": false, + "keepWithNext": false, + "keepLines": false, + "pageBreakBefore": false, + "lineWrap": "BREAK", + "breakLatinWord": "KEEP_WORD", + "breakNonLatinWord": "KEEP_WORD" + }, + "margin": { + "indent_hu": 0, + "left_hu": 0, + "right_hu": 0, + "before_hu": 500, + "after_hu": 300 + }, + "lineSpacing": { + "type": "PERCENT", + "value": 160 + }, + "borderFillIDRef": 2 + }, + { + "id": 22, + "tabPrIDRef": 3, + "align": "JUSTIFY", + "verticalAlign": "BASELINE", + "heading": { + "type": "NONE", + "idRef": 0, + "level": 0 + }, + "breakSetting": { + "widowOrphan": false, + "keepWithNext": false, + "keepLines": false, + "pageBreakBefore": false, + "lineWrap": "BREAK", + "breakLatinWord": "KEEP_WORD", + "breakNonLatinWord": "KEEP_WORD" + }, + "margin": { + "indent_hu": 0, + "left_hu": 0, + "right_hu": 0, + "before_hu": 0, + "after_hu": 500 + }, + "lineSpacing": { + "type": "PERCENT", + "value": 160 + }, + "borderFillIDRef": 2 + } + ], + "border_fills": { + "1": { + "id": 1, + "left": { + "type": "NONE", + "width": "0.1mm", + "color": "#000000" + }, + "right": { + "type": "NONE", + "width": "0.1mm", + "color": "#000000" + }, + "top": { + "type": "NONE", + "width": "0.1mm", + "color": "#000000" + }, + "bottom": { + "type": "NONE", + "width": "0.1mm", + "color": "#000000" + }, + "diagonal": { + "type": "SOLID", + "width": "0.1mm", + "color": "#000000" + }, + "css": { + "border-left": "none", + "border-right": "none", + "border-top": "none", + "border-bottom": "none" + } + }, + "2": { + "id": 2, + "left": { + "type": "NONE", + "width": "0.1mm", + "color": "#000000" + }, + "right": { + "type": "NONE", + "width": "0.1mm", + "color": "#000000" + }, + "top": { + "type": "NONE", + "width": "0.1mm", + "color": "#000000" + }, + "bottom": { + "type": "NONE", + "width": "0.1mm", + "color": "#000000" + }, + "diagonal": { + "type": "SOLID", + "width": "0.1mm", + "color": "#000000" + }, + "css": { + "border-left": "none", + "border-right": "none", + "border-top": "none", + "border-bottom": "none" + } + }, + "3": { + "id": 3, + "left": { + "type": "SOLID", + "width": "0.12mm", + "color": "#000000" + }, + "right": { + "type": "SOLID", + "width": "0.12mm", + "color": "#000000" + }, + "top": { + "type": "SOLID", + "width": "0.12mm", + "color": "#000000" + }, + "bottom": { + "type": "SOLID", + "width": "0.12mm", + "color": "#000000" + }, + "css": { + "border-left": "0.12mm solid #000000", + "border-right": "0.12mm solid #000000", + "border-top": "0.12mm solid #000000", + "border-bottom": "0.12mm solid #000000" + } + }, + "4": { + "id": 4, + "left": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "right": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "top": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "bottom": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "css": { + "border-left": "none", + "border-right": "none", + "border-top": "none", + "border-bottom": "none" + } + }, + "5": { + "id": 5, + "left": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "right": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "top": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "bottom": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "css": { + "border-left": "none", + "border-right": "none", + "border-top": "none", + "border-bottom": "none" + } + }, + "6": { + "id": 6, + "left": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "right": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "top": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "bottom": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "css": { + "border-left": "none", + "border-right": "none", + "border-top": "none", + "border-bottom": "none" + } + }, + "7": { + "id": 7, + "left": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "right": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "top": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "bottom": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "css": { + "border-left": "none", + "border-right": "none", + "border-top": "none", + "border-bottom": "none" + } + }, + "8": { + "id": 8, + "left": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "right": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "top": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "bottom": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "background": "#F3F3F3", + "css": { + "border-left": "none", + "border-right": "none", + "border-top": "none", + "border-bottom": "none", + "background-color": "#F3F3F3" + } + }, + "9": { + "id": 9, + "left": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "right": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "top": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "bottom": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "css": { + "border-left": "none", + "border-right": "none", + "border-top": "none", + "border-bottom": "none" + } + }, + "10": { + "id": 10, + "left": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "right": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "top": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "bottom": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "css": { + "border-left": "none", + "border-right": "none", + "border-top": "none", + "border-bottom": "none" + } + }, + "11": { + "id": 11, + "left": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "right": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "top": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "bottom": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "css": { + "border-left": "none", + "border-right": "none", + "border-top": "none", + "border-bottom": "none" + } + }, + "12": { + "id": 12, + "left": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "right": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "top": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "bottom": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "css": { + "border-left": "none", + "border-right": "none", + "border-top": "none", + "border-bottom": "none" + } + } + }, + "tables": [ + { + "index": 0, + "rowCnt": 3, + "colCnt": 3, + "repeatHeader": true, + "pageBreak": "CELL", + "rows": [ + [ + { + "borderFillIDRef": 4, + "isHeader": false, + "colAddr": 0, + "rowAddr": 0, + "colSpan": 1, + "rowSpan": 1, + "width_hu": 283, + "height_hu": 284, + "cellMargin": { + "left": 0, + "right": 0, + "top": 184, + "bottom": 0 + }, + "charPrIDRefs": [ + 9 + ], + "primaryCharPrIDRef": 9, + "paraPrIDRefs": [ + 4 + ], + "primaryParaPrIDRef": 4, + "styleIDRefs": [ + 0 + ] + }, + { + "borderFillIDRef": 5, + "isHeader": false, + "colAddr": 1, + "rowAddr": 0, + "colSpan": 1, + "rowSpan": 1, + "width_hu": 47063, + "height_hu": 284, + "cellMargin": { + "left": 0, + "right": 0, + "top": 184, + "bottom": 0 + }, + "charPrIDRefs": [ + 9 + ], + "primaryCharPrIDRef": 9, + "paraPrIDRefs": [ + 4 + ], + "primaryParaPrIDRef": 4, + "styleIDRefs": [ + 0 + ] + }, + { + "borderFillIDRef": 6, + "isHeader": false, + "colAddr": 2, + "rowAddr": 0, + "colSpan": 1, + "rowSpan": 1, + "width_hu": 283, + "height_hu": 284, + "cellMargin": { + "left": 0, + "right": 0, + "top": 184, + "bottom": 0 + }, + "charPrIDRefs": [ + 9 + ], + "primaryCharPrIDRef": 9, + "paraPrIDRefs": [ + 4 + ], + "primaryParaPrIDRef": 4, + "styleIDRefs": [ + 0 + ] + } + ], + [ + { + "borderFillIDRef": 7, + "isHeader": false, + "colAddr": 0, + "rowAddr": 1, + "colSpan": 1, + "rowSpan": 1, + "width_hu": 283, + "height_hu": 2315, + "cellMargin": { + "left": 0, + "right": 0, + "top": 184, + "bottom": 0 + }, + "charPrIDRefs": [ + 9 + ], + "primaryCharPrIDRef": 9, + "paraPrIDRefs": [ + 4 + ], + "primaryParaPrIDRef": 4, + "styleIDRefs": [ + 0 + ] + }, + { + "borderFillIDRef": 8, + "isHeader": false, + "colAddr": 1, + "rowAddr": 1, + "colSpan": 1, + "rowSpan": 1, + "width_hu": 47063, + "height_hu": 2315, + "cellMargin": { + "left": 0, + "right": 0, + "top": 425, + "bottom": 425 + }, + "text": "지침 기반의 정형 계산 * 과 행정 보조를 GUI 앱으로 통합해 반복 업무를 자동화한 실무 도구", + "charPrIDRefs": [ + 10, + 11, + 10 + ], + "primaryCharPrIDRef": 10, + "paraPrIDRefs": [ + 16 + ], + "primaryParaPrIDRef": 16, + "styleIDRefs": [ + 7 + ], + "lines": [ + "지침 기반의 정형 계산 * 과 행정 보조를 GUI 앱으로 통합해 반복 업무를 자동화한 실무 도구" + ] + }, + { + "borderFillIDRef": 9, + "isHeader": false, + "colAddr": 2, + "rowAddr": 1, + "colSpan": 1, + "rowSpan": 1, + "width_hu": 283, + "height_hu": 2315, + "cellMargin": { + "left": 0, + "right": 0, + "top": 184, + "bottom": 0 + }, + "charPrIDRefs": [ + 9 + ], + "primaryCharPrIDRef": 9, + "paraPrIDRefs": [ + 4 + ], + "primaryParaPrIDRef": 4, + "styleIDRefs": [ + 0 + ] + } + ], + [ + { + "borderFillIDRef": 10, + "isHeader": false, + "colAddr": 0, + "rowAddr": 2, + "colSpan": 1, + "rowSpan": 1, + "width_hu": 283, + "height_hu": 284, + "cellMargin": { + "left": 0, + "right": 0, + "top": 184, + "bottom": 0 + }, + "charPrIDRefs": [ + 9 + ], + "primaryCharPrIDRef": 9, + "paraPrIDRefs": [ + 4 + ], + "primaryParaPrIDRef": 4, + "styleIDRefs": [ + 0 + ] + }, + { + "borderFillIDRef": 11, + "isHeader": false, + "colAddr": 1, + "rowAddr": 2, + "colSpan": 1, + "rowSpan": 1, + "width_hu": 47063, + "height_hu": 284, + "cellMargin": { + "left": 0, + "right": 0, + "top": 184, + "bottom": 0 + }, + "charPrIDRefs": [ + 9 + ], + "primaryCharPrIDRef": 9, + "paraPrIDRefs": [ + 4 + ], + "primaryParaPrIDRef": 4, + "styleIDRefs": [ + 0 + ] + }, + { + "borderFillIDRef": 12, + "isHeader": false, + "colAddr": 2, + "rowAddr": 2, + "colSpan": 1, + "rowSpan": 1, + "width_hu": 283, + "height_hu": 284, + "cellMargin": { + "left": 0, + "right": 0, + "top": 184, + "bottom": 0 + }, + "charPrIDRefs": [ + 9, + 9 + ], + "primaryCharPrIDRef": 9, + "paraPrIDRefs": [ + 4, + 4 + ], + "primaryParaPrIDRef": 4, + "styleIDRefs": [ + 0, + 0 + ] + } + ] + ], + "colWidths_hu": [ + 283, + 47063, + 283 + ], + "colWidths_pct": [ + 1, + 99, + 1 + ] + } + ], + "section": { + "textDirection": "HORIZONTAL", + "hideFirstHeader": false, + "hideFirstFooter": false, + "hideFirstMasterPage": false, + "hideFirstPageNum": false, + "hideFirstEmptyLine": false, + "startNum": { + "pageStartsOn": "BOTH", + "page": 0 + } + }, + "styles": [ + { + "id": 0, + "paraPrIDRef": 3, + "charPrIDRef": 0, + "nextStyleIDRef": 0, + "type": "PARA", + "name": "바탕글", + "engName": "Normal" + }, + { + "id": 1, + "paraPrIDRef": 2, + "charPrIDRef": 3, + "nextStyleIDRef": 1, + "type": "PARA", + "name": "머리말", + "engName": "Header" + }, + { + "id": 2, + "paraPrIDRef": 1, + "charPrIDRef": 2, + "nextStyleIDRef": 2, + "type": "PARA", + "name": "쪽 번호", + "engName": "Page Number" + }, + { + "id": 3, + "paraPrIDRef": 0, + "charPrIDRef": 1, + "nextStyleIDRef": 3, + "type": "PARA", + "name": "각주", + "engName": "Footnote" + }, + { + "id": 4, + "paraPrIDRef": 0, + "charPrIDRef": 1, + "nextStyleIDRef": 4, + "type": "PARA", + "name": "미주", + "engName": "Endnote" + }, + { + "id": 5, + "paraPrIDRef": 4, + "charPrIDRef": 4, + "nextStyleIDRef": 5, + "type": "PARA", + "name": "표위", + "engName": "Memo" + }, + { + "id": 6, + "paraPrIDRef": 5, + "charPrIDRef": 0, + "nextStyleIDRef": 6, + "type": "PARA", + "name": "표옆", + "engName": "" + }, + { + "id": 7, + "paraPrIDRef": 7, + "charPrIDRef": 0, + "nextStyleIDRef": 7, + "type": "PARA", + "name": "표내용", + "engName": "" + }, + { + "id": 8, + "paraPrIDRef": 6, + "charPrIDRef": 5, + "nextStyleIDRef": 8, + "type": "PARA", + "name": "주)", + "engName": "" + }, + { + "id": 9, + "paraPrIDRef": 9, + "charPrIDRef": 6, + "nextStyleIDRef": 9, + "type": "PARA", + "name": "#큰아이콘", + "engName": "" + }, + { + "id": 10, + "paraPrIDRef": 14, + "charPrIDRef": 7, + "nextStyleIDRef": 10, + "type": "PARA", + "name": "개요1", + "engName": "" + }, + { + "id": 11, + "paraPrIDRef": 13, + "charPrIDRef": 8, + "nextStyleIDRef": 11, + "type": "PARA", + "name": "xl63", + "engName": "xl63" + } + ], + "numbering": { + "numberings": [ + { + "id": 1, + "start": 0, + "levels": [ + { + "level": 1, + "numFormat": "DIGIT", + "align": "LEFT", + "pattern": "^1." + }, + { + "level": 2, + "numFormat": "HANGUL_SYLLABLE", + "align": "LEFT", + "pattern": "^2." + }, + { + "level": 3, + "numFormat": "DIGIT", + "align": "LEFT", + "pattern": "^3)" + }, + { + "level": 4, + "numFormat": "HANGUL_SYLLABLE", + "align": "LEFT", + "pattern": "^4)" + }, + { + "level": 5, + "numFormat": "DIGIT", + "align": "LEFT", + "pattern": "(^5)" + }, + { + "level": 6, + "numFormat": "HANGUL_SYLLABLE", + "align": "LEFT", + "pattern": "(^6)" + }, + { + "level": 7, + "numFormat": "CIRCLED_DIGIT", + "align": "LEFT", + "pattern": "^7" + } + ] + } + ], + "bullets": [ + { + "id": 1, + "char": "-", + "useImage": false + } + ] + }, + "images": [ + { + "type": "image", + "width_hu": 1133, + "height_hu": 1133, + "width_mm": 4.0, + "height_mm": 4.0, + "offset": { + "x": 0, + "y": 0 + } + }, + { + "type": "image", + "width_hu": 624, + "height_hu": 624, + "width_mm": 2.2, + "height_mm": 2.2, + "offset": { + "x": 0, + "y": 0 + } + }, + { + "type": "image", + "width_hu": 624, + "height_hu": 624, + "width_mm": 2.2, + "height_mm": 2.2, + "offset": { + "x": 0, + "y": 0 + } + }, + { + "type": "image", + "width_hu": 1133, + "height_hu": 1133, + "width_mm": 4.0, + "height_mm": 4.0, + "offset": { + "x": 0, + "y": 0 + } + }, + { + "type": "image", + "width_hu": 624, + "height_hu": 624, + "width_mm": 2.2, + "height_mm": 2.2, + "offset": { + "x": 0, + "y": 0 + } + }, + { + "type": "image", + "width_hu": 624, + "height_hu": 624, + "width_mm": 2.2, + "height_mm": 2.2, + "offset": { + "x": 0, + "y": 0 + } + }, + { + "type": "image", + "width_hu": 624, + "height_hu": 624, + "width_mm": 2.2, + "height_mm": 2.2, + "offset": { + "x": 0, + "y": 0 + } + } + ], + "content_order": [ + { + "index": 0, + "paraPrIDRef": "22", + "styleIDRef": "0", + "type": "paragraph", + "text": "1. (스마트설계팀) SamanPro(V3.0)", + "charPrIDRef": "12", + "runs": [ + { + "charPrIDRef": "12", + "text": "1. (스마트설계팀) SamanPro(V3.0)" + } + ] + }, + { + "index": 1, + "paraPrIDRef": "12", + "styleIDRef": "9", + "type": "image", + "image_idx": 0, + "binaryItemIDRef": "image9", + "text": " 내용 요약", + "imgRect": { + "x": null, + "y": null, + "w": null, + "h": null + } + }, + { + "index": 2, + "paraPrIDRef": "8", + "styleIDRef": "7", + "type": "image", + "image_idx": 1, + "binaryItemIDRef": "image10", + "text": " 반복적 도로 설계 계산 작업과 행정의 자동화를 위한 통합 설계 플랫폼 구축", + "imgRect": { + "x": null, + "y": null, + "w": null, + "h": null + } + }, + { + "index": 3, + "paraPrIDRef": "15", + "styleIDRef": "7", + "type": "paragraph", + "text": "AI는 앱 개발 과정에서 코드 생성과 에러 해결을 위한 방식으로 활용", + "charPrIDRef": "13", + "runs": [ + { + "charPrIDRef": "13", + "text": "AI는 앱 개발 과정에서 코드 생성과 에러 해결을 위한 방식으로 활용" + } + ] + }, + { + "index": 4, + "paraPrIDRef": "8", + "styleIDRef": "7", + "type": "image", + "image_idx": 2, + "binaryItemIDRef": "image10", + "text": " 주요 기능", + "imgRect": { + "x": null, + "y": null, + "w": null, + "h": null + } + }, + { + "index": 5, + "paraPrIDRef": "15", + "styleIDRef": "7", + "type": "paragraph", + "text": "계산, 외부 데이터 확인(API) 및 제안서 작성 시 활용할 수 있는 프롬프트 등", + "charPrIDRef": "13", + "runs": [ + { + "charPrIDRef": "13", + "text": "계산, 외부 데이터 확인(API) 및 제안서 작성 시 활용할 수 있는 프롬프트 등" + } + ] + }, + { + "index": 6, + "paraPrIDRef": "19", + "styleIDRef": "7", + "type": "paragraph", + "text": " · 포장설계, 동결심도, 수리계산, 확폭계산, 편경사계산 등 설계 관련 계산 기능과 착수 일자와 과업기간 입력 시, 중공일자 표출되는 준공계 보조 계산 기능 등", + "charPrIDRef": "13", + "runs": [ + { + "charPrIDRef": "13", + "text": " · 포장설계, 동결심도, 수리계산, 확폭계산, 편경사계산 등 설계 관련 계산 기능과 착수 일자와 과업기간 입력 시, 중공일자 표출되는 준공계 보조 계산 기능 등" + } + ] + }, + { + "index": 7, + "paraPrIDRef": "20", + "styleIDRef": "7", + "type": "paragraph", + "text": " · 한국은행 API를 활용하여 (연도별/분기별) 건설투자 GDP 제공", + "charPrIDRef": "13", + "runs": [ + { + "charPrIDRef": "13", + "text": " · 한국은행 API를 활용하여 (연도별/분기별) 건설투자 GDP 제공" + } + ] + }, + { + "index": 8, + "paraPrIDRef": "20", + "styleIDRef": "7", + "type": "paragraph", + "text": " · 제안서 작성 시 AI에게 입력할 발주처(도로공사, 국토관리청, 지자체)별 기초 프롬프트*", + "charPrIDRef": "13", + "runs": [ + { + "charPrIDRef": "13", + "text": " · 제안서 작성 시 AI에게 입력할 발주처(도로공사, 국토관리청, 지자체)별 기초 프롬프트*" + } + ] + }, + { + "index": 9, + "paraPrIDRef": "10", + "styleIDRef": "0", + "type": "paragraph", + "text": " ※ 프롬프트 구성 : 역학과 목표, 입력 정의, 산출물 요구사항, 작업절차 단계, 출력 형식 등", + "charPrIDRef": "15", + "runs": [ + { + "charPrIDRef": "15", + "text": " ※ 프롬프트 구성 : 역학과 목표, 입력 정의, 산출물 요구사항, 작업절차 단계, 출력 형식 등" + } + ] + }, + { + "index": 10, + "paraPrIDRef": "11", + "styleIDRef": "7", + "type": "paragraph", + "text": "야근일자 계산기, 모니터 끄기, 자동종료 및 재시작, 계산기, pc 클리너 기능*", + "charPrIDRef": "16", + "runs": [ + { + "charPrIDRef": "16", + "text": "야근일자 계산기, 모니터 끄기, 자동종료 및 재시작, 계산기, pc 클리너 기능" + }, + { + "charPrIDRef": "17", + "text": "*" + } + ] + }, + { + "index": 11, + "paraPrIDRef": "10", + "styleIDRef": "0", + "type": "paragraph", + "text": " ※ GUI 기반의 앱으로 시간에 맞추어 자동종료, 재시작, PC 클린 등 수행", + "charPrIDRef": "15", + "runs": [ + { + "charPrIDRef": "15", + "text": " ※ GUI 기반의 앱으로 시간에 맞추어 자동종료, 재시작, PC 클린 등 수행" + } + ] + }, + { + "index": 12, + "paraPrIDRef": "12", + "styleIDRef": "9", + "type": "image", + "image_idx": 3, + "binaryItemIDRef": "image9", + "text": " 관련 의견", + "imgRect": { + "x": null, + "y": null, + "w": null, + "h": null + } + }, + { + "index": 13, + "paraPrIDRef": "21", + "styleIDRef": "9", + "type": "table", + "table_idx": 0, + "rowCnt": "3", + "colCnt": "3", + "borderFillIDRef": "3" + }, + { + "index": 14, + "paraPrIDRef": "10", + "styleIDRef": "0", + "type": "paragraph", + "text": "※ 도로배수시설 설계 및 유지관리지침, 설계유량(합리식(Rational formula), 흐름해석(Manning 공식) 등 반영", + "charPrIDRef": "15", + "runs": [ + { + "charPrIDRef": "15", + "text": "※ 도로배수시설 설계 및 유지관리지침, 설계유량(합리식(Rational formula), 흐름해석(Manning 공식) 등 반영" + } + ] + }, + { + "index": 15, + "paraPrIDRef": "17", + "styleIDRef": "7", + "type": "image", + "image_idx": 4, + "binaryItemIDRef": "image10", + "text": " 장점", + "imgRect": { + "x": null, + "y": null, + "w": null, + "h": null + } + }, + { + "index": 16, + "paraPrIDRef": "15", + "styleIDRef": "7", + "type": "paragraph", + "text": "비개발자가 AI를 통해 개발 및 사내에 공유 (경영진 92회 포함 총 712회 사용 등)", + "charPrIDRef": "13", + "runs": [ + { + "charPrIDRef": "13", + "text": "비개발자가 AI를 통해 개발 및 사내에 공유 (경영진 92회 포함 총 712회 사용 등)" + } + ] + }, + { + "index": 17, + "paraPrIDRef": "15", + "styleIDRef": "7", + "type": "paragraph", + "text": "(제안서 프롬프트) 질문-수집-생성 파이프라인까지 체계적으로 구축", + "charPrIDRef": "13", + "runs": [ + { + "charPrIDRef": "13", + "text": "(제안서 프롬프트) 질문-수집-생성 파이프라인까지 체계적으로 구축" + } + ] + }, + { + "index": 18, + "paraPrIDRef": "17", + "styleIDRef": "7", + "type": "image", + "image_idx": 5, + "binaryItemIDRef": "image10", + "text": " 확인 필요 지점 ", + "imgRect": { + "x": null, + "y": null, + "w": null, + "h": null + } + }, + { + "index": 19, + "paraPrIDRef": "15", + "styleIDRef": "7", + "type": "paragraph", + "text": "지침 개정 시, 계산 로직 또는 기준값 등을 사람이 확인, 반영하는 것?", + "charPrIDRef": "13", + "runs": [ + { + "charPrIDRef": "13", + "text": "지침 개정 시, 계산 로직 또는 기준값 등을 사람이 확인, 반영하는 것?" + } + ] + }, + { + "index": 20, + "paraPrIDRef": "18", + "styleIDRef": "7", + "type": "paragraph", + "text": " → 개정 반영 표준화 또는 파이프라인 등을 통하여 운영 체계를 구축하는 것에 대한 고려", + "charPrIDRef": "13", + "runs": [ + { + "charPrIDRef": "13", + "text": " → 개정 반영 표준화 또는 파이프라인 등을 통하여 운영 체계를 구축하는 것에 대한 고려" + } + ] + }, + { + "index": 21, + "paraPrIDRef": "17", + "styleIDRef": "7", + "type": "image", + "image_idx": 6, + "binaryItemIDRef": "image10", + "text": " 개선 방향 제안", + "imgRect": { + "x": null, + "y": null, + "w": null, + "h": null + } + }, + { + "index": 22, + "paraPrIDRef": "15", + "styleIDRef": "7", + "type": "paragraph", + "text": "(제안서 프롬프트) ① 상용 AI 모델의 업데이트 상황에 따른 품질 변동, ② 특정 모델에 최적화, ③ 단일 프롬프트에 모든 단계를 포함하여 중간 결과물의 유실될 가능성(단계를 나누거나 또는 컨텍스트를 반영하는 파이프라인 등 고려), ④ 결과물 검증 기준 추가 필요", + "charPrIDRef": "13", + "runs": [ + { + "charPrIDRef": "13", + "text": "(제안서 프롬프트) ① 상용 AI 모델의 업데이트 상황에 따른 품질 변동, ② 특정 모델에 최적화, ③ 단일 프롬프트에 모든 단계를 포함하여 중간 결과물의 유실될 가능성(단계를 나누거나 또는 컨텍스트를 반영하는 파이프라인 등 고려), ④ 결과물 검증 기준 추가 필요" + } + ] + }, + { + "index": 23, + "paraPrIDRef": "15", + "styleIDRef": "7", + "type": "paragraph", + "text": "(수리 계산 기준 표출) 기준과 버전 사항들도 함께 계산기 내에서 표출될 필요", + "charPrIDRef": "13", + "runs": [ + { + "charPrIDRef": "13", + "text": "(수리 계산 기준 표출) 기준과 버전 사항들도 함께 계산기 내에서 표출될 필요" + } + ] + }, + { + "index": 24, + "paraPrIDRef": "10", + "styleIDRef": "0", + "type": "paragraph", + "text": " (예) 수리계산(Box/Pipe) : 도로배수시설 설계 및 유지관리지침(2025) 반영 ", + "charPrIDRef": "15", + "runs": [ + { + "charPrIDRef": "15", + "text": " (예) 수리계산(Box/Pipe) : 도로배수시설 설계 및 유지관리지침(2025) 반영 " + } + ] + }, + { + "index": 25, + "paraPrIDRef": "15", + "styleIDRef": "7", + "type": "paragraph", + "text": "(계산 결과 출력) 기준, 입력 변수, 산식, 출력 결과값 등이 바로 활용될 수 있도록 한글(HWP), 엑셀이나 특정 템플릿 등으로 출력을 고려", + "charPrIDRef": "13", + "runs": [ + { + "charPrIDRef": "13", + "text": "(계산 결과 출력) 기준, 입력 변수, 산식, 출력 결과값 등이 바로 활용될 수 있도록 한글(HWP), 엑셀이나 특정 템플릿 등으로 출력을 고려" + } + ] + }, + { + "index": 26, + "paraPrIDRef": "15", + "styleIDRef": "7", + "type": "paragraph", + "text": "(향후 로드맵) AI 기반 설계 검토와 BIM 연동 등은 지금 기술 대비 난이도가 크게 상승(AI API 적용, 파이프라인 구축 등), 단계별 검증과 구체적 마일스톤 수립이 필요", + "charPrIDRef": "18", + "runs": [ + { + "charPrIDRef": "18", + "text": "(향후 로드맵) AI 기반 설계 검토와 BIM 연동 등은 지금 기술 대비 난이도가 크게 상승(AI API 적용, 파이프라인 구축 등), 단계별 검증과 구체적 마일스톤 수립이 필요" + } + ] + } + ] + }, + "css": "", + "fonts": {}, + "colors": { + "background": [ + "#F3F3F3" + ], + "border": [ + "#000000" + ], + "text": [] + }, + "border_fills": { + "1": { + "id": 1, + "left": { + "type": "NONE", + "width": "0.1mm", + "color": "#000000" + }, + "right": { + "type": "NONE", + "width": "0.1mm", + "color": "#000000" + }, + "top": { + "type": "NONE", + "width": "0.1mm", + "color": "#000000" + }, + "bottom": { + "type": "NONE", + "width": "0.1mm", + "color": "#000000" + }, + "diagonal": { + "type": "SOLID", + "width": "0.1mm", + "color": "#000000" + }, + "css": { + "border-left": "none", + "border-right": "none", + "border-top": "none", + "border-bottom": "none" + } + }, + "2": { + "id": 2, + "left": { + "type": "NONE", + "width": "0.1mm", + "color": "#000000" + }, + "right": { + "type": "NONE", + "width": "0.1mm", + "color": "#000000" + }, + "top": { + "type": "NONE", + "width": "0.1mm", + "color": "#000000" + }, + "bottom": { + "type": "NONE", + "width": "0.1mm", + "color": "#000000" + }, + "diagonal": { + "type": "SOLID", + "width": "0.1mm", + "color": "#000000" + }, + "css": { + "border-left": "none", + "border-right": "none", + "border-top": "none", + "border-bottom": "none" + } + }, + "3": { + "id": 3, + "left": { + "type": "SOLID", + "width": "0.12mm", + "color": "#000000" + }, + "right": { + "type": "SOLID", + "width": "0.12mm", + "color": "#000000" + }, + "top": { + "type": "SOLID", + "width": "0.12mm", + "color": "#000000" + }, + "bottom": { + "type": "SOLID", + "width": "0.12mm", + "color": "#000000" + }, + "css": { + "border-left": "0.12mm solid #000000", + "border-right": "0.12mm solid #000000", + "border-top": "0.12mm solid #000000", + "border-bottom": "0.12mm solid #000000" + } + }, + "4": { + "id": 4, + "left": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "right": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "top": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "bottom": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "css": { + "border-left": "none", + "border-right": "none", + "border-top": "none", + "border-bottom": "none" + } + }, + "5": { + "id": 5, + "left": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "right": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "top": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "bottom": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "css": { + "border-left": "none", + "border-right": "none", + "border-top": "none", + "border-bottom": "none" + } + }, + "6": { + "id": 6, + "left": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "right": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "top": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "bottom": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "css": { + "border-left": "none", + "border-right": "none", + "border-top": "none", + "border-bottom": "none" + } + }, + "7": { + "id": 7, + "left": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "right": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "top": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "bottom": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "css": { + "border-left": "none", + "border-right": "none", + "border-top": "none", + "border-bottom": "none" + } + }, + "8": { + "id": 8, + "left": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "right": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "top": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "bottom": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "background": "#F3F3F3", + "css": { + "border-left": "none", + "border-right": "none", + "border-top": "none", + "border-bottom": "none", + "background-color": "#F3F3F3" + } + }, + "9": { + "id": 9, + "left": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "right": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "top": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "bottom": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "css": { + "border-left": "none", + "border-right": "none", + "border-top": "none", + "border-bottom": "none" + } + }, + "10": { + "id": 10, + "left": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "right": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "top": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "bottom": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "css": { + "border-left": "none", + "border-right": "none", + "border-top": "none", + "border-bottom": "none" + } + }, + "11": { + "id": 11, + "left": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "right": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "top": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "bottom": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "css": { + "border-left": "none", + "border-right": "none", + "border-top": "none", + "border-bottom": "none" + } + }, + "12": { + "id": 12, + "left": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "right": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "top": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "bottom": { + "type": "NONE", + "width": "0.12mm", + "color": "#000000" + }, + "css": { + "border-left": "none", + "border-right": "none", + "border-top": "none", + "border-bottom": "none" + } + } + }, + "tables": [], + "style_summary": {} +} \ No newline at end of file diff --git a/templates/user/templates/tpl_1770301063/template.html b/templates/user/templates/tpl_1770301063/template.html new file mode 100644 index 0000000..a959f7d --- /dev/null +++ b/templates/user/templates/tpl_1770301063/template.html @@ -0,0 +1,507 @@ + + + + +Template + + + +
+ + + +
+

{{SECTION_1_TITLE}}

+ +
+ {{IMAGE_1}} +

{{IMAGE_1_CAPTION}}

+
+ +
+ {{IMAGE_2}} +

{{IMAGE_2_CAPTION}}

+
+ +

{{PARA_1}}

+ +
+ {{IMAGE_3}} +

{{IMAGE_3_CAPTION}}

+
+ +

{{PARA_2}}

+ +

{{PARA_3}}

+ +

{{PARA_4}}

+ +

{{PARA_5}}

+ +

{{PARA_6}}

+ +

{{PARA_7_RUN_1}}{{PARA_7_RUN_2}}

+ +

{{PARA_8}}

+ +
+ {{IMAGE_4}} +

{{IMAGE_4_CAPTION}}

+
+ + ++ + + + + + + + + + + + + {{TABLE_1_BODY}} + +
{{TABLE_1_H_C1}}{{TABLE_1_H_C2}}{{TABLE_1_H_C3}}
+ +

{{PARA_9}}

+ +
+ {{IMAGE_5}} +

{{IMAGE_5_CAPTION}}

+
+ +

{{PARA_10}}

+ +

{{PARA_11}}

+ +
+ {{IMAGE_6}} +

{{IMAGE_6_CAPTION}}

+
+ +

{{PARA_12}}

+ +

{{PARA_13}}

+ +
+ {{IMAGE_7}} +

{{IMAGE_7_CAPTION}}

+
+ +

{{PARA_14}}

+ +

{{PARA_15}}

+ +

{{PARA_16}}

+ +

{{PARA_17}}

+ +

{{PARA_18}}

+ +
+ + + + +
+ + \ No newline at end of file