hwpx-kit-cli 0.3.0__tar.gz → 0.4.1__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.
Files changed (93) hide show
  1. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/.claude-plugin/plugin.json +1 -1
  2. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/CLAUDE.md +1 -1
  3. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/PKG-INFO +6 -4
  4. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/README.md +5 -3
  5. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/pyproject.toml +1 -1
  6. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/skills/hwpx-form/SKILL.md +17 -1
  7. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/skills/table-calc/SKILL.md +11 -1
  8. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/src/hwpx_kit/adapter/hwpx_engine.py +65 -0
  9. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/src/hwpx_kit/cli.py +38 -0
  10. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/src/hwpx_kit/commands/fmt.py +13 -3
  11. hwpx_kit_cli-0.4.1/src/hwpx_kit/commands/header_footer.py +13 -0
  12. hwpx_kit_cli-0.4.1/src/hwpx_kit/commands/image_add.py +25 -0
  13. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/src/hwpx_kit/format.py +18 -0
  14. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/tests/test_format.py +39 -0
  15. hwpx_kit_cli-0.4.1/tests/test_image_header.py +152 -0
  16. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/.claude-plugin/marketplace.json +0 -0
  17. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  18. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  19. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/.github/workflows/tests.yml +0 -0
  20. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/.gitignore +0 -0
  21. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/LICENSE +0 -0
  22. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/commands/hwpx.md +0 -0
  23. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/docs/images/After.png +0 -0
  24. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/docs/images/Before.png +0 -0
  25. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/docs/images/Tasking.png +0 -0
  26. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/install.ps1 +0 -0
  27. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/install.sh +0 -0
  28. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/sample-values.json +0 -0
  29. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/scripts/build_skill_package.py +0 -0
  30. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/scripts/check_stdout_purity.py +0 -0
  31. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/scripts/make_sample.py +0 -0
  32. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/skills/doc-create/SKILL.md +0 -0
  33. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/skills/format-convert/SKILL.md +0 -0
  34. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/skills/gongmun-format/SKILL.md +0 -0
  35. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/skills/office-export/SKILL.md +0 -0
  36. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/src/hwpx_kit/__init__.py +0 -0
  37. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/src/hwpx_kit/adapter/__init__.py +0 -0
  38. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/src/hwpx_kit/adapter/base.py +0 -0
  39. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/src/hwpx_kit/adapter/kordoc_engine.py +0 -0
  40. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/src/hwpx_kit/adapter/office_readers.py +0 -0
  41. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/src/hwpx_kit/commands/__init__.py +0 -0
  42. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/src/hwpx_kit/commands/analyze.py +0 -0
  43. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/src/hwpx_kit/commands/convert.py +0 -0
  44. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/src/hwpx_kit/commands/export.py +0 -0
  45. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/src/hwpx_kit/commands/fill.py +0 -0
  46. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/src/hwpx_kit/commands/fill_batch.py +0 -0
  47. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/src/hwpx_kit/commands/generate.py +0 -0
  48. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/src/hwpx_kit/commands/inspect_doc.py +0 -0
  49. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/src/hwpx_kit/commands/outline.py +0 -0
  50. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/src/hwpx_kit/commands/page_break.py +0 -0
  51. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/src/hwpx_kit/commands/read.py +0 -0
  52. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/src/hwpx_kit/commands/render.py +0 -0
  53. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/src/hwpx_kit/commands/row_height.py +0 -0
  54. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/src/hwpx_kit/commands/table_clear.py +0 -0
  55. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/src/hwpx_kit/commands/table_copy.py +0 -0
  56. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/src/hwpx_kit/commands/table_map_cmd.py +0 -0
  57. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/src/hwpx_kit/commands/table_new.py +0 -0
  58. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/src/hwpx_kit/commands/table_rows.py +0 -0
  59. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/src/hwpx_kit/commands/table_set.py +0 -0
  60. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/src/hwpx_kit/commands/validate.py +0 -0
  61. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/src/hwpx_kit/inspect_rules.py +0 -0
  62. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/src/hwpx_kit/output.py +0 -0
  63. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/templates/Default_Template.hwpx +0 -0
  64. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/tests/__init__.py +0 -0
  65. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/tests/conftest.py +0 -0
  66. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/tests/fixtures/real/legacy-sample.hwp +0 -0
  67. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/tests/fixtures/real/seoul-attachment.hwpx +0 -0
  68. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/tests/fixtures/real/seoul-body.hwpx +0 -0
  69. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/tests/fixtures/real/seoul-report-brief.hwpx +0 -0
  70. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/tests/fixtures/real/seoul-report-cover.hwpx +0 -0
  71. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/tests/test_adapter.py +0 -0
  72. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/tests/test_analyze.py +0 -0
  73. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/tests/test_cli.py +0 -0
  74. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/tests/test_convert.py +0 -0
  75. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/tests/test_export.py +0 -0
  76. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/tests/test_fill.py +0 -0
  77. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/tests/test_fill_batch.py +0 -0
  78. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/tests/test_inspect.py +0 -0
  79. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/tests/test_integration.py +0 -0
  80. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/tests/test_kordoc.py +0 -0
  81. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/tests/test_office_readers.py +0 -0
  82. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/tests/test_output.py +0 -0
  83. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/tests/test_page_break.py +0 -0
  84. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/tests/test_read_validate.py +0 -0
  85. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/tests/test_real_forms.py +0 -0
  86. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/tests/test_row_height.py +0 -0
  87. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/tests/test_skill_package.py +0 -0
  88. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/tests/test_stdout_purity.py +0 -0
  89. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/tests/test_table_clear.py +0 -0
  90. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/tests/test_table_copy.py +0 -0
  91. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/tests/test_table_map_new.py +0 -0
  92. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/tests/test_table_rows.py +0 -0
  93. {hwpx_kit_cli-0.3.0 → hwpx_kit_cli-0.4.1}/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.3.0",
