# Hwpx

> HWP/HWPX 문서(.hwp, .hwpx) 변환·생성·읽기·편집, HTML 활동지→편집 가능한 HWPX 변환 스킬. 'HWP 변환', 'hwp를 hwpx로', '한글 문서', 'hwpx', 'HWPX', '한글파일', '.hwpx 만들어줘', '보고서', '공문', '기안문', '한글로 작성', '회의록', '제안서', '이미지 포함 문서', 'HTML을 HWPX로', 'K-Teacher 스타일', '컬러 활동지' 등의 요청 시 사용한다. Windows에서는 설치된 한컴오피스 Automation COM 엔진으로 HWP를 HWPX로 빠르게 변환한 뒤 읽기·편집을 이어가며, 사용할 수 없으면 내장 rhwp WASM 변환기로 폴백한다.

- Skill: `jkf87/hwpx` (Agent Skill, multi-file: 6 files)
- Install (CLI): `npx skillmds@latest add jkf87/hwpx`
- Raw SKILL.md: https://api.skillmd.com/api/skills/jkf87/hwpx/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: jkf87 (https://skillmd.com/u/jkf87)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/jkf87/hwpx

---


# HWPX 통합 문서 스킬

HWPX는 한컴오피스 한글의 개방형 문서 포맷이다. **ZIP 패키지 + XML 파트** 구조.

## 스킬 디렉토리

```
${CLAUDE_SKILL_DIR}/
├── SKILL.md
├── scripts/
│   ├── hwpx_helpers.py        # ★ 헬퍼 라이브러리 (배너/섹션바/이미지/빌드 함수)
│   ├── convert_hwp.py         # ★ HWP→HWPX 변환 (Workflow H)
│   ├── convert_hwp_hancom.ps1 # ★ Windows 한컴 COM 고속·일괄 변환 (Workflow H 우선)
│   ├── build_hwpx.py          # 템플릿+XML → .hwpx 조립
│   ├── fix_namespaces.py      # ★ 필수: 네임스페이스 후처리
│   ├── validate.py            # HWPX 구조 검증
│   ├── finalize_hwpx.py       # line cache removal, layout QA, Hancom open test
│   ├── analyze_template.py    # HWPX 심층 분석
│   ├── clone_form.py           # ★ 양식 복제 (Workflow F)
│   ├── fill_hwpx.py            # ★★ 양식 필드 채우기 + 머리말/꼬리말/쪽번호/표구조/수식 in-place (Workflow J)
│   ├── secure_fill.py          # ★ 개인정보(PII) 비경유 양식 채우기
│   ├── verify_hwpx.py         # ★ 서브에이전트 검수 도구
│   ├── text_extract.py        # 텍스트 추출
│   ├── build_problem_answer_sheet.py  # 문제지 1장 + 답안지 1장 생성
│   ├── md2hwpx.py             # 마크다운→HWPX 자동 변환
│   ├── gonmun.py              # ★ 행정안전부 표준 기안문(별지 제1호서식) 생성기 (Workflow G)
│   ├── gonmun_lint.py         # ★ 공문서 작성법 자동 검수기 (2025 편람)
│   ├── yoyak.py               # ★ 요약보고(결재선 달린 1~3쪽 약식 보고) 생성기 — 마크다운 입력 (Workflow Y)
│   ├── geomto.py              # ★ 기본계획·검토보고(로마숫자 장 배너, 표지 선택) 생성기 — 마크다운 입력 (Workflow Y)
│   ├── munche_lint.py         # ★ 개조식 보고서 원고 문체 검문기 (서술형 종결·수사적 대조·길이·표기) (Workflow Y)
│   ├── bodojaryo.py           # ★ 정부 표준 보도자료 생성기 (레퍼런스 복제 방식)
│   ├── gyehoek.py             # ★ 공공기관 계획서 생성기 (행안부 업무계획 복제, 제목/목차 토글)
│   ├── gyehoek_hook.py        # ★ PreToolUse 훅 — 계획서 생성 전 제목/목차 포함 여부 강제 질문
│   ├── report_placeholder_hook.py  # ★ PreToolUse 훅 — 기관명 미입력 보고서 전달 차단
│   └── office/{unpack,pack}.py
├── templates/
│   ├── base/                  # 베이스 Skeleton
│   ├── report/                # 보고서
│   ├── gonmun/                # 공문(간이형)
│   ├── gonmun2025/            # ★ 행정안전부 표준 기안문 별지 제1호서식 (맑은 고딕 11.5pt)
│   ├── yoyak/                 # ★ 요약보고 스타일(header.xml) — 실측값에서 파생, 원본 흔적 없음
│   ├── geomto/                # ★ 기본계획·검토보고 스타일(header.xml = yoyak + 장 배너·표지)
│   ├── minutes/               # 회의록
│   ├── proposal/              # 제안서
├── assets/
│   ├── report-template.hwpx
│   ├── gyehoek-reference.hwpx       # ★ 공공기관 계획서 기본양식(행안부 2025 업무계획) — gyehoek.py가 복제
│   ├── bodojaryo-reference.hwpx     # ★ 정부 표준 보도자료 양식(고정) — bodojaryo.py가 복제
│   └── problem-answer-reference.hwpx
└── references/
    ├── xml-structure.md       # XML 구조, 이미지 삽입, 표지/섹션 바 패턴
    ├── template-styles.md     # 템플릿별 스타일 ID 맵
    ├── troubleshooting.md     # 트러블슈팅
    ├── report-style.md        # 보고서 양식 상세
    ├── official-doc-style.md  # 공문서 양식 상세
    ├── gonmunseo-2025-writing-rules.md  # ★ 2025 개정 공문서 작성법
    ├── yoyak-bogo-style.md      # ★ 요약보고 서식 실측·파생 (Workflow Y)
    ├── geomto-bogo-style.md     # ★ 기본계획·검토보고 서식 실측·파생 (Workflow Y)
    ├── bogo-munche.md           # ★ 개조식 보고서 문체 — 실측 15건 통계·규칙·검문표 (Workflow Y)
    ├── kordoc-integration.md  # kordoc 장점 채택/보류 기준
    └── xml-internals.md       # 저수준 XML 구조
```

## 환경 설정

```bash
pip install python-hwpx lxml --break-system-packages
# HWP→HWPX 변환 (Workflow H)
# Windows: 한컴오피스 한글 + HwpAutomation 파일 경로 보안 모듈 권장(추가 Python 의존성 없음)
# 그 외/폴백: Node.js 18+, rhwp WASM 런타임은 scripts/vendor/rhwp에 고정·포함
```

## Mandatory Finalization And QA

Run this finalization sequence for every generated or edited `.hwpx` before
delivering it to a user:

```bash
python3 "${CLAUDE_SKILL_DIR}/scripts/fix_namespaces.py" output.hwpx
python3 "${CLAUDE_SKILL_DIR}/scripts/finalize_hwpx.py" output.hwpx --strip-linesegarray --layout
python3 "${CLAUDE_SKILL_DIR}/scripts/validate.py" output.hwpx --layout
```

On Windows with Hancom Office installed, add a real open test:

```bash
python3 "${CLAUDE_SKILL_DIR}/scripts/validate.py" output.hwpx --hancom
```

Rules:

1. After any XML-level text replacement, remove `hp:linesegarray`. These are
   Hancom line-layout caches; stale caches can make Hancom show a damaged-file
   restore warning even when ZIP/XML validation passes.
2. Treat `validate.py` as structural validation only unless `--layout` or
   `--hancom` is used. XML validity does not prove that Hancom can open the
   file or that long text fits the template.
3. For template forms, preserve the template structure. If content is too long,
   split the content into multiple paragraphs/list items and increase row
   heights. Do not change the template just to fit existing prose.
4. For subcategory body text, create real new paragraphs with the template body
   style or visible list/indent markers. Do not put several long sentences into
   one `<hp:t>` and rely on visual wrapping.
5. For table rows with increased cell heights, update every cell in that row and
   keep the table-level `hp:sz/@height` consistent with the row heights.

---

## ★ 워크플로우 선택 (Decision Tree)

> **반드시 아래 판단을 따른다.**

```
사용자 요청
 ├─ ".hwp 파일 → 변환/읽기/편집" → 워크플로우 H로 별도 HWPX 생성 후 해당 워크플로우 계속 ★★
 ├─ "마크다운/텍스트/URL → HWPX" → 워크플로우 A (콘텐츠→HWPX)
 ├─ "양식의 빈칸/필드 채워줘" (라벨-값, 체크박스, 괄호 빈칸) → 워크플로우 J (필드 채우기) ★★★
 ├─ "양식에 내용 채워줘" ({{플레이스홀더}} 템플릿) → 워크플로우 B (템플릿 치환)
 ├─ "HWPX 수정해줘" → 워크플로우 C (기존 문서 편집)
 ├─ "이 HWPX 양식으로 만들어줘" → 워크플로우 D (레퍼런스 기반)
 ├─ "이 양식 복제해서 내용 바꿔줘" → 워크플로우 F (양식 복제) ★
 ├─ "공문 작성해줘/공문서 검수해줘" → 워크플로우 G (공문서 작성법 준수) ★
 ├─ "요약보고/약식보고/단장님·부시장님 보고용 한 장" (결재선 + □❍- 개조식) → 워크플로우 Y (yoyak) ★
 ├─ "기본계획/검토보고/검토서 만들어줘" (Ⅰ Ⅱ Ⅲ 장 배너, 표지·결재란 선택) → 워크플로우 Y (geomto) ★
 ├─ "문제지 한장 답안지 한장", "문제지+답안지", "정답지 포함 활동지" → 워크플로우 I ★
 ├─ "HTML 디자인을 HWPX로", "K-Teacher 스타일", "컬러 활동지" → 워크플로우 K ★
 └─ "HWPX 읽어줘" → 워크플로우 E (읽기/추출)
```

