kordoc 4.7.3 → 4.8.1

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 (115) hide show
  1. package/README.md +90 -20
  2. package/dist/{-WYNJOQ6P.js → -BPSOA7WX.js} +11 -9
  3. package/dist/{chunk-P663PYWV.js → chunk-4CUEYYQF.js} +2 -2
  4. package/dist/{chunk-W5NZYPRQ.js → chunk-5EWPDL7S.js} +41 -9
  5. package/dist/chunk-5EWPDL7S.js.map +1 -0
  6. package/dist/{chunk-HJV4BVQI.js → chunk-66BZATFV.js} +2 -2
  7. package/dist/{chunk-JKJEBDP7.js → chunk-6KFW772K.js} +3 -3
  8. package/dist/{chunk-4HZN7EZZ.js → chunk-A6EAUP2J.js} +2 -2
  9. package/dist/{chunk-KSATSTC5.js → chunk-BN33O25L.js} +1 -1
  10. package/dist/{chunk-4KGGVCYX.js.map → chunk-BN33O25L.js.map} +1 -1
  11. package/dist/{chunk-LMHVMGEC.js → chunk-C5IU7Q3W.js} +3 -3
  12. package/dist/{chunk-E6DONLTM.cjs → chunk-D6NDAA72.cjs} +2 -2
  13. package/dist/{chunk-E6DONLTM.cjs.map → chunk-D6NDAA72.cjs.map} +1 -1
  14. package/dist/{chunk-3NKSSL44.js → chunk-DIB2I2HP.js} +3 -3
  15. package/dist/{chunk-QETZP25M.cjs → chunk-DVEJRBFI.cjs} +3 -3
  16. package/dist/{chunk-QETZP25M.cjs.map → chunk-DVEJRBFI.cjs.map} +1 -1
  17. package/dist/{chunk-46F5RMU7.js → chunk-ETMMB6O2.js} +2 -2
  18. package/dist/{chunk-R4LXZRYP.cjs → chunk-FZA5HW4K.cjs} +4 -4
  19. package/dist/{chunk-R4LXZRYP.cjs.map → chunk-FZA5HW4K.cjs.map} +1 -1
  20. package/dist/{chunk-QGATKC4R.cjs → chunk-H25XAYE4.cjs} +6 -6
  21. package/dist/{chunk-QGATKC4R.cjs.map → chunk-H25XAYE4.cjs.map} +1 -1
  22. package/dist/{chunk-RQVFFSAO.js → chunk-I6EWYT3Y.js} +2 -2
  23. package/dist/{chunk-IMFIACHP.js → chunk-I6Q5K2YF.js} +2 -2
  24. package/dist/{chunk-TXQQ7URG.js → chunk-KTAWKNYX.js} +2 -2
  25. package/dist/{chunk-EV53YE3W.cjs → chunk-L5CDTOC3.cjs} +1 -1
  26. package/dist/chunk-L5CDTOC3.cjs.map +1 -0
  27. package/dist/{chunk-KEGAEHWO.js → chunk-MYOGZQMD.js} +2 -2
  28. package/dist/{chunk-NYLYIN55.js → chunk-N7HKWVGP.js} +3 -3
  29. package/dist/{chunk-XOO6YEZE.js → chunk-NJMWYRLY.js} +3 -3
  30. package/dist/{chunk-CGJDCZA3.js → chunk-XGXKICRN.js} +2 -2
  31. package/dist/{chunk-4KGGVCYX.js → chunk-XINP35F4.js} +1 -1
  32. package/dist/{chunk-KSATSTC5.js.map → chunk-XINP35F4.js.map} +1 -1
  33. package/dist/{chunk-KE6DLZAW.cjs → chunk-XZEDL6B4.cjs} +11 -11
  34. package/dist/{chunk-KE6DLZAW.cjs.map → chunk-XZEDL6B4.cjs.map} +1 -1
  35. package/dist/{chunk-VZ3AQVBN.js → chunk-Y4H75LKS.js} +2 -2
  36. package/dist/{chunk-3P5YCJQE.js → chunk-YCSXG6SY.js} +2 -2
  37. package/dist/{chunk-Y7T5Z4DC.js → chunk-YOFMKKCA.js} +3 -3
  38. package/dist/chunks-HYXNV3WP.js +10 -0
  39. package/dist/cli.js +34 -22
  40. package/dist/cli.js.map +1 -1
  41. package/dist/{formula-V6EAYHMF.js → formula-7XQLPNSO.js} +3 -3
  42. package/dist/{formula-PDMZMBIP.cjs → formula-ODJ7GSUH.cjs} +9 -9
  43. package/dist/{formula-PDMZMBIP.cjs.map → formula-ODJ7GSUH.cjs.map} +1 -1
  44. package/dist/{formula-IFKZZPD2.js → formula-T4AIR2LX.js} +4 -4
  45. package/dist/{image-ocr-B7TMIG3F.js → image-ocr-EJLWGR3B.js} +8 -8
  46. package/dist/{image-ocr-XHBZONFZ.js → image-ocr-TZU64JJF.js} +6 -6
  47. package/dist/{image-ocr-75WGMVU6.cjs → image-ocr-WMTQ357I.cjs} +12 -12
  48. package/dist/{image-ocr-75WGMVU6.cjs.map → image-ocr-WMTQ357I.cjs.map} +1 -1
  49. package/dist/index.cjs +396 -364
  50. package/dist/index.cjs.map +1 -1
  51. package/dist/index.d.cts +42 -1
  52. package/dist/index.d.ts +42 -1
  53. package/dist/index.js +37 -5
  54. package/dist/index.js.map +1 -1
  55. package/dist/mcp.js +16 -16
  56. package/dist/{model-bundle-Q4HQXJQG.js → model-bundle-OT2OTSGH.js} +5 -5
  57. package/dist/{models-QQA7OQHY.js → models-MUVFWN7M.js} +5 -5
  58. package/dist/{parser-Z36MZUHS.js → parser-E6NQ666P.js} +7 -7
  59. package/dist/{parser-HT7GZOF2.js → parser-ERSNBILY.js} +7 -7
  60. package/dist/{parser-4CBOPTVY.cjs → parser-XVUFA35I.cjs} +27 -27
  61. package/dist/{parser-4CBOPTVY.cjs.map → parser-XVUFA35I.cjs.map} +1 -1
  62. package/dist/pdf-ocr-KG7X7WRC.js +13 -0
  63. package/dist/pdf-ocr-KMM24YFF.cjs +14 -0
  64. package/dist/{pdf-ocr-6ZNGDVC6.cjs.map → pdf-ocr-KMM24YFF.cjs.map} +1 -1
  65. package/dist/pdf-ocr-YTZL2UL4.js +16 -0
  66. package/dist/{profile-io-ACYRLCSR.js → profile-io-IRW6XCXH.js} +3 -3
  67. package/dist/{rasterize-QYCLEH2J.js → rasterize-BNF6QTLY.js} +2 -2
  68. package/dist/render-T7QXFTZA.js +10 -0
  69. package/dist/seal-ITE2NNNH.js +10 -0
  70. package/dist/{setup-MG3YYKXU.js → setup-BXOXVBU3.js} +3 -3
  71. package/dist/{watch-AZIY6AWC.js → watch-7PYYGQUJ.js} +10 -10
  72. package/package.json +2 -2
  73. package/dist/chunk-EV53YE3W.cjs.map +0 -1
  74. package/dist/chunk-W5NZYPRQ.js.map +0 -1
  75. package/dist/chunks-LWAOHTK2.js +0 -10
  76. package/dist/pdf-ocr-6ZNGDVC6.cjs +0 -14
  77. package/dist/pdf-ocr-NTXRJI4T.js +0 -13
  78. package/dist/pdf-ocr-TYMCEUPH.js +0 -16
  79. package/dist/render-5Y2US4IM.js +0 -10
  80. package/dist/seal-VU6JS2QD.js +0 -10
  81. /package/dist/{-WYNJOQ6P.js.map → -BPSOA7WX.js.map} +0 -0
  82. /package/dist/{chunk-P663PYWV.js.map → chunk-4CUEYYQF.js.map} +0 -0
  83. /package/dist/{chunk-HJV4BVQI.js.map → chunk-66BZATFV.js.map} +0 -0
  84. /package/dist/{chunk-JKJEBDP7.js.map → chunk-6KFW772K.js.map} +0 -0
  85. /package/dist/{chunk-4HZN7EZZ.js.map → chunk-A6EAUP2J.js.map} +0 -0
  86. /package/dist/{chunk-LMHVMGEC.js.map → chunk-C5IU7Q3W.js.map} +0 -0
  87. /package/dist/{chunk-3NKSSL44.js.map → chunk-DIB2I2HP.js.map} +0 -0
  88. /package/dist/{chunk-46F5RMU7.js.map → chunk-ETMMB6O2.js.map} +0 -0
  89. /package/dist/{chunk-RQVFFSAO.js.map → chunk-I6EWYT3Y.js.map} +0 -0
  90. /package/dist/{chunk-IMFIACHP.js.map → chunk-I6Q5K2YF.js.map} +0 -0
  91. /package/dist/{chunk-TXQQ7URG.js.map → chunk-KTAWKNYX.js.map} +0 -0
  92. /package/dist/{chunk-KEGAEHWO.js.map → chunk-MYOGZQMD.js.map} +0 -0
  93. /package/dist/{chunk-NYLYIN55.js.map → chunk-N7HKWVGP.js.map} +0 -0
  94. /package/dist/{chunk-XOO6YEZE.js.map → chunk-NJMWYRLY.js.map} +0 -0
  95. /package/dist/{chunk-CGJDCZA3.js.map → chunk-XGXKICRN.js.map} +0 -0
  96. /package/dist/{chunk-VZ3AQVBN.js.map → chunk-Y4H75LKS.js.map} +0 -0
  97. /package/dist/{chunk-3P5YCJQE.js.map → chunk-YCSXG6SY.js.map} +0 -0
  98. /package/dist/{chunk-Y7T5Z4DC.js.map → chunk-YOFMKKCA.js.map} +0 -0
  99. /package/dist/{chunks-LWAOHTK2.js.map → chunks-HYXNV3WP.js.map} +0 -0
  100. /package/dist/{formula-V6EAYHMF.js.map → formula-7XQLPNSO.js.map} +0 -0
  101. /package/dist/{formula-IFKZZPD2.js.map → formula-T4AIR2LX.js.map} +0 -0
  102. /package/dist/{image-ocr-B7TMIG3F.js.map → image-ocr-EJLWGR3B.js.map} +0 -0
  103. /package/dist/{image-ocr-XHBZONFZ.js.map → image-ocr-TZU64JJF.js.map} +0 -0
  104. /package/dist/{model-bundle-Q4HQXJQG.js.map → model-bundle-OT2OTSGH.js.map} +0 -0
  105. /package/dist/{models-QQA7OQHY.js.map → models-MUVFWN7M.js.map} +0 -0
  106. /package/dist/{parser-Z36MZUHS.js.map → parser-E6NQ666P.js.map} +0 -0
  107. /package/dist/{parser-HT7GZOF2.js.map → parser-ERSNBILY.js.map} +0 -0
  108. /package/dist/{pdf-ocr-NTXRJI4T.js.map → pdf-ocr-KG7X7WRC.js.map} +0 -0
  109. /package/dist/{pdf-ocr-TYMCEUPH.js.map → pdf-ocr-YTZL2UL4.js.map} +0 -0
  110. /package/dist/{profile-io-ACYRLCSR.js.map → profile-io-IRW6XCXH.js.map} +0 -0
  111. /package/dist/{rasterize-QYCLEH2J.js.map → rasterize-BNF6QTLY.js.map} +0 -0
  112. /package/dist/{render-5Y2US4IM.js.map → render-T7QXFTZA.js.map} +0 -0
  113. /package/dist/{seal-VU6JS2QD.js.map → seal-ITE2NNNH.js.map} +0 -0
  114. /package/dist/{setup-MG3YYKXU.js.map → setup-BXOXVBU3.js.map} +0 -0
  115. /package/dist/{watch-AZIY6AWC.js.map → watch-7PYYGQUJ.js.map} +0 -0
