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.
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/.claude-plugin/plugin.json +1 -1
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/PKG-INFO +10 -3
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/README.md +9 -2
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/pyproject.toml +1 -1
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/skills/hwpx-form/SKILL.md +16 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/skills/table-calc/SKILL.md +11 -1
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/src/hwpx_kit/cli.py +15 -3
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/src/hwpx_kit/commands/fill.py +25 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/src/hwpx_kit/commands/fmt.py +13 -3
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/src/hwpx_kit/format.py +18 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/tests/test_fill_batch.py +49 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/tests/test_format.py +39 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/.claude-plugin/marketplace.json +0 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/.github/workflows/tests.yml +0 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/.gitignore +0 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/CLAUDE.md +0 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/LICENSE +0 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/commands/hwpx.md +0 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/docs/images/After.png +0 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/docs/images/Before.png +0 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/docs/images/Tasking.png +0 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/install.ps1 +0 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/install.sh +0 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/sample-values.json +0 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/scripts/build_skill_package.py +0 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/scripts/check_stdout_purity.py +0 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/scripts/make_sample.py +0 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/skills/doc-create/SKILL.md +0 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/skills/format-convert/SKILL.md +0 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/skills/gongmun-format/SKILL.md +0 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/skills/office-export/SKILL.md +0 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/src/hwpx_kit/__init__.py +0 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/src/hwpx_kit/adapter/__init__.py +0 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/src/hwpx_kit/adapter/base.py +0 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/src/hwpx_kit/adapter/hwpx_engine.py +0 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/src/hwpx_kit/adapter/kordoc_engine.py +0 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/src/hwpx_kit/adapter/office_readers.py +0 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/src/hwpx_kit/commands/__init__.py +0 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/src/hwpx_kit/commands/analyze.py +0 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/src/hwpx_kit/commands/convert.py +0 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/src/hwpx_kit/commands/export.py +0 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/src/hwpx_kit/commands/fill_batch.py +0 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/src/hwpx_kit/commands/generate.py +0 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/src/hwpx_kit/commands/header_footer.py +0 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/src/hwpx_kit/commands/image_add.py +0 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/src/hwpx_kit/commands/inspect_doc.py +0 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/src/hwpx_kit/commands/outline.py +0 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/src/hwpx_kit/commands/page_break.py +0 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/src/hwpx_kit/commands/read.py +0 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/src/hwpx_kit/commands/render.py +0 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/src/hwpx_kit/commands/row_height.py +0 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/src/hwpx_kit/commands/table_clear.py +0 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/src/hwpx_kit/commands/table_copy.py +0 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/src/hwpx_kit/commands/table_map_cmd.py +0 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/src/hwpx_kit/commands/table_new.py +0 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/src/hwpx_kit/commands/table_rows.py +0 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/src/hwpx_kit/commands/table_set.py +0 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/src/hwpx_kit/commands/validate.py +0 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/src/hwpx_kit/inspect_rules.py +0 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/src/hwpx_kit/output.py +0 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/templates/Default_Template.hwpx +0 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/tests/__init__.py +0 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/tests/conftest.py +0 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/tests/fixtures/real/legacy-sample.hwp +0 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/tests/fixtures/real/seoul-attachment.hwpx +0 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/tests/fixtures/real/seoul-body.hwpx +0 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/tests/fixtures/real/seoul-report-brief.hwpx +0 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/tests/fixtures/real/seoul-report-cover.hwpx +0 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/tests/test_adapter.py +0 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/tests/test_analyze.py +0 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/tests/test_cli.py +0 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/tests/test_convert.py +0 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/tests/test_export.py +0 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/tests/test_fill.py +0 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/tests/test_image_header.py +0 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/tests/test_inspect.py +0 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/tests/test_integration.py +0 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/tests/test_kordoc.py +0 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/tests/test_office_readers.py +0 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/tests/test_output.py +0 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/tests/test_page_break.py +0 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/tests/test_read_validate.py +0 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/tests/test_real_forms.py +0 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/tests/test_row_height.py +0 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/tests/test_skill_package.py +0 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/tests/test_stdout_purity.py +0 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/tests/test_table_clear.py +0 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/tests/test_table_copy.py +0 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/tests/test_table_map_new.py +0 -0
- {hwpx_kit_cli-0.4.0 → hwpx_kit_cli-0.5.0}/tests/test_table_rows.py +0 -0
- {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
|
+
"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.
|
|
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
|
[](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
|
|
|
@@ -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
|
-
- 자동 테스트
|
|
218
|
+
- 자동 테스트 193개 (pytest)
|
|
212
219
|
- 실서식 수렴 테스트: 보도자료·신청서·계획서·회의록·공고문 5계열,
|
|
213
220
|
실제 공공기관 문서 30여 종으로 analyze→fill→validate 왕복
|
|
214
221
|
- 실사용 시나리오 테스트 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
|
|
|
@@ -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
|
-
- 자동 테스트
|
|
195
|
+
- 자동 테스트 193개 (pytest)
|
|
189
196
|
- 실서식 수렴 테스트: 보도자료·신청서·계획서·회의록·공고문 5계열,
|
|
190
197
|
실제 공공기관 문서 30여 종으로 analyze→fill→validate 왕복
|
|
191
198
|
- 실사용 시나리오 테스트 3회 (백지 생성 12분 완주, 장·표 동적 추가, 보도자료 변환)
|
|
@@ -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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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)
|
|
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
|
|
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
|