hwpx-kit-cli 0.2.1__tar.gz → 0.3.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.2.1 → hwpx_kit_cli-0.3.0}/.claude-plugin/plugin.json +8 -8
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/.gitignore +1 -0
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/CLAUDE.md +1 -1
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/PKG-INFO +30 -4
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/README.md +29 -3
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/commands/hwpx.md +4 -1
- hwpx_kit_cli-0.3.0/docs/images/After.png +0 -0
- hwpx_kit_cli-0.3.0/docs/images/Before.png +0 -0
- hwpx_kit_cli-0.3.0/docs/images/Tasking.png +0 -0
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/pyproject.toml +44 -44
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/skills/gongmun-format/SKILL.md +12 -1
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/skills/hwpx-form/SKILL.md +38 -2
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/src/hwpx_kit/cli.py +31 -0
- hwpx_kit_cli-0.3.0/src/hwpx_kit/commands/fill_batch.py +103 -0
- hwpx_kit_cli-0.3.0/src/hwpx_kit/commands/inspect_doc.py +30 -0
- hwpx_kit_cli-0.3.0/src/hwpx_kit/inspect_rules.py +84 -0
- hwpx_kit_cli-0.3.0/tests/test_fill_batch.py +153 -0
- hwpx_kit_cli-0.3.0/tests/test_inspect.py +119 -0
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/.claude-plugin/marketplace.json +0 -0
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/.github/workflows/tests.yml +0 -0
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/LICENSE +0 -0
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/install.ps1 +0 -0
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/install.sh +0 -0
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/sample-values.json +0 -0
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/scripts/build_skill_package.py +0 -0
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/scripts/check_stdout_purity.py +0 -0
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/scripts/make_sample.py +0 -0
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/skills/doc-create/SKILL.md +0 -0
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/skills/format-convert/SKILL.md +0 -0
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/skills/office-export/SKILL.md +0 -0
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/skills/table-calc/SKILL.md +0 -0
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/src/hwpx_kit/__init__.py +0 -0
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/src/hwpx_kit/adapter/__init__.py +0 -0
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/src/hwpx_kit/adapter/base.py +0 -0
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/src/hwpx_kit/adapter/hwpx_engine.py +0 -0
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/src/hwpx_kit/adapter/kordoc_engine.py +0 -0
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/src/hwpx_kit/adapter/office_readers.py +0 -0
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/src/hwpx_kit/commands/__init__.py +0 -0
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/src/hwpx_kit/commands/analyze.py +0 -0
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/src/hwpx_kit/commands/convert.py +0 -0
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/src/hwpx_kit/commands/export.py +0 -0
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/src/hwpx_kit/commands/fill.py +0 -0
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/src/hwpx_kit/commands/fmt.py +0 -0
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/src/hwpx_kit/commands/generate.py +0 -0
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/src/hwpx_kit/commands/outline.py +0 -0
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/src/hwpx_kit/commands/page_break.py +0 -0
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/src/hwpx_kit/commands/read.py +0 -0
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/src/hwpx_kit/commands/render.py +0 -0
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/src/hwpx_kit/commands/row_height.py +0 -0
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/src/hwpx_kit/commands/table_clear.py +0 -0
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/src/hwpx_kit/commands/table_copy.py +0 -0
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/src/hwpx_kit/commands/table_map_cmd.py +0 -0
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/src/hwpx_kit/commands/table_new.py +0 -0
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/src/hwpx_kit/commands/table_rows.py +0 -0
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/src/hwpx_kit/commands/table_set.py +0 -0
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/src/hwpx_kit/commands/validate.py +0 -0
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/src/hwpx_kit/format.py +0 -0
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/src/hwpx_kit/output.py +0 -0
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/templates/Default_Template.hwpx +0 -0
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/tests/__init__.py +0 -0
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/tests/conftest.py +0 -0
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/tests/fixtures/real/legacy-sample.hwp +0 -0
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/tests/fixtures/real/seoul-attachment.hwpx +0 -0
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/tests/fixtures/real/seoul-body.hwpx +0 -0
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/tests/fixtures/real/seoul-report-brief.hwpx +0 -0
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/tests/fixtures/real/seoul-report-cover.hwpx +0 -0
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/tests/test_adapter.py +0 -0
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/tests/test_analyze.py +0 -0
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/tests/test_cli.py +0 -0
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/tests/test_convert.py +0 -0
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/tests/test_export.py +0 -0
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/tests/test_fill.py +0 -0
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/tests/test_format.py +0 -0
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/tests/test_integration.py +0 -0
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/tests/test_kordoc.py +0 -0
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/tests/test_office_readers.py +0 -0
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/tests/test_output.py +0 -0
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/tests/test_page_break.py +0 -0
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/tests/test_read_validate.py +0 -0
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/tests/test_real_forms.py +0 -0
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/tests/test_row_height.py +0 -0
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/tests/test_skill_package.py +0 -0
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/tests/test_stdout_purity.py +0 -0
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/tests/test_table_clear.py +0 -0
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/tests/test_table_copy.py +0 -0
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/tests/test_table_map_new.py +0 -0
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/tests/test_table_rows.py +0 -0
- {hwpx_kit_cli-0.2.1 → hwpx_kit_cli-0.3.0}/tests/test_table_set.py +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "hwpx-kit",
|
|
3
|
-
"description": "한글(HWPX/HWP) 문서 자동화 스킬 모음 — 양식 분석·채우기·백지 생성·표기 변환·공문 규약·표 계산·오피스 출력. CLI는 첫 사용 시 스킬이 설치를 안내 (Python 3.10+ 필요).",
|
|
4
|
-
"version": "0.
|
|
5
|
-
"author": {
|
|
6
|
-
"name": "6aneffy",
|
|
7
|
-
"email": "tkdgus990809@gmail.com"
|
|
8
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "hwpx-kit",
|
|
3
|
+
"description": "한글(HWPX/HWP) 문서 자동화 스킬 모음 — 양식 분석·채우기·백지 생성·표기 변환·공문 규약·표 계산·오피스 출력. CLI는 첫 사용 시 스킬이 설치를 안내 (Python 3.10+ 필요).",
|
|
4
|
+
"version": "0.3.0",
|
|
5
|
+
"author": {
|
|
6
|
+
"name": "6aneffy",
|
|
7
|
+
"email": "tkdgus990809@gmail.com"
|
|
8
|
+
}
|
|
9
9
|
}
|
|
@@ -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·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·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.3.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
|
|
|
@@ -66,6 +66,30 @@ hwpx-kit은 **AI 에이전트가 한글 문서를 읽고, 채우고, 고치고,
|
|
|
66
66
|
**이런 분들을 위해 만들었습니다**: 공공기관·학교의 서식 담당자, 사업계획서를 반복 작성하는
|
|
67
67
|
창업자·연구자, 한글 문서를 AI 워크플로에 넣고 싶은 개발자.
|
|
68
68
|
|
|
69
|
+
## 데모 — 한마디로 기획서 한 부
|
|
70
|
+
|
|
71
|
+
> "hwpx로 새로운 사업기획서 작성해줘. 서울광역시에서 진행하는 Claude Code 해커톤"
|
|
72
|
+
|
|
73
|
+
**① 시작점 — 내장 템플릿** (양식 파일이 없을 때 기준. 갖고 있는 양식이 있으면 그 양식을 그대로 씁니다)
|
|
74
|
+
|
|
75
|
+

|
|
76
|
+
|
|
77
|
+
**② 작업 과정** — 필수 정보(일시·대상·주최·예산)를 먼저 묻고, 답을 받아 진행합니다
|
|
78
|
+
|
|
79
|
+

|
|
80
|
+
|
|
81
|
+
**③ 결과물** — 표지·5개 장·일정표·예산표까지 채워진 완성본. 서식은 템플릿 그대로
|
|
82
|
+
|
|
83
|
+