package/README.md CHANGED
@@ -82,10 +82,31 @@ MCP 등록 대신 스킬(SKILL.md) 형태로 쓰려면:
82
82
  * **📊 차트 생성 (v3.16)**: 마크다운의 ```chart 펜스(type/cat/계열 라인)가 한컴 네이티브 차트(OOXML chartSpace)로 생성됩니다 — 막대·선·원·도넛·영역·분산·방사형 등 20종, 계열/조각 색 지정 가능.
83
83
  * **🔴 도장/서명 자동 날인 (v3.16)**: "(인)"·"서명 또는 인" 같은 앵커 문구를 찾아 도장 PNG를 글 앞 부유로 배치합니다. 표/페이지를 키우지 않아 날인 후 서식이 밀리지 않습니다 (`kordoc seal`).
84
84
  * **✏️ 양식 자동 채우기**: 공문서 양식 템플릿(신청서, 보고서)에 값을 넣으면 자동으로 빈칸을 채웁니다. 원본 서식(글꼴, 크기, 정렬)을 100% 보존합니다.
85
+ * **👓 내장 OCR — API 키 없이 (v4.2)**: 스캔본·이미지 PDF와 PNG/JPG/WebP 이미지를 **로컬 CPU 추론**(PP-OCRv5 korean)으로 읽습니다. 외부 서비스도 API 키도 없이, 텍스트층이 깨진 페이지만 골라 OCR 하고, 래스터 괘선까지 찾아 스캔본에서도 표를 복원합니다.
86
+ * **📑 RAG·인용 대응 (v4.1~4.8)**: 헤딩·개조식 위계를 breadcrumb으로 보존한 구조 청크(`--format chunks`)와, 조판 캐시로 복원한 **실제 쪽 번호** 기준 페이지별 마크다운(`pages`)을 냅니다 — 답변에 "몇 쪽" 각주를 붙일 수 있습니다.
87
+ * **🕶️ 개인정보 마스킹 (v4.1)**: 주민번호·전화·이메일·카드·계좌를 탐지해 **원본 서식 그대로** 가린 문서를 냅니다 (`kordoc redact`). 자동 검출 보조 도구이므로 공개 전 사람 확인은 필수입니다.
85
88
  * **🤖 AI 에이전트 연동 (MCP)**: `Claude Desktop`, `Cursor`, `Codex`와 같은 도구에서 직접 `kordoc`을 호출해 문서를 읽고 코딩할 수 있습니다.
86
89
 
87
90
  ---
88
91
 
92
+ ## v4.8.0 변경사항
93
+
94
+ - **📑 `--format json` 에 페이지별 마크다운 (#68)**: JSON 결과에 `pages: [{ pageNumber, markdown }]` 이 기본으로 들어갑니다. v4.7.3 의 실제 페이지 경계(`pageMode: "layout"`)를 그대로 써서, RAG 인용 각주나 뷰어 페이지 동기화에 쪽 단위 본문을 바로 쓸 수 있습니다. 문서를 다시 파싱하지 않고 이미 만든 블록의 `pageNumber` 로 가르기 때문에 비용은 파싱 시간의 1% 미만입니다(24쪽 PDF 기준 parse 399ms / pages 2.5ms). `--pages 3-5` 를 쓰면 `pages` 에도 3~5쪽만 담기고, 페이지 번호를 매기지 않는 포맷(DOCX 등)에서는 `pages` 자체가 없습니다. 라이브러리에서는 `blocksToPages(blocks)` 로도 같은 값을 얻습니다. (@sorbetsharkroundhand 제안)
95
+
96
+ ```jsonc
97
+ {
98
+ "markdown": "# 전체 문서…",
99
+ "pages": [
100
+ { "pageNumber": 1, "markdown": "# 1페이지…" },
101
+ { "pageNumber": 2, "markdown": "2페이지…" }
102
+ ],
103
+ "blocks": [/* … */],
104
+ "metadata": { "pageCount": 2, "pageMode": "layout" }
105
+ }
106
+ ```
107
+
108
+ 여러 페이지에 걸친 표는 현 IR 구조상 시작 페이지의 한 블록이라, 표가 이어지는 중간 페이지의 `markdown` 은 빈 문자열입니다(항목 자체는 남깁니다 — 배열 길이 = 쪽수).
109
+
89
110
  ## v4.7.3 변경사항
90
111
 
91
112
  - **📄 HWP/HWPX 실제 페이지 경계 복원 (#66)**: 한컴 저장본의 조판 캐시(HWPX `linesegarray` / HWP5 `PARA_LINE_SEG`)로 실제 페이지를 복원합니다 — `pageCount`·블록별 `pageNumber` 가 섹션 근사가 아닌 **실제 쪽 번호**가 되고, `parse(buffer, { pages: "3-5" })` 가 실제 3~5쪽을 반환합니다. 신호 4종(vertpos 역행·명시 쪽나눔·분할 표의 셀 흐름 리셋·분할 직후 이중 카운트 억제)을 결합해 실코퍼스 hwp↔hwpx↔pdf 쌍 대조에서 페이지 수 전수 일치. 조판 캐시가 없는 생성 파일은 종전대로 섹션 근사로 동작하며, 새 메타데이터 **`pageMode: "layout" | "section"`** 으로 구분됩니다(근사 상태에서 `pages` 필터 사용 시 `PAGE_BOUNDARY_APPROXIMATE` 경고). RAG 페이지 인용·뷰어 동기화용. (@sorbetsharkroundhand 제안)
@@ -338,7 +359,6 @@ MCP 등록 대신 스킬(SKILL.md) 형태로 쓰려면:
338
359
  - **📐 실측 텍스트 메트릭 엔진**: 함초롬바탕 정품 TTF에서 글자 폭을 전수 추출해 한글 프로그램 없이 줄폭·줄바꿈을 계산합니다. 실제 결재문서의 조판 결과와 대조해 **줄바꿈점 98% 일치** 검증.
339
360
  - **🪗 자동 장평(`autoFit`)**: 한두 글자가 다음 줄로 넘어가는(orphan) 문단만 골라 장평을 95→90%로 줄여 한 줄에 담습니다. 공문서 작성 관행 그대로.
340
361
  - **📊 HTML 표 생성**: 병합(colspan/rowspan)·중첩 표가 든 마크다운도 `markdownToHwpx`로 구조 그대로 HWPX 표가 됩니다 — parse↔generate 표 라운드트립 완성.
341
- - **🎨 서식 프로필**: 표의 위상뿐 아니라 **테두리·음영·열 실측폭·셀 글꼴까지** 원본 없이 재현합니다. `hwpxToProfile(hwpx)`로 서식만 JSON으로 추출하고, `markdownToHwpx(md, { profile })`로 다른 문서에 그 서식을 입힙니다 — 원본 유출 없이 기관 서식만 공유·재현(이슈 #41, 스키마: [`docs/format-profile-spec.md`](docs/format-profile-spec.md)).
342
362
  - **🗂️ 다중값 채우기**: `fillForm` 값에 배열(`string[]`)을 주면 같은 라벨의 등장 순서대로 하나씩 소진 — 반복 양식·명부형 표(헤더+여러 행) 채우기.
343
363
  - **🛡️ 무결성 픽스**: 채우기/패치 후 한컴이 "문서가 변조되었습니다" 경고를 띄우던 문제(줄 레이아웃 캐시 잔존), 생성 표 테두리가 보이지 않던 문제(borderFill id 규약) 수정.
344
364
 
@@ -593,9 +613,12 @@ MCP 등록 대신 스킬(SKILL.md) 형태로 쓰려면:
593
613
  npm install kordoc
594
614
  ```
