hwpx-kit-cli 0.3.0__tar.gz → 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/.claude-plugin/plugin.json +1 -1
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/CLAUDE.md +1 -1
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/PKG-INFO +6 -4
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/README.md +5 -3
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/pyproject.toml +1 -1
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/skills/hwpx-form/SKILL.md +17 -1
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/src/hwpx_kit/adapter/hwpx_engine.py +65 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/src/hwpx_kit/cli.py +35 -0
- hwpx_kit_cli-0.4.0/src/hwpx_kit/commands/header_footer.py +13 -0
- hwpx_kit_cli-0.4.0/src/hwpx_kit/commands/image_add.py +25 -0
- hwpx_kit_cli-0.4.0/tests/test_image_header.py +152 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/.claude-plugin/marketplace.json +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/.github/workflows/tests.yml +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/.gitignore +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/LICENSE +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/commands/hwpx.md +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/docs/images/After.png +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/docs/images/Before.png +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/docs/images/Tasking.png +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/install.ps1 +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/install.sh +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/sample-values.json +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/scripts/build_skill_package.py +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/scripts/check_stdout_purity.py +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/scripts/make_sample.py +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/skills/doc-create/SKILL.md +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/skills/format-convert/SKILL.md +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/skills/gongmun-format/SKILL.md +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/skills/office-export/SKILL.md +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/skills/table-calc/SKILL.md +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/src/hwpx_kit/__init__.py +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/src/hwpx_kit/adapter/__init__.py +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/src/hwpx_kit/adapter/base.py +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/src/hwpx_kit/adapter/kordoc_engine.py +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/src/hwpx_kit/adapter/office_readers.py +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/src/hwpx_kit/commands/__init__.py +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/src/hwpx_kit/commands/analyze.py +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/src/hwpx_kit/commands/convert.py +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/src/hwpx_kit/commands/export.py +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/src/hwpx_kit/commands/fill.py +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/src/hwpx_kit/commands/fill_batch.py +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/src/hwpx_kit/commands/fmt.py +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/src/hwpx_kit/commands/generate.py +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/src/hwpx_kit/commands/inspect_doc.py +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/src/hwpx_kit/commands/outline.py +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/src/hwpx_kit/commands/page_break.py +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/src/hwpx_kit/commands/read.py +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/src/hwpx_kit/commands/render.py +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/src/hwpx_kit/commands/row_height.py +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/src/hwpx_kit/commands/table_clear.py +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/src/hwpx_kit/commands/table_copy.py +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/src/hwpx_kit/commands/table_map_cmd.py +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/src/hwpx_kit/commands/table_new.py +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/src/hwpx_kit/commands/table_rows.py +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/src/hwpx_kit/commands/table_set.py +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/src/hwpx_kit/commands/validate.py +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/src/hwpx_kit/format.py +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/src/hwpx_kit/inspect_rules.py +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/src/hwpx_kit/output.py +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/templates/Default_Template.hwpx +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/tests/__init__.py +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/tests/conftest.py +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/tests/fixtures/real/legacy-sample.hwp +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/tests/fixtures/real/seoul-attachment.hwpx +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/tests/fixtures/real/seoul-body.hwpx +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/tests/fixtures/real/seoul-report-brief.hwpx +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/tests/fixtures/real/seoul-report-cover.hwpx +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/tests/test_adapter.py +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/tests/test_analyze.py +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/tests/test_cli.py +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/tests/test_convert.py +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/tests/test_export.py +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/tests/test_fill.py +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/tests/test_fill_batch.py +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/tests/test_format.py +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/tests/test_inspect.py +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/tests/test_integration.py +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/tests/test_kordoc.py +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/tests/test_office_readers.py +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/tests/test_output.py +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/tests/test_page_break.py +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/tests/test_read_validate.py +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/tests/test_real_forms.py +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/tests/test_row_height.py +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/tests/test_skill_package.py +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/tests/test_stdout_purity.py +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/tests/test_table_clear.py +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/tests/test_table_copy.py +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/tests/test_table_map_new.py +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/tests/test_table_rows.py +0 -0
- {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.0}/tests/test_table_set.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hwpx-kit",
|
|
3
3
|
"description": "한글(HWPX/HWP) 문서 자동화 스킬 모음 — 양식 분석·채우기·백지 생성·표기 변환·공문 규약·표 계산·오피스 출력. CLI는 첫 사용 시 스킬이 설치를 안내 (Python 3.10+ 필요).",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.4.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "6aneffy",
|
|
7
7
|
"email": "tkdgus990809@gmail.com"
|
|
@@ -26,7 +26,7 @@ skills/*/SKILL.md → Claude Code 플러그인 스킬들 (.claude-plugin/plugin.
|
|
|
26
26
|
└─ office-export → hwpx→docx(변환)·pptx/xlsx(재구성, officecli 선택 의존)
|
|
27
27
|
|
|
28
28
|
CLI (cli.py) — JSON 봉투, 종료코드 0/1/2
|
|
29
|
-
└─ commands/ (analyze·fill·fill-batch·inspect·read·validate·convert·export·render·generate·fmt·row-add·row-del·row-height·table-clear·table-set·table-copy·table-map·table-new·page-break·outline)
|
|
29
|
+
└─ commands/ (analyze·fill·fill-batch·inspect·image-add·header-footer·read·validate·convert·export·render·generate·fmt·row-add·row-del·row-height·table-clear·table-set·table-copy·table-map·table-new·page-break·outline)
|
|
30
30
|
├─ format.py ← 순수함수 (금액한글화·요일·만나이) — 엔진 임포트 금지, fmt는 파일 인수 없음(FILE_NOT_FOUND 가드 우회)
|
|
31
31
|
└─ adapter/ ← 명령 계층은 어댑터 인터페이스만 사용 (엔진 직접 import 금지)
|
|
32
32
|
├─ hwpx_engine.py → python-hwpx (hwpx 분석·채우기·검증·표 조작, 순수 Python)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hwpx-kit-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: 한글(HWPX) 문서 자동화 CLI — Korean HWPX document automation for AI agents: analyze, fill, read, validate, table ops
|
|
5
5
|
Project-URL: Homepage, https://github.com/6aneffy/hwpx-kit
|
|
6
6
|
Project-URL: Repository, https://github.com/6aneffy/hwpx-kit
|
|
@@ -33,7 +33,7 @@ Description-Content-Type: text/markdown
|
|
|
33
33
|
[](https://pypi.org/project/hwpx-kit-cli/)
|
|
34
34
|
[](https://www.python.org/)
|
|
35
35
|
[](https://claude.com/claude-code)
|
|
36
|
-
[](tests/)
|
|
37
37
|
|
|
38
38
|
</div>
|
|
39
39
|
|
|
@@ -154,6 +154,8 @@ hwpx-kit row-add 문서.hwpx --table 3 --like 2 --count 3 --out 결과.hwpx
|
|
|
154
154
|
hwpx-kit row-del 문서.hwpx --table 3 --rows 5-7 --out 결과.hwpx # 행 삭제
|
|
155
155
|
hwpx-kit row-height 문서.hwpx --table 3 --like 1 --rows 2-5 --out 결과.hwpx # 행 높이 정돈
|
|
156
156
|
hwpx-kit page-break 문서.hwpx --table 5 --out 결과.hwpx # 쪽나눔 (새 장 시작)
|
|
157
|
+
hwpx-kit image-add 문서.hwpx --image 도장.png --at-text "(인)" --width-mm 20 --out 결과.hwpx # 직인·사진
|
|
158
|
+
hwpx-kit header-footer 문서.hwpx --footer "대외비" --page-number center --out 결과.hwpx
|
|
157
159
|
|
|
158
160
|
# 변환·생성
|
|
159
161
|
hwpx-kit fmt --amount 12340 --json # 금12,340원(금일만이천삼백사십원)
|
|
@@ -190,7 +192,7 @@ hwpx-kit render 문서.hwpx --out p.svg # SVG 미리보기 (kordoc)
|
|
|
190
192
|
## 아키텍처
|
|
191
193
|
|
|
192
194
|
```
|
|
193
|
-
Claude Code 스킬 6종 ──→ hwpx-kit CLI (
|
|
195
|
+
Claude Code 스킬 6종 ──→ hwpx-kit CLI (25 명령, JSON 봉투)
|
|
194
196
|
└─ adapter/ ← 엔진 격리 계층
|
|
195
197
|
├─ python-hwpx hwpx 분석·채우기·표 조작
|
|
196
198
|
├─ pypdf·python-docx·openpyxl 타 포맷 읽기
|
|
@@ -206,7 +208,7 @@ templates/ 기본 계획서 템플릿 (표지·장 헤더·표 골격, 마커
|
|
|
206
208
|
|
|
207
209
|
## 검증
|
|
208
210
|
|
|
209
|
-
- 자동 테스트
|
|
211
|
+
- 자동 테스트 186개 (pytest)
|
|
210
212
|
- 실서식 수렴 테스트: 보도자료·신청서·계획서·회의록·공고문 5계열,
|
|
211
213
|
실제 공공기관 문서 30여 종으로 analyze→fill→validate 왕복
|
|
212
214
|
- 실사용 시나리오 테스트 3회 (백지 생성 12분 완주, 장·표 동적 추가, 보도자료 변환)
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
[](https://pypi.org/project/hwpx-kit-cli/)
|
|
11
11
|
[](https://www.python.org/)
|
|
12
12
|
[](https://claude.com/claude-code)
|
|
13
|
-
[](tests/)
|
|
14
14
|
|
|
15
15
|
</div>
|
|
16
16
|
|
|
@@ -131,6 +131,8 @@ hwpx-kit row-add 문서.hwpx --table 3 --like 2 --count 3 --out 결과.hwpx
|
|
|
131
131
|
hwpx-kit row-del 문서.hwpx --table 3 --rows 5-7 --out 결과.hwpx # 행 삭제
|
|
132
132
|
hwpx-kit row-height 문서.hwpx --table 3 --like 1 --rows 2-5 --out 결과.hwpx # 행 높이 정돈
|
|
133
133
|
hwpx-kit page-break 문서.hwpx --table 5 --out 결과.hwpx # 쪽나눔 (새 장 시작)
|
|
134
|
+
hwpx-kit image-add 문서.hwpx --image 도장.png --at-text "(인)" --width-mm 20 --out 결과.hwpx # 직인·사진
|
|
135
|
+
hwpx-kit header-footer 문서.hwpx --footer "대외비" --page-number center --out 결과.hwpx
|
|
134
136
|
|
|
135
137
|
# 변환·생성
|
|
136
138
|
hwpx-kit fmt --amount 12340 --json # 금12,340원(금일만이천삼백사십원)
|
|
@@ -167,7 +169,7 @@ hwpx-kit render 문서.hwpx --out p.svg # SVG 미리보기 (kordoc)
|
|
|
167
169
|
## 아키텍처
|
|
168
170
|
|
|
169
171
|
```
|
|
170
|
-
Claude Code 스킬 6종 ──→ hwpx-kit CLI (
|
|
172
|
+
Claude Code 스킬 6종 ──→ hwpx-kit CLI (25 명령, JSON 봉투)
|
|
171
173
|
└─ adapter/ ← 엔진 격리 계층
|
|
172
174
|
├─ python-hwpx hwpx 분석·채우기·표 조작
|
|
173
175
|
├─ pypdf·python-docx·openpyxl 타 포맷 읽기
|
|
@@ -183,7 +185,7 @@ templates/ 기본 계획서 템플릿 (표지·장 헤더·표 골격, 마커
|
|
|
183
185
|
|
|
184
186
|
## 검증
|
|
185
187
|
|
|
186
|
-
- 자동 테스트
|
|
188
|
+
- 자동 테스트 186개 (pytest)
|
|
187
189
|
- 실서식 수렴 테스트: 보도자료·신청서·계획서·회의록·공고문 5계열,
|
|
188
190
|
실제 공공기관 문서 30여 종으로 analyze→fill→validate 왕복
|
|
189
191
|
- 실사용 시나리오 테스트 3회 (백지 생성 12분 완주, 장·표 동적 추가, 보도자료 변환)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: hwpx-kit
|
|
3
|
-
description: HWPX 양식 자동화 — 한글(HWPX/HWP) 양식 분석, 내용 채우기, 본문 추출, 구조 검증, .hwp→.hwpx 변환. 사용자가 hwpx/hwp 파일을 주며 "이 양식에 채워줘", "여기에 써줘", "이 명단으로 30명분 만들어줘"(일괄 생성), "이 엑셀 표 넣어줘", "이 문서 읽어줘/요약해줘", "hwp 변환해줘" 할 때 사용. Triggers: fill this Korean form, mail merge certificates, read this hwp/hwpx file, Hangul document automation.
|
|
3
|
+
description: HWPX 양식 자동화 — 한글(HWPX/HWP) 양식 분석, 내용 채우기, 본문 추출, 구조 검증, .hwp→.hwpx 변환. 사용자가 hwpx/hwp 파일을 주며 "이 양식에 채워줘", "여기에 써줘", "이 명단으로 30명분 만들어줘"(일괄 생성), "이 엑셀 표 넣어줘", "도장 찍어줘/사진 붙여줘", "쪽번호 넣어줘", "이 문서 읽어줘/요약해줘", "hwp 변환해줘" 할 때 사용. Triggers: fill this Korean form, mail merge certificates, read this hwp/hwpx file, Hangul document automation.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# hwpx-kit: HWPX 양식 자동화
|
|
@@ -157,6 +157,22 @@ CLI `hwpx-kit`으로 hwpx 문서를 다룬다. 항상 `--json`을 붙여 실행
|
|
|
157
157
|
금지문자는 `_` 치환. 원본 불변은 fill과 동일
|
|
158
158
|
5. 개인정보 든 명단 파일은 사용자 문서와 동일한 기밀 취급 — 커밋·업로드 금지
|
|
159
159
|
|
|
160
|
+
## 사진·직인 넣기 — "여기 도장 찍어줘", "사진 붙여줘"
|
|
161
|
+
|
|
162
|
+
`hwpx-kit image-add <문서.hwpx> --image <파일.png> ... --out <결과.hwpx>`
|
|
163
|
+
- **직인/서명**: 대상 문단 원문으로 — `--at-text "서명: (인)" --width-mm 20`
|
|
164
|
+
(원문은 read 출력에서 복사. 이미지 파일은 사용자에게 받는다 — 도장을 그려주지 않는다)
|
|
165
|
+
- **사진 첨부 칸**(표 안): `--table N --cell R,C --width-mm 35 --height-mm 45`
|
|
166
|
+
(증명사진 규격 예시. 좌표는 table-map으로 확인)
|
|
167
|
+
- 글자처럼취급으로 들어가므로 셀 크기가 이미지에 맞을수록 깔끔 — 크게 넣으면
|
|
168
|
+
행이 부푼다. 삽입 후 validate + 한글 육안 확인 안내
|
|
169
|
+
- 사용자 제공 사진·도장 이미지도 문서와 동일한 기밀 취급
|
|
170
|
+
|
|
171
|
+
## 머리말·꼬리말·쪽번호 — "쪽번호 넣어줘"
|
|
172
|
+
|
|
173
|
+
`hwpx-kit header-footer <문서.hwpx> [--header "텍스트"] [--footer "텍스트"] [--page-number center] --out <결과.hwpx>`
|
|
174
|
+
— 쪽번호 위치는 left/center/right. 문서관리번호·대외비 표기 등에 사용.
|
|
175
|
+
|
|
160
176
|
## 엑셀 표를 한글 문서에 넣기 — "이 엑셀 표 문서에 넣어줘"
|
|
161
177
|
|
|
162
178
|
복붙(서식 깨짐) 대신 도구 조합으로:
|
|
@@ -671,6 +671,71 @@ class HwpxEngineAdapter:
|
|
|
671
671
|
self._mark_sections_dirty()
|
|
672
672
|
return len(targets)
|
|
673
673
|
|
|
674
|
+
_HWPUNIT_PER_MM = 7200 / 25.4 # 1mm ≈ 283.46 hwpunit
|
|
675
|
+
|
|
676
|
+
def insert_image(self, image_path: str, *, at_text: str | None = None,
|
|
677
|
+
table_index: int | None = None, cell: tuple[int, int] | None = None,
|
|
678
|
+
width_mm: float = 20.0, height_mm: float | None = None) -> dict:
|
|
679
|
+
"""이미지를 문단(원문 앵커) 또는 표 셀에 글자처럼취급으로 삽입.
|
|
680
|
+
|
|
681
|
+
엔진 2단계: add_image(바이너리 등록) → paragraph.add_picture(배치).
|
|
682
|
+
height_mm 생략 시 정사각(width와 동일) — 비율 유지가 필요하면 호출자가 지정.
|
|
683
|
+
"""
|
|
684
|
+
import os as _os
|
|
685
|
+
|
|
686
|
+
ext = _os.path.splitext(image_path)[1].lower().lstrip(".")
|
|
687
|
+
if ext == "jpeg":
|
|
688
|
+
ext = "jpg"
|
|
689
|
+
if ext not in ("png", "jpg", "bmp", "gif"):
|
|
690
|
+
raise ValueError(f"지원하지 않는 이미지 형식: .{ext} (png/jpg/bmp/gif)")
|
|
691
|
+
with open(image_path, "rb") as f:
|
|
692
|
+
image_data = f.read()
|
|
693
|
+
|
|
694
|
+
width = int(width_mm * self._HWPUNIT_PER_MM)
|
|
695
|
+
height = int((height_mm if height_mm is not None else width_mm) * self._HWPUNIT_PER_MM)
|
|
696
|
+
|
|
697
|
+
with quiet_engine():
|
|
698
|
+
if cell is not None:
|
|
699
|
+
if table_index is None:
|
|
700
|
+
raise ValueError("--cell은 --table과 함께 지정하세요.")
|
|
701
|
+
from hwpx.tools import table_navigation as tn
|
|
702
|
+
|
|
703
|
+
tables = tn._collect_document_tables(self._doc)
|
|
704
|
+
if not 0 <= table_index < len(tables):
|
|
705
|
+
raise ValueError(f"표 인덱스 범위 밖: {table_index} (표 {len(tables)}개)")
|
|
706
|
+
r, c = cell
|
|
707
|
+
paragraphs = tables[table_index].table.cell(r, c).paragraphs
|
|
708
|
+
target = list(paragraphs)[0]
|
|
709
|
+
else:
|
|
710
|
+
target = self._find_anchor_paragraph(anchor_text=at_text,
|
|
711
|
+
after_table=table_index)
|
|
712
|
+
item_id = self._doc.add_image(image_data, ext)
|
|
713
|
+
target.add_picture(item_id, width=width, height=height, treat_as_char=True)
|
|
714
|
+
return {"binary_item_id": item_id, "width": width, "height": height}
|
|
715
|
+
|
|
716
|
+
def set_header_footer(self, *, header: str | None = None,
|
|
717
|
+
footer: str | None = None,
|
|
718
|
+
page_number: str | None = None) -> list[str]:
|
|
719
|
+
"""머리말/꼬리말 텍스트·쪽번호 설정. 적용 항목 이름 목록 반환."""
|
|
720
|
+
applied: list[str] = []
|
|
721
|
+
align_map = {"left": "LEFT", "center": "CENTER", "right": "RIGHT"}
|
|
722
|
+
with quiet_engine():
|
|
723
|
+
if header is not None:
|
|
724
|
+
self._doc.set_header_text(header)
|
|
725
|
+
applied.append("header")
|
|
726
|
+
if footer is not None:
|
|
727
|
+
self._doc.set_footer_text(footer)
|
|
728
|
+
applied.append("footer")
|
|
729
|
+
if page_number is not None:
|
|
730
|
+
align = align_map.get(page_number.lower())
|
|
731
|
+
if align is None:
|
|
732
|
+
raise ValueError(f"쪽번호 위치는 left/center/right 중 하나: {page_number}")
|
|
733
|
+
self._doc.set_page_number(align=align)
|
|
734
|
+
applied.append("page_number")
|
|
735
|
+
if not applied:
|
|
736
|
+
raise ValueError("--header/--footer/--page-number 중 하나는 지정하세요.")
|
|
737
|
+
return applied
|
|
738
|
+
|
|
674
739
|
def save_copy(self, out_path: str) -> str:
|
|
675
740
|
out_abs = os.path.abspath(out_path)
|
|
676
741
|
if out_abs == self._source_path:
|
|
@@ -104,6 +104,25 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
104
104
|
pin.add_argument("--checks", help="쉼표 구분: residue,gongmun,pii (생략 시 전부)")
|
|
105
105
|
pin.add_argument("--json", action="store_true")
|
|
106
106
|
|
|
107
|
+
pim = sub.add_parser("image-add", help="이미지(사진·직인) 삽입 — 문단 앵커 또는 표 셀에 글자처럼취급")
|
|
108
|
+
pim.add_argument("file")
|
|
109
|
+
pim.add_argument("--image", required=True, help="이미지 파일 (png/jpg/bmp/gif)")
|
|
110
|
+
pim.add_argument("--at-text", help="이 원문 문단에 삽입 (공백 정규화 전체 일치)")
|
|
111
|
+
pim.add_argument("--table", type=int, help="표 인덱스 (--cell과 함께)")
|
|
112
|
+
pim.add_argument("--cell", help="셀 좌표 'R,C' (0-기준)")
|
|
113
|
+
pim.add_argument("--width-mm", type=float, default=20.0, help="가로 크기 mm (기본 20)")
|
|
114
|
+
pim.add_argument("--height-mm", type=float, help="세로 크기 mm (생략 시 가로와 동일)")
|
|
115
|
+
pim.add_argument("--out", required=True, help="출력 hwpx 경로 (원본 불변)")
|
|
116
|
+
pim.add_argument("--json", action="store_true")
|
|
117
|
+
|
|
118
|
+
phf = sub.add_parser("header-footer", help="머리말/꼬리말 텍스트·쪽번호 설정")
|
|
119
|
+
phf.add_argument("file")
|
|
120
|
+
phf.add_argument("--header", help="머리말 텍스트")
|
|
121
|
+
phf.add_argument("--footer", help="꼬리말 텍스트")
|
|
122
|
+
phf.add_argument("--page-number", help="쪽번호 위치: left/center/right")
|
|
123
|
+
phf.add_argument("--out", required=True, help="출력 hwpx 경로 (원본 불변)")
|
|
124
|
+
phf.add_argument("--json", action="store_true")
|
|
125
|
+
|
|
107
126
|
pra = sub.add_parser("row-add", help="표 행 추가 — 기준 행 서식·높이 승계, 내용은 비움 (세로 병합 걸리면 거부)")
|
|
108
127
|
pra.add_argument("file")
|
|
109
128
|
pra.add_argument("--table", type=int, required=True, help="표 인덱스 (0-기준)")
|
|
@@ -245,6 +264,22 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
245
264
|
)
|
|
246
265
|
if not data["clean"]:
|
|
247
266
|
exit_code = 2
|
|
267
|
+
elif args.command == "image-add":
|
|
268
|
+
from hwpx_kit.commands.image_add import run_image_add
|
|
269
|
+
|
|
270
|
+
data = run_image_add(
|
|
271
|
+
args.file, image_path=args.image, at_text=args.at_text,
|
|
272
|
+
table=args.table, cell=args.cell,
|
|
273
|
+
width_mm=args.width_mm, height_mm=args.height_mm,
|
|
274
|
+
out_path=args.out,
|
|
275
|
+
)
|
|
276
|
+
elif args.command == "header-footer":
|
|
277
|
+
from hwpx_kit.commands.header_footer import run_header_footer
|
|
278
|
+
|
|
279
|
+
data = run_header_footer(
|
|
280
|
+
args.file, header=args.header, footer=args.footer,
|
|
281
|
+
page_number=args.page_number, out_path=args.out,
|
|
282
|
+
)
|
|
248
283
|
elif args.command == "row-add":
|
|
249
284
|
from hwpx_kit.commands.table_rows import run_row_add
|
|
250
285
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""header-footer 명령 — 머리말/꼬리말 텍스트·쪽번호 설정."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from hwpx_kit.adapter.hwpx_engine import HwpxEngineAdapter
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def run_header_footer(path: str, header: str | None, footer: str | None,
|
|
8
|
+
page_number: str | None, out_path: str) -> dict:
|
|
9
|
+
ad = HwpxEngineAdapter.open(path)
|
|
10
|
+
applied = ad.set_header_footer(header=header, footer=footer,
|
|
11
|
+
page_number=page_number)
|
|
12
|
+
out = ad.save_copy(out_path)
|
|
13
|
+
return {"file": path, "out": out, "applied": applied}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""image-add 명령 — 이미지(사진·직인)를 문단 앵커 또는 표 셀에 삽입."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from hwpx_kit.adapter.hwpx_engine import HwpxEngineAdapter
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def run_image_add(path: str, image_path: str, at_text: str | None,
|
|
8
|
+
table: int | None, cell: str | None,
|
|
9
|
+
width_mm: float, height_mm: float | None, out_path: str) -> dict:
|
|
10
|
+
if (at_text is None) == (cell is None):
|
|
11
|
+
raise ValueError("--at-text 또는 (--table + --cell) 중 정확히 하나를 지정하세요.")
|
|
12
|
+
cell_rc = None
|
|
13
|
+
if cell is not None:
|
|
14
|
+
try:
|
|
15
|
+
r, c = cell.split(",")
|
|
16
|
+
cell_rc = (int(r), int(c))
|
|
17
|
+
except ValueError:
|
|
18
|
+
raise ValueError(f"--cell 형식은 'R,C': {cell!r}") from None
|
|
19
|
+
|
|
20
|
+
ad = HwpxEngineAdapter.open(path)
|
|
21
|
+
info = ad.insert_image(image_path, at_text=at_text, table_index=table,
|
|
22
|
+
cell=cell_rc, width_mm=width_mm, height_mm=height_mm)
|
|
23
|
+
out = ad.save_copy(out_path)
|
|
24
|
+
return {"file": path, "out": out, "image": image_path,
|
|
25
|
+
"inserted": True, **info}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""image-add / header-footer — 이미지 삽입·머리말/꼬리말/쪽번호.
|
|
2
|
+
|
|
3
|
+
이미지는 엔진 add_image(바이너리 등록)+add_picture(문단 배치) 2단계 —
|
|
4
|
+
저장 왕복 생존과 원본 불변이 계약. mm 크기는 hwpx 단위로 환산된다.
|
|
5
|
+
"""
|
|
6
|
+
import json
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
# 1x1 투명 PNG
|
|
11
|
+
PNG_1PX = bytes.fromhex(
|
|
12
|
+
"89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c489"
|
|
13
|
+
"0000000d4944415478da63f8ffff3f0300050001ff5cccd90000000049454e44ae426082"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
from hwpx_kit.cli import main
|
|
17
|
+
from hwpx_kit.commands.header_footer import run_header_footer
|
|
18
|
+
from hwpx_kit.commands.image_add import run_image_add
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@pytest.fixture()
|
|
22
|
+
def doc_with_anchor(tmp_path):
|
|
23
|
+
from hwpx.document import HwpxDocument
|
|
24
|
+
|
|
25
|
+
from hwpx_kit.output import quiet_engine
|
|
26
|
+
|
|
27
|
+
with quiet_engine():
|
|
28
|
+
doc = HwpxDocument.new()
|
|
29
|
+
doc.add_paragraph("신청서")
|
|
30
|
+
doc.add_paragraph("서명: (인)")
|
|
31
|
+
t = doc.add_table(2, 2)
|
|
32
|
+
t.set_cell_text(0, 0, "사진")
|
|
33
|
+
path = str(tmp_path / "form.hwpx")
|
|
34
|
+
doc.save_to_path(path)
|
|
35
|
+
return path
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@pytest.fixture()
|
|
39
|
+
def png_file(tmp_path):
|
|
40
|
+
p = tmp_path / "stamp.png"
|
|
41
|
+
p.write_bytes(PNG_1PX)
|
|
42
|
+
return str(p)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _image_count(path):
|
|
46
|
+
from hwpx.document import HwpxDocument
|
|
47
|
+
|
|
48
|
+
from hwpx_kit.output import quiet_engine
|
|
49
|
+
|
|
50
|
+
with quiet_engine():
|
|
51
|
+
return len(HwpxDocument.open(path).list_images())
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_image_add_at_text(doc_with_anchor, png_file, tmp_path):
|
|
55
|
+
out = str(tmp_path / "o.hwpx")
|
|
56
|
+
result = run_image_add(doc_with_anchor, image_path=png_file,
|
|
57
|
+
at_text="서명: (인)", table=None, cell=None,
|
|
58
|
+
width_mm=20, height_mm=None, out_path=out)
|
|
59
|
+
assert result["inserted"] is True
|
|
60
|
+
assert _image_count(out) == 1
|
|
61
|
+
assert _image_count(doc_with_anchor) == 0 # 원본 불변
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_image_add_into_table_cell(doc_with_anchor, png_file, tmp_path):
|
|
65
|
+
out = str(tmp_path / "o.hwpx")
|
|
66
|
+
result = run_image_add(doc_with_anchor, image_path=png_file,
|
|
67
|
+
at_text=None, table=0, cell="1,0",
|
|
68
|
+
width_mm=25, height_mm=30, out_path=out)
|
|
69
|
+
assert result["inserted"] is True
|
|
70
|
+
assert _image_count(out) == 1
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_image_add_validates_roundtrip(doc_with_anchor, png_file, tmp_path):
|
|
74
|
+
from hwpx_kit.commands.validate import run_validate
|
|
75
|
+
|
|
76
|
+
out = str(tmp_path / "o.hwpx")
|
|
77
|
+
run_image_add(doc_with_anchor, image_path=png_file,
|
|
78
|
+
at_text="서명: (인)", table=None, cell=None,
|
|
79
|
+
width_mm=20, height_mm=None, out_path=out)
|
|
80
|
+
assert run_validate(out)["valid"] is True
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_image_add_rejects_missing_anchor(doc_with_anchor, png_file, tmp_path):
|
|
84
|
+
with pytest.raises(ValueError, match="문단"):
|
|
85
|
+
run_image_add(doc_with_anchor, image_path=png_file,
|
|
86
|
+
at_text="없는 문장", table=None, cell=None,
|
|
87
|
+
width_mm=20, height_mm=None, out_path=str(tmp_path / "o.hwpx"))
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_image_add_rejects_bad_format(doc_with_anchor, tmp_path):
|
|
91
|
+
bad = tmp_path / "note.txt"
|
|
92
|
+
bad.write_text("텍스트")
|
|
93
|
+
with pytest.raises(ValueError, match="형식"):
|
|
94
|
+
run_image_add(doc_with_anchor, image_path=str(bad),
|
|
95
|
+
at_text="서명: (인)", table=None, cell=None,
|
|
96
|
+
width_mm=20, height_mm=None, out_path=str(tmp_path / "o.hwpx"))
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_cli_image_add(doc_with_anchor, png_file, tmp_path, capsys):
|
|
100
|
+
out = str(tmp_path / "o.hwpx")
|
|
101
|
+
code = main(["image-add", doc_with_anchor, "--image", png_file,
|
|
102
|
+
"--at-text", "서명: (인)", "--width-mm", "20",
|
|
103
|
+
"--out", out, "--json"])
|
|
104
|
+
env = json.loads(capsys.readouterr().out.strip().splitlines()[-1])
|
|
105
|
+
assert code == 0 and env["ok"] is True
|
|
106
|
+
assert _image_count(out) == 1
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# ── header-footer ─────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
def _headers_footers(path):
|
|
112
|
+
from hwpx.document import HwpxDocument
|
|
113
|
+
|
|
114
|
+
from hwpx_kit.output import quiet_engine
|
|
115
|
+
|
|
116
|
+
with quiet_engine():
|
|
117
|
+
d = HwpxDocument.open(path)
|
|
118
|
+
return d.headers
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def test_header_footer_set(doc_with_anchor, tmp_path):
|
|
122
|
+
out = str(tmp_path / "o.hwpx")
|
|
123
|
+
result = run_header_footer(doc_with_anchor, header="문서관리번호 2026-01",
|
|
124
|
+
footer=None, page_number=None, out_path=out)
|
|
125
|
+
assert "header" in result["applied"]
|
|
126
|
+
# 재열기 후에도 구조 유효 + 헤더 존재 (증발 회귀 감지)
|
|
127
|
+
from hwpx_kit.commands.validate import run_validate
|
|
128
|
+
|
|
129
|
+
assert run_validate(out)["valid"] is True
|
|
130
|
+
assert len(_headers_footers(out)) >= 1
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def test_page_number_set(doc_with_anchor, tmp_path):
|
|
134
|
+
out = str(tmp_path / "o.hwpx")
|
|
135
|
+
result = run_header_footer(doc_with_anchor, header=None, footer=None,
|
|
136
|
+
page_number="center", out_path=out)
|
|
137
|
+
assert "page_number" in result["applied"]
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def test_header_footer_requires_one(doc_with_anchor, tmp_path):
|
|
141
|
+
with pytest.raises(ValueError):
|
|
142
|
+
run_header_footer(doc_with_anchor, header=None, footer=None,
|
|
143
|
+
page_number=None, out_path=str(tmp_path / "o.hwpx"))
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def test_cli_header_footer(doc_with_anchor, tmp_path, capsys):
|
|
147
|
+
out = str(tmp_path / "o.hwpx")
|
|
148
|
+
code = main(["header-footer", doc_with_anchor, "--footer", "대외비",
|
|
149
|
+
"--page-number", "center", "--out", out, "--json"])
|
|
150
|
+
env = json.loads(capsys.readouterr().out.strip().splitlines()[-1])
|
|
151
|
+
assert code == 0 and env["ok"] is True
|
|
152
|
+
assert set(env["data"]["applied"]) == {"footer", "page_number"}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|