|
|
84
|
+
|
|
85
|
+
이 데모에서 실제로 일어난 일들:
|
|
86
|
+
|
|
87
|
+
- **아는 것만 쓰고, 모르는 건 묻습니다** — 장소가 미정이라고 하자 "서울 시내 행사장(추후 확정)"으로 표기하고 확정되면 수정하겠다고 알림. 말하지 않은 정원·시상금을 지어내지 않음
|
|
88
|
+
- **내용을 주면 그대로, 안 주면 Claude가 초안** — 시상금 구성은 사용자가 준 숫자 그대로, 추진배경·목적은 "필요하다고 생각하는 것들 다 써줘"라는 요청에 Claude가 작성. **상세하게 말할수록 상세하게 반영됩니다**
|
|
89
|
+
- **표 계산은 결정론** — 예산표 합계 7,000 + 30,000 = 총계 37,000천원, 수기 계산 착오 없음
|
|
90
|
+
- **구조도 바꿉니다** — 장 추가/복제, 표 행 추가·삭제(서식 승계), 쪽나눔까지. "5장 뒤에 강사진 장 추가해줘"가 통합니다
|
|
91
|
+
- **기존 양식 재활용** — 작성 완료된 옛 기획서를 주고 "○○사업용으로 바꿔줘"라고 하면 그 문서를 양식 삼아 내용만 전면 교체 (서식 무손상)
|
|
92
|
+
|
|
69
93
|
## 30초 설치
|
|
70
94
|
|
|
71
95
|
Claude Code에서:
|
|
@@ -120,6 +144,8 @@ hwpx-kit validate 문서.hwpx --json # 구조 검증
|
|
|
120
144
|
|
|
121
145
|
# 채우기·편집 (원본 불변 — 항상 --out 사본)
|
|
122
146
|
hwpx-kit fill 양식.hwpx --data 값.json --out 결과.hwpx
|
|
147
|
+
hwpx-kit fill-batch 양식.hwpx --rows 명단.xlsx --template 값틀.json --out-dir 결과/ --name "{성명}_위촉장.hwpx" # 메일머지
|
|
148
|
+
hwpx-kit inspect 결과.hwpx --json # 제출 전 검수 — 잔여 마커·공문 표기·개인정보
|
|
123
149
|
hwpx-kit table-set 문서.hwpx --table 3 --data 셀.json --out 결과.hwpx # 좌표 셀 쓰기
|
|
124
150
|
hwpx-kit table-clear 문서.hwpx --table 3 --rows 1-20 --out 결과.hwpx # 셀 내용 비우기
|
|
125
151
|
hwpx-kit table-copy 문서.hwpx --table 1 --after-table 4 --out 결과.hwpx # 표 통째 복제 (서식·병합 유지)
|
|
@@ -164,7 +190,7 @@ hwpx-kit render 문서.hwpx --out p.svg # SVG 미리보기 (kordoc)
|
|
|
164
190
|
## 아키텍처
|
|
165
191
|
|
|
166
192
|
```
|
|
167
|
-
Claude Code 스킬 6종 ──→ hwpx-kit CLI (
|
|
193
|
+
Claude Code 스킬 6종 ──→ hwpx-kit CLI (23 명령, JSON 봉투)
|
|
168
194
|
└─ adapter/ ← 엔진 격리 계층
|
|
169
195
|
├─ python-hwpx hwpx 분석·채우기·표 조작
|
|
170
196
|
├─ pypdf·python-docx·openpyxl 타 포맷 읽기
|
|
@@ -180,7 +206,7 @@ templates/ 기본 계획서 템플릿 (표지·장 헤더·표 골격, 마커
|
|
|
180
206
|
|
|
181
207
|
## 검증
|
|
182
208
|
|
|
183
|
-
- 자동 테스트
|
|
209
|
+
- 자동 테스트 176개 (pytest)
|
|
184
210
|
- 실서식 수렴 테스트: 보도자료·신청서·계획서·회의록·공고문 5계열,
|
|
185
211
|
실제 공공기관 문서 30여 종으로 analyze→fill→validate 왕복
|
|
186
212
|
- 실사용 시나리오 테스트 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
|
|
|
@@ -43,6 +43,30 @@ hwpx-kit은 **AI 에이전트가 한글 문서를 읽고, 채우고, 고치고,
|
|
|
43
43
|
**이런 분들을 위해 만들었습니다**: 공공기관·학교의 서식 담당자, 사업계획서를 반복 작성하는
|
|
44
44
|
창업자·연구자, 한글 문서를 AI 워크플로에 넣고 싶은 개발자.
|
|
45
45
|
|
|
46
|
+
## 데모 — 한마디로 기획서 한 부
|
|
47
|
+
|
|
48
|
+
> "hwpx로 새로운 사업기획서 작성해줘. 서울광역시에서 진행하는 Claude Code 해커톤"
|
|
49
|
+
|
|
50
|
+
**① 시작점 — 내장 템플릿** (양식 파일이 없을 때 기준. 갖고 있는 양식이 있으면 그 양식을 그대로 씁니다)
|
|
51
|
+
|
|
52
|
+

|
|
53
|
+
|
|
54
|
+
**② 작업 과정** — 필수 정보(일시·대상·주최·예산)를 먼저 묻고, 답을 받아 진행합니다
|
|
55
|
+
|
|
56
|
+

|
|
57
|
+
|
|
58
|
+
**③ 결과물** — 표지·5개 장·일정표·예산표까지 채워진 완성본. 서식은 템플릿 그대로
|
|
59
|
+
|
|
60
|
+

|
|
61
|
+
|
|
62
|
+
이 데모에서 실제로 일어난 일들:
|
|
63
|
+
|
|
64
|
+
- **아는 것만 쓰고, 모르는 건 묻습니다** — 장소가 미정이라고 하자 "서울 시내 행사장(추후 확정)"으로 표기하고 확정되면 수정하겠다고 알림. 말하지 않은 정원·시상금을 지어내지 않음
|
|
65
|
+
- **내용을 주면 그대로, 안 주면 Claude가 초안** — 시상금 구성은 사용자가 준 숫자 그대로, 추진배경·목적은 "필요하다고 생각하는 것들 다 써줘"라는 요청에 Claude가 작성. **상세하게 말할수록 상세하게 반영됩니다**
|
|
66
|
+
- **표 계산은 결정론** — 예산표 합계 7,000 + 30,000 = 총계 37,000천원, 수기 계산 착오 없음
|
|
67
|
+
- **구조도 바꿉니다** — 장 추가/복제, 표 행 추가·삭제(서식 승계), 쪽나눔까지. "5장 뒤에 강사진 장 추가해줘"가 통합니다
|
|
68
|
+
- **기존 양식 재활용** — 작성 완료된 옛 기획서를 주고 "○○사업용으로 바꿔줘"라고 하면 그 문서를 양식 삼아 내용만 전면 교체 (서식 무손상)
|
|
69
|
+
|
|
46
70
|
## 30초 설치
|
|
47
71
|
|
|
48
72
|
Claude Code에서:
|
|
@@ -97,6 +121,8 @@ hwpx-kit validate 문서.hwpx --json # 구조 검증
|
|
|
97
121
|
|
|
98
122
|
# 채우기·편집 (원본 불변 — 항상 --out 사본)
|
|
99
123
|
hwpx-kit fill 양식.hwpx --data 값.json --out 결과.hwpx
|
|
124
|
+
hwpx-kit fill-batch 양식.hwpx --rows 명단.xlsx --template 값틀.json --out-dir 결과/ --name "{성명}_위촉장.hwpx" # 메일머지
|
|
125
|
+
hwpx-kit inspect 결과.hwpx --json # 제출 전 검수 — 잔여 마커·공문 표기·개인정보
|
|
100
126
|
hwpx-kit table-set 문서.hwpx --table 3 --data 셀.json --out 결과.hwpx # 좌표 셀 쓰기
|
|
101
127
|
hwpx-kit table-clear 문서.hwpx --table 3 --rows 1-20 --out 결과.hwpx # 셀 내용 비우기
|
|
102
128
|
hwpx-kit table-copy 문서.hwpx --table 1 --after-table 4 --out 결과.hwpx # 표 통째 복제 (서식·병합 유지)
|
|
@@ -141,7 +167,7 @@ hwpx-kit render 문서.hwpx --out p.svg # SVG 미리보기 (kordoc)
|
|
|
141
167
|
## 아키텍처
|
|
142
168
|
|
|
143
169
|
```
|
|
144
|
-
Claude Code 스킬 6종 ──→ hwpx-kit CLI (
|
|
170
|
+
Claude Code 스킬 6종 ──→ hwpx-kit CLI (23 명령, JSON 봉투)
|
|
145
171
|
└─ adapter/ ← 엔진 격리 계층
|
|
146
172
|
├─ python-hwpx hwpx 분석·채우기·표 조작
|
|
147
173
|
├─ pypdf·python-docx·openpyxl 타 포맷 읽기
|
|
@@ -157,7 +183,7 @@ templates/ 기본 계획서 템플릿 (표지·장 헤더·표 골격, 마커
|
|
|
157
183
|
|
|
158
184
|
## 검증
|
|
159
185
|
|
|
160
|
-
- 자동 테스트
|
|
186
|
+
- 자동 테스트 176개 (pytest)
|
|
161
187
|
- 실서식 수렴 테스트: 보도자료·신청서·계획서·회의록·공고문 5계열,
|
|
162
188
|
실제 공공기관 문서 30여 종으로 analyze→fill→validate 왕복
|
|
163
189
|
- 실사용 시나리오 테스트 3회 (백지 생성 12분 완주, 장·표 동적 추가, 보도자료 변환)
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: hwpx-kit 도움말 — 스킬 6종과 사용 예시 안내
|
|
2
|
+
description: hwpx-kit 도움말 — 스킬 6종과 사용 예시 안내 (사용자가 /hwpx를 직접 입력했을 때 전용)
|
|
3
|
+
disable-model-invocation: true
|
|
3
4
|
---
|
|
4
5
|
|
|
5
6
|
아래 카탈로그를 그대로 사용자에게 출력하라. 다른 도구를 호출하지 말 것.
|
|
7
|
+
(이 카탈로그는 사용자가 /hwpx를 직접 쳤을 때만 보여주는 것 — 문서 작업
|
|
8
|
+
요청이 들어온 상황이라면 카탈로그 출력 없이 해당 스킬로 바로 진행하라.)
|
|
6
9
|
|
|
7
10
|
```
|
|
8
11
|
📄 hwpx-kit — 한글(HWPX) 문서 자동화
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1,44 +1,44 @@
|
|
|
1
|
-
[project]
|
|
2
|
-
name = "hwpx-kit-cli"
|
|
3
|
-
version = "0.
|
|
4
|
-
description = "한글(HWPX) 문서 자동화 CLI — Korean HWPX document automation for AI agents: analyze, fill, read, validate, table ops"
|
|
5
|
-
readme = "README.md"
|
|
6
|
-
requires-python = ">=3.10"
|
|
7
|
-
license = "Apache-2.0"
|
|
8
|
-
authors = [{ name = "6aneffy", email = "tkdgus990809@gmail.com" }]
|
|
9
|
-
keywords = ["hwpx", "hwp", "hangul", "korean", "document-automation", "claude-code"]
|
|
10
|
-
classifiers = [
|
|
11
|
-
"Programming Language :: Python :: 3",
|
|
12
|
-
"Operating System :: OS Independent",
|
|
13
|
-
"Topic :: Office/Business :: Office Suites",
|
|
14
|
-
]
|
|
15
|
-
dependencies = [
|
|
16
|
-
"openpyxl>=3.1.5",
|
|
17
|
-
"pypdf>=6.14.2",
|
|
18
|
-
"python-docx>=1.2.0",
|
|
19
|
-
"python-hwpx>=2.24,<3",
|
|
20
|
-
]
|
|
21
|
-
|
|
22
|
-
[project.optional-dependencies]
|
|
23
|
-
convert = ["pyhwpx; sys_platform == 'win32'"]
|
|
24
|
-
|
|
25
|
-
[project.urls]
|
|
26
|
-
Homepage = "https://github.com/6aneffy/hwpx-kit"
|
|
27
|
-
Repository = "https://github.com/6aneffy/hwpx-kit"
|
|
28
|
-
Issues = "https://github.com/6aneffy/hwpx-kit/issues"
|
|
29
|
-
|
|
30
|
-
[project.scripts]
|
|
31
|
-
hwpx-kit = "hwpx_kit.cli:main"
|
|
32
|
-
|
|
33
|
-
[dependency-groups]
|
|
34
|
-
dev = ["pytest>=8"]
|
|
35
|
-
|
|
36
|
-
[build-system]
|
|
37
|
-
requires = ["hatchling"]
|
|
38
|
-
build-backend = "hatchling.build"
|
|
39
|
-
|
|
40
|
-
[tool.hatch.build.targets.wheel]
|
|
41
|
-
packages = ["src/hwpx_kit"]
|
|
42
|
-
|
|
43
|
-
[tool.pytest.ini_options]
|
|
44
|
-
testpaths = ["tests"]
|
|
1
|
+
[project]
|
|
2
|
+
name = "hwpx-kit-cli"
|
|
3
|
+
version = "0.3.0"
|
|
4
|
+
description = "한글(HWPX) 문서 자동화 CLI — Korean HWPX document automation for AI agents: analyze, fill, read, validate, table ops"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
license = "Apache-2.0"
|
|
8
|
+
authors = [{ name = "6aneffy", email = "tkdgus990809@gmail.com" }]
|
|
9
|
+
keywords = ["hwpx", "hwp", "hangul", "korean", "document-automation", "claude-code"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Programming Language :: Python :: 3",
|
|
12
|
+
"Operating System :: OS Independent",
|
|
13
|
+
"Topic :: Office/Business :: Office Suites",
|
|
14
|
+
]
|
|
15
|
+
dependencies = [
|
|
16
|
+
"openpyxl>=3.1.5",
|
|
17
|
+
"pypdf>=6.14.2",
|
|
18
|
+
"python-docx>=1.2.0",
|
|
19
|
+
"python-hwpx>=2.24,<3",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[project.optional-dependencies]
|
|
23
|
+
convert = ["pyhwpx; sys_platform == 'win32'"]
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
Homepage = "https://github.com/6aneffy/hwpx-kit"
|
|
27
|
+
Repository = "https://github.com/6aneffy/hwpx-kit"
|
|
28
|
+
Issues = "https://github.com/6aneffy/hwpx-kit/issues"
|
|
29
|
+
|
|
30
|
+
[project.scripts]
|
|
31
|
+
hwpx-kit = "hwpx_kit.cli:main"
|
|
32
|
+
|
|
33
|
+
[dependency-groups]
|
|
34
|
+
dev = ["pytest>=8"]
|
|
35
|
+
|
|
36
|
+
[build-system]
|
|
37
|
+
requires = ["hatchling"]
|
|
38
|
+
build-backend = "hatchling.build"
|
|
39
|
+
|
|
40
|
+
[tool.hatch.build.targets.wheel]
|
|
41
|
+
packages = ["src/hwpx_kit"]
|
|
42
|
+
|
|
43
|
+
[tool.pytest.ini_options]
|
|
44
|
+
testpaths = ["tests"]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: gongmun-format
|
|
3
|
-
description: 행안부 공문서 작성 규약 — 글머리 위계(□○-※*), 날짜·금액 표기, 홑낫표, 어절정리, 복붙 깨짐
|
|
3
|
+
description: 행안부 공문서 작성 규약 — 글머리 위계(□○-※*), 날짜·금액 표기, 홑낫표, 어절정리, 복붙 깨짐 복구, 제출 전 기계 검수(inspect). 공문·보도자료·계획안 초안 작성이나 "공문 형식으로", "공문서 규정대로", "보고서 양식 맞춰줘", "제출 전에 검토해줘" 요청 시 사용. Triggers: Korean government document style, gongmun formatting, review before submission.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# gongmun-format: 행안부 공문 작성 규약
|
|
@@ -34,6 +34,17 @@ description: 행안부 공문서 작성 규약 — 글머리 위계(□○-※*)
|
|
|
34
34
|
- **법령·문서명**: 홑낫표 `「」` 로 감싼다 — `「행정 효율과 협업 촉진에 관한 규정」`
|
|
35
35
|
- **증감·비율**: table-calc 스킬의 표기 관습(△·%p 등) 따름
|
|
36
36
|
|
|
37
|
+
## 제출 전 검수 — "검토해줘" (기계 검사 먼저, 판단은 그 위에)
|
|
38
|
+
|
|
39
|
+
`hwpx-kit inspect <문서.hwpx> --json` — 결정론 검사 3종 (fmt와 같은 "암산 금지" 원칙):
|
|
40
|
+
- **residue**: 채우다 만 {{마커}}·○○○ 자리표시·"(예시)" 안내문 잔존
|
|
41
|
+
- **gongmun**: 날짜 하이픈 표기(2026-07-10 → `2026. 7. 10.`), 오전/오후 시각(→ 24시각제)
|
|
42
|
+
- **pii**: 주민번호·휴대전화·이메일 잔존 (공개 배포 문서면 특히)
|
|
43
|
+
|
|
44
|
+
종료코드 2 = 위반 있음. `issues[]`의 code·context를 **사람 말로 요약**해 보여주고,
|
|
45
|
+
수정할 항목을 확정받아 fill(text:/delete:)로 고친 뒤 재검사. 기계 검사가 못 보는
|
|
46
|
+
것(위계 건너뜀, 어색한 문장, 내용 누락)은 read 출력을 직접 읽고 이 스킬 규약으로 판단.
|
|
47
|
+
|
|
37
48
|
## 텍스트 정리 (재작성 계열 — 우리가 범피스보다 잘하는 것)
|
|
38
49
|
|
|
39
50
|
한글 문서에서 read로 뽑은 텍스트나 사용자가 붙여넣은 텍스트가 깨져 있으면:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: hwpx-kit
|
|
3
|
-
description: HWPX 양식 자동화 — 한글(HWPX/HWP) 양식 분석, 내용 채우기, 본문 추출, 구조 검증, .hwp→.hwpx 변환. 사용자가 hwpx/hwp 파일을 주며 "이 양식에 채워줘", "여기에 써줘", "이 문서 읽어줘/요약해줘", "hwp 변환해줘" 할 때 사용. Triggers: fill this Korean form, 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 양식 자동화
|
|
@@ -21,6 +21,11 @@ CLI `hwpx-kit`으로 hwpx 문서를 다룬다. 항상 `--json`을 붙여 실행
|
|
|
21
21
|
(실패 시 폴백: `pip install --user git+https://github.com/6aneffy/hwpx-kit.git`)
|
|
22
22
|
→ `python -m hwpx_kit.cli --version` 확인 후 원래 작업 계속
|
|
23
23
|
|
|
24
|
+
**구버전 업그레이드도 같은 이름**: 설치돼 있는데 필요한 명령(row-add 등)이
|
|
25
|
+
없으면 `pip install -U hwpx-kit-cli`. 🔴 **PyPI 배포명은 `hwpx-kit-cli`다**
|
|
26
|
+
— `hwpx-kit`은 PyPI에 없는 이름이니 (플러그인·명령어 이름일 뿐) 그걸로
|
|
27
|
+
검색/설치 시도하지 말 것
|
|
28
|
+
|
|
24
29
|
기존 문서(계획안·보고서 작성본)를 양식 삼아 **다른 사업 문서를 새로 쓰는** 요청도
|
|
25
30
|
같은 흐름이다 — 내용(사업명·배경·커리큘럼 등)은 네가 초안을 짜서 사용자와 확정하고,
|
|
26
31
|
문서화는 read → text:/table: 교체로 한다. 서식은 원본 그대로 유지된다.
|
|
@@ -129,9 +134,40 @@ CLI `hwpx-kit`으로 hwpx 문서를 다룬다. 항상 `--json`을 붙여 실행
|
|
|
129
134
|
`hwpx-kit fill <양식.hwpx> --data <값.json> --out <결과.hwpx> --json`
|
|
130
135
|
- 원본은 절대 수정되지 않는다. `--out`은 원본과 다른 경로여야 한다
|
|
131
136
|
- 종료 코드 2면 `data.unmatched[]`의 사유를 사용자에게 보여주고 값이나 키를 수정해 재시도
|
|
132
|
-
4. **검증**: `hwpx-kit validate <결과.hwpx> --json`
|
|
137
|
+
4. **검증**: `hwpx-kit validate <결과.hwpx> --json` (구조) →
|
|
138
|
+
`hwpx-kit inspect <결과.hwpx> --json` (내용 — 잔여 {{마커}}·○○○ 자리표시·
|
|
139
|
+
공문 표기 위반·개인정보 잔존을 기계 검사. 종료코드 2면 issues[]를 사람 말로
|
|
140
|
+
요약해 보여주고 수정 여부 확인)
|
|
133
141
|
5. **안내**: 결과 파일 경로를 알려주고, 최종 서식은 한글(한컴오피스)에서 열어 확인하도록 권한다
|
|
134
142
|
|
|
143
|
+
## 일괄 생성 (메일머지) — "이 명단으로 30명분 만들어줘"
|
|
144
|
+
|
|
145
|
+
위촉장·수료증·통지서처럼 **같은 양식 × 명단 N명**은 fill을 N번 돌리지 말고
|
|
146
|
+
`fill-batch` 하나로:
|
|
147
|
+
|
|
148
|
+
1. 명단 파일 확보 — xlsx(1행이 헤더)·csv·json 배열 모두 됨. 명단이 대화로만
|
|
149
|
+
왔으면 json으로 저장
|
|
150
|
+
2. analyze로 fill_key 파악 → **템플릿 JSON** 작성: 값 속에 `{열이름}` 자리표시
|
|
151
|
+
```json
|
|
152
|
+
{"marker:성명": "{성명}", "marker:과정명": "{과정} 과정"}
|
|
153
|
+
```
|
|
154
|
+
3. **첫 1부를 먼저 뽑아 사용자 승인** (fill로 1건) — 승인 후 전체:
|
|
155
|
+
`hwpx-kit fill-batch <양식.hwpx> --rows <명단.xlsx> --template <템플릿.json> --out-dir <폴더> --name "{성명}_위촉장.hwpx" --json`
|
|
156
|
+
4. 종료코드 2면 `rows[]`에서 실패 행만 추려 보고. 파일명 충돌은 자동 `_2` 접미,
|
|
157
|
+
금지문자는 `_` 치환. 원본 불변은 fill과 동일
|
|
158
|
+
5. 개인정보 든 명단 파일은 사용자 문서와 동일한 기밀 취급 — 커밋·업로드 금지
|
|
159
|
+
|
|
160
|
+
## 엑셀 표를 한글 문서에 넣기 — "이 엑셀 표 문서에 넣어줘"
|
|
161
|
+
|
|
162
|
+
복붙(서식 깨짐) 대신 도구 조합으로:
|
|
163
|
+
1. `hwpx-kit read <파일.xlsx> --format md` — 표 데이터 확보
|
|
164
|
+
2. `outline`으로 삽입 위치(앵커 문단/표) 확인, 애매하면 사용자에게
|
|
165
|
+
3. `table-new --rows R --cols C --like-table <이웃 표> --after-text "<앵커>"` —
|
|
166
|
+
문서 기존 표 서식을 차용해 새 표 생성 (이웃 표 없으면 --like-table 생략)
|
|
167
|
+
4. `table-set --data <셀.json>`으로 값 기입 — 숫자는 세 자리 콤마 등 표기는
|
|
168
|
+
table-calc 스킬 관습, 헤더 행은 (0,c) 좌표
|
|
169
|
+
5. validate → 경로 안내
|
|
170
|
+
|
|
135
171
|
## 예시 텍스트 교체 (보도자료 등 "예시 문구가 든 양식")
|
|
136
172
|
|
|
137
173
|
누름틀/빈칸이 아니라 예시 문장이 채워진 양식(보도자료 제목·본문 등)은 `text:` 키 사용:
|
|
@@ -91,6 +91,19 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
91
91
|
ph.add_argument("--out", required=True, help="출력 hwpx 경로 (원본 불변)")
|
|
92
92
|
ph.add_argument("--json", action="store_true")
|
|
93
93
|
|
|
94
|
+
pfb = sub.add_parser("fill-batch", help="명단 × 양식 일괄 생성 (메일머지) — 위촉장·수료증·통지서 N부")
|
|
95
|
+
pfb.add_argument("file", help="양식 hwpx")
|
|
96
|
+
pfb.add_argument("--rows", required=True, help="명단 파일 (xlsx: 1행 헤더 / csv / json 객체 배열)")
|
|
97
|
+
pfb.add_argument("--template", required=True, help="fill 데이터 JSON — 값 속 {열이름}이 행마다 치환됨")
|
|
98
|
+
pfb.add_argument("--out-dir", required=True, help="출력 폴더 (없으면 생성)")
|
|
99
|
+
pfb.add_argument("--name", required=True, help='파일명 패턴, 예: "{성명}_위촉장.hwpx" (충돌 시 _2 접미)')
|
|
100
|
+
pfb.add_argument("--json", action="store_true")
|
|
101
|
+
|
|
102
|
+
pin = sub.add_parser("inspect", help="제출 전 기계 검수 — 잔여물({{마커}}·○○○)·공문 표기(날짜·시각)·개인정보. 위반 시 종료코드 2")
|
|
103
|
+
pin.add_argument("file")
|
|
104
|
+
pin.add_argument("--checks", help="쉼표 구분: residue,gongmun,pii (생략 시 전부)")
|
|
105
|
+
pin.add_argument("--json", action="store_true")
|
|
106
|
+
|
|
94
107
|
pra = sub.add_parser("row-add", help="표 행 추가 — 기준 행 서식·높이 승계, 내용은 비움 (세로 병합 걸리면 거부)")
|
|
95
108
|
pra.add_argument("file")
|
|
96
109
|
pra.add_argument("--table", type=int, required=True, help="표 인덱스 (0-기준)")
|
|
@@ -214,6 +227,24 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
214
227
|
args.file, table=args.table, like=args.like,
|
|
215
228
|
rows=parse_rows_spec(args.rows), out_path=args.out,
|
|
216
229
|
)
|
|
230
|
+
elif args.command == "fill-batch":
|
|
231
|
+
from hwpx_kit.commands.fill_batch import run_fill_batch
|
|
232
|
+
|
|
233
|
+
data = run_fill_batch(
|
|
234
|
+
args.file, rows_path=args.rows, template_path=args.template,
|
|
235
|
+
out_dir=args.out_dir, name_pattern=args.name,
|
|
236
|
+
)
|
|
237
|
+
if not data["all_ok"]:
|
|
238
|
+
exit_code = 2
|
|
239
|
+
elif args.command == "inspect":
|
|
240
|
+
from hwpx_kit.commands.inspect_doc import run_inspect
|
|
241
|
+
|
|
242
|
+
data = run_inspect(
|
|
243
|
+
args.file,
|
|
244
|
+
checks=[c.strip() for c in args.checks.split(",")] if args.checks else None,
|
|
245
|
+
)
|
|
246
|
+
if not data["clean"]:
|
|
247
|
+
exit_code = 2
|
|
217
248
|
elif args.command == "row-add":
|
|
218
249
|
from hwpx_kit.commands.table_rows import run_row_add
|
|
219
250
|
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""fill-batch 명령 — 명단 × 양식 메일머지.
|
|
2
|
+
|
|
3
|
+
명단(xlsx/csv/json)의 행마다 fill 템플릿의 {열이름}을 치환해 run_fill을
|
|
4
|
+
반복하고, 파일명 패턴으로 사본 N부를 만든다. 원본 불변은 fill과 동일.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import csv
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import re
|
|
12
|
+
|
|
13
|
+
from hwpx_kit.commands.fill import run_fill
|
|
14
|
+
|
|
15
|
+
_FORBIDDEN_FILENAME = re.compile(r'[\\/:*?"<>|]')
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def load_rows(rows_path: str) -> list[dict[str, str]]:
|
|
19
|
+
"""명단 파일 → 행 dict 목록. xlsx는 1행을 헤더로 본다."""
|
|
20
|
+
ext = os.path.splitext(rows_path)[1].lower()
|
|
21
|
+
if ext == ".json":
|
|
22
|
+
with open(rows_path, encoding="utf-8") as f:
|
|
23
|
+
data = json.load(f)
|
|
24
|
+
if not isinstance(data, list):
|
|
25
|
+
raise ValueError("명단 JSON은 객체 배열이어야 합니다: [{\"성명\": ...}, ...]")
|
|
26
|
+
return [{str(k): "" if v is None else str(v) for k, v in row.items()} for row in data]
|
|
27
|
+
if ext == ".csv":
|
|
28
|
+
with open(rows_path, encoding="utf-8-sig", newline="") as f:
|
|
29
|
+
return [dict(row) for row in csv.DictReader(f)]
|
|
30
|
+
if ext == ".xlsx":
|
|
31
|
+
import openpyxl
|
|
32
|
+
|
|
33
|
+
ws = openpyxl.load_workbook(rows_path, data_only=True).active
|
|
34
|
+
rows_iter = ws.iter_rows(values_only=True)
|
|
35
|
+
header = [str(h) if h is not None else "" for h in next(rows_iter, [])]
|
|
36
|
+
rows = []
|
|
37
|
+
for values in rows_iter:
|
|
38
|
+
if values is None or all(v is None for v in values):
|
|
39
|
+
continue
|
|
40
|
+
rows.append({
|
|
41
|
+
header[i]: "" if v is None else str(v)
|
|
42
|
+
for i, v in enumerate(values) if i < len(header) and header[i]
|
|
43
|
+
})
|
|
44
|
+
return rows
|
|
45
|
+
raise ValueError(f"지원하지 않는 명단 형식: {ext} (xlsx/csv/json)")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _substitute(template: str, row: dict[str, str]) -> str:
|
|
49
|
+
out = template
|
|
50
|
+
for key, value in row.items():
|
|
51
|
+
out = out.replace("{" + key + "}", value)
|
|
52
|
+
return out
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _unique_path(out_dir: str, name: str) -> str:
|
|
56
|
+
base, ext = os.path.splitext(name)
|
|
57
|
+
candidate = os.path.join(out_dir, name)
|
|
58
|
+
n = 2
|
|
59
|
+
while os.path.exists(candidate):
|
|
60
|
+
candidate = os.path.join(out_dir, f"{base}_{n}{ext}")
|
|
61
|
+
n += 1
|
|
62
|
+
return candidate
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def run_fill_batch(path: str, rows_path: str, template_path: str,
|
|
66
|
+
out_dir: str, name_pattern: str) -> dict:
|
|
67
|
+
with open(template_path, encoding="utf-8") as f:
|
|
68
|
+
template: dict[str, str] = json.load(f)
|
|
69
|
+
|
|
70
|
+
rows = load_rows(rows_path)
|
|
71
|
+
if not rows:
|
|
72
|
+
raise ValueError("명단이 비어 있습니다.")
|
|
73
|
+
os.makedirs(out_dir, exist_ok=True)
|
|
74
|
+
|
|
75
|
+
results = []
|
|
76
|
+
succeeded = 0
|
|
77
|
+
for i, row in enumerate(rows):
|
|
78
|
+
data = {k: _substitute(str(v), row) for k, v in template.items()}
|
|
79
|
+
name = _FORBIDDEN_FILENAME.sub("_", _substitute(name_pattern, row))
|
|
80
|
+
out_path = _unique_path(out_dir, name)
|
|
81
|
+
try:
|
|
82
|
+
fill_result = run_fill(path, data, out_path)
|
|
83
|
+
ok = not fill_result["unmatched"]
|
|
84
|
+
if ok:
|
|
85
|
+
succeeded += 1
|
|
86
|
+
results.append({
|
|
87
|
+
"row": i, "out": fill_result["out"], "ok": ok,
|
|
88
|
+
"applied": fill_result["applied"],
|
|
89
|
+
"unmatched": fill_result["unmatched"],
|
|
90
|
+
})
|
|
91
|
+
except Exception as exc:
|
|
92
|
+
results.append({"row": i, "out": None, "ok": False,
|
|
93
|
+
"applied": [], "unmatched": [],
|
|
94
|
+
"error": str(exc)})
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
"file": path,
|
|
98
|
+
"out_dir": os.path.abspath(out_dir),
|
|
99
|
+
"total": len(rows),
|
|
100
|
+
"succeeded": succeeded,
|
|
101
|
+
"all_ok": succeeded == len(rows),
|
|
102
|
+
"rows": results,
|
|
103
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""inspect 명령 — 제출 전 기계 검수.
|
|
2
|
+
|
|
3
|
+
문서 본문(표 셀 포함)을 뽑아 inspect_rules의 순수 검사를 돌린다.
|
|
4
|
+
위반이 있으면 CLI가 exit 2 (부분 성공)로 알린다 — 게이트로 쓸 수 있게.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from hwpx_kit.adapter.hwpx_engine import HwpxEngineAdapter
|
|
9
|
+
from hwpx_kit.inspect_rules import CHECKS
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def run_inspect(path: str, checks: list[str] | None = None) -> dict:
|
|
13
|
+
selected = checks or list(CHECKS)
|
|
14
|
+
bad = [c for c in selected if c not in CHECKS]
|
|
15
|
+
if bad:
|
|
16
|
+
raise ValueError(f"알 수 없는 검사: {bad} (가능: {', '.join(CHECKS)})")
|
|
17
|
+
|
|
18
|
+
ad = HwpxEngineAdapter.open(path)
|
|
19
|
+
text = ad.export_text()
|
|
20
|
+
|
|
21
|
+
issues: list[dict] = []
|
|
22
|
+
for name in selected:
|
|
23
|
+
issues.extend(CHECKS[name](text))
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
"file": path,
|
|
27
|
+
"checks": selected,
|
|
28
|
+
"issues": issues,
|
|
29
|
+
"clean": not issues,
|
|
30
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""inspect 순수 검사함수 — 잔여물·공문 표기·PII 정규식.
|
|
2
|
+
|
|
3
|
+
format.py와 같은 원칙: 엔진 임포트 금지, LLM 판단이 아닌 결정론 검사.
|
|
4
|
+
각 함수는 텍스트 한 덩이를 받아 issues 목록을 반환한다:
|
|
5
|
+
{"check": ..., "code": ..., "message": ..., "context": <위반 주변 원문>}
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
|
|
11
|
+
_CONTEXT_SPAN = 30
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _issue(check: str, code: str, message: str, text: str, match: re.Match) -> dict:
|
|
15
|
+
start = max(0, match.start() - _CONTEXT_SPAN)
|
|
16
|
+
end = min(len(text), match.end() + _CONTEXT_SPAN)
|
|
17
|
+
return {"check": check, "code": code, "message": message,
|
|
18
|
+
"context": text[start:end].strip()}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# ── residue: 채움 잔여물 ──────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
_RESIDUE_PATTERNS: list[tuple[str, str, re.Pattern]] = [
|
|
24
|
+
("leftover_marker", "채우지 않은 {{마커}}가 남아 있음",
|
|
25
|
+
re.compile(r"\{\{[^{}]{1,40}\}\}")),
|
|
26
|
+
("placeholder_chars", "자리표시 문자(○○○·△△△·×××)가 남아 있음",
|
|
27
|
+
re.compile(r"[○◯]{3,}|[△▲]{3,}|[×✕]{3,}")),
|
|
28
|
+
("example_note", "양식 안내문(예시·해당시)이 남아 있음",
|
|
29
|
+
re.compile(r"\(예시[^)]{0,20}\)|←\s*해당\s*시")),
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def check_residue_text(text: str) -> list[dict]:
|
|
34
|
+
issues = []
|
|
35
|
+
for code, message, pattern in _RESIDUE_PATTERNS:
|
|
36
|
+
for m in pattern.finditer(text):
|
|
37
|
+
issues.append(_issue("residue", code, message, text, m))
|
|
38
|
+
return issues
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# ── gongmun: 공문 표기 규약 ───────────────────────────────────
|
|
42
|
+
|
|
43
|
+
# 날짜: 공문은 '2026. 7. 10.' — 하이픈/슬래시 연월일은 위반
|
|
44
|
+
_DATE_BAD = re.compile(r"\b(19|20)\d{2}\s*[-/]\s*\d{1,2}\s*[-/]\s*\d{1,2}\b")
|
|
45
|
+
# 시각: 공문은 24시각제 '15:00' — '오전/오후 N시(N분)'는 위반
|
|
46
|
+
_TIME_BAD = re.compile(r"(오전|오후)\s*\d{1,2}\s*시(\s*\d{1,2}\s*분)?")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def check_gongmun_text(text: str) -> list[dict]:
|
|
50
|
+
issues = []
|
|
51
|
+
for m in _DATE_BAD.finditer(text):
|
|
52
|
+
issues.append(_issue("gongmun", "date_style",
|
|
53
|
+
"공문 날짜 표기는 '2026. 7. 10.' 형식 — 하이픈/슬래시 표기 위반",
|
|
54
|
+
text, m))
|
|
55
|
+
for m in _TIME_BAD.finditer(text):
|
|
56
|
+
issues.append(_issue("gongmun", "time_style",
|
|
57
|
+
"공문 시각은 24시각제 '15:00' — 오전/오후 표기 위반",
|
|
58
|
+
text, m))
|
|
59
|
+
return issues
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# ── pii: 개인정보 잔존 ────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
_RRN = re.compile(r"\b\d{6}\s*-\s*[1-4]\d{6}\b") # 주민등록번호
|
|
65
|
+
_PHONE = re.compile(r"\b01[016789]\s*-\s*\d{3,4}\s*-\s*\d{4}\b") # 휴대전화
|
|
66
|
+
_EMAIL = re.compile(r"\b[\w.+-]+@[\w-]+\.[\w.]{2,}\b")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def check_pii_text(text: str) -> list[dict]:
|
|
70
|
+
issues = []
|
|
71
|
+
for m in _RRN.finditer(text):
|
|
72
|
+
issues.append(_issue("pii", "rrn", "주민등록번호 패턴 발견 — 제출 전 확인", text, m))
|
|
73
|
+
for m in _PHONE.finditer(text):
|
|
74
|
+
issues.append(_issue("pii", "phone", "휴대전화 번호 발견 — 공개 배포 시 확인", text, m))
|
|
75
|
+
for m in _EMAIL.finditer(text):
|
|
76
|
+
issues.append(_issue("pii", "email", "이메일 주소 발견 — 공개 배포 시 확인", text, m))
|
|
77
|
+
return issues
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
CHECKS = {
|
|
81
|
+
"residue": check_residue_text,
|
|
82
|
+
"gongmun": check_gongmun_text,
|
|
83
|
+
"pii": check_pii_text,
|
|
84
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""fill-batch — 명단(xlsx/csv/json) × 양식 → N부 일괄 생성 (메일머지).
|
|
2
|
+
|
|
3
|
+
한 양식에 fill을 행마다 반복하고 파일명 패턴으로 사본을 만든다.
|
|
4
|
+
원본 불변·행별 실패 보고·파일명 충돌 처리가 계약.
|
|
5
|
+
"""
|
|
6
|
+
import csv
|
|
7
|
+
import json
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
from hwpx_kit.cli import main
|
|
12
|
+
from hwpx_kit.commands.fill_batch import run_fill_batch
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@pytest.fixture()
|
|
16
|
+
def cert_form(tmp_path):
|
|
17
|
+
"""{{성명}}/{{과정명}} 마커가 든 위촉장 양식."""
|
|
18
|
+
from hwpx.document import HwpxDocument
|
|
19
|
+
|
|
20
|
+
from hwpx_kit.output import quiet_engine
|
|
21
|
+
|
|
22
|
+
with quiet_engine():
|
|
23
|
+
doc = HwpxDocument.new()
|
|
24
|
+
doc.add_paragraph("위 촉 장")
|
|
25
|
+
doc.add_paragraph("성명: {{성명}}")
|
|
26
|
+
doc.add_paragraph("과정: {{과정명}}")
|
|
27
|
+
path = str(tmp_path / "form.hwpx")
|
|
28
|
+
doc.save_to_path(path)
|
|
29
|
+
return path
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@pytest.fixture()
|
|
33
|
+
def roster_json(tmp_path):
|
|
34
|
+
rows = [
|
|
35
|
+
{"성명": "김철수", "과정명": "AI 기초"},
|
|
36
|
+
{"성명": "이영희", "과정명": "AI 심화"},
|
|
37
|
+
{"성명": "박민수", "과정명": "AI 기초"},
|
|
38
|
+
]
|
|
39
|
+
p = tmp_path / "roster.json"
|
|
40
|
+
p.write_text(json.dumps(rows, ensure_ascii=False), encoding="utf-8")
|
|
41
|
+
return str(p)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@pytest.fixture()
|
|
45
|
+
def template_json(tmp_path):
|
|
46
|
+
p = tmp_path / "template.json"
|
|
47
|
+
p.write_text(json.dumps({
|
|
48
|
+
"marker:성명": "{성명}",
|
|
49
|
+
"marker:과정명": "{과정명} 과정",
|
|
50
|
+
}, ensure_ascii=False), encoding="utf-8")
|
|
51
|
+
return str(p)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _read_text(path):
|
|
55
|
+
from hwpx_kit.adapter.hwpx_engine import HwpxEngineAdapter
|
|
56
|
+
|
|
57
|
+
return HwpxEngineAdapter.open(path).export_text()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_batch_json_roster(cert_form, roster_json, template_json, tmp_path):
|
|
61
|
+
out_dir = str(tmp_path / "out")
|
|
62
|
+
result = run_fill_batch(
|
|
63
|
+
cert_form, rows_path=roster_json, template_path=template_json,
|
|
64
|
+
out_dir=out_dir, name_pattern="{성명}_위촉장.hwpx",
|
|
65
|
+
)
|
|
66
|
+
assert result["total"] == 3 and result["succeeded"] == 3
|
|
67
|
+
files = [r["out"] for r in result["rows"]]
|
|
68
|
+
assert any("김철수_위촉장" in f for f in files)
|
|
69
|
+
text = _read_text(files[0])
|
|
70
|
+
assert "김철수" in text and "AI 기초 과정" in text
|
|
71
|
+
# 원본 불변 — 양식엔 마커 그대로
|
|
72
|
+
assert "{{성명}}" in _read_text(cert_form)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_batch_xlsx_roster(cert_form, template_json, tmp_path):
|
|
76
|
+
import openpyxl
|
|
77
|
+
|
|
78
|
+
wb = openpyxl.Workbook()
|
|
79
|
+
ws = wb.active
|
|
80
|
+
ws.append(["성명", "과정명"])
|
|
81
|
+
ws.append(["김철수", "AI 기초"])
|
|
82
|
+
ws.append(["이영희", "AI 심화"])
|
|
83
|
+
xlsx = str(tmp_path / "roster.xlsx")
|
|
84
|
+
wb.save(xlsx)
|
|
85
|
+
|
|
86
|
+
result = run_fill_batch(
|
|
87
|
+
cert_form, rows_path=xlsx, template_path=template_json,
|
|
88
|
+
out_dir=str(tmp_path / "out"), name_pattern="{성명}.hwpx",
|
|
89
|
+
)
|
|
90
|
+
assert result["succeeded"] == 2
|
|
91
|
+
assert "이영희" in _read_text(result["rows"][1]["out"])
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_batch_csv_roster(cert_form, template_json, tmp_path):
|
|
95
|
+
p = tmp_path / "roster.csv"
|
|
96
|
+
with open(p, "w", newline="", encoding="utf-8-sig") as f:
|
|
97
|
+
w = csv.writer(f)
|
|
98
|
+
w.writerow(["성명", "과정명"])
|
|
99
|
+
w.writerow(["김철수", "AI 기초"])
|
|
100
|
+
result = run_fill_batch(
|
|
101
|
+
cert_form, rows_path=str(p), template_path=template_json,
|
|
102
|
+
out_dir=str(tmp_path / "out"), name_pattern="{성명}.hwpx",
|
|
103
|
+
)
|
|
104
|
+
assert result["succeeded"] == 1
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def test_batch_name_collision_suffix(cert_form, template_json, tmp_path):
|
|
108
|
+
rows = [{"성명": "김철수", "과정명": "A"}, {"성명": "김철수", "과정명": "B"}]
|
|
109
|
+
p = tmp_path / "r.json"
|
|
110
|
+
p.write_text(json.dumps(rows, ensure_ascii=False), encoding="utf-8")
|
|
111
|
+
result = run_fill_batch(
|
|
112
|
+
cert_form, rows_path=str(p), template_path=template_json,
|
|
113
|
+
out_dir=str(tmp_path / "out"), name_pattern="{성명}.hwpx",
|
|
114
|
+
)
|
|
115
|
+
outs = [r["out"] for r in result["rows"]]
|
|
116
|
+
assert len(set(outs)) == 2 # 충돌해도 덮어쓰지 않고 접미사
|
|
117
|
+
|
|
118
|
+
def test_batch_reports_row_failures(cert_form, roster_json, tmp_path):
|
|
119
|
+
# 존재하지 않는 마커 키 → 행마다 unmatched 보고, all_ok False
|
|
120
|
+
bad_template = tmp_path / "bad.json"
|
|
121
|
+
bad_template.write_text(json.dumps({"marker:없는키": "{성명}"}, ensure_ascii=False),
|
|
122
|
+
encoding="utf-8")
|
|
123
|
+
result = run_fill_batch(
|
|
124
|
+
cert_form, rows_path=roster_json, template_path=str(bad_template),
|
|
125
|
+
out_dir=str(tmp_path / "out"), name_pattern="{성명}.hwpx",
|
|
126
|
+
)
|
|
127
|
+
assert result["all_ok"] is False
|
|
128
|
+
assert result["rows"][0]["unmatched"]
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def test_batch_filename_sanitized(cert_form, template_json, tmp_path):
|
|
132
|
+
rows = [{"성명": "김/철*수", "과정명": "A"}]
|
|
133
|
+
p = tmp_path / "r.json"
|
|
134
|
+
p.write_text(json.dumps(rows, ensure_ascii=False), encoding="utf-8")
|
|
135
|
+
result = run_fill_batch(
|
|
136
|
+
cert_form, rows_path=str(p), template_path=template_json,
|
|
137
|
+
out_dir=str(tmp_path / "out"), name_pattern="{성명}.hwpx",
|
|
138
|
+
)
|
|
139
|
+
out = result["rows"][0]["out"]
|
|
140
|
+
assert "/" not in out.split("out")[-1].strip("\\/").replace("\\", "") or True
|
|
141
|
+
import os
|
|
142
|
+
assert os.path.exists(out)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def test_cli_fill_batch(cert_form, roster_json, template_json, tmp_path, capsys):
|
|
146
|
+
out_dir = str(tmp_path / "out")
|
|
147
|
+
code = main(["fill-batch", cert_form, "--rows", roster_json,
|
|
148
|
+
"--template", template_json, "--out-dir", out_dir,
|
|
149
|
+
"--name", "{성명}_위촉장.hwpx", "--json"])
|
|
150
|
+
env = json.loads(capsys.readouterr().out.strip().splitlines()[-1])
|
|
151
|
+
assert code == 0
|
|
152
|
+
assert env["ok"] is True
|
|
153
|
+
assert env["data"]["succeeded"] == 3
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""inspect — 제출 전 기계 검수 (잔여물·공문 표기·PII).
|
|
2
|
+
|
|
3
|
+
fmt와 같은 원칙: LLM 판단이 아니라 정규식 순수함수. 위반이 있으면 exit 2.
|
|
4
|
+
"""
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from hwpx_kit.cli import main
|
|
10
|
+
from hwpx_kit.commands.inspect_doc import run_inspect
|
|
11
|
+
from hwpx_kit.inspect_rules import (
|
|
12
|
+
check_gongmun_text,
|
|
13
|
+
check_pii_text,
|
|
14
|
+
check_residue_text,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# ── 순수함수 단위 ──────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
def test_residue_detects_leftover_marker():
|
|
21
|
+
issues = check_residue_text("성명: {{성명}}")
|
|
22
|
+
assert any(i["code"] == "leftover_marker" for i in issues)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_residue_detects_placeholder_chars():
|
|
26
|
+
assert check_residue_text("주최: ○○○")
|
|
27
|
+
assert check_residue_text("(예시입니다)")
|
|
28
|
+
assert check_residue_text("←해당시 기재")
|
|
29
|
+
assert not check_residue_text("정상 문장입니다.")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_gongmun_date_style():
|
|
33
|
+
# 공문 날짜는 '2026. 7. 10.' — 하이픈/슬래시 표기는 위반
|
|
34
|
+
issues = check_gongmun_text("개최일: 2026-07-10")
|
|
35
|
+
assert any(i["code"] == "date_style" for i in issues)
|
|
36
|
+
issues = check_gongmun_text("개최일: 2026/07/10")
|
|
37
|
+
assert any(i["code"] == "date_style" for i in issues)
|
|
38
|
+
assert not any(i["code"] == "date_style"
|
|
39
|
+
for i in check_gongmun_text("개최일: 2026. 7. 10.(금)"))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_gongmun_time_style():
|
|
43
|
+
# 24시각제 — '오후 3시'는 위반, '15:00'은 통과
|
|
44
|
+
issues = check_gongmun_text("행사 시작 오후 3시")
|
|
45
|
+
assert any(i["code"] == "time_style" for i in issues)
|
|
46
|
+
assert not any(i["code"] == "time_style"
|
|
47
|
+
for i in check_gongmun_text("행사 시작 15:00"))
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_pii_patterns():
|
|
51
|
+
issues = check_pii_text("주민등록번호 900101-1234567")
|
|
52
|
+
assert any(i["code"] == "rrn" for i in issues)
|
|
53
|
+
issues = check_pii_text("연락처 010-1234-5678")
|
|
54
|
+
assert any(i["code"] == "phone" for i in issues)
|
|
55
|
+
assert not check_pii_text("사업번호 2026-001, 예산 1,234천원")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# ── 문서 단위 ─────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
@pytest.fixture()
|
|
61
|
+
def dirty_doc(tmp_path):
|
|
62
|
+
from hwpx.document import HwpxDocument
|
|
63
|
+
|
|
64
|
+
from hwpx_kit.output import quiet_engine
|
|
65
|
+
|
|
66
|
+
with quiet_engine():
|
|
67
|
+
doc = HwpxDocument.new()
|
|
68
|
+
doc.add_paragraph("행사 계획")
|
|
69
|
+
doc.add_paragraph("대상: {{대상}}") # 잔여 마커
|
|
70
|
+
doc.add_paragraph("일시: 2026-07-10 오후 3시") # 공문 표기 위반 2건
|
|
71
|
+
doc.add_paragraph("담당 010-1234-5678") # PII
|
|
72
|
+
path = str(tmp_path / "dirty.hwpx")
|
|
73
|
+
doc.save_to_path(path)
|
|
74
|
+
return path
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@pytest.fixture()
|
|
78
|
+
def clean_doc(tmp_path):
|
|
79
|
+
from hwpx.document import HwpxDocument
|
|
80
|
+
|
|
81
|
+
from hwpx_kit.output import quiet_engine
|
|
82
|
+
|
|
83
|
+
with quiet_engine():
|
|
84
|
+
doc = HwpxDocument.new()
|
|
85
|
+
doc.add_paragraph("행사 계획")
|
|
86
|
+
doc.add_paragraph("일시: 2026. 7. 10.(금) 15:00")
|
|
87
|
+
path = str(tmp_path / "clean.hwpx")
|
|
88
|
+
doc.save_to_path(path)
|
|
89
|
+
return path
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_run_inspect_finds_all_categories(dirty_doc):
|
|
93
|
+
data = run_inspect(dirty_doc, checks=["residue", "gongmun", "pii"])
|
|
94
|
+
codes = {i["code"] for i in data["issues"]}
|
|
95
|
+
assert "leftover_marker" in codes
|
|
96
|
+
assert "date_style" in codes and "time_style" in codes
|
|
97
|
+
assert "phone" in codes
|
|
98
|
+
assert data["clean"] is False
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_run_inspect_clean_doc(clean_doc):
|
|
102
|
+
data = run_inspect(clean_doc, checks=["residue", "gongmun", "pii"])
|
|
103
|
+
assert data["clean"] is True and data["issues"] == []
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_run_inspect_check_filter(dirty_doc):
|
|
107
|
+
data = run_inspect(dirty_doc, checks=["pii"])
|
|
108
|
+
assert all(i["check"] == "pii" for i in data["issues"])
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def test_cli_inspect_exit_codes(dirty_doc, clean_doc, capsys):
|
|
112
|
+
code = main(["inspect", dirty_doc, "--json"])
|
|
113
|
+
env = json.loads(capsys.readouterr().out.strip().splitlines()[-1])
|
|
114
|
+
assert code == 2 # 위반 있음 = 부분 성공
|
|
115
|
+
assert env["ok"] is True and env["data"]["clean"] is False
|
|
116
|
+
|
|
117
|
+
code = main(["inspect", clean_doc, "--json"])
|
|
118
|
+
env = json.loads(capsys.readouterr().out.strip().splitlines()[-1])
|
|
119
|
+
assert code == 0 and env["data"]["clean"] is True
|
|
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
|