595
615
 
596
- PDF 파싱(pdfjs-dist)·수식 OCR 등 선택 의존성은 **기본 설치**됩니다 (optionalDependencies).
616
+ PDF 파싱(pdfjs-dist)·OCR(onnxruntime·sharp·pdfium) 등 선택 의존성은 **기본 설치**됩니다 (optionalDependencies).
597
617
  설치 용량을 줄이려면 `npm install kordoc --omit=optional` 로 스킵할 수 있습니다 —
598
- 이 경우 PDF 파싱·수식 OCR·인쇄 렌더 등 일부 기능이 제한됩니다.
618
+ 이 경우 PDF 파싱·OCR·PNG 래스터 등 일부 기능이 제한됩니다.
619
+
620
+ `markdownToPdf`/`blocksToPdf`(인쇄 렌더)만 `puppeteer-core` 를 **선택적 peer 의존성**으로 쓰며
621
+ 자동 설치되지 않습니다 — 쓰려면 `npm install puppeteer-core` 를 따로 하세요.
599
622
 
600
623
  ## 빠른 시작
601
624
 
@@ -611,10 +634,16 @@ const result = await parse(buffer)
611
634
  if (result.success) {
612
635
  console.log(result.markdown) // 마크다운 텍스트
613
636
  console.log(result.blocks) // IRBlock[] 구조화 데이터
614
- console.log(result.metadata) // { title, author, createdAt, ... }
637
+ console.log(result.metadata) // { title, author, createdAt, pageMode, ... }
638
+ console.log(result.pages) // [{ pageNumber, markdown }] — 쪽 단위 본문 (v4.8)
615
639
  }