### ⚠️ 원본 형식 보존 규칙 (사용자가 .hwp 파일을 제공한 경우)

> **HWP 바이너리는 직접 수정하지 않는다. 읽기·편집 등 작업에 HWP 내용이 필요하면
> 원본을 그대로 보존하고 별도 `.hwpx`를 자동 생성한 뒤 후속 워크플로우를 계속한다.**

```
입력 파일 확인
 ├─ .hwp 파일
 │   ├─ "HWPX로 변환해줘" → 워크플로우 H (원본 유지, 별도 .hwpx 출력)
 │   ├─ "읽어줘/텍스트 추출" → 워크플로우 H → E
 │   └─ "수정해줘/채워줘" → 워크플로우 H → C/J/F (별도 HWPX 결과)
 └─ .hwpx 파일 → 기존 워크플로우 판별 (아래)
```

변환으로 생긴 HWPX는 반드시 검증한 뒤 사용한다. 사용자가 결과 형식을 HWP로
유지하라고 명시했거나 변환을 금지한 경우에는 자동 변환하지 말고 제한을 안내한다.

### ⚠️ 자동 판별 규칙 (사용자가 양식 파일을 제공한 경우)

> **사용자가 `.hwpx` 파일을 주고 "이걸로 테스트", "내용 바꿔줘", "이 양식으로" 등을 요청하면
> 먼저 `clone_form.py --analyze`로 구조를 확인한다.**

```
양식 분석 결과
 ├─ 빈 값 셀/체크박스/괄호 빈칸이 있는 신청서·서식 → 워크플로우 J (필드 채우기) ★★★
 ├─ 테이블 ≥ 1개 또는 이미지 ≥ 1개, 기존 문구를 새 문구로 교체 → 워크플로우 F (양식 복제) ★★★
 ├─ 테이블 0개, 이미지 0개, 단순 텍스트 → 워크플로우 C 또는 D 가능
 └─ 판단 불가 → `fill_hwpx.py analyze` 먼저 실행 — 타겟이 있으면 J, 없으면 F
```

> **절대 하지 말 것:**
> - `<hp:t>` 노드를 순차적으로 새 텍스트로 덮어쓰기 — **런(run) 소실, 서식 파괴**
> - lxml로 텍스트 노드를 직접 조작 — **네임스페이스/속성 손실 위험**
> - 새 section0.xml을 처음부터 작성 (Workflow A/D) — **구조 97.5% 손실**
>
> **반드시 할 것:**
> - `clone_form.py`의 `clone()` 함수 또는 ZIP-level 문자열 치환 사용
> - 치환은 `str.replace()` 기반으로 XML 구조를 건드리지 않음

---

## 워크플로우 I: 문제지 1장 + 답안지 1장 생성

> 학생용 문제지와 교사용 답안지를 한 파일 안에 2쪽 구조로 만든다. 1쪽은 `문제지`, 2쪽은 `답안지`이며, 전체를 표 기반으로 구성한다.

### 입력 JSON

```json
{
  "title": "수업 제목",
  "unit": "영상 수업",
  "subtitle": "핵심 내용과 실천 목표",
  "subject": "국어",
  "main_actor": "학생",
  "scenes": [
    {"title": "도입", "summary": "핵심 내용을 한 문장으로 정리한다."},
    {"title": "전개", "summary": "중요 장면과 근거를 정리한다."},
    {"title": "정리", "summary": "배운 점과 실천 목표를 쓴다."}
  ],
  "change": "변화나 배운 점 예시 답안",
  "theme": "핵심 주제 예시 답안"
}
```

### 생성 명령

```bash
python3 "${CLAUDE_SKILL_DIR}/scripts/build_problem_answer_sheet.py" \
  --input-json lesson.json \
  --output lesson-sheet.hwpx
python3 "${CLAUDE_SKILL_DIR}/scripts/validate.py" lesson-sheet.hwpx
```

### 품질 기준

- `assets/problem-answer-reference.hwpx`에서 header/secPr/style을 가져온다.
- 문제지와 답안지 사이에는 `pageBreak="1"`이 정확히 1개 있어야 한다.
- 구조 검증은 `validate.py`로 통과해야 한다.
- 최종 HWPX의 `Contents/section0.xml`에는 `문제지`, `답안지`, `첫 번째 활동`, `두 번째 활동`, `세 번째 활동`, `정답`, `예시 답안` 텍스트가 있어야 한다.
- JSON 입력에 `\\n`이 들어와도 실제 줄바꿈으로 정규화한다.

---

## 워크플로우 K: K-Teacher 학생 활동지 HTML → HWPX

K-Teacher가 실제 생성하는 학생 활동지 HTML을 편집 가능한 HWPX 표·문단·네이티브 둥근 도형으로 변환한다. 임의의 브라우저 CSS를 복제하는 범용 변환기가 아니라 `.doc-header`, `section.block`, `student_task`, `source_card`, `answer_box`, `exit_ticket`, 자료표와 쪽 나누기를 결정론적으로 매핑한다. 디자인 기준은 저장소 루트 `DESIGN.md`를 따른다.

```bash
python3 "${CLAUDE_SKILL_DIR}/scripts/html2hwpx.py" input.html output.hwpx \
  --keep-xml build/html2hwpx
```

변환 단계와 지원 HTML은 [references/html-to-hwpx.md](references/html-to-hwpx.md)를 따른다. 결과는 스크립트 내부에서 `fix_namespaces.py`, `finalize_hwpx.py --strip-linesegarray --layout`, `validate.py --layout`까지 통과해야 한다.

---

## 워크플로우 A: 콘텐츠 → HWPX (가장 중요!)

> **마크다운·텍스트·URL → 구조화된 HWPX 문서. 이 워크플로우가 핵심.**

> 마크다운 한 편을 그대로 문서로 만들 때는 `md2hwpx.py` 가 가장 빠르다.
> 표지·배너처럼 문단을 직접 조립해야 하는 구조가 필요하면 `hwpx_helpers.py` 를
> import 해 아래 흐름을 따른다.

### 전체 흐름

```
[1] 소스 자료 읽기
[2] 구조 파싱 (제목, 섹션, 본문, 이미지)
[3] 템플릿 선택 → 해당 템플릿의 스타일 ID만 사용 (references/template-styles.md)
    ⚠️ 템플릿 간 ID는 호환되지 않음! report charPr를 base에 쓰면 깨짐
[4] hwpx_helpers.py를 import하여 Python 빌드 스크립트 작성
[5] build_hwpx.py로 .hwpx 조립
[6] 이미지가 있으면 add_images_to_hwpx() + update_content_hpf()
[7] fix_namespaces.py 후처리 (필수!)
[8] validate.py 검증
```

> 사용 예: `from hwpx_helpers import *` → `make_first_para()` → `make_body_para()` → `make_image_para()`

### section0.xml 핵심 규칙

1. **첫 문단 첫 run에 secPr + colPr 필수** — 없으면 문서가 안 열림
2. **모든 문단 id는 고유 정수**
3. **XML 특수문자 `<>&"` 반드시 이스케이프**
4. **표지→본문 사이 `pageBreak="1"` 문단 삽입**

> XML 구조 상세: [references/xml-structure.md](references/xml-structure.md)

### 빌드 명령

```bash
# 1. section0.xml을 임시 파일로 작성 (Python 스크립트로 생성)

# 2. 빌드 (예: report 템플릿)
python3 "${CLAUDE_SKILL_DIR}/scripts/build_hwpx.py" \
  --template report \
  --section /tmp/section0.xml \
  --title "문서 제목" \
  --output result.hwpx

# 3. 네임스페이스 후처리 (필수!)
python3 "${CLAUDE_SKILL_DIR}/scripts/fix_namespaces.py" result.hwpx

# 4. 검증
python3 "${CLAUDE_SKILL_DIR}/scripts/validate.py" result.hwpx
```

### Python 빌드 스크립트 패턴

> **`scripts/hwpx_helpers.py`를 import하여 검증된 함수를 재사용한다.**