4
+ "version": "0.4.1",
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.0
3
+ Version: 0.4.1
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
  [![PyPI](https://img.shields.io/pypi/v/hwpx-kit-cli.svg)](https://pypi.org/project/hwpx-kit-cli/)
34
34
  [![Python](https://img.shields.io/badge/python-3.10%2B-3776AB.svg)](https://www.python.org/)
35
35
  [![Claude Code](https://img.shields.io/badge/Claude%20Code-plugin-d97757.svg)](https://claude.com/claude-code)
36
- [![Tests](https://img.shields.io/badge/tests-176%20passing-brightgreen.svg)](tests/)
36
+ [![Tests](https://img.shields.io/badge/tests-190%20passing-brightgreen.svg)](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 (23 명령, JSON 봉투)
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
- - 자동 테스트 176개 (pytest)
211
+ - 자동 테스트 190개 (pytest)
210
212
  - 실서식 수렴 테스트: 보도자료·신청서·계획서·회의록·공고문 5계열,
211
213
  실제 공공기관 문서 30여 종으로 analyze→fill→validate 왕복
212
214
  - 실사용 시나리오 테스트 3회 (백지 생성 12분 완주, 장·표 동적 추가, 보도자료 변환)
@@ -10,7 +10,7 @@
10
10
  [![PyPI](https://img.shields.io/pypi/v/hwpx-kit-cli.svg)](https://pypi.org/project/hwpx-kit-cli/)
11
11
  [![Python](https://img.shields.io/badge/python-3.10%2B-3776AB.svg)](https://www.python.org/)
12
12
  [![Claude Code](https://img.shields.io/badge/Claude%20Code-plugin-d97757.svg)](https://claude.com/claude-code)
13
- [![Tests](https://img.shields.io/badge/tests-176%20passing-brightgreen.svg)](tests/)
13
+ [![Tests](https://img.shields.io/badge/tests-190%20passing-brightgreen.svg)](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 (23 명령, JSON 봉투)
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
- - 자동 테스트 176개 (pytest)
188
+ - 자동 테스트 190개 (pytest)
187
189
  - 실서식 수렴 테스트: 보도자료·신청서·계획서·회의록·공고문 5계열,
188
190
  실제 공공기관 문서 30여 종으로 analyze→fill→validate 왕복
189
191
  - 실사용 시나리오 테스트 3회 (백지 생성 12분 완주, 장·표 동적 추가, 보도자료 변환)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "hwpx-kit-cli"
3
- version = "0.3.0"
3
+ version = "0.4.1"
4
4
  description = "한글(HWPX) 문서 자동화 CLI — Korean HWPX document automation for AI agents: analyze, fill, read, validate, table ops"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -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
  복붙(서식 깨짐) 대신 도구 조합으로:
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: table-calc
3
- description: 보고서 표 수치 작업 — 증감(△·▲▽·↑↓·%·%p), 비율, 순번, 세자리 콤마, 합계·역합계 계산과 공무원 관습 표기. hwpx 표에 "증감 채워줘", "비율 계산해서 넣어줘", "합계 맞춰줘", "표 계산해줘" 요청 시 사용. Triggers: calculate table totals in Korean report style.
3
+ description: 보고서 표 수치 작업 — 증감(△·▲▽·↑↓·%·%p), 비율, 순번, 세자리 콤마, 합계·역합계 계산과 공무원 관습 표기. hwpx 표에 "증감 채워줘", "비율 계산해서 넣어줘", "합계 맞춰줘", "표 계산해줘", "천원/백만원 단위로 바꿔줘" 요청 시 사용. Triggers: calculate table totals in Korean report style.
4
4
  ---
5
5
 
6
6
  # table-calc: 표 수치 계산·표기
@@ -36,6 +36,16 @@ description: 보고서 표 수치 작업 — 증감(△·▲▽·↑↓·%·%p),
36
36
  - **검산**: 채우기 전에 합계 행이 항목 합과 일치하는지 확인, 불일치하면 채우지
37
37
  말고 사용자에게 보고
38
38
 
39
+ ## 표 단위 변환 — "이 표 천원/백만원 단위로 바꿔줘"
40
+
41
+ 1. `table-map`으로 대상 표의 수치 셀 수집 (원 단위 원본 값)
42
+ 2. 셀마다 `hwpx-kit fmt --scale <원값> --unit 천원` (또는 백만원) — 사사오입
43
+ 반올림+콤마를 도구가 보장 (암산 금지). 여러 값이면 fmt를 && 체이닝
44
+ 3. `table-set --data`로 일괄 기입 + 표 위/아래에 `(단위: 천원)` 캡션이 없으면
45
+ text: 교체나 table-set으로 추가 (캡션 위치는 문서 관례 — 보통 표 우측 상단)
46
+ 4. **검산**: 변환 후 합계 행 = 항목 합 (반올림 누적 오차로 ±1 어긋나면
47
+ 합계 셀은 원값 합의 변환값으로 맞추고 사용자에게 알림)
48
+
39
49
  ## 착지 (문서에 쓰기)
40
50
 
41
51
  계산 결과는 hwpx-form 스킬의 fill 계약으로 셀에 넣는다 — 새 방식을 만들지 말 것:
@@ -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-기준)")
@@ -177,6 +196,8 @@ def _build_parser() -> argparse.ArgumentParser:
177
196
  pt.add_argument("--date", help="날짜 YYYYMMDD → YYYY.M.D.(요일)")
178
197
  pt.add_argument("--age", help="생년월일 YYMMDD → YYMMDD(만나이)")
179
198
  pt.add_argument("--base", help="만나이 기준일 YYYYMMDD (기본 오늘)")
199
+ pt.add_argument("--scale", help="원 단위 금액 → --unit 단위로 환산 (반올림·콤마)")
200
+ pt.add_argument("--unit", default="천원", choices=["천원", "백만원"], help="--scale 대상 단위 (기본 천원)")
180
201
  pt.add_argument("--json", action="store_true")
181
202
 
182
203
  return p
@@ -221,6 +242,7 @@ def main(argv: list[str] | None = None) -> int:
221
242
  data = run_fmt(
222
243
  amount=args.amount, date=args.date, age=args.age,
223
244
  base=args.base, style=args.style,
245
+ scale=args.scale, unit=args.unit,
224
246
  )
225
247
  elif args.command == "row-height":
226
248
  data = run_row_height(
@@ -245,6 +267,22 @@ def main(argv: list[str] | None = None) -> int:
245
267
  )
246
268
  if not data["clean"]:
247
269
  exit_code = 2
270
+ elif args.command == "image-add":
271
+ from hwpx_kit.commands.image_add import run_image_add
272
+
273
+ data = run_image_add(
274
+ args.file, image_path=args.image, at_text=args.at_text,
275
+ table=args.table, cell=args.cell,
276
+ width_mm=args.width_mm, height_mm=args.height_mm,
277
+ out_path=args.out,
278
+ )
279
+ elif args.command == "header-footer":
280
+ from hwpx_kit.commands.header_footer import run_header_footer
281
+
282
+ data = run_header_footer(
283
+ args.file, header=args.header, footer=args.footer,
284
+ page_number=args.page_number, out_path=args.out,
285
+ )
248
286
  elif args.command == "row-add":
249
287
  from hwpx_kit.commands.table_rows import run_row_add
250
288
 
@@ -4,7 +4,7 @@
4
4
  """
5
5
  from __future__ import annotations
6
6
 
7
- from hwpx_kit.format import format_amount, gongmun_date, korean_age
7
+ from hwpx_kit.format import format_amount, gongmun_date, korean_age, scale_amount
8
8
 
9
9
 
10
10
  def run_fmt(
@@ -13,10 +13,20 @@ def run_fmt(
13
13
  age: str | None = None,
14
14
  base: str | None = None,
15
15
  style: str = "gongmun",
16
+ scale: str | None = None,
17
+ unit: str = "천원",
16
18
  ) -> dict:
17
- targets = [k for k, v in (("amount", amount), ("date", date), ("age", age)) if v is not None]
19
+ targets = [k for k, v in (("amount", amount), ("date", date), ("age", age),
20
+ ("scale", scale)) if v is not None]
18
21
  if len(targets) != 1:
19
- raise ValueError("--amount / --date / --age 중 정확히 하나를 지정하세요.")
22
+ raise ValueError("--amount / --date / --age / --scale 중 정확히 하나를 지정하세요.")
23
+
24
+ if scale is not None:
25
+ cleaned = scale.replace(",", "").strip()
26
+ if not cleaned.isdigit():
27
+ raise ValueError(f"금액은 정수여야 합니다: {scale}")
28
+ return {"kind": "scale", "input": scale, "unit": unit,
29
+ "result": scale_amount(int(cleaned), unit)}
20
30
 
21
31
  if amount is not None:
22
32
  cleaned = amount.replace(",", "").strip()
@@ -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}
@@ -92,3 +92,21 @@ def korean_age(yymmdd: str, base: str | None = None) -> str:
92
92
  if age < 0:
93
93
  raise ValueError("기준일이 생년월일보다 앞섭니다.")
94
94
  return f"{digits}({age})"
95
+
96
+
97
+ _SCALE_UNITS = {"천원": 1_000, "백만원": 1_000_000}
98
+
99
+
100
+ def scale_amount(won: int, unit: str) -> str:
101
+ """원 단위 금액 → 천원/백만원 단위 문자열 (반올림, 세 자리 콤마).
102
+
103
+ 예산표 "단위: 천원" 관습용. 반내림이 필요한 특수 규정은 호출자가 처리.
104
+ """
105
+ from decimal import ROUND_HALF_UP, Decimal
106
+
107
+ divisor = _SCALE_UNITS.get(unit)
108
+ if divisor is None:
109
+ raise ValueError(f"지원 단위: {', '.join(_SCALE_UNITS)} (받은 값: {unit})")
110
+ # 파이썬 round()는 은행가 반올림(0.5→0) — 행정 관습은 사사오입(0.5→1)
111
+ scaled = int((Decimal(won) / Decimal(divisor)).quantize(Decimal(1), rounding=ROUND_HALF_UP))
112
+ return f"{scaled:,}"
@@ -144,3 +144,42 @@ def test_cli_fmt_requires_exactly_one(capsys):
144
144
  env = _last_json(capsys)
145
145
  assert code == 1
146
146
  assert env["ok"] is False
147
+
148
+
149
+ # ── 표 단위 변환 (천원/백만원) ────────────────────────────────
150
+
151
+ def test_scale_amount_to_cheonwon():
152
+ from hwpx_kit.format import scale_amount
153
+
154
+ # 관습: 반올림, 세 자리 콤마
155
+ assert scale_amount(1234567, "천원") == "1,235"
156
+ assert scale_amount(1000, "천원") == "1"
157
+ assert scale_amount(37000000, "천원") == "37,000"
158
+
159
+
160
+ def test_scale_amount_to_baekmanwon():
161
+ from hwpx_kit.format import scale_amount
162
+
163
+ assert scale_amount(37000000, "백만원") == "37"
164
+ assert scale_amount(1234567890, "백만원") == "1,235"
165
+ assert scale_amount(500000, "백만원") == "1" # 반올림 올라감
166
+
167
+
168
+ def test_scale_amount_rejects_unknown_unit():
169
+ import pytest as _pytest
170
+
171
+ from hwpx_kit.format import scale_amount
172
+
173
+ with _pytest.raises(ValueError):
174
+ scale_amount(1000, "억원")
175
+
176
+
177
+ def test_cli_fmt_scale(capsys):
178
+ import json as _json
179
+
180
+ from hwpx_kit.cli import main
181
+
182
+ code = main(["fmt", "--scale", "1,234,567", "--unit", "천원", "--json"])
183
+ env = _json.loads(capsys.readouterr().out.strip().splitlines()[-1])
184
+ assert code == 0
185
+ assert env["data"]["result"] == "1,235"
@@ -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