목차(클릭하세요)
표와 텍스트 크기까지 지원해주는 hwpx 최신화 스킬을 사용해보기
이 스킬은 스킬이 작동하면 (1)분량과 (2) 문체, (3)중점 내용 등을 지정해줄 수 있다는 것이 가장 큰 특징
1. 개발자 및 스킬 설치하기
1-1. 개발자
•
1-2.스킬 준비
•
깃허브 다운로드 방식이 X,
•
압축파일 다운로드
•
test용 파일
1-3.스킬 설치하기
•
클로드의 사용자지정-’스킬’에서 ‘스킬 업로드를 선택하기
1-4.스킬 사용전 확인해볼 것
•
설치된 스킬로 가서 코드를 선택하게 되면 해당 스킬이 어떻게 구성되어 있는지 마크다운 형태로 확인할 수 있음
•
보안취약점 등이 있을 수 있으므로 꼭 스킬의 원본내용을 확인해보는 습관을 들이자!
2. 스킬 분석하기
2-1. 아키텍처 이해
•
대부분의 문서 자동화 스킬은 가운데 층(조작 엔진)만 존재함. 이 스킬은 양쪽 끝에 게이트를 세운 것이 핵심 차별점이며, 결과물 품질을 실제로 결정하는 것도 이 두 게이트임.
flowchart LR
A["🚪 입구 게이트<br/>0단계<br/>분량·문체·중점 확정"]
B["⚙️ 조작 엔진<br/>1~7단계<br/>구조분석→XML수정→재패키징"]
C["🔒 출구 게이트<br/>5-3·8단계<br/>잔존·훼손·분량 검증"]
A --> B --> C
C -.->|"미달 시 반려"| BMermaid
복사
1-2. 층별 역할
층 | 단계 | 핵심 산출물 |
입구 게이트 | 0단계 | 목표 문단 수, 문체, 섹션별 배분표 |
조작 엔진 | 1~7단계 | 인덱스 맵, 수정된 section0.xml, hwpx |
출구 게이트 | 5-3·8단계 | assert 통과 여부 |
2-2. 특장점
모호한 요구를 검증 가능한 수치로 변환
"10페이지짜리 만들어줘"는 채점이 불가능한 요구임.
•
이를 환산식으로 바꾸어 기계적 채점이 가능하도록 만든 것이 이 스킬의 장점
본문 목표 문단 수 = (목표 페이지 - 표지/목차) × 11 (±10%)
검증: assert ratio >= 0.85
Plain Text
복사
항목 | 내용 |
하한선 코드화 | "미달 상태로 전달 금지"를 assert로 강제: 요청 10p에 3p 주고 끝내는 고질병 차단 |
개조식 보정 계수 | 문체가 분량에 미치는 영향을 +10~20%로 분리: 실제 써보지 않으면 안 나오는 값 |
금지+대안 세트 | "미사여구 반복 금지"와 "소제목 세분화·― 확장·※ 추가"를 한 쌍으로 제시 |
2-3. HWPX 고유 함정 3종
스펙 문서로는 절대 안 나오고, 깨져본 사람만 쓸 수 있는 내용임.
① linesegarray = 줄 배치 캐시
텍스트를 바꾸면 캐시가 무효화되어 글자가 겹침. 삭제하면 한컴이 재계산함.
② targeted 삭제 (v5.1 정교화)
하위 전체를 재귀 삭제했더니 미수정 문단의 음수 spacing 캐시(spacing=-240)까지 날아가 제목 표의 빨간 줄이 깨짐. nearest_p()로 최소 스코프만 삭제하도록 수정됨.
캐시 무효화 범위를 "변경 지점 기준 최소 스코프"로 좁힌 것: 데이터베이스의 row-level lock과 같은 발상임. 테이블 전체를 잠그면 안전하지만 부작용이 큼.
③ run은 텍스트 컨테이너가 아님 (5-0-1)run은 secPr(페이지 설정)·ctrl(쪽번호)·tbl(표) 같은 구조 요소를 담는 그릇임. 여분 run을 지웠다가 붉은 테두리 표가 통째로 소실된 사건이 그대로 규칙이 됨.
대응이 3중으로 걸려 있음:
•
사전: audit_runs()로 삭제 금지 run 판별
•
실행: 제목 문단은 replace_text_anywhere만 허용
•
사후: assert tbls + assert runs_now == RUNS_ORIGINAL
2-4. 사고 → 규칙 승격 루프
•
description에 개정 이력이 있고, 규칙마다 (실제 발생한 치명 오류) 주석이 붙어 있음.
•
문서 안에서 학습 루프가 돌고 있어 시간이 지날수록 단단해지는 종류의 스킬임.
3. 스킬 Test
•
첨부된 pdf파일로 어느정도의 성능을 보이는지 확인
•
특히, 이 스킬은 내가 원하는 형태의 OUTPUT 레이아웃을 미리 넣었을때 그 특장점이 발휘됨
•
스킬이 작동하면 분량과 문체, 중점 내용 등을 지정해주어야 함
3-1. 만들어진 결과물과 시간
•
기존 한글 skill처럼 만들어지는데는 약 3분~5분 정도의 시간이 소요되는 듯
•
결과물 형식과 레이아웃에 맞는 완벽한 결과물이 나옴
3-2. 기존 교과서의 내용을 학습지로 옮겨보는 test
•
이번에 test할 내용을 실제 업무에서 가장 많이 발생하는 상황
•
이미 완성된 pptx, pdf의 내용을 한글파일로 옮기는 작업
•
마찬가지로 스킬이 정상작동하면 사용자에게 몇가지 요청사항이 들어옴
•
특히 해당 강의자료에는 단순 텍스트 뿐만 아니라 많은 양의 캡쳐, 이미지 파일이 포함되어 있는데 이를 어떻게 한글파일로 옮길지가 가장 중요한 Test요소임
•
결과를 확인해보니 정말 놀라웠다!
•
완벽한 초안은 아니지만, 옮기고 싶은 내용과 형태가 거의 훼손되지 않은 상태로 hwpx가 완성됨
•
동시에 이 스킬이 가진 한계점도 확실하게 확인할 수 있었음
◦
바로 원본 파일에 있는 이미지파일은 가져올 수 없다는 것
4. 스킬_나만의 방식으로 업데이트하기
4-1.스킬 트리거 확인하기
•
클로드와 함께 해당 스킬의 발동조건(=트리거)를 확인해보면 다음과 같은 취약점이 있음
•
이 스킬의 발동 표면은 description 한 줄뿐이며, 그 안에서 실제로 작동하는 강한 키워드는 hwpx라는 확장자와 공문·기안문이라는 문서 종류 두 가지임.
•
양식을 첨부하면이라는 조건절이 붙어 있어 파일 첨부 없이 "공문 써줘"만 들어오면 회피될 소지가 있고, 동의어 키워드가 하나도 등록돼 있지 않아 "품의서", "결재 문서", "양식 채워줘" 같은 실제 중요 키워드를 인식하지 못함.
•
결정적으로 docx 스킬이 "report, memo, letter, template"을 명시적으로 선점하고 있어, 사용자가 hwpx를 직접 언급하지 않는 한 이 스킬이 작동되지 않을 가능성이 매우 큼
[개선점] 무엇보다 키워드 나열과 우선순위 선언을 추가해야 함
□ 트리거 키워드 명시적 나열
○ 현재 동의어 등록 0건: 실사용 어휘 미포착
•
"hwpx, hwp, 한글 파일, 공문, 기안문, 품의서, 계획서, 결재 문서, 양식 채워줘" 등록 필요
□ docx 스킬 대비 우선순위 선언
○ docx가 "report·memo·letter·template" 선점: 보고서·계획서 요청 시 선점 당함
•
"한글(hwpx) 결과물 요구 시 public/docx보다 우선 적용" 문구 삽입 필요
□ 조건절 회피 여지 제거
○ 양식을 첨부하면이라는 if절이 미첨부 요청에서 발동 차단
•
양식을 첨부받아로 서술 전환 후, "★ .hwpx 첨부 + 문서 작성 요청 = 최우선 적용" 규칙 문장 추가
□ .hwp 구포맷 사전 분기 배치
○ 파싱 불가 파일이 0단계 3질문 통과 후 실패: 사용자 응답 낭비 발생
― 0단계보다 앞에 확장자 검사 → hwpx 변환 안내 분기 신설 필요
□ description 서술 관점 전환
○ 현재 "무엇을 하는가" 중심 기술: 트리거 판단 정보로 기능 미흡
•
"언제 호출되어야 하는가" 중심으로 재작성
4-2.스킬 작동로직 개선하기
•
hwpx가 아닌 hwp가 입력되면 사용자에게 경고문구를 날리고
•
사용자가 출력form을 지정하지 않은 경우 sample을 제시할 수 있도록 작동로직 세분화
[변경 지점]
항목 | v5.1 | v5.2 |
포맷 검증 | 없음 | 매직바이트 게이트, hwp는 경고만 |
마커 | □○―※ 하드코딩 | 레벨 기반 자동 탐지 |
대응 양식 | 사실상 1종 | 6종 전수 검증 |
양식 미첨부 | 진행 불가 | 내장 6종 선택 |
표 중심 양식 | 붕괴 | B/C 유형 분리 경로 |
목표 문단 수 | TARGET=55 주석 | SPEC 자동 산출 |
분량 검증 | 하한만 | 양방향 밴드 |
5. yangphago 한글스킬 완성본(26년 7월 기준)
•
앞으로도 한글 스킬은 주기적으로 업데이트 할 예정
---
name: hwpx-yangphago-2026up
description: |
한글(hwpx) 공문·기안문·보고서·계획서 양식을 받아 주제에 맞는 본문을 작성한다.
원본 서식(붉은 테두리·쪽번호·표)을 100% 보존한 채 텍스트만 교체하며,
작성 전 ①분량 ②문체 ③중점 방향을 순서대로 질문·확정한다. (v5.2)
트리거: 아래 [행위]와 [문서]가 함께 나타날 때 발동한다.
행위 - 작성, 써줘, 만들어줘, 채워줘, 기안, 초안, 양식 채우기
문서 - 공문, 공문서, 기안문, 품의서, 보고서, 계획서, 한글 양식, hwpx 양식
★ .hwpx 첨부 + 작성 요청 → 최우선 적용
★ 한글(hwpx) 결과물이 필요하면 public/docx 스킬보다 우선
★ 미발동 조건: 요약·추출·변환·비교·읽기 요청은 본 스킬 대상이 아니다
(kordoc MCP의 parse_document / compare_documents 영역)
★ .hwp(구포맷) 입력은 본문 대량 작성 요청일 때만 hwpx 변환 안내 후 진행
---
# HWPX 공문/기안문 자동 채우기 스킬 (v5.2)
## 0단계: 3단계 사전 질문 (★★★ 반드시 가장 먼저, 순서대로 실행)
> **아래 3개 질문을 모두 확정하기 전에는 본문 작성을 시작하지 않는다.**
> 질문은 반드시 **한 번에 하나씩, ①→②→③ 순서로** 진행한다 (한 턴에 몰아서 묻지 않는다).
> 사용자가 이미 답을 명시한 항목은 질문을 생략하고 그 값으로 확정한다.
> 3개 항목이 모두 확정되면 **"목표 N페이지 · ○○식 · 중점: ○○ 기준으로 본문 약 M개 문단을 설계합니다"라고 선언**하고 작업을 시작한다.
### 0-1. 질문 ① — 목표 분량 (페이지 수)
- "완성본을 **총 몇 페이지** 분량으로 만들까요? (예: 1p 요약 / 3p 보고 / 5p 상세 / 10p 계획서)"
- 필요 시 추가로: 섹션별 비중(개요는 짧게, 방안은 길게 등) 선호 여부
사용자가 이미 분량을 명시했다면(예: "5p 내외", "10페이지짜리") 질문 없이 그 값을 목표로 확정한다.
### 0-2. 질문 ② — 문체 (개조식 vs 서술식)
분량 확정 후 다음을 질문한다:
- "본문 문체는 **개조식**과 **서술식** 중 어떤 방식으로 작성할까요?"
- **개조식**: 명사형 종결("~함", "~추진", "~필요"), 짧은 요점 나열 — 공공기관 보고서 표준
- **서술식**: 완결형 문장("~합니다", "~할 계획입니다") — 대외 공문·설명 자료에 적합
**문체별 작성 규칙:**
| 구분 | 개조식 | 서술식 |
|------|--------|--------|
| 종결 | 명사형(~함/~임/~됨/~추진/~마련) 또는 체언 종결 | 경어체(~합니다/~습니다) 완결 문장 |
| 문장 길이 | 1문단 = 1행 내외의 핵심 요지 | 1문단 = 1~2문장의 완결 서술 |
| 조사·수식어 | 최소화, 핵심 명사 중심 | 자연스러운 문장 흐름 유지 |
| 분량 환산 영향 | 문단당 줄 수가 짧아짐 → 같은 페이지에 문단 수 +10~20% 필요 | 0-4 환산표 기본값 적용 |
> 개조식 확정 시 0-4의 목표 문단 수에 **+10~20%를 가산**하여 설계한다.
### 0-3. 질문 ③ — 내용 중점 방향
문체 확정 후 다음을 질문한다:
- "내용은 **어느 쪽에 중점**을 둘까요? (예: ⑴ 추진 배경·필요성 논리 강화 / ⑵ 실행 방안·운영 계획 구체화 / ⑶ 기대효과·성과지표 부각 / ⑷ 예산·일정 등 관리 정보 중심)"
- 문서 주제에 맞게 선택지를 조정하되, 2~4개의 상호배타적 방향을 제시한다
**중점 방향은 0-5 섹션별 문단 배분에 직접 반영한다:**
- 선택된 중점 영역의 섹션에 전체 본문 문단의 **35~45%를 배분** (최대 배분 섹션으로 지정)
- 나머지 섹션은 잔여 분량을 논리 흐름에 따라 안배
- 중점 섹션은 □ 소제목을 더 세분화하고 ―/※ 세부 문단을 우선 확장한다
### 0-4. 페이지 → 문단 수 환산 (분량 설계표)
한글 보고서 양식(A4, 상하 여백 표준, 휴먼명조 15pt, 문단 위 간격 포함) 기준 실측 환산:
| 요소 | 1페이지당 분량 |
|------|---------------|
| □ 소제목(헤드라인M 16) | 문단 위 여백 포함 약 2줄 점유 |
| ○/―/※ 본문(15pt/13pt) | 1문단 = 1~2줄 (긴 문장은 2줄로 계산) |
| **본문 문단 수 환산치** | **1페이지 ≈ 본문 10~13문단** (표지·목차 제외) |
**목표 문단 수 공식:**
```
본문 목표 문단 수 = (목표 페이지 수 - 표지/목차 페이지 수) × 11 (±10%)
```
예시:
- 5p 요청, 표지 없음 → 약 **55문단** (□ 8~10개 + ○/―/※ 45개 내외)
- 10p 요청, 표지+목차 2p → 본문 8p → 약 **88문단** (□ 12~16개 + ○/―/※ 70개 내외)
- 1p 요약 → 약 11문단
### 0-5. 섹션별 문단 배분 계획 수립 (★ 질문 ③의 중점 방향 반영)
본문 작성 전에 **섹션별 문단 배분표를 먼저 작성**하고 나서 content_list를 만든다:
```
예: 10p 계획서(본문 88문단) 배분
Ⅰ. 개요 : 10문단 (□2, ○3, ―4, ※1)
Ⅱ. 추진배경 : 14문단 (□3, ○4, ―5, ※2)
Ⅲ. 현황및문제점 : 22문단 (□4, ○6, ―8, ※4)
Ⅳ. 개선방안 : 28문단 (□5, ○8, ―10, ※5) ← 질문 ③에서 확정한 중점 섹션에 최대(35~45%) 배분
Ⅴ. 향후계획 : 14문단 (□3, ○4, ―5, ※2)
```
**분량을 늘리는 올바른 방법** (같은 말 반복 금지):
- (개조식 확정 시) 목표 문단 수에 +10~20% 가산분을 먼저 반영
- □ 소제목 자체를 세분화 (예: "교육 운영" → "기수 편성", "실습 인프라", "교수진 구성" 3개로 분리)
- ○ 항목마다 ― 세부 근거 2~3개씩 확장 (수치·일정·대상·방법을 각각 별도 ― 로)
- ※ 참고·유의사항·기대효과를 섹션마다 1~2개 추가
- 원문(PDF 등)에 있는 구체적 수치·사례·고유명사를 빠짐없이 활용 (요약하며 버리지 말 것)
---
## 1단계: HWPX 파일 해제
```bash
mkdir -p hwpx_work && cd hwpx_work
cp 원본.hwpx 원본.zip
unzip -o 원본.zip -d original
```
해제 후 핵심 파일:
- Contents/section0.xml: 본문 (수정 대상)
- Contents/header.xml: 서식 정의
- mimetype, META-INF/, BinData/, Preview/: 수정하지 않음
---
## 2단계: section XML 구조 분석 (★ 반드시 실행)
단순히 텍스트를 순회하는 것이 아니라, **문단의 부모 구조(parent tag)** 를 함께 파악해야 한다.
한글 보고서 양식에서 본문 단락(□,○,―,※)은 흔히 **모든 섹션이 동일한 `<sec>` 요소의 직계 자식**으로 연결된다.
이 구조를 무시하면 섹션 경계 탐색이 실패하여 전체 본문이 삭제되는 치명적 오류가 발생한다.
```python
from lxml import etree
with open('original/Contents/section0.xml', 'rb') as f:
tree = etree.parse(f)
root = tree.getroot()
# ★ sec 요소 찾기
sec_elem = None
for elem in root.iter():
if etree.QName(elem.tag).localname == 'sec':
sec_elem = elem
break
# ★ sec 직계 자식 인덱스 맵핑 (구조 파악 필수)
sec_children = list(sec_elem)
print(f"sec 직계 자식 수: {len(sec_children)}")
for i, child in enumerate(sec_children):
local = etree.QName(child.tag).localname
if local == 'p':
texts = [t.text for t in child.iter()
if etree.QName(t.tag).localname == 't' and t.text and t.text.strip()]
if texts:
print(f"sec_child[{i}] p: {'|'.join(texts)[:70]}")
else:
print(f"sec_child[{i}] {local}")
```
이 출력으로 **각 섹션 본문의 정확한 start/end 인덱스**를 확인한 뒤 다음 단계로 진행한다.
---
## 3단계: XML 수정
### ★★★ 가장 중요한 규칙: linesegarray 삭제 ★★★
텍스트를 수정한 `<hp:p>` 에서 반드시 `<linesegarray>` 자식 요소를 삭제해야 한다.
linesegarray는 원본 편집기가 저장한 "줄 배치 캐시"이다.
텍스트를 변경하면 이 캐시가 무효화되어 글자가 겹쳐 보이는 현상이 발생한다.
삭제하면 한컴오피스가 파일을 열 때 자동으로 줄 배치를 재계산한다.
```python
def remove_linesegarray(p_element):
"""수정된 문단에서 linesegarray를 삭제한다. 필수!"""
for child in list(p_element):
if etree.QName(child.tag).localname == 'linesegarray':
p_element.remove(child)
```
### 절대 금지 사항
- 절대로 XML을 문자열(f-string, concat, replace)로 조합하지 않는다.
- 절대로 XML 선언(<?xml ...?>)을 수동으로 추가하지 않는다.
- 절대로 section0.xml 전체를 새로 작성하지 않는다.
- 절대로 .replace()나 re.sub()로 XML을 조작하지 않는다.
- **절대로 텍스트 내용으로 섹션 경계를 탐색하지 않는다 → 2단계의 인덱스 맵핑을 사용한다.**
---
## 4단계: 섹션 본문 교체 (★★★ sec 공유 구조 대응)
### 핵심 헬퍼 함수
```python
import copy
def clone_para(ref_p, run_texts):
"""
ref_p를 깊은 복사하여 각 run의 텍스트를 교체.
run_texts: ['run0에 넣을 텍스트', 'run1에 넣을 텍스트', ...]
여분의 run은 제거. linesegarray 삭제 필수.
"""
new_p = copy.deepcopy(ref_p)
remove_linesegarray(new_p)
runs = [c for c in new_p if etree.QName(c.tag).localname == 'run']
for i, txt in enumerate(run_texts):
if i < len(runs):
for t in runs[i]:
if etree.QName(t.tag).localname == 't':
t.text = txt
break
for r in runs[len(run_texts):]:
new_p.remove(r)
return new_p
def replace_section_body(sec_elem, body_start_idx, body_end_idx,
ref_box, ref_circle, ref_dash, ref_note,
content_list):
"""
sec_elem : 모든 본문 단락의 공통 부모 sec 요소
body_start_idx: 교체 시작 자식 인덱스 (첫 □ 위치)
body_end_idx : 교체 끝 자식 인덱스 + 1 (exclusive)
ref_box : □ 두 run 구조 참조 단락 (deepcopy 원본)
ref_circle : ○ 단락 참조
ref_dash : ― 단락 참조
ref_note : ※ 단락 참조
content_list : [("box"|"circle"|"dash"|"note", "텍스트"), ...]
"""
to_remove = list(sec_elem)[body_start_idx:body_end_idx]
for child in to_remove:
sec_elem.remove(child)
sym_map = {
"box": (" □ ", True),
"circle": (" ○ ", False),
"dash": (" ― ", False),
"note": (" ※ ", False),
}
ref_map = {
"box": ref_box, "circle": ref_circle,
"dash": ref_dash, "note": ref_note,
}
insert_pos = body_start_idx
for typ, text in content_list:
sym, two_run = sym_map[typ]
ref_p = ref_map[typ]
if two_run:
new_p = clone_para(ref_p, [sym, text])
else:
new_p = clone_para(ref_p, [sym + text])
sec_elem.insert(insert_pos, new_p)
insert_pos += 1
```
### ★★★ 반드시 역순(Ⅳ→Ⅲ→Ⅱ→Ⅰ)으로 처리
여러 섹션을 순서대로(Ⅰ→Ⅱ→...) 처리하면 삽입/삭제로 인해 이후 섹션 인덱스가 틀어진다.
**반드시 마지막 섹션부터 역순으로 호출한다.**
```python
# 2단계 출력으로 확인한 실제 인덱스를 아래에 입력
# 참조 단락은 수정 전에 반드시 deepcopy로 저장
sec_children = list(sec_elem)
ref_box = copy.deepcopy(sec_children[23]) # □ 두 run 구조 (실제 인덱스로 교체)
ref_circle = copy.deepcopy(sec_children[24]) # ○
ref_dash = copy.deepcopy(sec_children[25]) # ―
ref_note = copy.deepcopy(sec_children[31]) # ※ (있는 섹션에서 가져옴)
# ★ 역순 처리 (Ⅳ → Ⅲ → Ⅱ → Ⅰ)
replace_section_body(sec_elem, sec4_start, sec4_end, ref_box, ref_circle, ref_dash, ref_note, sec4_content)
replace_section_body(sec_elem, sec3_start, sec3_end, ref_box, ref_circle, ref_dash, ref_note, sec3_content)
replace_section_body(sec_elem, sec2_start, sec2_end, ref_box, ref_circle, ref_dash, ref_note, sec2_content)
replace_section_body(sec_elem, sec1_start, sec1_end, ref_box, ref_circle, ref_dash, ref_note, sec1_content)
```
> **순서 처리가 불가피한 경우**: 각 호출 후 `delta = len(content) - (end - start)`를 계산하여
> 이후 섹션 인덱스에 누적 합산한다.
---
## 5단계: 단순 텍스트 교체 (제목·날짜·기관명 등)
> **★ 제목 작성 원칙**: 보고서 제목은 **15자 내외**로 간결하게 작성한다.
> 핵심 키워드 중심으로 압축하되, 20자를 초과하지 않도록 한다.
> (예: "2026년 AI교육 추진계획" → O / "2026년도 인공지능 디지털 교육 추진 및 운영 계획" → X)
### ★★★ 5-0. 교체 전 반드시 텍스트의 실제 위치(depth) 확인
**교체 필수 대상 (누락 금지):**
| 위치 | 자리표시 텍스트 예 | 비고 |
|------|-------------------|------|
| 표지 제목 | "보고서 양식(제목)" | 통상 표 내부 |
| 표지 날짜 | "2026. 1. 1." | 통상 직계 run |
| 표지 기관명 | "기관명" | 통상 직계 run |
| **본문 1p 상단 제목 박스** | **"제 목"** | **통상 표 내부 — 누락 빈발, 반드시 확인** |
| 섹션 헤더(Ⅰ~) | " 추진 배경" 등 | 표 내부 |
| 목차 항목 | "Ⅰ. 개요" 등 | 표 내부 |
> 2단계 구조 분석 출력에서 위 자리표시 텍스트가 나타나는 **모든 인덱스**를 교체 대상 목록으로 만들고,
> 하나라도 빠뜨리지 않았는지 교체 후 전체 스캔(5-3)으로 검증한다.
**표지 제목·섹션 헤더는 문단의 직계 run이 아니라 `tbl > tr > tc > subList > p` 내부에 들어 있는 양식이 흔하다.**
이 경우 직계 run만 수정하는 `set_run_text`를 쓰면 ① 표 안의 자리표시 텍스트("보고서 양식(제목)" 등)는 그대로 남고
② 새 텍스트가 표 바깥에 이중으로 삽입되는 치명적 오류가 발생한다.
### ★★★ 5-0-1. 표지 제목 문단에서 run 삭제 절대 금지 (붉은 테두리 소실 방지)
표지/1p 상단의 제목 문단(통상 sec_children[0])은 다음과 같은 복합 구조인 경우가 대부분이다:
```
p
├─ run: secPr(페이지 설정) + ctrl(colPr) ← 삭제 시 페이지 레이아웃 파괴
├─ run: ctrl(pageNum) ← 삭제 시 쪽번호 소실
└─ run: tbl(borderFillIDRef=N) ← ★ 제목을 감싸는 "붉은 테두리 박스" 표
└─ tr > tc > subList > p > run > t: "제목 자리표시"
```
**따라서 제목 문단에는 `set_run_text(..., remove_extra_runs=True)`를 절대 사용하지 않는다.**
run을 삭제하면 붉은 테두리 표(tbl) 자체가 통째로 사라져 제목 박스가 소실된다 (실제 발생한 치명 오류).
**제목 교체의 유일한 허용 방식**: `replace_text_anywhere`(targeted lineseg 방식)로 표 내부 `t` 노드의 텍스트만 교체한다.
run·tbl·secPr·ctrl 등 구조 요소는 어떤 경우에도 추가·삭제하지 않는다.
**run 삭제가 필요해 보이는 문단은 먼저 run 구성을 감사(audit)한다:**
```python
def audit_runs(p_elem):
"""각 run에 t 외의 구조 요소(secPr/ctrl/tbl 등)가 있는지 보고.
구조 요소가 하나라도 있으면 그 run은 절대 삭제 금지."""
for i, r in enumerate(c for c in p_elem if etree.QName(c.tag).localname == 'run'):
kinds = sorted({etree.QName(c.tag).localname for c in r})
protected = bool(set(kinds) - {'t'})
print(f"run[{i}] children={kinds} {'★삭제금지' if protected else ''}")
```
교체 후 아래 검증을 반드시 실행한다 (5-3 스캔과 별도):
```python
# 제목 테두리 표 + run 구조 보존 검증 (필수)
p0 = list(sec_elem)[0] # 제목 문단 인덱스는 2단계 출력 기준
tbls = [c for c in p0.iter() if etree.QName(c.tag).localname == 'tbl']
assert tbls, "제목 테두리 표(tbl)가 사라짐 — run 삭제 금지 규칙 위반"
runs_now = sum(1 for c in p0 if etree.QName(c.tag).localname == 'run')
assert runs_now == RUNS_ORIGINAL, f"제목 문단 run 수 변동({RUNS_ORIGINAL}→{runs_now}) — 구조 훼손"
# RUNS_ORIGINAL: 2단계 분석 시점에 기록해 둔 원본 run 수
```
**검증 실패 시 복구**: 수정본을 고치려 들지 말고, 원본 section0.xml에서 해당 문단을 통째로 가져와
`sec_elem.replace(손상된_p, 원본_p_deepcopy)` 후 텍스트 교체만 다시 수행한다.
교체 전 아래로 **해당 텍스트가 직계 run에 있는지, 표 내부에 있는지 반드시 확인**한다:
```python
def locate_text(p_elem, needle):
"""needle 텍스트가 직계 run에 있는지, tbl 내부에 있는지 판별"""
for r in p_elem:
if etree.QName(r.tag).localname != 'run':
continue
for c in r:
local = etree.QName(c.tag).localname
if local == 't' and c.text and needle in c.text:
return 'direct' # 직계 run → set_run_text 사용
if local == 'tbl':
for t in c.iter():
if etree.QName(t.tag).localname == 't' and t.text and needle in t.text:
return 'nested_tbl' # 표 내부 → replace_text_anywhere 사용
return None
```
### 5-1. 위치별 교체 함수
**(A) 직계 run에 있을 때** — 기존 방식:
```python
def set_run_text(p_elem, run_idx, new_text, remove_extra_runs=False):
"""p_elem의 run_idx번째 run에 텍스트 설정. linesegarray 삭제."""
runs = [c for c in p_elem if etree.QName(c.tag).localname == 'run']
if run_idx < len(runs):
for t in runs[run_idx]:
if etree.QName(t.tag).localname == 't':
t.text = new_text
break
if remove_extra_runs:
for r in runs[run_idx+1:]:
p_elem.remove(r)
remove_linesegarray(p_elem)
```
**(B) 표 내부(subList/tc)에 있을 때** — 자리표시 텍스트를 기준으로 t 노드를 직접 찾아 교체:
> ★★★ **linesegarray는 "실제 텍스트를 수정한 최소 단위 hp:p의 직계"만 삭제**한다 (targeted 방식).
> p_elem 하위 전체를 재귀 삭제하면 안 된다 — 제목 표의 얇은 셀 등 **미수정 문단의 음수 spacing lineseg 캐시**
> (예: `spacing=-240`)까지 지워져 줄 높이가 재계산되면서 빨간 줄 양식이 깨지는 치명 오류가 실제 발생했다.
```python
def remove_own_lineseg(p):
"""해당 p의 '직계' linesegarray만 삭제 (하위 표 내부는 건드리지 않음)"""
for c in list(p):
if etree.QName(c.tag).localname == 'linesegarray':
p.remove(c)
def nearest_p(elem):
cur = elem.getparent()
while cur is not None and etree.QName(cur.tag).localname != 'p':
cur = cur.getparent()
return cur
def replace_text_anywhere(p_elem, old_text, new_text):
"""p_elem 하위 전체(표 내부 포함)에서 old_text와 일치하는 t 노드만 교체.
lineseg 캐시는 '그 t가 속한 최소 단위 p'의 직계만 삭제하고,
미수정 문단(빈 셀·장식 셀 등)의 캐시는 반드시 보존한다."""
replaced = 0
for t in p_elem.iter():
if etree.QName(t.tag).localname == 't' and t.text and old_text in t.text:
t.text = t.text.replace(old_text, new_text)
replaced += 1
host = nearest_p(t)
if host is not None:
remove_own_lineseg(host)
assert replaced > 0, f"'{old_text}' 를 찾지 못함 — 자리표시 텍스트를 2단계 출력에서 재확인할 것"
return replaced
```
### 5-2. 사용 예 (인덱스는 2단계 출력으로 확인)
```python
# ① 위치 판별 후 ② 알맞은 함수 선택
loc = locate_text(sec_children[5], "보고서 양식(제목)")
if loc == 'nested_tbl':
replace_text_anywhere(sec_children[5], "보고서 양식(제목)", "보고서 제목")
else:
set_run_text(sec_children[5], 0, "보고서 제목", remove_extra_runs=True)
set_run_text(sec_children[11], 0, "2026. 7. 1.") # 날짜: 통상 직계 run
set_run_text(sec_children[16], 0, "기관명") # 기관명: 통상 직계 run
```
### 5-3. ★ 교체 후 자리표시 잔존 전체 스캔 (필수)
개별 문단 검증에 더해, **저장 직전 문서 전체를 스캔**하여 양식의 자리표시 텍스트가 하나라도 남아있으면 실패 처리한다:
```python
PLACEHOLDERS = ["보고서 양식(제목)", "제 목", "기관명", "헤드라인M 폰트",
"휴면명조", "중고딕", "세부내용"] # 양식에 따라 2단계 출력 기준으로 보완
leftover = []
for p in sec_elem.iter():
if etree.QName(p.tag).localname == 't' and p.text:
for ph in PLACEHOLDERS:
if ph in p.text:
leftover.append((ph, p.text[:50]))
assert not leftover, f"자리표시 텍스트 잔존: {leftover} — 교체 누락, 5-0 대상 목록 재확인"
```
또한 새로 넣은 제목이 이중 삽입되지 않았는지 확인한다:
```python
texts = [t.text for t in sec_children[5].iter()
if etree.QName(t.tag).localname == 't' and t.text and t.text.strip()]
assert texts.count("보고서 제목") == 1, "제목이 없거나 중복 삽입됨"
```
> "세부내용"은 목차의 [붙 임]/[참 고] 항목에 쓰이는 자리표시다. 실제 붙임이 없으면 잔존을 허용하되,
> 그 외 자리표시는 잔존 시 반드시 교체 후 재패키징한다.
### 5-4. 목차(TOC) 교체 시 주의
목차 문단도 표 내부 구조인 경우가 많다. 항목별 교체 시 **치환 문자열이 서로 겹치지 않는지** 반드시 확인한다.
(예: '. 추진배경'과 '. 현황 및 문제점'을 각각 다른 제목으로 치환할 때, 앞선 치환 결과가 뒤 치환 패턴과 겹치면 중복 제목이 발생)
안전한 방식: 목차의 `t` 노드를 **순서대로 순회하며 번호(Ⅰ,Ⅱ,...) 기준으로 매핑**하여 교체하고,
본문 섹션 수보다 목차 항목이 많으면 초과 항목(번호+제목)의 텍스트를 비운다.
교체 후 목차 전체 텍스트를 출력해 중복·불일치가 없는지 검증한다.
---
## 6단계: XML 저장
```python
enc = tree.docinfo.encoding or 'UTF-8'
sa = tree.docinfo.standalone
tree.write('original/Contents/section0.xml',
xml_declaration=True,
encoding=enc,
standalone=sa)
```
---
## 7단계: HWPX 재패키징
```python
import zipfile, os
output_path = '결과물.hwpx'
with zipfile.ZipFile(output_path, 'w') as zf:
mimetype_path = os.path.join('original', 'mimetype')
if os.path.exists(mimetype_path):
zf.write(mimetype_path, 'mimetype', compress_type=zipfile.ZIP_STORED)
for dirpath, dirnames, filenames in os.walk('original'):
for filename in filenames:
filepath = os.path.join(dirpath, filename)
arcname = os.path.relpath(filepath, 'original')
if arcname == 'mimetype':
continue
zf.write(filepath, arcname, compress_type=zipfile.ZIP_DEFLATED)
```
---
## 8단계: 검증 (무결성 + ★분량 검증)
### 8-1. 파일 무결성
```python
with zipfile.ZipFile(output_path, 'r') as zf:
assert zf.testzip() is None, "ZIP 손상"
with zf.open('Contents/section0.xml') as f:
tree = etree.parse(f)
print("무결성 검증 완료")
```
### 8-2. ★★★ 분량 검증 (미달 시 반드시 보강 후 재패키징)
0단계에서 확정한 목표 문단 수와 실제 생성 문단 수를 비교한다.
**목표 대비 85% 미만이면 결과물을 전달하지 말고 본문을 보강**한다.
```python
root = tree.getroot()
sec = next(e for e in root.iter() if etree.QName(e.tag).localname == 'sec')
body_count = 0
for p in sec:
texts = ''.join(t.text or '' for t in p.iter()
if etree.QName(t.tag).localname == 't')
s = texts.strip()
# 본문 문단만 카운트 (□/○/ㅇ/―/-/※/* 로 시작하는 실질 내용 문단)
if s and s[0] in '□○ㅇ―-※*':
body_count += 1
TARGET = 55 # ← 0단계에서 확정한 목표 문단 수 입력
ratio = body_count / TARGET
print(f"본문 문단: {body_count} / 목표: {TARGET} ({ratio:.0%})")
assert ratio >= 0.85, (
f"분량 미달({ratio:.0%}) — 0-5 배분표로 돌아가 ―/※ 세부 문단을 추가하고 "
f"재작성할 것. 미달 상태로 사용자에게 전달 금지")
```
보강 방법은 0-5의 "분량을 늘리는 올바른 방법"을 따른다 (소제목 세분화, ― 세부근거 확장, ※ 추가).
글자 수 늘리기용 중복 서술이나 미사여구 반복은 금지.
---
## 9단계: 공문 작성 원칙
- **문체는 0-2에서 확정한 값을 따른다** (개조식: 명사형 종결 / 서술식: 합니다·습니다체)
- 문체 미확정 상태로 이 단계에 도달하는 것 자체가 오류 — 0단계로 돌아가 질문할 것
- 두괄식 서술 (결론 → 배경 → 세부내용)
- 본문 순서: 목적/배경 → 세부 내용 → 요청/협조 사항 → 붙임
- 관용 표현: "~와 관련하여", "아래와 같이", "~하여 주시기 바랍니다"
---
## ★ 작업 체크리스트
| 순서 | 확인 항목 |
|------|-----------|
| ⓪ | **3단계 사전 질문 완료** — ①분량 ②문체(개조식/서술식) ③내용 중점을 순서대로 질문·확정, 확정 선언 후 문단 수 환산·배분표 작성 |
| ① | 2단계 구조 분석 실행 → `sec` 직계 자식 인덱스 맵 출력 확인 |
| ② | 참조 단락(`ref_box`, `ref_circle`, `ref_dash`, `ref_note`)을 수정 전에 `deepcopy`로 저장 |
| ③ | content_list 길이가 0-3 배분표와 일치하는지 확인 후 섹션 본문 교체는 **역순(마지막 섹션부터)** 처리 |
| ④ | 텍스트를 수정한 모든 `<hp:p>`에서 `linesegarray` 삭제 확인 |
| ④-1 | **제목·헤더 교체 전 `locate_text`로 위치 판별** — 표 내부(nested_tbl)면 `replace_text_anywhere` 사용 |
| ④-1a | **제목 문단에서 run 삭제 절대 금지(5-0-1)** — 붉은 테두리 표(tbl) 보존 assert 통과 확인 |
| ④-2 | **5-0 교체 대상표의 전 항목 처리** — 특히 본문 1p 상단 "제 목" 박스 누락 여부 확인 |
| ④-3 | **저장 직전 자리표시 잔존 전체 스캔(5-3) 통과** — 잔존·중복 삽입 시 재작업 |
| ④-4 | 목차 교체 시 치환 패턴 겹침 확인(5-4) — 번호 기준 순차 매핑, 초과 항목 비우기, 교체 후 전체 출력 검증 |
| ⑤ | `mimetype` 비압축 첫 번째 삽입 확인 |
| ⑥ | 최종 ZIP 검증 통과 확인 |
| ⑦ | **분량 검증(8-2) 통과** — 목표 문단 수 대비 85% 이상, 미달 시 보강 후 재패키징 |
Markdown
복사
5-1.스킬다운로드
•
단순히 skill만 설치하면 안되고, 양식과 py스크립트 파일을 하나의 압축파일로 만들어서 스킬 업로드 필요
•
파일구조
hwpx-yangphago-2026up/
├─ SKILL.md
├─ scripts/
│ ├─ profile_form.py ← profiler.py 이름 변경
│ ├─ replace_body.py
│ ├─ fill_cells.py
│ └─ toc.py
└─ assets/forms/ ← 양식 6종 원본
Markdown
복사
•
스킬의 구성 = 요리사에게 주는 3가지
○ SKILL.md : 레시피(만드는 방법을 적은 글)
○ scripts : 이미 검증된 조리도구
○ forms : 미리 준비된 그릇(양식 파일)
※ 레시피만 주면 도구도 그릇도 매번 새로 만들어야 함
•
스킬 사용시 유의점
◦
5-2.스킬을 코덱스에 설치하기
•
스킬 설치 자체를 코덱스에게 요청하기
•
코덱스의 스킬 설치 폴더는 아래 그림에서 확인가능
5-.코덱스에서 사용하기
•
잘 만들어진 스킬은 클로드에서도 코덱스에서도 정상 작동!
•
물론 동일한 스킬을 설치&사용하더라도 각 모델의 특성, 성능에 따라 결과물은 상당히 달라지는 것으로 확인됨
•
코덱스에서는 5분 넘도록 계획만 수립하고, 파이썬 스크립트를 수정했음
