616
640
  ```
617
641
 
642
+ `pages` 는 블록에 쪽 번호가 붙는 포맷(HWP·HWPX·PDF, XLS(X)는 시트=한 쪽)에서만 나옵니다 —
643
+ DOCX 처럼 쪽을 매기지 않는 포맷에서는 없는 사실을 만들지 않고 필드 자체를 생략합니다.
644
+ 경계의 신뢰도는 `metadata.pageMode` (`"layout"` = 조판 캐시 기반 실제 쪽 /
645
+ `"section"` = 섹션 근사)로 구분합니다.
646
+
618
647
  ### 문서 비교 (신구대조표)
619
648
 
620
649
  ```typescript
@@ -800,23 +829,44 @@ for (const p of r.pageQuality ?? []) {
800
829
  npx kordoc 사업계획서.hwpx # 터미널 출력
801
830
  npx kordoc 보고서.hwp -o 보고서.md # 파일 저장
802
831
  npx kordoc *.pdf -d ./변환결과/ # 일괄 변환
803
- npx kordoc 검토서.hwpx --format json # JSON (blocks + metadata 포함)
832
+ npx kordoc 검토서.hwpx --format json # JSON (blocks + pages + metadata 포함)
833
+ npx kordoc 검토서.pdf --format chunks # RAG용 구조 청크 JSON (breadcrumb + 표 독립 청크)
804
834
  npx kordoc 보고서.hwpx --pages 1-3 # 페이지 범위
835
+ npx kordoc 스캔본.pdf --ocr # 내장 OCR (필요 페이지 자동 판정, --ocr-force로 전 페이지)
836
+ npx kordoc 잠긴문서.hwpx --password '암호' # 열기 암호가 걸린 HWPX/HWP3/HWP5
837
+ npx kordoc 시험지.pdf --no-tables # PDF 표 감지 끄기 (2단 시험지 등 읽기순 우선)
805
838
  npx kordoc fill 신청서.hwpx -f '성명=홍길동,주소=서울' -o 결과.hwpx # 양식 채우기
806
839
  npx kordoc fill 신청서.hwpx -j values.json -o 결과.hwpx # JSON 파일로 채우기
807
840
  npx kordoc fill 신청서.hwpx --dry-run # 필드 목록만 확인 (누름틀 포함)
808
841
  npx kordoc fill --template gian -j 값.json -o 기안문.hwpx # 내장 표준 기안문 서식 채우기
809
842
  npx kordoc fill --list-templates # 내장 서식 목록 + 필드
843
+ npx kordoc fill 신청서.hwpx -j 값.json --formats '{"날짜":"yy.mm.dd"}' # 필드별 값 서식 (라벨→포맷)
844
+ npx kordoc fill 신청서.hwpx -j 값.json --require-unique # 한 키가 2곳 이상 매칭되면 거부 (반복 라벨 오염 방지)
845
+ npx kordoc fill 신청서.hwpx -j 값.json --mask # 채운 값 미노출 (stdout 에 본문 대신 안내만)
810
846
  npx kordoc generate 보고서.md -o 보고서.hwpx --preset 보고서 # 마크다운 → 공문서 HWPX
811
847
  npx kordoc patch 원본.hwpx 편집.md -o 반영.hwpx # 서식 보존 라운드트립 패치 (.hwp도 자동 분기)
812
848
  npx kordoc seal 신청서.hwpx --image 도장.png --anchor "(인)" -o 날인.hwpx # 도장/서명 날인
813
849
  npx kordoc validate 산출물.hwpx # HWPX 구조 검증 (ZIP·필수 파트·XML)
814
- npx kordoc lint 보고서.hwpx # 공문서 표기법 검수 13룰 (v4.0.1)
850
+ npx kordoc lint 보고서.md # 공문서 표기법 검수 13룰 — 입력은 md/txt('-'=stdin), error 있으면 exit 1
851
+ npx kordoc redact 민원서류.hwpx -o 마스킹.hwpx # 개인정보 탐지 + 서식 보존 마스킹 (v4.1)
852
+ npx kordoc redact 민원서류.hwpx --mask-char '*' -o 마스킹.hwpx # 마스크 문자 지정 (기본 ●)
853
+ npx kordoc profile 기관서식.hwpx # 표 서식 프로필(JSON) 추출 → generate --profile 로 재현
815
854
  npx kordoc render 결재문서.hwpx -o 미리보기.svg # 레이아웃 보존 SVG 렌더 (캐시 없는 문서는 자동 reflow 조판, --no-reflow로 끔)
855
+ npx kordoc render 결재문서.hwpx --reflow-mode charAll -o 미리보기.svg # reflow 줄바꿈: keep(어절, 기본) | charAll(글자)
856
+ npx kordoc models --status # OCR 모델 상태 (--export/--import 로 폐쇄망 사이드로드)
857
+ npx kordoc check-ocr-models --status-only # OCR 모델 상태만 JSON 출력 (옵션 빼면 없는 모델을 받는다)
858
+ npx kordoc check-formula-models --status-only # 수식 OCR 모델(MFD+MFR+tokenizer, ~155MB) 상태만 출력
816
859
  npx kordoc watch ./수신함 -d ./변환결과 # 폴더 감시 모드
817
860
  npx kordoc watch ./문서 --webhook https://api/hook # 웹훅 알림
818
861
  ```
819
862
 
863
+ > `check-ocr-models` · `check-formula-models` 는 이름과 달리 **없거나 SHA 가 안 맞으면 내려받습니다**.
864
+ > 상태만 보려면 `--status-only` 를 붙이세요 (수식 모델은 ~155MB).
865
+ >
866
+ > `kordoc lint` 는 **텍스트(마크다운/txt)를 검사합니다** — HWPX 를 바로 넘기면 안 됩니다.
867
+ > 생성물을 검수하려면 원고 마크다운을 넘기거나, `kordoc 문서.hwpx | kordoc lint -` 로 파이프하세요.
868
+ > (공문서 생성 시에는 `generate` 가 같은 13룰 경고를 함께 냅니다.)
869
+
820
870
  ## MCP 서버 (Claude / Cursor / Windsurf / Codex)
821
871
 
822
872
  **자동 설치 (추천)**:
@@ -896,7 +946,8 @@ codex mcp add kordoc -- npx -y kordoc mcp
896
946
  | `parseXls(buffer, options?)` | XLS (Excel 97~2003, BIFF8) 전용 |
897
947
  | `parseDocx(buffer, options?)` | DOCX 전용 |
898
948
  | `parseHwpml(buffer, options?)` | HWPML (XML 기반 HWP) 전용 |
899
- | `detectFormat(buffer)` | `"hwpx" \| "hwp" \| "hwp3" \| "hwpml" \| "pdf" \| "xlsx" \| "xls" \| "docx" \| "unknown"` |
949
+ | `parseImage(buffer, options?)` | 이미지(PNG/JPG/WebP) 전용 내장 OCR 상시 적용 (v4.2.1) |
950
+ | `detectFormat(buffer)` | `"hwpx" \| "hwp" \| "hwp3" \| "hwpml" \| "pdf" \| "xlsx" \| "xls" \| "docx" \| "image" \| "unknown"` |
900
951
 
901
952
  ### 고급 함수
902
953
 
@@ -908,18 +959,25 @@ codex mcp add kordoc -- npx -y kordoc mcp
908
959
  | `fillForm(input, values, outputFormat?)` | 양식 템플릿에 값 채우기 — outputFormat: `"markdown"`(기본)/`"hwpx"`/`"hwpx-preserve"`, 반환 `{ output, format, fill }` |
909
960
  | `fillFormFields(blocks, values)` | IRBlock[] 기반 필드 값 교체 |
910
961
  | `fillHwpx(buffer, values)` | HWPX XML 직접 조작 (원본 서식 보존) |
962
+ | `extractClickHereFields(buffer)` | HWPX 누름틀(CLICK_HERE) 필드 조사 — 이름·안내문 목록 (v4.3) |
963
+ | `resolveBuiltinTemplate(name)` / `readBuiltinTemplate(t)` | 내장 표준 기안문 서식 조회·로드 (`gian`/`gian-simple`, v4.3) |
911
964
  | `patchHwpx(original, editedMarkdown, options?)` | 편집 마크다운 → 원본 HWPX 서식 보존 in-place 패치 (v3.0) |
912
965
  | `patchHwp(original, editedMarkdown, options?)` | 편집 마크다운 → 원본 HWP 5.x 바이너리 서식 보존 패치 (v3.0.1) |
913
966
  | `openHwpxDocument(bytes, options?)` | 에디터용 블록 단위 증분 패치 세션 `HwpxSession` (v3.1) |
914
967
  | `patchHwpxBlocks(bytes, edits, options?)` | 세션 없이 블록 편집 1회 패치 (v3.1) |
915
- | `markdownToHwpx(markdown, options?)` | Markdown → HWPX 역변환 (테마 옵션 지원) |
916
- | `markdownToPdf(markdown, options?)` | MarkdownPDF 생성 (Print Renderer) |
917
- | `blocksToPdf(blocks, options?)` | IRBlock[] → PDF 생성 |
918
- | `renderHtml(blocks, options?)` | IRBlock[] → 인쇄용 HTML |
968
+ | `markdownToHwpx(markdown, options?)` | Markdown → HWPX 역변환 (테마·서식 프로필·페이지 옵션 지원) |
969
+ | `hwpxToProfile(buffer)` | 참조 HWPX 서식 프로필 JSON — `markdownToHwpx(md, { profile })` 로 재현 (v3.18) |
970
+ | `markdownToPdf(markdown, options?)` | Markdown → PDF 생성 (Print Renderer — `puppeteer-core` 별도 설치 필요) |
971
+ | `blocksToPdf(blocks, options?)` | IRBlock[] → PDF 생성 (동일하게 `puppeteer-core` 필요) |
972
+ | `renderHtml(blocks, options?)` | IRBlock[] → 인쇄용 HTML (puppeteer 불필요) |
919
973
  | `renderHwpxToSvg(buffer, options?)` | HWPX → 레이아웃 보존 SVG — 다페이지·형광펜·도형, 캐시 없으면 `reflow` (v3.10~15) |
920
974
  | `placeSealHwpx(buffer, seals)` | 도장/서명 이미지를 앵커 문구 위에 부유 배치 (v3.16) |
921
975
  | `validateHwpx(buffer)` | HWPX 구조 검증 — ZIP·mimetype·필수 파트·XML 웰폼드 (v3.16) |
976
+ | `lintGongmunText(text)` | 공문서 표기법 검수 13룰 — 텍스트/마크다운 입력 (v4.0.1) |
977
+ | `redactMarkdown(text, options?)` / `redactText(...)` | 개인정보 탐지 + 마스킹 (기본 룰: 주민번호·전화·이메일·카드·계좌, v4.1) |
978
+ | `blocksToChunks(blocks, options?)` | RAG용 구조 청크 — 헤딩·개조식 위계 breadcrumb + 표 독립 청크 (v4.1) |
922
979
  | `blocksToMarkdown(blocks)` | IRBlock[] → Markdown 문자열 |
980
+ | `blocksToPages(blocks)` | IRBlock[] → `[{ pageNumber, markdown }]` 페이지별 마크다운 (v4.8) |
923
981
 
924
982
  ### 타입
925
983
 
@@ -929,11 +987,17 @@ import type {
929
987
  IRBlock, IRBlockType, IRTable, IRCell, CellContext,
930
988
  DocumentMetadata, ParseOptions, ErrorCode, OutlineItem,
931
989
  DiffResult, BlockDiff, CellDiff, DiffChangeType,
932
- FormField, FormResult, FillResult, HwpxFillResult, FillOutputFormat, FillFormOutput,
990
+ FormField, FormResult, FormFieldType, FormFieldSchema, FormSchemaResult,
991
+ FillResult, HwpxFillResult, FillOutputFormat, FillFormOutput,
992
+ ClickHereField, BuiltinTemplate,
933
993
  PatchOptions, PatchResult, PatchSkip,
934
- HwpxTheme, MarkdownToHwpxOptions,
994
+ HwpxTheme, MarkdownToHwpxOptions, PageOptions,
935
995
  PrintPreset, PrintOptions, PageMargin,
936
996
  RenderSvgOptions, RenderSvgResult,
997
+ SealOp, SealPlacement, PlaceSealResult,
998
+ ValidateResult, ValidateIssue,
999
+ RedactRule, RedactOptions, RedactHit, RedactTextResult,
1000
+ DocChunk, ChunkOptions, GongmunLintFinding,
937
1001
  OcrProvider, WatchOptions,
938
1002
  } from "kordoc"
939
1003
  ```
@@ -942,19 +1006,22 @@ import type {
942
1006
 
943
1007
  | 포맷 | 엔진 | 특징 |
944
1008
  |------|------|------|
945
- | **HWPX** (한컴 2020+) | ZIP + XML DOM | 매니페스트, 중첩 테이블, 병합 셀, 손상 ZIP 복구 |
946
- | **HWP 5.x** (한컴 레거시) | OLE2 + CFB | 배포용 복호화, 손상 CFB 복구, 각주/하이퍼링크, 21종 제어문자, 이미지 추출 |
947
- | **HWP 3.x** (1996~2002) | 단일 binary | 상용조합형→유니코드, 5,893자 한자/기호 lookup, nested paragraph 추출 |
1009
+ | **HWPX** (한컴 2020+) | ZIP + XML DOM | 매니페스트, 중첩 테이블, 병합 셀, 손상 ZIP 복구, 조판 캐시 기반 실제 페이지 경계, 열기 암호 |
1010
+ | **HWP 5.x** (한컴 레거시) | OLE2 + CFB | 배포용 복호화, 열기 암호, 손상 CFB 복구, 각주/하이퍼링크, 21종 제어문자, 이미지 추출, 실제 페이지 경계 |
1011
+ | **HWP 3.x** (1996~2002) | 단일 binary | 상용조합형→유니코드, 5,893자 한자/기호 lookup, nested paragraph 추출, 아래아(옛한글), 열기 암호 |
948
1012
  | **HWPML 2.x** (XML 기반 HWP) | XML DOM | HeadingType 기반 헤딩 감지, 병합 셀, DoS 방어 |
949
- | **PDF** | pdfjs-dist | 선 기반 테이블, XY-Cut 읽기 순서, 헤딩 감지, OCR, 텍스트 품질 신호 |
950
- | **XLSX** (Excel) | ZIP + XML DOM | 공유 문자열, 병합 셀, 다중 시트, 수식 표시 |
1013
+ | **PDF** | pdfjs-dist | 선 기반 테이블, XY-Cut 읽기 순서, 2단 지면, 헤딩 감지, OCR, 밑줄·링크, 이미지 추출, 텍스트 품질 신호 |
1014
+ | **XLSX** (Excel) | ZIP + XML DOM | 공유 문자열, 병합 셀, 다중 시트, 수식 표시, 날짜 셀 ISO 변환 |
951
1015
  | **XLS** (Excel 97~2003) | OLE2 + BIFF8 | Workbook 스트림, SST 공유 문자열, 셀/시트 추출 |
952
- | **DOCX** (Word) | ZIP + XML DOM | 스타일 heading, 번호 매기기, 각주, 이미지 추출 |
1016
+ | **DOCX** (Word) | ZIP + XML DOM | 스타일 heading, 번호 매기기, 각주, 하이퍼링크, 이미지 추출 |
1017
+ | **이미지** (PNG/JPG/WebP) | sharp + 내장 OCR | 스크린샷·스캔 이미지 직접 입력, 래스터 괘선 감지로 표 복원 |
953
1018
 
954
1019
  ## 보안
955
1020
 
956
1021
  프로덕션급 보안 강화: ZIP bomb 방지, XXE/Billion Laughs 방지, 압축 폭탄 방지, 경로 순회 차단, MCP 에러 정제, 파일 크기 제한(500MB). 자세한 내용은 [SECURITY.md](./SECURITY.md) 참조.
957
1022
 
1023
+ **폐쇄망(내부망) 배포** — `KORDOC_OFFLINE=1` 로 모든 아웃바운드 통신(OCR 모델 다운로드·watch webhook)을 차단하고, `KORDOC_ROOT=<디렉토리>` 로 MCP 서버의 파일 접근을 해당 디렉토리 하위로 제한합니다(둘 다 opt-in). 절차·근거는 [docs/offline-deployment.md](docs/offline-deployment.md) 참조. (v4.7.2)
1024
+
958
1025
  ## 만든 사람
959
1026
 
960
1027
  대한민국 지방공무원. 광진구청에서 7년간 HWP 파일과 싸우다가 이걸 만들었습니다.
@@ -971,7 +1038,10 @@ import type {
971
1038
  secure-fill 포맷엔진, validate 검사셋
972
1039
  - **OpenDataLoader PDF** (Apache 2.0, Hancom Inc.) — PDF 테이블 감지 알고리즘
973
1040
  - **hml-equation-parser** (Apache 2.0, Open Bapul) — HML 수식 파싱
974
- - **PaddleOCR** (Apache 2.0, PaddlePaddle) — OCR 엔진 파생
1041
+ - **PaddleOCR** (Apache 2.0, PaddlePaddle) — 텍스트 OCR 엔진 파생 (PP-OCRv5 korean)
1042
+ - **Pix2Text** (MIT, breezedeus) — 수식 OCR(MFD/MFR) 알고리즘 포팅. 모델은 런타임
1043
+ 다운로드이며 재배포하지 않습니다 — MFD 가중치의 기반인 Ultralytics YOLOv8 은
1044
+ AGPL-3.0 이므로, 수식 OCR 에 의존하는 상용·비공개 제품은 해당 조건을 별도 확인하세요
975
1045
  - **cfb** (Apache 2.0, SheetJS) — HWP5 OLE2 컨테이너 파싱
976
1046
  - **pdfjs-dist** (Apache 2.0, Mozilla) — PDF 텍스트 추출
977
1047
  - **JSZip** (MIT, Stuart Knightley 외) — ZIP 기반 포맷 파싱
@@ -4,6 +4,7 @@ import {
4
4
  HwpxSession,
5
5
  PRESET_ALIAS,
6
6
  ValueCursor,
7
+ blocksToPages,
7
8
  blocksToPdf,
8
9
  compare,
9
10
  detectFormat,
@@ -52,9 +53,9 @@ import {
52
53
  resolveBuiltinTemplate,
53
54
  unknownFontWarnings,
54
55
  validateHwpx
55
- } from "./chunk-W5NZYPRQ.js";
56
+ } from "./chunk-5EWPDL7S.js";
56
57
  import "./chunk-7FCNOKOR.js";
57
- import "./chunk-4KGGVCYX.js";
58
+ import "./chunk-XINP35F4.js";
58
59
  import "./chunk-MOL7MDBG.js";
59
60
  import {
60
61
  applySplices,
@@ -62,7 +63,7 @@ import {
62
63
  buildRangeSplices,
63
64
  placeSealHwpx,
64
65
  scanSectionXml
65
- } from "./chunk-NYLYIN55.js";
66
+ } from "./chunk-N7HKWVGP.js";
66
67
  import {
67
68
  DEFAULT_REDACT_RULES,
68
69
  redactMarkdown,
@@ -70,10 +71,10 @@ import {
70
71
  } from "./chunk-DZIXKL7E.js";
71
72
  import {
72
73
  blocksToChunks
73
- } from "./chunk-CGJDCZA3.js";
74
+ } from "./chunk-XGXKICRN.js";
74
75
  import {
75
76
  blocksToMarkdown
76
- } from "./chunk-KEGAEHWO.js";
77
+ } from "./chunk-MYOGZQMD.js";
77
78
  import {
78
79
  SPACE_EM_FIXED,
79
80
  SPACE_EM_FONT,
@@ -83,11 +84,11 @@ import {
83
84
  renderHwpxToSvg,
84
85
  simulateWrap,
85
86
  simulateWrapKeepWord
86
- } from "./chunk-XOO6YEZE.js";
87
- import "./chunk-VZ3AQVBN.js";
87
+ } from "./chunk-NJMWYRLY.js";
88
+ import "./chunk-Y4H75LKS.js";
88
89
  import {
89
90
  VERSION
90
- } from "./chunk-IMFIACHP.js";
91
+ } from "./chunk-I6Q5K2YF.js";
91
92
  export {
92
93
  BUILTIN_TEMPLATES,
93
94
  DEFAULT_REDACT_RULES,
@@ -100,6 +101,7 @@ export {
100
101
  applySplices,
101
102
  blocksToChunks,
102
103
  blocksToMarkdown,
104
+ blocksToPages,
103
105
  blocksToPdf,
104
106
  buildParagraphSplices,
105
107
  buildRangeSplices,
@@ -161,4 +163,4 @@ export {
161
163
  unknownFontWarnings,
162
164
  validateHwpx
163
165
  };
164
- //# sourceMappingURL=-WYNJOQ6P.js.map
166
+ //# sourceMappingURL=-BPSOA7WX.js.map
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  sanitizeHref
3
- } from "./chunk-46F5RMU7.js";
3
+ } from "./chunk-ETMMB6O2.js";
4
4
 
5
5
  // src/shared/pua.ts
6
6
  var BMP_SYMBOL_MAP = {
@@ -778,4 +778,4 @@ export {
778
778
  encodePng,
779
779
  inlineImagesIntoMarkdown
780
780
  };
781
- //# sourceMappingURL=chunk-P663PYWV.js.map
781
+ //# sourceMappingURL=chunk-4CUEYYQF.js.map
@@ -6,7 +6,7 @@ import {
6
6
  HEADING_RATIO_H1,
7
7
  HEADING_RATIO_H2,
8
8
  HEADING_RATIO_H3
9
- } from "./chunk-4KGGVCYX.js";
9
+ } from "./chunk-XINP35F4.js";
10
10
  import {
11
11
  parsePageRange
12
12
  } from "./chunk-MOL7MDBG.js";
@@ -22,7 +22,7 @@ import {
22
22
  paraTextPureT,
23
23
  patchZipEntries,
24
24
  scanSectionXml
25
- } from "./chunk-NYLYIN55.js";
25
+ } from "./chunk-N7HKWVGP.js";
26
26
  import {
27
27
  MAX_COLS,
28
28
  MAX_ROWS,
@@ -32,14 +32,14 @@ import {
32
32
  dedupeRunningHeaders,
33
33
  flattenLayoutTables,
34
34
  mapPuaText
35
- } from "./chunk-KEGAEHWO.js";
35
+ } from "./chunk-MYOGZQMD.js";
36
36
  import {
37
37
  SPACE_EM_FIXED,
38
38
  charWidthEm1000,
39
39
  fitRatioForFewerLines,
40
40
  measureTextWidth,
41
41
  simulateWrap
42
- } from "./chunk-XOO6YEZE.js";
42
+ } from "./chunk-NJMWYRLY.js";
43
43
  import {
44
44
  MAX_DECOMPRESS_SIZE,
45
45
  MAX_XML_DEPTH,
@@ -55,7 +55,7 @@ import {
55
55
  findChildByLocalName,
56
56
  localName,
57
57
  rawTextContent
58
- } from "./chunk-VZ3AQVBN.js";
58
+ } from "./chunk-Y4H75LKS.js";
59
59
  import {
60
60
  KordocError,
61
61
  classifyError,
@@ -67,7 +67,7 @@ import {
67
67
  sanitizeHref,
68
68
  stripDtd,
69
69
  toArrayBuffer
70
- } from "./chunk-IMFIACHP.js";
70
+ } from "./chunk-I6Q5K2YF.js";
71
71
 