```python
import subprocess, sys
from pathlib import Path
sys.path.insert(0, str(Path("${CLAUDE_SKILL_DIR}/scripts")))
from hwpx_helpers import *

SKILL_DIR = Path("${CLAUDE_SKILL_DIR}")
REF_HWPX = SKILL_DIR / "assets" / "gyehoek-reference.hwpx"
OUTPUT = Path("output.hwpx")

# 1. secPr 추출
secpr, colpr = extract_secpr_and_colpr(REF_HWPX)

# 2. section0.xml 조립
parts = []
parts.append(f'<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>')
parts.append(f'<hs:sec {NS_DECL}>')
parts.append(make_first_para(secpr, colpr))
parts.append(make_text_para("문서 제목", charpr="7", parapr="20"))
parts.append(make_empty_line())
parts.append(make_body_para("가.", "본문 내용"))
parts.append(f'</hs:sec>')
section_xml = "\n".join(parts)

# 3. 빌드
Path("/tmp/section0.xml").write_text(section_xml, encoding="utf-8")
subprocess.run(["python3", str(SKILL_DIR/"scripts/build_hwpx.py"),
    "--template", "report",
    "--section", "/tmp/section0.xml", "--output", str(OUTPUT)], check=True)

# 4. (이미지 있으면) add_images_to_hwpx() + update_content_hpf()

# 5. 후처리 + 검증
subprocess.run(["python3", str(SKILL_DIR/"scripts/fix_namespaces.py"), str(OUTPUT)], check=True)
subprocess.run(["python3", str(SKILL_DIR/"scripts/validate.py"), str(OUTPUT)])
```

### hwpx_helpers.py 제공 함수

| 함수 | 설명 |
|------|------|
| `next_id()` | 고유 ID 생성 |
| `xml_escape(text)` | XML 특수문자 이스케이프 |
| `extract_secpr_and_colpr(hwpx)` | HWPX에서 secPr+colPr 추출 |
| `make_first_para(secpr, colpr)` | 첫 문단 (secPr 포함) |
| `make_empty_line()` | 빈 줄 |
| `make_page_break()` | 페이지 넘김 |
| `make_text_para(text, charpr, parapr)` | 텍스트 문단 |
| `make_body_para(marker, text)` | 본문 (마커+내용) |
| `make_image_para(binary_item_id, w, h)` | 이미지 (전체 hp:pic 구조) |
| `add_images_to_hwpx(path, images)` | ZIP에 이미지 추가 |
| `update_content_hpf(path, images)` | content.hpf에 이미지 등록 |
| `NS_DECL` | 네임스페이스 선언 상수 |

> 스타일 ID 상세: [references/template-styles.md](references/template-styles.md)

### 이미지 포함 시

> **이미지 `<hp:pic>` 구조가 불완전하면 한컴오피스가 크래시한다.**
> 반드시 [references/xml-structure.md](references/xml-structure.md)의 "이미지 삽입" 섹션을 읽고 전체 구조를 사용할 것.

---

## 워크플로우 B: 템플릿 치환

> **기존 양식의 플레이스홀더를 교체. 양식 문서에 적합.**

```
[1] 양식 파일 복사 → [2] ObjectFinder로 텍스트 조사
[3] 플레이스홀더 매핑 → [4] ZIP-level 치환 → [5] fix_namespaces.py → [6] 검증
```

### ZIP-level 치환

```python
import zipfile, os

def zip_replace(src, dst, replacements):
    tmp = dst + ".tmp"
    with zipfile.ZipFile(src, "r") as zin:
        with zipfile.ZipFile(tmp, "w", zipfile.ZIP_DEFLATED) as zout:
            for item in zin.infolist():
                data = zin.read(item.filename)
                if item.filename.startswith("Contents/") and item.filename.endswith(".xml"):
                    text = data.decode("utf-8")
                    for old, new in replacements.items():
                        text = text.replace(old, new)
                    data = text.encode("utf-8")
                if item.filename == "mimetype":
                    zout.writestr(item, data, compress_type=zipfile.ZIP_STORED)
                else:
                    zout.writestr(item, data)
    os.replace(tmp, dst)
```

### 양식 선택 정책

1. 사용자 업로드 양식 → 해당 파일 사용
2. `${CLAUDE_SKILL_DIR}/assets/report-template.hwpx`
3. HwpxDocument.new()는 최후의 수단

---

## 워크플로우 C: 기존 문서 편집

```bash
python3 "${CLAUDE_SKILL_DIR}/scripts/office/unpack.py" doc.hwpx ./unpacked/
# XML 편집 후
python3 "${CLAUDE_SKILL_DIR}/scripts/office/pack.py" ./unpacked/ edited.hwpx
python3 "${CLAUDE_SKILL_DIR}/scripts/fix_namespaces.py" edited.hwpx
```

## 워크플로우 D: 레퍼런스 기반 생성

```bash
python3 "${CLAUDE_SKILL_DIR}/scripts/analyze_template.py" reference.hwpx
# header.xml 추출 후 동일 스타일 ID로 새 section0.xml 작성
python3 "${CLAUDE_SKILL_DIR}/scripts/build_hwpx.py" \
  --header /tmp/ref_header.xml --section /tmp/new_section.xml --output result.hwpx
python3 "${CLAUDE_SKILL_DIR}/scripts/fix_namespaces.py" result.hwpx
```

## 워크플로우 E: 읽기/추출

```bash
python3 "${CLAUDE_SKILL_DIR}/scripts/text_extract.py" doc.hwpx
python3 "${CLAUDE_SKILL_DIR}/scripts/text_extract.py" doc.hwpx --format markdown
```

---

## 워크플로우 J: 양식 필드 채우기 (★★ 원본 보존 최강 — 신청서/서식에 필수)

> **원본 HWPX의 양식 필드만 채우고 나머지는 바이트 단위로 보존한다.**
> LLM은 JSON만 작성한다 — XML을 손으로 쓰는 단계가 없으므로 어떤 LLM에서도 같은 결과가 나온다.
>
> - XML은 DOM 재직렬화 없이 `<hp:t>` 텍스트만 문자열 splice로 교체 → **fix_namespaces.py 불필요**
> - ZIP은 변경된 section XML 엔트리만 재작성 → 이미지·header.xml·mimetype 등 **나머지 엔트리는 바이트 동일**
> - 값 셀의 첫 `<hp:run>`의 charPrIDRef를 유지 → **글꼴/크기/굵기 보존**
> - 수정된 문단의 `hp:linesegarray`(줄배치 캐시)를 **외과적으로 자동 제거** → stale 캐시로
>   인한 한컴 '손상 파일' 경고 방지. 무수정 문단의 캐시는 보존되므로
>   `finalize_hwpx.py --strip-linesegarray`(전체 제거)를 추가로 돌릴 필요 없음.
>   레이아웃 경고 검사가 필요하면 `finalize_hwpx.py --layout`만 사용.

### 채우기 전략 (자동 적용)

| 전략 | 패턴 | 예 |
|------|------|-----|
| 인셀 패턴 | 체크박스/괄호 빈칸/어노테이션 | `□동의`→`☑동의`, `일반(  )통`→`일반(3)통`, `(한자：  )`→`(한자：洪吉童)` |
| 라벨-값 셀 | 라벨 셀의 오른쪽 셀 교체 | `성명 │ (빈칸)` → `성명 │ 홍길동` |
| 헤더 행 | 첫 행이 전부 라벨인 표 | `품명│수량` 헤더 아래 데이터 행 채움 |
| 인라인 | 표 밖 문단의 "라벨: 값" | `작성자: 미정` → `작성자: 김철수` |

라벨 매칭은 정규화(공백/콜론/괄호 제거) + 접두사 퍼지 매칭(60% 이상 겹침)이므로
`"성  명："`도 키 `"성명"`으로 매칭된다.

### 전체 흐름 (3단계 파이프라인)

```bash
# [1] 분석 — 채울 수 있는 타겟을 JSON으로 출력 (key를 그대로 values의 키로 사용)
python3 "${CLAUDE_SKILL_DIR}/scripts/fill_hwpx.py" analyze form.hwpx

# [2] values.json 작성 — analyze가 출력한 key에 값만 매핑
#     {"성명": "홍길동", "연락처": "010-1234-5678", "동의": "☑"}

# [3] 채우기 + 검증
python3 "${CLAUDE_SKILL_DIR}/scripts/fill_hwpx.py" fill form.hwpx output.hwpx --values values.json
python3 "${CLAUDE_SKILL_DIR}/scripts/fill_hwpx.py" verify output.hwpx --values values.json --original form.hwpx
```

- `fill`의 출력 JSON에서 `unmatched`가 비어 있어야 한다. 남아 있으면 `analyze`의 key와
  values의 키가 일치하는지 확인하고 다시 실행한다.
- `verify --original`은 ① 모든 값이 실제로 문서에 들어갔는지 ② 섹션 XML 외 엔트리가
  바이트 동일한지 검사한다. `"ok": true`가 아니면 결과물을 사용자에게 주지 않는다.
- 종료 코드: 0=성공, 2=채워진 항목 없음/검증 실패 → 워크플로우 F로 폴백.

### 내용 수정: `replace` — 문구 교체 (run 경계 무관)

한컴은 한 문장을 여러 `<hp:run>`/`<hp:t>`로 쪼개 저장하는 경우가 많아
clone_form.py의 단순 문자열 치환이 놓칠 수 있다. `replace`는 문단 단위로
텍스트를 이어붙여 찾으므로 쪼개진 문구도 잡고, 각 run의 charPrIDRef는 유지된다.

