hwpx-kit-cli 0.4.0__tar.gz → 0.5.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.
Files changed (93) hide show
  1. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/.claude-plugin/plugin.json +1 -1
  2. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/PKG-INFO +10 -3
  3. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/README.md +9 -2
  4. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/pyproject.toml +1 -1
  5. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/skills/hwpx-form/SKILL.md +16 -0
  6. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/skills/table-calc/SKILL.md +11 -1
  7. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/src/hwpx_kit/cli.py +15 -3
  8. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/src/hwpx_kit/commands/fill.py +25 -0
  9. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/src/hwpx_kit/commands/fmt.py +13 -3
  10. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/src/hwpx_kit/format.py +18 -0
  11. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/tests/test_fill_batch.py +49 -0
  12. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/tests/test_format.py +39 -0
  13. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/.claude-plugin/marketplace.json +0 -0
  14. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  15. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  16. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/.github/workflows/tests.yml +0 -0
  17. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/.gitignore +0 -0
  18. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/CLAUDE.md +0 -0
  19. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/LICENSE +0 -0
  20. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/commands/hwpx.md +0 -0
  21. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/docs/images/After.png +0 -0
  22. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/docs/images/Before.png +0 -0
  23. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/docs/images/Tasking.png +0 -0
  24. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/install.ps1 +0 -0
  25. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/install.sh +0 -0
  26. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/sample-values.json +0 -0
  27. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/scripts/build_skill_package.py +0 -0
  28. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/scripts/check_stdout_purity.py +0 -0
  29. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/scripts/make_sample.py +0 -0
  30. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/skills/doc-create/SKILL.md +0 -0
  31. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/skills/format-convert/SKILL.md +0 -0
  32. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/skills/gongmun-format/SKILL.md +0 -0
  33. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/skills/office-export/SKILL.md +0 -0
  34. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/src/hwpx_kit/__init__.py +0 -0
  35. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/src/hwpx_kit/adapter/__init__.py +0 -0
  36. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/src/hwpx_kit/adapter/base.py +0 -0
  37. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/src/hwpx_kit/adapter/hwpx_engine.py +0 -0
  38. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/src/hwpx_kit/adapter/kordoc_engine.py +0 -0
  39. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/src/hwpx_kit/adapter/office_readers.py +0 -0
  40. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/src/hwpx_kit/commands/__init__.py +0 -0
  41. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/src/hwpx_kit/commands/analyze.py +0 -0
  42. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/src/hwpx_kit/commands/convert.py +0 -0
  43. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/src/hwpx_kit/commands/export.py +0 -0
  44. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/src/hwpx_kit/commands/fill_batch.py +0 -0
  45. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/src/hwpx_kit/commands/generate.py +0 -0
  46. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/src/hwpx_kit/commands/header_footer.py +0 -0
  47. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/src/hwpx_kit/commands/image_add.py +0 -0
  48. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/src/hwpx_kit/commands/inspect_doc.py +0 -0
  49. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/src/hwpx_kit/commands/outline.py +0 -0
  50. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/src/hwpx_kit/commands/page_break.py +0 -0
  51. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/src/hwpx_kit/commands/read.py +0 -0
  52. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/src/hwpx_kit/commands/render.py +0 -0
  53. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/src/hwpx_kit/commands/row_height.py +0 -0
  54. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/src/hwpx_kit/commands/table_clear.py +0 -0
  55. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/src/hwpx_kit/commands/table_copy.py +0 -0
  56. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/src/hwpx_kit/commands/table_map_cmd.py +0 -0
  57. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/src/hwpx_kit/commands/table_new.py +0 -0
  58. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/src/hwpx_kit/commands/table_rows.py +0 -0
  59. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/src/hwpx_kit/commands/table_set.py +0 -0
  60. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/src/hwpx_kit/commands/validate.py +0 -0
  61. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/src/hwpx_kit/inspect_rules.py +0 -0
  62. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/src/hwpx_kit/output.py +0 -0
  63. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/templates/Default_Template.hwpx +0 -0
  64. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/tests/__init__.py +0 -0
  65. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/tests/conftest.py +0 -0
  66. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/tests/fixtures/real/legacy-sample.hwp +0 -0
  67. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/tests/fixtures/real/seoul-attachment.hwpx +0 -0
  68. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/tests/fixtures/real/seoul-body.hwpx +0 -0
  69. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/tests/fixtures/real/seoul-report-brief.hwpx +0 -0
  70. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/tests/fixtures/real/seoul-report-cover.hwpx +0 -0
  71. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/tests/test_adapter.py +0 -0
  72. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/tests/test_analyze.py +0 -0
  73. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/tests/test_cli.py +0 -0
  74. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/tests/test_convert.py +0 -0
  75. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/tests/test_export.py +0 -0
  76. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/tests/test_fill.py +0 -0
  77. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/tests/test_image_header.py +0 -0
  78. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/tests/test_inspect.py +0 -0
  79. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/tests/test_integration.py +0 -0
  80. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/tests/test_kordoc.py +0 -0
  81. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/tests/test_office_readers.py +0 -0
  82. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/tests/test_output.py +0 -0
  83. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/tests/test_page_break.py +0 -0
  84. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/tests/test_read_validate.py +0 -0
  85. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/tests/test_real_forms.py +0 -0
  86. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/tests/test_row_height.py +0 -0
  87. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/tests/test_skill_package.py +0 -0
  88. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/tests/test_stdout_purity.py +0 -0
  89. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/tests/test_table_clear.py +0 -0
  90. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/tests/test_table_copy.py +0 -0
  91. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/tests/test_table_map_new.py +0 -0
  92. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/tests/test_table_rows.py +0 -0
  93. {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.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.0",
4
+ "version": "0.5.0",
5
5
  "author": {
6
6
  "name": "6aneffy",
7
7
  "email": "tkdgus990809@gmail.com"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hwpx-kit-cli
3
- Version: 0.4.0
3
+ Version: 0.5.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
  [![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-186%20passing-brightgreen.svg)](tests/)
36
+ [![Tests](https://img.shields.io/badge/tests-193%20passing-brightgreen.svg)](tests/)
37
37
 
38
38
  </div>
39
39
 
@@ -204,11 +204,18 @@ templates/ 기본 계획서 템플릿 (표지·장 헤더·표 골격, 마커
204
204
  설계 불변 원칙: **① 원본 절대 수정 금지 ② `--json` stdout은 봉투 한 줄만
205
205
  ③ 서식은 항상 보존** (셀을 비워도 글자모양이 유지되어 재기입 시 원래 서식을 물려받음).
206
206
 
207
+ ## 민감정보 처리 (secure 모드)
208
+
209
+ 주민번호·계좌 같은 값은 **AI 모델을 거치지 않고** 채울 수 있습니다:
210
+ 사용자가 값 파일을 직접 작성 → 스킬은 경로만 전달 → `fill --secure`가 CLI에서만 읽어 채움.
211
+ `--secure`는 하나라도 못 채우면 산출물 없이 실패하고(부분 채움 방지), 출력에 값을 노출하지 않습니다.
212
+ 제출 전 `inspect`로 개인정보 잔존(주민번호·전화·이메일 패턴)도 기계 검사할 수 있습니다.
213
+
207
214
  > 🚧 같은 코어를 MCP 서버로 노출하는 버전도 제작 중입니다 (Claude Desktop 등 MCP 클라이언트용).
208
215
 
209
216
  ## 검증
210
217
 
211
- - 자동 테스트 186개 (pytest)
218
+ - 자동 테스트 193개 (pytest)
212
219
  - 실서식 수렴 테스트: 보도자료·신청서·계획서·회의록·공고문 5계열,
213
220
  실제 공공기관 문서 30여 종으로 analyze→fill→validate 왕복
214
221
  - 실사용 시나리오 테스트 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-186%20passing-brightgreen.svg)](tests/)
13
+ [![Tests](https://img.shields.io/badge/tests-193%20passing-brightgreen.svg)](tests/)
14
14
 
15
15
  </div>
16
16
 
@@ -181,11 +181,18 @@ templates/ 기본 계획서 템플릿 (표지·장 헤더·표 골격, 마커
181
181
  설계 불변 원칙: **① 원본 절대 수정 금지 ② `--json` stdout은 봉투 한 줄만
182
182
  ③ 서식은 항상 보존** (셀을 비워도 글자모양이 유지되어 재기입 시 원래 서식을 물려받음).
183
183
 
184
+ ## 민감정보 처리 (secure 모드)
185
+
186
+ 주민번호·계좌 같은 값은 **AI 모델을 거치지 않고** 채울 수 있습니다:
187
+ 사용자가 값 파일을 직접 작성 → 스킬은 경로만 전달 → `fill --secure`가 CLI에서만 읽어 채움.
188
+ `--secure`는 하나라도 못 채우면 산출물 없이 실패하고(부분 채움 방지), 출력에 값을 노출하지 않습니다.
189
+ 제출 전 `inspect`로 개인정보 잔존(주민번호·전화·이메일 패턴)도 기계 검사할 수 있습니다.
190
+
184
191
  > 🚧 같은 코어를 MCP 서버로 노출하는 버전도 제작 중입니다 (Claude Desktop 등 MCP 클라이언트용).
185
192
 
186
193
  ## 검증
187
194
 
188
- - 자동 테스트 186개 (pytest)
195
+ - 자동 테스트 193개 (pytest)
189
196
  - 실서식 수렴 테스트: 보도자료·신청서·계획서·회의록·공고문 5계열,
190
197
  실제 공공기관 문서 30여 종으로 analyze→fill→validate 왕복
191
198
  - 실사용 시나리오 테스트 3회 (백지 생성 12분 완주, 장·표 동적 추가, 보도자료 변환)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "hwpx-kit-cli"
3
- version = "0.4.0"
3
+ version = "0.5.0"
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"
@@ -157,6 +157,22 @@ CLI `hwpx-kit`으로 hwpx 문서를 다룬다. 항상 `--json`을 붙여 실행
157
157
  금지문자는 `_` 치환. 원본 불변은 fill과 동일
158
158
  5. 개인정보 든 명단 파일은 사용자 문서와 동일한 기밀 취급 — 커밋·업로드 금지
159
159
 
160
+ ## 민감정보(주민번호·계좌 등) 채우기 — secure 프로토콜
161
+
162
+ 주민번호·계좌·급여처럼 민감한 값이 들어가는 서식은 **값이 너(Claude)를 거치지
163
+ 않게** 처리한다:
164
+
165
+ 1. 사용자에게 값 JSON 파일을 **직접** 만들어 달라고 안내 (메모장 등):
166
+ `{"table:주민등록번호": "여기에 직접 입력", ...}` — 키 목록은 네가
167
+ analyze 결과로 만들어주되 **값은 비워서** 건넨다
168
+ 2. 파일을 받으면 **절대 Read하지 말고 경로만** 사용:
169
+ `hwpx-kit fill <양식> --data <값파일> --out <결과> --secure --json`
170
+ - --secure = 하나라도 못 채우면 산출물 없이 실패 (반쯤 채워진 개인정보
171
+ 문서가 남지 않음), 출력 JSON에 값 비노출
172
+ 3. 채움 확인은 값을 보지 않고 간접으로: `inspect <결과> --checks residue`
173
+ (빈칸·마커 잔존 없음 = 전부 채워짐)
174
+ 4. 끝나면 값 파일 삭제를 사용자에게 권한다. 값 파일·결과물 모두 커밋·업로드 금지
175
+
160
176
  ## 사진·직인 넣기 — "여기 도장 찍어줘", "사진 붙여줘"
161
177
 
162
178
  `hwpx-kit image-add <문서.hwpx> --image <파일.png> ... --out <결과.hwpx>`
@@ -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 계약으로 셀에 넣는다 — 새 방식을 만들지 말 것:
@@ -50,6 +50,8 @@ def _build_parser() -> argparse.ArgumentParser:
50
50
  pf.add_argument("file")
51
51
  pf.add_argument("--data", required=True, help="fill_key: 값 매핑 JSON 파일")
52
52
  pf.add_argument("--out", required=True, help="출력 hwpx 경로")
53
+ pf.add_argument("--secure", action="store_true",
54
+ help="엄격 모드(민감정보용) — 하나라도 못 채우면 산출물 없이 실패, 출력에 값 비노출")
53
55
  pf.add_argument("--json", action="store_true")
54
56
 
55
57
  pr = sub.add_parser("read", help="본문을 Markdown/텍스트로 추출")
@@ -196,6 +198,8 @@ def _build_parser() -> argparse.ArgumentParser:
196
198
  pt.add_argument("--date", help="날짜 YYYYMMDD → YYYY.M.D.(요일)")
197
199
  pt.add_argument("--age", help="생년월일 YYMMDD → YYMMDD(만나이)")
198
200
  pt.add_argument("--base", help="만나이 기준일 YYYYMMDD (기본 오늘)")
201
+ pt.add_argument("--scale", help="원 단위 금액 → --unit 단위로 환산 (반올림·콤마)")
202
+ pt.add_argument("--unit", default="천원", choices=["천원", "백만원"], help="--scale 대상 단위 (기본 천원)")
199
203
  pt.add_argument("--json", action="store_true")
200
204
 
201
205
  return p
@@ -227,9 +231,16 @@ def main(argv: list[str] | None = None) -> int:
227
231
  elif args.command == "fill":
228
232
  with open(args.data, encoding="utf-8") as fh:
229
233
  mapping = json.load(fh)
230
- data = run_fill(args.file, mapping, args.out)
231
- if data["unmatched"]:
232
- exit_code = 2
234
+ if args.secure:
235
+ from hwpx_kit.commands.fill import run_fill_secure
236
+
237
+ data = run_fill_secure(args.file, mapping, args.out)
238
+ if not data["ok"]:
239
+ exit_code = 1
240
+ else:
241
+ data = run_fill(args.file, mapping, args.out)
242
+ if data["unmatched"]:
243
+ exit_code = 2
233
244
  elif args.command == "read":
234
245
  data = run_read(args.file, fmt=args.format)
235
246
  elif args.command == "convert":
@@ -240,6 +251,7 @@ def main(argv: list[str] | None = None) -> int:
240
251
  data = run_fmt(
241
252
  amount=args.amount, date=args.date, age=args.age,
242
253
  base=args.base, style=args.style,
254
+ scale=args.scale, unit=args.unit,
243
255
  )
244
256
  elif args.command == "row-height":
245
257
  data = run_row_height(
@@ -77,6 +77,31 @@ def _apply_one(ad: HwpxEngineAdapter, fill_key: str, value: str) -> str | None:
77
77
  return "알 수 없는 fill_key 형식 (clickhere:/marker:/table:/text:/delete:/bold:/underline: 중 하나여야 함)"
78
78
 
79
79
 
80
+ def run_fill_secure(path: str, data: dict[str, str], out_path: str) -> dict:
81
+ """엄격 모드 — 민감정보(PII) 채우기용.
82
+
83
+ ① 하나라도 못 채우면 산출물을 남기지 않는다 (반쯤 채워진 개인정보 문서 방지)
84
+ ② 결과에 값을 절대 노출하지 않는다 — 키 목록·개수만.
85
+ 값 파일은 CLI만 읽는 것이 전제 (스킬 규약: Claude는 경로만 전달).
86
+ """
87
+ import os as _os
88
+
89
+ result = run_fill(path, data, out_path)
90
+ if result["unmatched"]:
91
+ try:
92
+ _os.remove(result["out"])
93
+ except OSError:
94
+ pass
95
+ return {
96
+ "ok": False,
97
+ "out": None,
98
+ "applied_count": len(result["applied"]),
99
+ "unmatched_keys": [u["key"] for u in result["unmatched"]],
100
+ "reason": "일부 키를 채우지 못해 산출물을 만들지 않았습니다 (--secure 엄격 모드)",
101
+ }
102
+ return {"ok": True, "out": result["out"], "applied_count": len(result["applied"])}
103
+
104
+
80
105
  def run_fill(path: str, data: dict[str, str], out_path: str) -> dict:
81
106
  ad = HwpxEngineAdapter.open(path)
82
107
  applied: list[str] = []
@@ -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()
@@ -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:,}"
@@ -151,3 +151,52 @@ def test_cli_fill_batch(cert_form, roster_json, template_json, tmp_path, capsys)
151
151
  assert code == 0
152
152
  assert env["ok"] is True
153
153
  assert env["data"]["succeeded"] == 3
154
+
155
+
156
+ # ── fill --secure (엄격 모드) ─────────────────────────────────
157
+
158
+ def test_fill_secure_strict_no_partial(cert_form, tmp_path):
159
+ """--secure: 하나라도 못 채우면 산출물을 남기지 않는다 (반쯤 채워진 PII 문서 방지)."""
160
+ import os
161
+
162
+ from hwpx_kit.commands.fill import run_fill_secure
163
+
164
+ out = str(tmp_path / "o.hwpx")
165
+ result = run_fill_secure(cert_form, {"marker:성명": "김철수", "marker:없는키": "x"}, out)
166
+ assert result["ok"] is False
167
+ assert not os.path.exists(out) # 부분 산출물 없음
168
+ # 실패 사유에 값이 노출되지 않는다
169
+ import json as _json
170
+
171
+ assert "김철수" not in _json.dumps(result, ensure_ascii=False)
172
+
173
+
174
+ def test_fill_secure_success(cert_form, tmp_path):
175
+ import os
176
+
177
+ from hwpx_kit.commands.fill import run_fill_secure
178
+
179
+ out = str(tmp_path / "o.hwpx")
180
+ result = run_fill_secure(cert_form, {"marker:성명": "김철수", "marker:과정명": "AI"}, out)
181
+ assert result["ok"] is True and os.path.exists(out)
182
+ assert result["applied_count"] == 2
183
+ # 값 자체는 결과 JSON에 없음
184
+ import json as _json
185
+
186
+ assert "김철수" not in _json.dumps(result, ensure_ascii=False)
187
+
188
+
189
+ def test_cli_fill_secure(cert_form, tmp_path, capsys):
190
+ import json as _json
191
+
192
+ from hwpx_kit.cli import main
193
+
194
+ values = tmp_path / "v.json"
195
+ values.write_text(_json.dumps({"marker:성명": "김철수", "marker:과정명": "AI"},
196
+ ensure_ascii=False), encoding="utf-8")
197
+ out = str(tmp_path / "o.hwpx")
198
+ code = main(["fill", cert_form, "--data", str(values), "--out", out,
199
+ "--secure", "--json"])
200
+ captured = capsys.readouterr().out
201
+ assert code == 0
202
+ assert "김철수" not in captured # stdout에 값 비노출
@@ -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"
File without changes
File without changes
File without changes
File without changes
File without changes