72
72
  // src/index.ts
73
73
  import { readFile } from "fs/promises";
@@ -21434,6 +21434,29 @@ async function fillHwpx(hwpxBuffer, values, blockedLabels) {
21434
21434
  };
21435
21435
  }
21436
21436
 
21437
+ // src/page-markdown.ts
21438
+ function blocksToPages(blocks) {
21439
+ const firstNumbered = blocks.find((b) => typeof b.pageNumber === "number")?.pageNumber;
21440
+ if (firstNumbered === void 0) return void 0;
21441
+ const byPage = /* @__PURE__ */ new Map();
21442
+ let current = firstNumbered;
21443
+ for (const block of blocks) {
21444
+ if (typeof block.pageNumber === "number") current = block.pageNumber;
21445
+ const bucket = byPage.get(current);
21446
+ if (bucket) bucket.push(block);
21447
+ else byPage.set(current, [block]);
21448
+ }
21449
+ const numbers = [...byPage.keys()];
21450
+ const min = Math.min(...numbers);
21451
+ const max = Math.max(...numbers);
21452
+ const pages = [];
21453
+ for (let pageNumber = min; pageNumber <= max; pageNumber++) {
21454
+ const pageBlocks = byPage.get(pageNumber);
21455
+ pages.push({ pageNumber, markdown: pageBlocks ? blocksToMarkdown(pageBlocks) : "" });
21456
+ }
21457
+ return pages;
21458
+ }
21459
+
21437
21460
  // src/hwpx/generator.ts