```bash
# map.json: {"옛 문구": "새 문구", ...}
python3 "${CLAUDE_SKILL_DIR}/scripts/fill_hwpx.py" replace doc.hwpx out.hwpx --map map.json
```

출력의 `replaced`에 문구별 교체 횟수, `not_found`에 못 찾은 문구가 보고된다.
`not_found`가 있으면 `analyze` 또는 text_extract.py로 원본 문구를 다시 확인한다.

### 내용 추가: `add-row` — 표 행 추가 (스타일 100% 보존)

기존 행의 XML을 통째로 복제해 표 끝에 붙이므로 셀 너비·테두리·글꼴이 그대로다.
cellAddr rowAddr, 표 rowCnt, 문단 id가 자동 갱신된다.

```bash
# rows.json: [["모니터","5"], ["키보드","10"]]  — 행당 셀 수와 일치해야 함
python3 "${CLAUDE_SKILL_DIR}/scripts/fill_hwpx.py" add-row doc.hwpx out.hwpx \
  --table 1 --rows rows.json          # --table은 analyze의 table 번호
```

> rowSpan 병합이 있는 표는 좌표가 깨질 수 있어 **자동 거부**된다(exit 1).
> 이 경우 행 추가 대신 사용자에게 양식 구조 한계를 알린다.

### 내용 추가: `add-para` — 본문 문단 추가

기준 문구가 있는 문단을 복제해 그 뒤에 삽입한다. paraPr/charPr를 물려받아
스타일이 유지된다. 기준 문단에 secPr/표/개체가 있으면 거부된다(exit 1) —
일반 텍스트 문단을 기준으로 다시 지정한다.

```bash
python3 "${CLAUDE_SKILL_DIR}/scripts/fill_hwpx.py" add-para doc.hwpx out.hwpx \
  --after "기준 문구" --text "추가할 문단"
# 여러 개: --paras paras.json  ([{"after": "...", "text": "..."}])
```

### 머리말·꼬리말·쪽번호: `set-header` / `set-footer` / `set-pagenum`

기존 .hwpx에 페이지 머리말(상단)·꼬리말(하단)·자동 쪽번호를 사후 삽입/갱신/제거한다.
섹션 첫 문단(secPr) 뒤에 `<hp:ctrl>` 봉투로 넣으며, 본문 바이트는 보존된다. 같은 종류가
이미 있으면 **새로 만들지 않고 갱신**(중복 방지)한다.

```bash
# 머리말/꼬리말 삽입·갱신 (--apply BOTH|EVEN|ODD, --align LEFT|CENTER|RIGHT)
python3 "${CLAUDE_SKILL_DIR}/scripts/fill_hwpx.py" set-header doc.hwpx out.hwpx --text "대외주의" --align center
python3 "${CLAUDE_SKILL_DIR}/scripts/fill_hwpx.py" set-footer doc.hwpx out.hwpx --text "한국연구재단"

# 자동 쪽번호 (--where footer|header). 해당 머리말/꼬리말이 있으면 그 안에 번호를 추가
python3 "${CLAUDE_SKILL_DIR}/scripts/fill_hwpx.py" set-pagenum doc.hwpx out.hwpx --where footer --align center

# 제거
python3 "${CLAUDE_SKILL_DIR}/scripts/fill_hwpx.py" remove-header doc.hwpx out.hwpx
python3 "${CLAUDE_SKILL_DIR}/scripts/fill_hwpx.py" remove-footer doc.hwpx out.hwpx
```

- `--align`은 **best-effort**: header.xml에 해당 가로정렬(`<hh:align horizontal=...>`) paraPr가
  이미 있으면 그 id를 재사용하고, 없으면 기본 정렬로 폴백한다(응답 JSON의 `align` 필드에 표시).
  정부 표준 양식(report/gonmun2025 등)은 CENTER paraPr를 보유해 가운데 정렬이 바로 적용된다.
- 텍스트 갱신 시 기존 머리말의 id·정렬·applyPageType는 보존된다(`--align`/`--apply` 미지정 시).
  한 문서에 머리말/꼬리말 슬롯이 여러 개면 **전부** 같은 텍스트로 갱신한다(정부 양식은
  머리말 슬롯을 2개 두기도 해서, 첫 개만 채우면 일부 페이지에 안 보이는 사고가 난다).

### 표 구조/스타일: `set-cell` / `add-col` / `del-row` / `merge-cells`

기존 표의 '모양'을 바꾼다(claw-hwp hwpx-edit.js 포팅, 순수 stdlib·원본 보존).
좌표 모델은 `analyze`/`fill --cells`와 동일(--table=섹션 내 문서순서, --row/--col=cellAddr).

```bash
# 셀 배경색/테두리 — 배경은 borderFill 복제 후 셀 borderFillIDRef를 repoint
python3 "${CLAUDE_SKILL_DIR}/scripts/fill_hwpx.py" set-cell doc.hwpx out.hwpx --table 0 --row 0 --col 1 --bg FFE600 --border on
# 열 추가 (끝 또는 --at 위치) — 새 열 값은 --cells ["행0","행1",...] JSON 파일
python3 "${CLAUDE_SKILL_DIR}/scripts/fill_hwpx.py" add-col doc.hwpx out.hwpx --table 0 --cells newcol.json
# 행 삭제
python3 "${CLAUDE_SKILL_DIR}/scripts/fill_hwpx.py" del-row doc.hwpx out.hwpx --table 0 --row 2
# 사각 범위 셀 병합 (앵커 ~ 끝)
python3 "${CLAUDE_SKILL_DIR}/scripts/fill_hwpx.py" merge-cells doc.hwpx out.hwpx --table 0 --row 0 --col 0 --row2 0 --col2 2
```

- **rowSpan/colSpan이 이미 있는 표는 좌표 재계산 안전을 위해 거부(exit 1)** — span 없는 일반 격자에서 동작. 배경색은 header.xml의 borderFill만 추가(itemCnt 보정), 그 외 엔트리 보존.

### 수식: `add-equation`

본문(--after) 또는 표 셀(--table/--row/--col)에 네이티브 한컴 수식(`<hp:equation>`)을 삽입.
자기완결 봉투라 외부 의존이 없다(claw buildEquationXml 1:1). **수식 문법은 `references/equation-syntax.md` 참조**(분수·근호·적분·행렬·그리스문자 등).

```bash
python3 "${CLAUDE_SKILL_DIR}/scripts/fill_hwpx.py" add-equation doc.hwpx out.hwpx --after "기준 문구" --script "x^2+y^2=z^2"
# 셀에: --table 0 --row 1 --col 1 --script "int _0 ^1 x^2 dx = 1 over 3"  (선택 --size 1200 = 12pt)
```

### 개인정보 양식: `secure_fill.py` (PII 비경유)

주민번호·계좌 등 PII가 **모델 컨텍스트/로그/stdout를 거치지 않게** 양식을 채운다.
값은 프로필 파일에서 in-process로만 읽고, 출력엔 키 이름·개수·마스킹값만 나온다(claw secure-fill 포팅).

```bash
python3 "${CLAUDE_SKILL_DIR}/scripts/secure_fill.py" detect form.hwpx           # 채울 키 목록(값 비출력)
python3 "${CLAUDE_SKILL_DIR}/scripts/secure_fill.py" fill form.hwpx out.hwpx --profile profile.json --shred-profile
python3 "${CLAUDE_SKILL_DIR}/scripts/secure_fill.py" verify out.hwpx --profile profile.json   # 마스킹 보고
python3 "${CLAUDE_SKILL_DIR}/scripts/secure_fill.py" shred profile.json          # 프로필 안전 삭제(0덮어쓰기+unlink)
```

- ⚠️ **프로필 파일을 `cat`/출력하지 말 것** — PII 누출. 기본 ephemeral, 작업 후 `--shred-profile` 또는 `shred` 권장.
  전화/주민번호/날짜는 칸 모양에 맞춰 자동 변환(값·변환값 모두 비출력). `shred`는 cwd·홈·임시 디렉토리 밖 경로는 거부.

### 글자/문단 서식: `set-text-style` / `set-para-style`

기존 본문 문단의 글자모양(charPr)·문단모양(paraPr)을 바꾼다. 대상 문단의 현재
모양을 복제·변형한 새 모양을 header.xml에 추가(itemCnt 보정)하고 IDRef를 그쪽으로
바꾼다 — 대상 문단만 영향, 나머지 보존. 대상은 `--after "문구"` 또는 `--para N`
(0-base, `last`/`-1`=마지막; 미지정 시 마지막 문단).

```bash
# 글자: 굵게/기울임/밑줄 + 색(RRGGBB) + 크기(pt)
python3 "${CLAUDE_SKILL_DIR}/scripts/fill_hwpx.py" set-text-style doc.hwpx out.hwpx --after "제목 문구" --bold --color C00000 --size 16
# 문단: 정렬 + 줄간격(%)
python3 "${CLAUDE_SKILL_DIR}/scripts/fill_hwpx.py" set-para-style doc.hwpx out.hwpx --after "제목 문구" --align center --line-spacing 180
```

- 글자모양은 대상 문단 첫 run의 charPr를 기준으로 복제하므로 글꼴/크기 계열이 유지된다(요청한 항목만 변경). 한 문단의 모든 run에 적용된다.
- ⚠️ 문단모양은 복제·변형 방식이라 **한컴오피스 데스크톱에선 유지되지만 한컴독스(웹) 라운드트립 시 정렬이 초기화**될 수 있다(claw 동일 한계). 데스크톱 산출물엔 문제없다.

### 직인/서명·이미지: `place-seal` / `insert-image`

서명/직인 이미지를 문서에 넣는다(BinData 추가 + content.hpf 등록 + section 참조, 원본 보존).
**직인 PNG는 사용자가 제공**한다(생성 기능은 Pillow 의존이라 미포함).

```bash
# 직인/서명: 기준 문구(발신명의 등) 위에 떠있는(floating) 그림으로
python3 "${CLAUDE_SKILL_DIR}/scripts/fill_hwpx.py" place-seal doc.hwpx out.hwpx --image seal.png --anchor "발신명의" --size-mm 20 --dx-mm 0 --dy-mm 0
# 일반 이미지: 새 문단 블록(기본) 또는 --inline(글자처럼)
python3 "${CLAUDE_SKILL_DIR}/scripts/fill_hwpx.py" insert-image doc.hwpx out.hwpx --image fig.png --after "그림 위치" --size-mm 60 40
```

### 각주·미주·하이퍼링크·책갈피: `add-footnote` / `add-endnote` / `add-hyperlink` / `add-bookmark`

대상 문단(`--after "문구"` 또는 `--para N`)에 삽입.

```bash
python3 "${CLAUDE_SKILL_DIR}/scripts/fill_hwpx.py" add-footnote doc.hwpx out.hwpx --after "본문 문구" --text "각주 내용"
python3 "${CLAUDE_SKILL_DIR}/scripts/fill_hwpx.py" add-hyperlink doc.hwpx out.hwpx --para -1 --url "https://example.kr" --text "바로가기"
python3 "${CLAUDE_SKILL_DIR}/scripts/fill_hwpx.py" add-bookmark doc.hwpx out.hwpx --after "장 제목" --name "ch1"
```

### 페이지·다단·쪽/단 나누기: `set-page` / `set-columns` / `page-break` / `column-break`

```bash
python3 "${CLAUDE_SKILL_DIR}/scripts/fill_hwpx.py" set-page doc.hwpx out.hwpx --orientation landscape --margin-mm 15 --size a4
python3 "${CLAUDE_SKILL_DIR}/scripts/fill_hwpx.py" set-columns doc.hwpx out.hwpx --count 2 --gap-mm 8
python3 "${CLAUDE_SKILL_DIR}/scripts/fill_hwpx.py" page-break doc.hwpx out.hwpx --after "여기서 쪽 나눔"   # 해제: --off
```

- ⚠️ `set-page`/`set-columns`는 secPr(pagePr/margin/colPr)를 정확히 보존하며 속성만 바꾼다. 섹션이 여러 개면 모든 secPr가 동일 적용된다(다중 섹션 개별 설정 미지원).

### 목록: `set-bullet-list` / `set-number-list` / `clear-list`

본문 문단을 글머리표(•)·번호목록으로 전환/해제. 범위는 `--para N --to M`.

```bash
python3 "${CLAUDE_SKILL_DIR}/scripts/fill_hwpx.py" set-bullet-list doc.hwpx out.hwpx --para 3 --to 6 --char "▶"
python3 "${CLAUDE_SKILL_DIR}/scripts/fill_hwpx.py" set-number-list doc.hwpx out.hwpx --after "목록 시작"
```

- ⚠️ **데스크톱 한컴 기준**. 한컴독스(웹)는 네이티브와 fingerprint가 다른 목록을 silent-strip할 수 있다(claw 동일 한계). 데스크톱 산출물엔 문제없다.

### 차트: `insert-chart`

OOXML 차트를 삽입(col/bar/line/area/pie). 범주·계열은 JSON 파일.

```bash
echo '["1월","2월","3월"]' > cat.json
echo '[{"name":"매출","values":[10,20,15]}]' > series.json
python3 "${CLAUDE_SKILL_DIR}/scripts/fill_hwpx.py" insert-chart doc.hwpx out.hwpx --type col --cat cat.json --series series.json --after "차트 위치"
```

### 문서 테마: `set-theme` (+ md2hwpx `--theme`)

제목/머리 글자색과 표 머리행 배경색을 테마 한 단어로 일괄 적용(원본 보존).
한국 공문서용 정제 세트 **기본·남색·진녹·진회색**(영문 default/navy/green/charcoal).

```bash
# 기존 문서 in-place (제목 charPr 색 + 표 머리행 배경색)
python3 "${CLAUDE_SKILL_DIR}/scripts/fill_hwpx.py" set-theme doc.hwpx out.hwpx --theme 남색
# 색 직접 지정(테마 override)
python3 "${CLAUDE_SKILL_DIR}/scripts/fill_hwpx.py" set-theme doc.hwpx out.hwpx --heading-color 1F3864 --table-header-color D6DCE5
# 새 문서 생성 시 테마
python3 "${CLAUDE_SKILL_DIR}/scripts/md2hwpx.py" in.md -o out.hwpx --theme 남색
```

- 제목/머리 판별은 **본문보다 큰 글자(charPr height)** 휴리스틱. 글꼴 변경은 fontface 등록이 필요해 새 문서 생성 경로에서만(in-place는 색).

### 도형/글상자: `insert-shape` / `insert-textbox`

대상 문단(`--after`/`--para`) 뒤에 사각형·글상자를 floating으로 삽입.

```bash
python3 "${CLAUDE_SKILL_DIR}/scripts/fill_hwpx.py" insert-textbox doc.hwpx out.hwpx --after "여기" --text "참고 메모" --fill FFF2CC --line BF9000 --rounding 24
python3 "${CLAUDE_SKILL_DIR}/scripts/fill_hwpx.py" insert-shape doc.hwpx out.hwpx --para last --width-mm 40 --height-mm 15 --fill DDEBF7 --rounding 24
```

`--rounding`은 네이티브 `hp:rect@ratio` 값(0~100)이다. 0은 직각이며,
활동지 카드에는 20~28 정도가 가장 안정적이다.

### 이미지 편집: `list-images` / `resize-image` / `replace-image` / `delete-image`

문서 내 그림을 인덱스로 편집. 먼저 `list-images`로 인덱스 확인.

```bash
python3 "${CLAUDE_SKILL_DIR}/scripts/fill_hwpx.py" list-images doc.hwpx
python3 "${CLAUDE_SKILL_DIR}/scripts/fill_hwpx.py" resize-image doc.hwpx out.hwpx --index 0 --width-mm 30   # 높이 생략=비율 유지
python3 "${CLAUDE_SKILL_DIR}/scripts/fill_hwpx.py" replace-image doc.hwpx out.hwpx --index 0 --image new.png
python3 "${CLAUDE_SKILL_DIR}/scripts/fill_hwpx.py" delete-image doc.hwpx out.hwpx --index 0
```

### 좌표 지정 폴백: `fill --cells`

라벨 휴리스틱이 안 통하는 복잡한 표는 `analyze`가 보고한 좌표로 직접 채운다.

```bash
# cells.json: [{"table":0,"row":2,"col":1,"value":"텍스트"}]
python3 "${CLAUDE_SKILL_DIR}/scripts/fill_hwpx.py" fill form.hwpx out.hwpx --cells cells.json
# --values와 --cells는 동시 사용 가능 (라벨 매칭 후 좌표 채움 순서)
```

## 워크플로우 R: 문서 재현 (레퍼런스 분석 → 새 내용 조판) ★★

> **레퍼런스 문서 한 편을 주면 그 편집 규범을 뽑아, 새 내용을 같은 규범으로
> 조판한다.** 복제 후 치환(Workflow F)과 달리 **내용 길이가 원본과 달라도
> 레이아웃이 어긋나지 않는다.**

### 왜 치환 대신 조판인가

레퍼런스의 문단 서식은 재사용 가능한 스타일 체계가 아니라 **줄마다 손으로
맞춘 일회용 기하학**이다(실측: gyehoek-reference 는 paraPr 242개가 의미 속성
기준으로도 206종 — 사실상 문단마다 다르다). 그래서 복제 후 텍스트만 바꾸면
각 줄이 '옛 텍스트에 맞춰진 들여쓰기·여백·셀 높이' 를 그대로 물고 있게 되고,
빈 박스·페이지 쪼개짐·칸 밀림이 생긴다.

이 워크플로우는 다르게 나눈다.

| 무엇 | 어떻게 |
|---|---|
| header.xml (글꼴·스타일·테두리 정의) | 원본 것을 **그대로** 쓴다 → 서식 충실도·유효성 보장 |
| section0.xml (본문 구조) | 내용에 맞춰 **새로 조판**한다 → 필요한 만큼 문단·행 생성 |
| 줄배치 캐시(`linesegarray`) | **아예 만들지 않는다** → 한컴이 열 때 재계산 |