21438
21461
  import JSZip8 from "jszip";
21439
21462
 
@@ -28869,6 +28892,14 @@ async function parse(input, options) {
28869
28892
  return { success: false, fileType: "unknown", error: "\uBE48 \uBC84\uD37C\uC774\uAC70\uB098 \uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 \uC785\uB825\uC785\uB2C8\uB2E4.", code: "EMPTY_INPUT" };
28870
28893
  }
28871
28894
  const format = detectFormat(buffer);
28895
+ const result = await dispatch(format, buffer, opts);
28896
+ if (result.success && !result.pages) {
28897
+ const pages = blocksToPages(result.blocks);
28898
+ if (pages) return { ...result, pages };
28899
+ }
28900
+ return result;
28901
+ }
28902
+ async function dispatch(format, buffer, opts) {
28872
28903
  switch (format) {
28873
28904
  case "hwpx": {
28874
28905
  const zipFormat = await detectZipFormat(buffer);
@@ -28895,7 +28926,7 @@ async function parse(input, options) {
28895
28926
  }
28896
28927
  async function parseImage(buffer, options) {
28897
28928
  try {
28898
- const { parseImageDocument } = await import("./image-ocr-B7TMIG3F.js");
28929
+ const { parseImageDocument } = await import("./image-ocr-EJLWGR3B.js");
28899
28930
  const { blocks, warnings } = await parseImageDocument(buffer, options);
28900
28931
  return {
28901
28932
  success: true,
@@ -28954,7 +28985,7 @@ async function parseHwp(buffer, options) {
28954
28985
  async function parsePdf(buffer, options) {
28955
28986
  let parsePdfDocument;
28956
28987
  try {
28957
- const mod = await import("./parser-HT7GZOF2.js");
28988
+ const mod = await import("./parser-ERSNBILY.js");
28958
28989
  parsePdfDocument = mod.parsePdfDocument;
28959
28990
  } catch {
28960
28991
  return {
@@ -29064,6 +29095,7 @@ export {
29064
29095
  fillFormFields,
29065
29096
  extractClickHereFields,
29066
29097
  fillHwpx,
29098
+ blocksToPages,
29067
29099
  PRESET_ALIAS,
29068
29100
  normalizeGongmunPreset,
29069
29101
  incompatibleGongmunWarnings,
@@ -29100,4 +29132,4 @@ export {
29100
29132
  parseHwpml,
29101
29133
  fillForm
29102
29134
  };
29103
- //# sourceMappingURL=chunk-W5NZYPRQ.js.map
29135
+ //# sourceMappingURL=chunk-5EWPDL7S.js.map