### 사용법

```bash
# [0] 레퍼런스가 .hwp(바이너리)면 먼저 변환한다 — analyze 는 .hwpx 만 읽는다
python3 scripts/convert_hwp.py <ref.hwp> -o <ref.hwpx>

# [1] 레퍼런스에서 편집 규범 추출 (스타일 공급용 base.hwpx 도 함께 보관)
python3 scripts/doc_spec.py analyze <ref.hwpx> -o spec/

# [2] 원고를 그 규범으로 조판
python3 scripts/doc_spec.py render spec/ <content.md> -o out.hwpx \
    [--cover-page] [--toc] [--org 기관명] [--date "2026. 8. 21."]
```

`--cover-page` 는 제목을 **독립 표지 쪽**으로 세우고 그 아래 날짜·기관을 넣는다.
`--toc` 는 장(`##`) 제목을 모아 **목차 쪽**을 만든다. 보고서·계획서처럼 분량이
있는 문서에 쓴다.

`analyze` 가 뽑는 것 — 본문 계층별 대표 서식(□/ㅇ/-/*/※/￭/⇒), 제목 배너
(표지·장·절), 강조 박스, 제목 있는 박스, 데이터 표, 이미지 배치.

### 원고 문법

```markdown
# 문서 제목                  → 표지 배너
## Ⅰ. 장 제목                → 장 배너
### 1 절 제목                → 절 배너([번호][제목])
□ 대항목 / ㅇ 중항목 / - 소항목 / * 각주 / ※ 참고 / ￭ 실적
- 목록 항목                  → 들여쓰기 깊이로 계층 지정(0칸=중항목, 2칸=소항목)
  - 두 칸 들여쓴 하위 항목       기호를 직접 쓰지 않아도 레퍼런스의 기호가 붙는다
⇒ 결론 문장                  → 강조 박스로 조판
::: <박스 제목>              → 제목 있는 박스
￭ 박스 안 항목
:::
| 분야 | 과제명 | 일정 |     → 데이터 표(행 수만큼 자동 생성)
| --- | --- | --- |
![](사진.png)                → 이미지(원본 비율 유지, 본문 폭에 맞춤)
```

### 조판 품질 검문 (render 후 자동 실행)

```bash
python3 scripts/doc_spec.py lint <out.hwpx>
```

결정론(같은 입력 → 같은 바이트)은 품질을 보장하지 못한다. 실제로 난 사고 셋
(글자 겹침·배너 3줄 접힘·빈 박스)은 전부 결정론적이었다 — 매번 똑같이 깨졌다.
그래서 레이아웃 자체를 검사한다.

- **오류(전달 금지)**: 줄배치 캐시 잔존, `lineWrap="SQUEEZE"`(글자 겹침),
  표 칸 수 불일치
- **경고**: 칸이 내용보다 낮음(잘림), 칸이 지나치게 높음(빈 박스),
  칸·그림이 본문 폭 초과

`render` 는 끝나고 이 검문을 자동으로 돌린다. 오류가 있으면 exit 2 로 멈춘다.

산출물도 배포 전 필수 게이트(`check --strict`)를 그대로 통과시켜야 한다.

### ★★★ 치환 게이트: replace 를 돌리기 전/후 반드시 통과시킬 것

> **레퍼런스 복제(Workflow F/계획서·보도자료) 계열에서 실제로 가장 많이 난 사고는
> "치환이 조용히 안 먹어 원본 부처 문구가 그대로 배포되는 것"이다.**
> 원인은 사람 주의력으로 못 막는 종류라 도구로 막는다(실측, gyehoek-reference 기준):
> · `‧`(U+2027) 89회 / `·`(U+00B7) 86회가 섞여 있다 — 눈으로 구별 불가.
>   텍스트를 보고 키를 타이핑하면 약 50% 확률로 빗나간다.
> · 글머리 글리프가 사설영역 문자(U+F02B1 등 21개)라 옮길 때 유실된다.
> · 28개 문단에 `<hp:fwSpace/>`·`<hp:tab/>`이 있고, `<hp:t>` 사이에 낀 것은
>   문구를 쪼갠다. 매처는 `<hp:t>` 텍스트만 이어붙이므로 그 경계를 넘는 키는 실패한다.
> · 눈으로 한 줄로 보이는 것이 실제로는 별개 문단 2~3개인 경우가 있다.

```bash
# [1] 키는 손으로 타이핑하지 말고, 매처와 같은 방식으로 문서에서 뽑아 쓴다
python3 scripts/map_preflight.py dump <base.hwpx> [--grep 검색어]

# [2] replace 전 사전검증 — 못 맞출 키를 원인과 교정안까지 짚어준다
python3 scripts/map_preflight.py check <base.hwpx> --map map.json [--fix fixed.json]

# [3] 치환 (하나라도 못 찾으면 자동 진단 후 exit 2 로 차단된다)
python3 scripts/fill_hwpx.py replace <base.hwpx> <out.hwpx> --map map.json

# [4] ★ 잔재 전수 대조 — 맵에 '아예 안 넣은' 문단에 원본이 남았는지는
#     이것만 잡는다. 하드코딩 단어 grep 으로는 반드시 놓친다.
python3 scripts/map_preflight.py residue <out.hwpx> --against <base.hwpx> \
    [--ignore "유지할 범용 라벨"]
```

- `check` 가 **"전부 매칭"** 이 아니면 replace 를 돌리지 마라.
- `residue` 가 **0개**가 아니면 사용자에게 전달하지 마라. 남은 문단은
  ① 맵에 추가해 치환하거나 ② 의도한 범용 라벨이면 `--ignore` 로 명시 제외한다.
- `replace` 는 이제 **부분 성공을 성공으로 보고하지 않는다**(not_found 있으면 exit 2).
  의도적으로 넘기려면 `--allow-unmatched` 를 명시해야 한다.

### ★★★ 필수 게이트: 사용자에게 파일을 주기 전 반드시 통과시킬 것

> **모든 .hwpx 산출물은 사용자에게 전달(open·복사·첨부·"완성했습니다" 보고)하기
> 직전에 아래를 반드시 실행한다. 어떤 워크플로우(생성/변환/편집)로 만들었든 예외 없다.**
> validate.py(XML 유효성)·verify(값 존재)를 통과해도 한컴이 문서를 못 여는 일이 있다.

```bash
python3 "${CLAUDE_SKILL_DIR}/scripts/fill_hwpx.py" check output.hwpx --strict
```

- **exit 0**: 통과 → 전달 가능
- **exit 2**: 아래 표대로 수정한 뒤 **다시 check가 통과할 때까지** 전달 금지

| 사고 | check 신호 | 수정 방법 |
|------|-----------|-----------|
| **손상된 문서 대화상자** | `errors`: secPr에 pagePr/margin 누락·pageWidth 등 비표준 속성 | 정상 HWPX의 `<hp:secPr>...</hp:secPr>`을 이식. 애초에 정상 파일을 베이스로 작업 |
| **빈 페이지로 열림** | `raw_llm_suspect: true`: 미리보기·줄배치 부재(한컴 미경유) | 정상 HWPX(한컴 저장본/워크플로우 H 변환본)를 베이스로 fill/replace. 또는 한컴에서 한 번 열어 저장 |
| **모든 글자에 네모 테두리** | `char_border_bug: true`: charPr 다수가 SOLID 테두리 borderFill 참조 | `fill_hwpx.py fix-borders output.hwpx` 실행 후 재check |
| **글자가 세로로 뒤집힘** | `vertical_misconvert: true`: 셀 textDirection이 대부분 VERTICAL | 의도한 세로쓰기인지 원본과 비교한 뒤, 오변환일 때만 textDirection VERTICAL→HORIZONTAL 교정 |

> ⚠️ **이 게이트를 건너뛰면 안 된다.** 과거 사고가 전부 여기서 잡혔어야 했다:
> 가짜 secPr(손상 문서), raw 파일(빈 페이지), 글자 테두리 — 셋 다 `check --strict`가
> 잡는다. fill의 `verify`에도 이 점검이 자동 포함된다.
>
> **특히 글자 테두리는 변환(convert)을 안 거치는 경로(기존 hwpx 편집)에서도 생기므로,
> "변환했으니 괜찮다"고 넘기지 말고 반드시 최종 산출물에 check를 돌릴 것.**

### 안전망: 배포 차단 훅 (Claude Code 환경 자동화)

`scripts/hwpx_guard_hook.py`를 PreToolUse 훅(matcher: Bash)으로 등록하면, .hwpx를
`open`/`cp`/`mv`로 전달하기 직전 자동으로 **글자 테두리는 제거**하고 **secPr·raw
문제는 차단**한다. 이는 위 필수 게이트의 **백업 안전망**이지 대체가 아니다 — 훅이
없는 환경(다른 에이전트 등)에서는 위 게이트를 LLM이 직접 지켜야 한다. 등록 방법은
스크립트 상단 주석 참조.

`scripts/report_placeholder_hook.py`(PreToolUse, matcher: Bash)는 보고서 템플릿
(`assets/report-template.hwpx`)의 중립 플레이스홀더 **'〔기관명 입력〕'이 남은 .hwpx를 실제
보고서로 전달(open/Downloads·Desktop 복사)하려 하면 차단**한다. 이 placeholder는
템플릿 구조 보존을 위해 파일에 남겨두되, 전달 전 반드시 `fill_hwpx.py replace`로 실제
기관명으로 교체해야 한다(내부 작업용 복제는 막지 않음). 등록 방법은 스크립트 상단 주석 참조.

### 워크플로우 J vs F vs B 선택 기준

| 상황 | 도구 |
|------|-----|
| 빈 양식(신청서·서식)의 필드 채우기 — 라벨/체크박스/빈칸 | **J `fill`** |
| 작성된 문서의 기존 문구를 새 문구로 교체 | **J `replace`** (run 분할 대응) → 실패 시 F |
| 표에 데이터 행 추가 | **J `add-row`** |
| 머리말/꼬리말/쪽번호 사후 추가·제거 | **J `set-header`/`set-footer`/`set-pagenum`/`remove-*`** |
| 표 셀 배경/테두리·열추가·행삭제·셀병합 | **J `set-cell`/`add-col`/`del-row`/`merge-cells`** |
| 수식 삽입(본문/셀) | **J `add-equation`** (문법: references/equation-syntax.md) |
| 본문 글자/문단 서식(굵게·색·크기·정렬·줄간격) | **J `set-text-style`/`set-para-style`** |
| 직인/서명·이미지 삽입 | **J `place-seal`/`insert-image`** (이미지 사용자 제공) |
| 각주·미주·하이퍼링크·책갈피 | **J `add-footnote`/`add-endnote`/`add-hyperlink`/`add-bookmark`** |
| 페이지 설정·다단·쪽/단 나누기 | **J `set-page`/`set-columns`/`page-break`/`column-break`** |
| 글머리표·번호목록 전환 | **J `set-bullet-list`/`set-number-list`/`clear-list`** (데스크톱 기준) |
| 차트 삽입(막대/선/원 등) | **J `insert-chart`** |
| 문서 테마(제목색·표머리색 일괄) | **J `set-theme`** / md2hwpx `--theme` |
| 도형·글상자 삽입 | **J `insert-shape`/`insert-textbox`** |
| 기존 이미지 크기변경·교체·삭제 | **J `list-images`/`resize-image`/`replace-image`/`delete-image`** |
| 개인정보(주민번호·계좌) 양식 채우기 | **`secure_fill.py`** (PII 비경유) |
| 라벨 매칭 실패한 복잡한 표 | **J `fill --cells`** (좌표 지정) |
| XML 전역 일괄 치환 (메타데이터 포함) | F (clone_form.py) |
| `{{이름}}` 같은 플레이스홀더가 박힌 전용 템플릿 | B |

> J가 타겟을 못 찾으면(`analyze`의 target_count가 0) `replace`(문구 교체)나
> F로 전환한다. **.hwp 입력은 원본을 보존한 별도 HWPX로 워크플로우 H 변환한 뒤
> J를 적용하며, 결과가 HWPX라는 점을 사용자에게 명확히 알린다.**

---

## 워크플로우 F: 양식 복제 (★ 복잡한 양식에 필수)

> **기존 HWPX를 통째로 복사 + 텍스트만 치환. 테이블·이미지·스타일 100% 보존.**
>
> ⚠️ **테이블 5개 이상 또는 이미지 포함이면 반드시 워크플로우 F 사용.**
> 워크플로우 D는 header만 재활용하고 section을 새로 만들기 때문에 구조의 97.5%를 잃는다.

> ### ★ 정부 표준 보도자료 (고정 양식)
>
> 보도자료는 표 5개·로고 이미지 6개로 구성되어 **반드시 복제 방식**을 쓴다.
> 실제 정부 보도자료를 `assets/bodojaryo-reference.hwpx`로 고정해 두었고,
> `scripts/bodojaryo.py`가 이를 복제해 **표·로고·글꼴을 100% 보존**하면서 본문(□/ㅇ/*)과
> 머리표(보도시점·제목·부제·담당자)만 교체한다.
>
> ```bash
> python3 scripts/bodojaryo.py --sample --output 보도자료.hwpx        # 샘플
> python3 scripts/bodojaryo.py --input bodo.json --output 보도자료.hwpx  # JSON 입력
> python3 scripts/gonmun_lint.py --hwpx 보도자료.hwpx --format text       # 본문 작성법 검수
> ```
> 양식 구조·JSON 스키마는 `scripts/bodojaryo.py` 헤더 주석 참조. 본문 마커는 `□`(대) → `ㅇ`(하위,
> ○ 아님) → `*`(각주). 로고는 레퍼런스 것이 들어가므로 본인 기관용은 한컴에서 이미지만 교체한다.

> ### ★ 공공기관 계획서 (기본 양식 = 행안부 2025 업무계획)
>
> 계획서는 표 24개로 구성되어 **복제 방식**을 쓴다. 실제 행정안전부 「2025년 주요업무 추진계획」을
> `assets/gyehoek-reference.hwpx`로 채택했고(기존 저품질 체육과 문서 교체), `scripts/gyehoek.py`가
> 이를 복제해 표·글꼴을 보존하면서 **표지 제목·작성연월을 교체**하고 **표지/목차(순서)를 토글**한다.
>
> ⚠️ **계획서 생성 전에는 `gyehoek_hook.py`(PreToolUse 훅)가 제목·목차 포함 여부를 사용자에게
> 먼저 묻도록 강제한다.** 즉 두 결정(아래 플래그)을 명시하지 않고 `gyehoek.py`를 실행하면 훅이
> 차단하므로, **반드시 사용자에게 먼저 질문**한 뒤 결정값을 붙여 실행한다.
>
> ```bash
> # 제목 넣음 + 목차 넣음
> python3 scripts/gyehoek.py --title "2026년 ○○ 추진계획" --date "2026. 1." --toc --output 계획서.hwpx
> # 제목 없음 + 목차 없음
> python3 scripts/gyehoek.py --no-title --no-toc --output 계획서.hwpx
> ```
> 플래그: 제목 `--title "..."` / `--no-title`,  목차 `--toc` / `--no-toc`. (훅: settings.json PreToolUse 등록)

### 전체 흐름

```
[1] 원본 양식 분석:  clone_form.py --analyze sample.hwpx
[2] 구문 치환 맵 작성 (JSON): {"원본 문구": "새 문구", ...}
[3] (선택) 키워드 폴백 맵 작성: {"재난": "교육위기", "안전": "AI교육", ...}
[4] 복제 실행:  clone_form.py sample.hwpx output.hwpx --map map.json --keywords kw.json
[5] fix_namespaces.py 후처리 (필수!)
[6] validate.py 검증
```

### 2단계 치환 전략

| 단계 | 범위 | 용도 |
|------|------|------|
| Phase 1 (--map) | 전체 XML | 긴 문구·문장 단위 치환 |
| Phase 2 (--keywords) | `<hp:t>` 내부만 | 남은 키워드 개별 치환 (폴백) |

> 키워드는 길이 내림차순 정렬하여 "재난안전관리"가 "재난"보다 먼저 매칭된다.
> Phase 2는 `<hp:t>` 태그 안의 텍스트만 대상이므로 XML 구조를 손상시키지 않는다.

### CLI 사용법

```bash
# 분석
python3 "${CLAUDE_SKILL_DIR}/scripts/clone_form.py" --analyze sample.hwpx

# 복제 (구문 치환만)
python3 "${CLAUDE_SKILL_DIR}/scripts/clone_form.py" \
  sample.hwpx output.hwpx --map replacements.json

# 복제 (구문 + 키워드 폴백)
python3 "${CLAUDE_SKILL_DIR}/scripts/clone_form.py" \
  sample.hwpx output.hwpx --map map.json --keywords keywords.json --validate

# 후처리 (필수!)
python3 "${CLAUDE_SKILL_DIR}/scripts/fix_namespaces.py" output.hwpx
python3 "${CLAUDE_SKILL_DIR}/scripts/validate.py" output.hwpx
```

### Python API

```python
from clone_form import clone, analyze, extract_texts, validate_result

# 분석
texts = analyze("sample.hwpx")

# 복제
clone("sample.hwpx", "output.hwpx",
      replacements={"원본 문구": "새 문구"},
      keywords={"재난": "교육위기"},
      title="새 문서 제목", creator="작성자")

# 검증
result = validate_result("sample.hwpx", "output.hwpx",
                         replacements={...}, keywords={...})
print(f"커버리지: {result['coverage_pct']:.1f}%")
```

### 워크플로우 D vs F 비교

| 항목 | D (레퍼런스 기반) | F (양식 복제) |
|------|------------------|--------------|
| 원본 구조 보존 | ~2.5% | **100%** |
| 테이블 | ❌ 재구성 필요 | ✅ 그대로 |
| 이미지 | ❌ BinData 누락 | ✅ 그대로 |
| 스타일 | ⚠️ ID 매칭 필요 | ✅ 그대로 |
| 적합한 경우 | 간단한 텍스트 문서 | **복잡한 양식** |

---

## 서브에이전트 검수 (★ 권장)

> **문서 생성 후 별도 서브에이전트를 생성하여 품질 검증을 수행한다.**
> 생성 에이전트와 검수 에이전트를 분리하면 실수를 줄일 수 있다.

### 검수 도구

```bash
# 원본과 비교 검수 (구조 보존 확인)
python3 "${CLAUDE_SKILL_DIR}/scripts/verify_hwpx.py" \
  --source original.hwpx --result output.hwpx

# 단독 검수 (XML 유효성 + 구조 체크)
python3 "${CLAUDE_SKILL_DIR}/scripts/verify_hwpx.py" --result output.hwpx

# JSON 리포트 출력 (자동화용)
python3 "${CLAUDE_SKILL_DIR}/scripts/verify_hwpx.py" \
  --source original.hwpx --result output.hwpx --json report.json
```

### 검수 항목

| 검사 | 내용 | FAIL 조건 |
|------|------|-----------|
| mimetype | 첫 엔트리 + ZIP_STORED | 위치·압축 불일치 |
| 필수 파일 | header.xml, section0.xml 등 | 누락 시 |
| XML 유효성 | 모든 XML 파싱 가능 | 파싱 오류 |
| 런 보존 | 원본 대비 런(run) 수 | **감소 시 FAIL** |
| 테이블·이미지 | 원본 대비 수량 | 감소 시 FAIL |
| section 크기 | 원본 대비 비율 | 50% 미만 시 FAIL |

### 서브에이전트 워크플로우 예시

```
[메인 에이전트]
  1. clone_form.py로 문서 생성
  2. fix_namespaces.py 후처리
  ↓
[검수 서브에이전트 생성]
  3. verify_hwpx.py --source --result 실행
  4. text_extract.py로 텍스트 추출 확인
  5. fill_hwpx.py check --strict 실행 (★ 필수 게이트)
  6. PASS/FAIL 리포트 반환
  ↓
[메인 에이전트]
  7. FAIL이면 수정 후 재검수 (check exit 2 → 해당 수정 후 재check)
  8. check --strict exit 0일 때만 사용자에게 전달
```

---

## 워크플로우 Y: 요약보고 (결재선 달린 약식 보고) ★

> 지자체·공공기관 실무부서가 **단장·팀장 결재를 받아 올리는 1~3쪽 보고**.
> 왼쪽 위 로고(또는 기관명), 오른쪽 위 **결재선 표**(보고일 / 직위 | 성명),
> 색 띠 사이의 제목, 그 아래 `□ 소제목 → ❍ 항목 → - 세부 → ⇒ 결론` 개조식.
> 기안문(Workflow G)과 다르다 — 수신·발신명의·결문이 없고, 장 배너(Ⅰ Ⅱ Ⅲ)도 없다.

### 트리거

- "요약보고", "약식보고", "○○님 보고용 한 장", "현황 보고 짧게", "결재선 넣어서"
- 입력이 마크다운 한 장이고 결재자가 2~3명인 짧은 보고

### 사용

```bash
python3 scripts/yoyak.py 보고.md -o 보고.hwpx          # 마크다운 → HWPX
python3 scripts/yoyak.py --emit-sample > 보고.md        # 문법 예시 뽑기
```

마크다운 문법 (`scripts/yoyak.py` 독스트링이 원본):

```markdown
---
기관: ○○시 ○○추진단          ← 로고 없을 때 왼쪽 위 글자
로고: logo.png                  ← 있으면 기관 대신 그림 (선택)
보고일: 2026. 8. 21.(금)
결재: 추진단장 홍길동(1234) / 정책팀장 김철수(1235)   ← '/' 구분, 마지막 낱말 = 성명
줄간격: 160                     ← 선택. 쪽에 맞출 때 140~170
---
# 제목
리드문 (선택)
## 소제목              → □
- 항목                 → ❍
  - 세부               →    -
⇒ 결론                 → ⇒ (굵게)
※ 참고                 → ※ (13pt)
| 구분 | A | B |       → 표 (머리행 음영, 칸 너비는 글자 수 비례)
![설명](그림.png)
```

강조: `**굵게**` `__밑줄__` `++파랑 굵게++` `==형광==` (표 안에서는 `!!빨강!!` 추가). 겹치지 않는다.

### 형식의 세 층 — 무엇을 바꿔도 되나

| 층 | 무엇 | 바꾸려면 |
|---|---|---|
| 기관 | 결재선 표 모양·점선, 제목 띠 색(#2F5597), 글꼴 벌(HY헤드라인M·휴먼명조·HY울릉도M·한양신명조), 글머리 기호 | `scripts/yoyak.py` 상수 + `templates/yoyak/header.xml` |
| 내용 | 결재선 칸 높이(인원수), 제목 폭(용지), 표 칸 너비(글자 수), 빈 줄 | **계산** — 박아 둔 좌표 없음 |
| 글쓴이 | 줄간격, 강조, 결재자 수 | 마크다운 front matter |

### 검증

- **결정론** — 같은 마크다운이면 같은 바이트(모든 zip 엔트리 1980-01-01, 날짜 메타 = 보고일). `tests/test_yoyak.py` 가 해시로 확인한다.
- **원본 흔적 0** — 실측 원본의 기관명·인명·로고·전용 글꼴이 템플릿에 없는지 테스트가 grep 한다.
- 마무리 게이트는 다른 워크플로우와 같다: `validate.py` → `fill_hwpx.py check --strict` → `verify_hwpx.py`.

자세한 실측값과 파생 과정: [references/yoyak-bogo-style.md](references/yoyak-bogo-style.md)

### 같은 부서의 다쪽 서식 — 기본계획·검토보고 (`scripts/geomto.py`)

> 제목 아래 본문이 **`▐Ⅰ▌ 추진배경 → ▐Ⅱ▌ 현황 → ▐Ⅲ▌ 세부계획 → ▐Ⅳ▌ 기대효과 → ▐Ⅴ▌ 행정사항`**
> 로마숫자 장 배너로 나뉘는 계획서·검토보고. 결재선 표 대신 **표지**(문서정보·결재란·제목
> 띠·기관명)를 붙일 수 있다. "기본계획", "검토보고", "검토서", "장 나눠서 계획서" 에 쓴다.

```bash
python3 scripts/geomto.py 계획.md -o 계획.hwpx
python3 scripts/geomto.py --emit-sample > 계획.md
```

요약보고 문법에 더해:

```markdown
---
부제: - ○○을 위한 -            ← 제목 위 한 줄 (선택)
작성: 2026. 8. 21. ○○추진단 홍길동   ← 오른쪽 위 (선택)
표지: true                      ← 표지 한 장 (선택)
기관: ○○시 / 부서: ○○추진단 / 문서번호: / 보존기간: / 결재일자: / 공개여부:
결재: 주무관 / 팀장 / 추진단장 / 부시장 / 시장   ← 표지 결재란 직위, 칸 수 = 인원수
---
# 제목
> 리드 요약 (제목 바로 아래 → 테두리 박스)
## 추진배경            → ▐Ⅰ▌ 추진배경  (번호 자동)
- 항목 / 세부 / 3단    → ❍ / - / ▸
> 본문 중간 박스
* 각주                 → * (14pt)
---                    → 쪽 나눔
```

헤더는 요약보고 것의 상위 집합이라(`templates/geomto/header.xml` = yoyak + 장 배너·표지
스타일) 본문 규칙·표·강조가 요약보고와 같다. 실측값: [references/geomto-bogo-style.md](references/geomto-bogo-style.md)

### 원고 문체 — 조판 전에 검문한다 (`scripts/munche_lint.py`)

> 서식이 맞아도 **문체가 다르면 그 부서 문서로 안 보인다.** 실측 15건은 개조식이다 —
> `~다`로 끝나는 줄 0%, `A가 아니라 B다` 식 수사 0건, 물음표·느낌표 0, ❍ 항목 중앙 31자,
> ⇒ 결론 30자 명사 종결, 리드문은 `~하고자 함.` 한 문장, 항목 머리에 `(괄호 소제목)`.
> 내용 에이전트는 [references/bogo-munche.md](references/bogo-munche.md) 를 읽고 쓰고,
> 행정병에게 넘기기 전에 검문기를 돌린다.

```bash
python3 scripts/munche_lint.py 원고.md        # 위반(error) 있으면 exit 2 — 고친 뒤 조판
```

LLM 이 잘 빠지는 버릇을 잡는다: `~해야 현장에 남는다`(서술형), `도구가 아니라 사람이다`(수사적
대조), `가르치는 일은 사람이, 반복되는 일은 AI가`(대구 슬로건), `— 설명`(줄표 덧붙임).

## 워크플로우 G: 공문서 작성법 준수 (2025 개정) ★

> **공문서(기안문) 본문 작성 시 2025 개정 공문서 작성법을 자동 적용.**
> 공문

…(truncated)
