kordoc 4.14.0 → 4.14.2

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 (135) hide show
  1. package/README.md +18 -2
  2. package/dist/{-SLX6IB6Y.js → -AQKXLFIM.js} +10 -10
  3. package/dist/{chunk-WAPKUYGP.cjs → chunk-247ANM72.cjs} +11 -11
  4. package/dist/chunk-247ANM72.cjs.map +1 -0
  5. package/dist/{chunk-EOTAPX2M.js → chunk-3DUADAY2.js} +3 -3
  6. package/dist/{chunk-B7MOYHVR.cjs → chunk-422427XJ.cjs} +6 -6
  7. package/dist/chunk-422427XJ.cjs.map +1 -0
  8. package/dist/{chunk-HG5LXXCC.js → chunk-7IAQ4HOK.js} +1 -1
  9. package/dist/{chunk-XVIJLHCW.js.map → chunk-7IAQ4HOK.js.map} +1 -1
  10. package/dist/{chunk-JIQWI7S2.js → chunk-A7QC2ZVP.js} +3 -3
  11. package/dist/{chunk-HCHA3Y2T.js → chunk-C7FQBSON.js} +2 -2
  12. package/dist/chunk-DCZVOIEO.cjs.map +1 -1
  13. package/dist/{chunk-7VUHZFGQ.js → chunk-DOGPOY4B.js} +2 -2
  14. package/dist/{chunk-6T6MB5ON.js → chunk-EDNJLUSE.js} +2 -2
  15. package/dist/{chunk-EMQZX5QQ.js → chunk-EISO3POT.js} +301 -124
  16. package/dist/chunk-EISO3POT.js.map +1 -0
  17. package/dist/{chunk-LCGAO5LF.js → chunk-EP7KOI2V.js} +3 -3
  18. package/dist/{chunk-WRIHDGGY.cjs → chunk-EQZI77PP.cjs} +3 -3
  19. package/dist/chunk-EQZI77PP.cjs.map +1 -0
  20. package/dist/{chunk-XVIJLHCW.js → chunk-FOJZNMMK.js} +1 -1
  21. package/dist/{chunk-HG5LXXCC.js.map → chunk-FOJZNMMK.js.map} +1 -1
  22. package/dist/{chunk-PLXNW3J4.js → chunk-GDW4YP7K.js} +2 -2
  23. package/dist/chunk-GS7T56RP.cjs.map +1 -1
  24. package/dist/{chunk-ANKDK5DF.cjs → chunk-HR2XNERB.cjs} +2 -2
  25. package/dist/chunk-HR2XNERB.cjs.map +1 -0
  26. package/dist/{chunk-3U3PJGVO.js → chunk-ILU76UF3.js} +13 -2
  27. package/dist/chunk-ILU76UF3.js.map +1 -0
  28. package/dist/{chunk-J3SM7VLM.js → chunk-JQEFP45L.js} +2 -2
  29. package/dist/{chunk-M5NRJMA4.js → chunk-KVWV4ULP.js} +3 -3
  30. package/dist/{chunk-COUGOFT3.cjs → chunk-L7DMSMAG.cjs} +1 -1
  31. package/dist/chunk-L7DMSMAG.cjs.map +1 -0
  32. package/dist/{chunk-GMX6EKCY.js → chunk-NS55SM7A.js} +2 -2
  33. package/dist/{chunk-XH34WJKB.js → chunk-QC5T6D4J.js} +2 -2
  34. package/dist/{chunk-STLNL53H.cjs → chunk-QOMNEWCQ.cjs} +4 -4
  35. package/dist/chunk-QOMNEWCQ.cjs.map +1 -0
  36. package/dist/{chunk-CDQJKMZ5.js → chunk-TNCKOL2O.js} +2 -2
  37. package/dist/{chunk-HH7T6SMB.js → chunk-TSFS5VKD.js} +3072 -1721
  38. package/dist/chunk-TSFS5VKD.js.map +1 -0
  39. package/dist/{chunk-RCTWZYAM.js → chunk-UET462HB.js} +2 -2
  40. package/dist/{chunk-A2JDWBKG.js → chunk-VQ5W4MYO.js} +2 -2
  41. package/dist/{chunk-ZWTEE63A.js → chunk-ZWD734EY.js} +2 -2
  42. package/dist/{chunk-UBNJXMYX.js → chunk-ZWOFQRIB.js} +14 -6
  43. package/dist/chunk-ZWOFQRIB.js.map +1 -0
  44. package/dist/chunks-SX77P5CV.js +10 -0
  45. package/dist/cli.js +1066 -1038
  46. package/dist/cli.js.map +1 -1
  47. package/dist/{formula-VSZIJZXP.js → formula-5S7WWXGH.js} +4 -4
  48. package/dist/{formula-VYDTA27M.js → formula-JGFTXWYT.js} +3 -3
  49. package/dist/{formula-TAYYDV73.cjs → formula-PJJ6S3Z2.cjs} +9 -9
  50. package/dist/formula-PJJ6S3Z2.cjs.map +1 -0
  51. package/dist/{image-ocr-5WBAIBPQ.js → image-ocr-FLIL7MGN.js} +6 -6
  52. package/dist/{image-ocr-N6IBZJAK.js → image-ocr-WCAYXLOA.js} +8 -8
  53. package/dist/{image-ocr-ZX4W2RBS.cjs → image-ocr-ZTYAGV3W.cjs} +12 -12
  54. package/dist/image-ocr-ZTYAGV3W.cjs.map +1 -0
  55. package/dist/index.cjs +3010 -1465
  56. package/dist/index.cjs.map +1 -1
  57. package/dist/index.d.cts +11 -6
  58. package/dist/index.d.ts +11 -6
  59. package/dist/index.js +2547 -1002
  60. package/dist/index.js.map +1 -1
  61. package/dist/mcp.js +973 -932
  62. package/dist/mcp.js.map +1 -1
  63. package/dist/{model-bundle-5CBFVIXA.js → model-bundle-FSPZZ2EM.js} +5 -5
  64. package/dist/{models-L74RBKK3.js → models-CJPAQK5R.js} +5 -5
  65. package/dist/page-range-P7SDW6LR.cjs.map +1 -1
  66. package/dist/{parser-66NMJUOG.js → parser-A4TMPJAM.js} +7 -7
  67. package/dist/{parser-BK66Y5JD.js → parser-SCUTZAIJ.js} +7 -7
  68. package/dist/{parser-WM2DMVLY.cjs → parser-VCXE4I5A.cjs} +27 -27
  69. package/dist/parser-VCXE4I5A.cjs.map +1 -0
  70. package/dist/pdf-ocr-76SAUXMZ.js +13 -0
  71. package/dist/pdf-ocr-CPU55CVF.js +16 -0
  72. package/dist/pdf-ocr-MBZZ57LD.cjs +14 -0
  73. package/dist/pdf-ocr-MBZZ57LD.cjs.map +1 -0
  74. package/dist/{profile-io-JZ3FDHDK.js → profile-io-LBM263K7.js} +3 -3
  75. package/dist/{rasterize-33WOOVSM.js → rasterize-46RG4ZF6.js} +3 -3
  76. package/dist/{render-MLN4ZLVD.js → render-NGP5T24S.js} +7 -7
  77. package/dist/seal-RGUPOLXD.js +10 -0
  78. package/dist/{setup-G6AEN4J7.js → setup-AU4OCHNH.js} +3 -3
  79. package/dist/visual-NZLYIYEM.js +19 -0
  80. package/dist/{watch-OY6XKGC5.js → watch-MKPUCPXM.js} +11 -11
  81. package/package.json +2 -2
  82. package/dist/chunk-3U3PJGVO.js.map +0 -1
  83. package/dist/chunk-ANKDK5DF.cjs.map +0 -1
  84. package/dist/chunk-B7MOYHVR.cjs.map +0 -1
  85. package/dist/chunk-COUGOFT3.cjs.map +0 -1
  86. package/dist/chunk-EMQZX5QQ.js.map +0 -1
  87. package/dist/chunk-HH7T6SMB.js.map +0 -1
  88. package/dist/chunk-STLNL53H.cjs.map +0 -1
  89. package/dist/chunk-UBNJXMYX.js.map +0 -1
  90. package/dist/chunk-WAPKUYGP.cjs.map +0 -1
  91. package/dist/chunk-WRIHDGGY.cjs.map +0 -1
  92. package/dist/chunks-PEHRXW2P.js +0 -10
  93. package/dist/formula-TAYYDV73.cjs.map +0 -1
  94. package/dist/image-ocr-ZX4W2RBS.cjs.map +0 -1
  95. package/dist/parser-WM2DMVLY.cjs.map +0 -1
  96. package/dist/pdf-ocr-322MBLDK.js +0 -16
  97. package/dist/pdf-ocr-A4UBXLM2.js +0 -13
  98. package/dist/pdf-ocr-G4TBDUYL.cjs +0 -14
  99. package/dist/pdf-ocr-G4TBDUYL.cjs.map +0 -1
  100. package/dist/seal-JQWSNHRG.js +0 -10
  101. package/dist/visual-VZ7QGAUC.js +0 -19
  102. /package/dist/{-SLX6IB6Y.js.map → -AQKXLFIM.js.map} +0 -0
  103. /package/dist/{chunk-EOTAPX2M.js.map → chunk-3DUADAY2.js.map} +0 -0
  104. /package/dist/{chunk-JIQWI7S2.js.map → chunk-A7QC2ZVP.js.map} +0 -0
  105. /package/dist/{chunk-HCHA3Y2T.js.map → chunk-C7FQBSON.js.map} +0 -0
  106. /package/dist/{chunk-7VUHZFGQ.js.map → chunk-DOGPOY4B.js.map} +0 -0
  107. /package/dist/{chunk-6T6MB5ON.js.map → chunk-EDNJLUSE.js.map} +0 -0
  108. /package/dist/{chunk-LCGAO5LF.js.map → chunk-EP7KOI2V.js.map} +0 -0
  109. /package/dist/{chunk-PLXNW3J4.js.map → chunk-GDW4YP7K.js.map} +0 -0
  110. /package/dist/{chunk-J3SM7VLM.js.map → chunk-JQEFP45L.js.map} +0 -0
  111. /package/dist/{chunk-M5NRJMA4.js.map → chunk-KVWV4ULP.js.map} +0 -0
  112. /package/dist/{chunk-GMX6EKCY.js.map → chunk-NS55SM7A.js.map} +0 -0
  113. /package/dist/{chunk-XH34WJKB.js.map → chunk-QC5T6D4J.js.map} +0 -0
  114. /package/dist/{chunk-CDQJKMZ5.js.map → chunk-TNCKOL2O.js.map} +0 -0
  115. /package/dist/{chunk-RCTWZYAM.js.map → chunk-UET462HB.js.map} +0 -0
  116. /package/dist/{chunk-A2JDWBKG.js.map → chunk-VQ5W4MYO.js.map} +0 -0
  117. /package/dist/{chunk-ZWTEE63A.js.map → chunk-ZWD734EY.js.map} +0 -0
  118. /package/dist/{chunks-PEHRXW2P.js.map → chunks-SX77P5CV.js.map} +0 -0
  119. /package/dist/{formula-VSZIJZXP.js.map → formula-5S7WWXGH.js.map} +0 -0
  120. /package/dist/{formula-VYDTA27M.js.map → formula-JGFTXWYT.js.map} +0 -0
  121. /package/dist/{image-ocr-5WBAIBPQ.js.map → image-ocr-FLIL7MGN.js.map} +0 -0
  122. /package/dist/{image-ocr-N6IBZJAK.js.map → image-ocr-WCAYXLOA.js.map} +0 -0
  123. /package/dist/{model-bundle-5CBFVIXA.js.map → model-bundle-FSPZZ2EM.js.map} +0 -0
  124. /package/dist/{models-L74RBKK3.js.map → models-CJPAQK5R.js.map} +0 -0
  125. /package/dist/{parser-66NMJUOG.js.map → parser-A4TMPJAM.js.map} +0 -0
  126. /package/dist/{parser-BK66Y5JD.js.map → parser-SCUTZAIJ.js.map} +0 -0
  127. /package/dist/{pdf-ocr-322MBLDK.js.map → pdf-ocr-76SAUXMZ.js.map} +0 -0
  128. /package/dist/{pdf-ocr-A4UBXLM2.js.map → pdf-ocr-CPU55CVF.js.map} +0 -0
  129. /package/dist/{profile-io-JZ3FDHDK.js.map → profile-io-LBM263K7.js.map} +0 -0
  130. /package/dist/{rasterize-33WOOVSM.js.map → rasterize-46RG4ZF6.js.map} +0 -0
  131. /package/dist/{render-MLN4ZLVD.js.map → render-NGP5T24S.js.map} +0 -0
  132. /package/dist/{seal-JQWSNHRG.js.map → seal-RGUPOLXD.js.map} +0 -0
  133. /package/dist/{setup-G6AEN4J7.js.map → setup-AU4OCHNH.js.map} +0 -0
  134. /package/dist/{visual-VZ7QGAUC.js.map → visual-NZLYIYEM.js.map} +0 -0
  135. /package/dist/{watch-OY6XKGC5.js.map → watch-MKPUCPXM.js.map} +0 -0
package/dist/mcp.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  assertWithinRoot,
4
4
  getAccessRoot,
5
5
  isOfflineMode
6
- } from "./chunk-PLXNW3J4.js";
6
+ } from "./chunk-GDW4YP7K.js";
7
7
  import {
8
8
  APPROVAL_MAX,
9
9
  BODY_FONTS,
@@ -42,40 +42,45 @@ import {
42
42
  resolveBuiltinTemplate,
43
43
  unknownFontWarnings,
44
44
  usesGaejosikMunche
45
- } from "./chunk-EMQZX5QQ.js";
46
- import "./chunk-UBNJXMYX.js";
45
+ } from "./chunk-EISO3POT.js";
46
+ import "./chunk-ZWOFQRIB.js";
47
47
  import "./chunk-DZIXKL7E.js";
48
- import "./chunk-6T6MB5ON.js";
48
+ import "./chunk-EDNJLUSE.js";
49
49
  import {
50
50
  detectFormat,
51
51
  detectOle2Format,
52
52
  detectZipFormat,
53
53
  extractHwp5MetadataOnly
54
- } from "./chunk-HH7T6SMB.js";
54
+ } from "./chunk-TSFS5VKD.js";
55
55
  import "./chunk-7FCNOKOR.js";
56
- import "./chunk-HG5LXXCC.js";
56
+ import "./chunk-7IAQ4HOK.js";
57
57
  import "./chunk-MOL7MDBG.js";
58
- import "./chunk-3U3PJGVO.js";
58
+ import "./chunk-ILU76UF3.js";
59
59
  import {
60
60
  blocksToMarkdown
61
- } from "./chunk-XH34WJKB.js";
62
- import "./chunk-HCHA3Y2T.js";
61
+ } from "./chunk-QC5T6D4J.js";
62
+ import "./chunk-C7FQBSON.js";
63
63
  import {
64
64
  KordocError,
65
65
  VERSION,
66
66
  classifyError,
67
67
  sanitizeError,
68
68
  toArrayBuffer
69
- } from "./chunk-RCTWZYAM.js";
69
+ } from "./chunk-UET462HB.js";
70
70
 
71
71
  // src/mcp.ts
72
72
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
73
73
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
74
+ import { realpathSync as realpathSync2 } from "fs";
75
+ import { pathToFileURL } from "url";
76
+
77
+ // src/mcp/tools-parse.ts
74
78
  import { z } from "zod";
79
+
80
+ // src/mcp/shared.ts
75
81
  import { realpathSync, openSync, readSync, closeSync, existsSync } from "fs";
76
- import { readFile, writeFile, mkdir, stat, realpath } from "fs/promises";
77
- import { pathToFileURL } from "url";
78
- import { resolve, isAbsolute, extname, dirname, basename, join } from "path";
82
+ import { readFile, stat } from "fs/promises";
83
+ import { resolve, isAbsolute, extname, dirname, basename } from "path";
79
84
  var ALLOWED_EXTENSIONS = /* @__PURE__ */ new Set([".hwp", ".hwpx", ".hml", ".pdf", ".xls", ".xlsx", ".docx"]);
80
85
  var PARSE_EXTENSIONS = /* @__PURE__ */ new Set([...ALLOWED_EXTENSIONS, ".png", ".jpg", ".jpeg", ".webp"]);
81
86
  var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".gif", ".bmp"]);
@@ -173,338 +178,340 @@ function detectFormatFromHeader(resolved) {
173
178
  closeSync(fd);
174
179
  }
175
180
  }
176
- var server = new McpServer({
177
- name: "kordoc",
178
- version: VERSION
179
- });
180
- server.tool(
181
- "parse_document",
182
- "\uD55C\uAD6D \uBB38\uC11C \uD30C\uC77C(HWP, HWPX, PDF, XLSX, DOCX)\uACFC \uC774\uBBF8\uC9C0(PNG/JPG/WebP)\uB97C \uB9C8\uD06C\uB2E4\uC6B4\uC73C\uB85C \uBCC0\uD658\uD569\uB2C8\uB2E4. \uD30C\uC77C \uACBD\uB85C\uB97C \uC785\uB825\uD558\uBA74 \uD3EC\uB9F7\uC744 \uC790\uB3D9 \uAC10\uC9C0\uD558\uC5EC \uD14D\uC2A4\uD2B8\uB97C \uCD94\uCD9C\uD569\uB2C8\uB2E4. \uC774\uBBF8\uC9C0\uB294 OCR(\uB0B4\uC7A5 PP-OCRv5)\uC774 \uC790\uB3D9 \uC801\uC6A9\uB418\uACE0 \uD45C \uAD18\uC120\uB3C4 \uBCF5\uC6D0\uB429\uB2C8\uB2E4.",
183
- {
184
- file_path: z.string().min(1).describe("\uD30C\uC2F1\uD560 \uBB38\uC11C \uD30C\uC77C\uC758 \uC808\uB300 \uACBD\uB85C (HWP, HWPX, PDF, XLSX, DOCX, PNG/JPG/WebP)"),
185
- ocr: z.union([z.boolean(), z.literal("force")]).optional().describe('\uC2A4\uCE94/\uC774\uBBF8\uC9C0 PDF \uD14D\uC2A4\uD2B8 OCR (\uB0B4\uC7A5 PP-OCRv5 korean, \uCCAB \uC0AC\uC6A9 \uC2DC ~18MB \uC790\uB3D9 \uB2E4\uC6B4\uB85C\uB4DC). true=\uD14D\uC2A4\uD2B8\uCE35\uC774 \uC5C6\uAC70\uB098 \uAE68\uC9C4 \uD398\uC774\uC9C0\uB9CC \uC778\uC2DD\uD558\uACE0 \uC815\uC0C1 \uD398\uC774\uC9C0\uB294 \uADF8\uB300\uB85C \uB461\uB2C8\uB2E4. "force"=\uD14D\uC2A4\uD2B8\uCE35\uC774 \uC788\uC5B4\uB3C4 \uBB34\uC2DC\uD558\uACE0 \uC804 \uD398\uC774\uC9C0 \uAC15\uC81C \uC7AC\uC778\uC2DD. parse \uACB0\uACFC\uC5D0 NEEDS_OCR \uACBD\uACE0\uAC00 \uC788\uC73C\uBA74 \uC774 \uC635\uC158\uC73C\uB85C \uC7AC\uC2DC\uB3C4\uD558\uC138\uC694'),
186
- remove_header_footer: z.boolean().optional().describe("PDF \uBA38\uB9AC\uAE00/\uBC14\uB2E5\uAE00 \uC790\uB3D9 \uC81C\uAC70 (\uAE30\uBCF8 true \u2014 false\uB85C \uB044\uAE30, CLI --no-header-footer \uB300\uC751)"),
187
- formula_ocr: z.boolean().optional().describe("PDF \uC218\uC2DD OCR \uD65C\uC131\uD654 (MFD+MFR ONNX, \uCCAB \uC0AC\uC6A9 \uC2DC \uBAA8\uB378 ~155MB \uC790\uB3D9 \uB2E4\uC6B4\uB85C\uB4DC, CLI --formula-ocr \uB300\uC751)"),
188
- dedupe_running_headers: z.boolean().optional().describe("HWP5 \uB808\uC774\uC544\uC6C3 \uD45C \uD398\uC774\uC9C0 \uBC18\uBCF5 \uB7EC\uB2DD \uD5E4\uB354 \uC911\uBCF5 \uC81C\uAC70 (\uAE30\uBCF8 off \u2014 \uBD99\uC784\uBCC4 \uC7AC\uBC88\uD638 \uC624\uC0AD\uC81C \uC8FC\uC758, CLI --dedupe-headers \uB300\uC751)"),
189
- keep_trailing_empty_cols: z.boolean().optional().describe("\uD45C \uC624\uB978\uCABD \uB05D \uBE48 \uC5F4(\uC11C\uC2DD \uC785\uB825\uB780) \uBCF4\uC874 (#47, \uAE30\uBCF8 off: \uD6C4\uD589 \uBE48 \uC5F4 \uD2B8\uB9BC, CLI --keep-empty-cols \uB300\uC751)"),
190
- keep_empty_paragraphs: z.boolean().optional().describe("\uBE48 \uBB38\uB2E8 \uBCF4\uC874 \u2014 \uBCF8\uBB38\uC740 \uBE48 paragraph \uBE14\uB85D, \uD45C \uC140\uC740 \uBE48 \uC904 (#57, \uAE30\uBCF8 off, CLI --keep-empty-paragraphs \uB300\uC751)"),
191
- password: z.string().optional().describe("\uC554\uD638\uB85C \uBCF4\uD638\uB41C \uBB38\uC11C\uC758 \uC5F4\uAE30 \uC554\uD638 (#59, HWPX\xB7HWP3\xB7HWP5 \uC9C0\uC6D0. \uD30C\uC2F1\uC774 ENCRYPTED\uB85C \uC2E4\uD328\uD558\uBA74 \uC774 \uC635\uC158\uC73C\uB85C \uC7AC\uC2DC\uB3C4\uD558\uC138\uC694. \uD55C\uCEF4 DRM \uBCF4\uD638 \uBB38\uC11C\uB294 \uD574\uB2F9 \uC5C6\uC74C)"),
192
- tables: z.boolean().optional().describe("PDF \uD45C \uAC10\uC9C0 (\uAE30\uBCF8 true \u2014 false\uB85C \uB044\uAE30, CLI --no-tables \uB300\uC751). \uD14C\uB450\uB9AC \uBC15\uC2A4\uB97C \uD45C\uB85C \uC624\uC778\uD574 \uC77D\uAE30 \uC21C\uC11C\uAC00 \uB4A4\uC9D1\uD788\uB294 \uBB38\uC11C(2\uB2E8 \uC2DC\uD5D8\uC9C0 \uB4F1)\uC5D0\uC11C \uC790\uC5F0 \uC77D\uAE30\uC21C \uD14D\uC2A4\uD2B8\uB9CC \uBF51\uC2B5\uB2C8\uB2E4 (#64)")
193
- },
194
- async ({ file_path, ocr, remove_header_footer, formula_ocr, dedupe_running_headers, keep_trailing_empty_cols, keep_empty_paragraphs, password, tables }) => {
195
- try {
196
- const { buffer, resolved } = await readValidatedFile(file_path, MAX_FILE_SIZE, PARSE_EXTENSIONS);
197
- const format = detectFormat(buffer);
198
- if (format === "unknown") {
199
- return {
200
- content: [{ type: "text", text: `\uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 \uD30C\uC77C \uD615\uC2DD\uC785\uB2C8\uB2E4: ${file_path}` }],
201
- isError: true
202
- };
203
- }
204
- const result = await parse(buffer, {
205
- filePath: resolved,
206
- ...ocr !== void 0 ? { ocr } : {},
207
- ...remove_header_footer !== void 0 ? { removeHeaderFooter: remove_header_footer } : {},
208
- ...formula_ocr ? { formulaOcr: true } : {},
209
- ...dedupe_running_headers ? { dedupeRunningHeaders: true } : {},
210
- ...keep_trailing_empty_cols ? { keepTrailingEmptyCols: true } : {},
211
- ...keep_empty_paragraphs ? { keepEmptyParagraphs: true } : {},
212
- ...password ? { password } : {},
213
- ...tables === false ? { tables: false } : {}
214
- });
215
- if (!result.success) {
216
- return {
217
- content: [{ type: "text", text: `\uD30C\uC2F1 \uC2E4\uD328 (${result.fileType}): ${result.error}` }],
218
- isError: true
219
- };
220
- }
221
- const markdown = result.markdown;
222
- const meta = [
223
- `\uD3EC\uB9F7: ${result.fileType.toUpperCase()}`,
224
- result.pageCount ? `\uD398\uC774\uC9C0: ${result.pageCount}` : null,
225
- result.metadata?.title ? `\uC81C\uBAA9: ${result.metadata.title}` : null,
226
- result.metadata?.author ? `\uC791\uC131\uC790: ${result.metadata.author}` : null,
227
- result.isImageBased ? result.warnings?.some((w) => w.code === "OCR_APPLIED") ? "\uC774\uBBF8\uC9C0 \uAE30\uBC18 PDF (OCR \uC801\uC6A9)" : "\uC774\uBBF8\uC9C0 \uAE30\uBC18 PDF (\uD14D\uC2A4\uD2B8 \uCD94\uCD9C \uBD88\uAC00 \u2014 ocr: true \uB85C \uC7AC\uC2DC\uB3C4 \uAC00\uB2A5)" : null
228
- ].filter(Boolean).join(" | ");
229
- const parts = [`[${meta}]`];
230
- if (result.outline && result.outline.length > 0) {
231
- const outlineText = result.outline.map((o) => `${" ".repeat(o.level - 1)}- ${o.text}`).join("\n");
232
- parts.push(`
181
+
182
+ // src/mcp/tools-parse.ts
183
+ function registerParseTools(server2) {
184
+ server2.tool(
185
+ "parse_document",
186
+ "\uD55C\uAD6D \uBB38\uC11C \uD30C\uC77C(HWP, HWPX, PDF, XLSX, DOCX)\uACFC \uC774\uBBF8\uC9C0(PNG/JPG/WebP)\uB97C \uB9C8\uD06C\uB2E4\uC6B4\uC73C\uB85C \uBCC0\uD658\uD569\uB2C8\uB2E4. \uD30C\uC77C \uACBD\uB85C\uB97C \uC785\uB825\uD558\uBA74 \uD3EC\uB9F7\uC744 \uC790\uB3D9 \uAC10\uC9C0\uD558\uC5EC \uD14D\uC2A4\uD2B8\uB97C \uCD94\uCD9C\uD569\uB2C8\uB2E4. \uC774\uBBF8\uC9C0\uB294 OCR(\uB0B4\uC7A5 PP-OCRv5)\uC774 \uC790\uB3D9 \uC801\uC6A9\uB418\uACE0 \uD45C \uAD18\uC120\uB3C4 \uBCF5\uC6D0\uB429\uB2C8\uB2E4.",
187
+ {
188
+ file_path: z.string().min(1).describe("\uD30C\uC2F1\uD560 \uBB38\uC11C \uD30C\uC77C\uC758 \uC808\uB300 \uACBD\uB85C (HWP, HWPX, PDF, XLSX, DOCX, PNG/JPG/WebP)"),
189
+ ocr: z.union([z.boolean(), z.literal("force")]).optional().describe('\uC2A4\uCE94/\uC774\uBBF8\uC9C0 PDF \uD14D\uC2A4\uD2B8 OCR (\uB0B4\uC7A5 PP-OCRv5 korean, \uCCAB \uC0AC\uC6A9 \uC2DC ~18MB \uC790\uB3D9 \uB2E4\uC6B4\uB85C\uB4DC). true=\uD14D\uC2A4\uD2B8\uCE35\uC774 \uC5C6\uAC70\uB098 \uAE68\uC9C4 \uD398\uC774\uC9C0\uB9CC \uC778\uC2DD\uD558\uACE0 \uC815\uC0C1 \uD398\uC774\uC9C0\uB294 \uADF8\uB300\uB85C \uB461\uB2C8\uB2E4. "force"=\uD14D\uC2A4\uD2B8\uCE35\uC774 \uC788\uC5B4\uB3C4 \uBB34\uC2DC\uD558\uACE0 \uC804 \uD398\uC774\uC9C0 \uAC15\uC81C \uC7AC\uC778\uC2DD. parse \uACB0\uACFC\uC5D0 NEEDS_OCR \uACBD\uACE0\uAC00 \uC788\uC73C\uBA74 \uC774 \uC635\uC158\uC73C\uB85C \uC7AC\uC2DC\uB3C4\uD558\uC138\uC694'),
190
+ remove_header_footer: z.boolean().optional().describe("PDF \uBA38\uB9AC\uAE00/\uBC14\uB2E5\uAE00 \uC790\uB3D9 \uC81C\uAC70 (\uAE30\uBCF8 true \u2014 false\uB85C \uB044\uAE30, CLI --no-header-footer \uB300\uC751)"),
191
+ formula_ocr: z.boolean().optional().describe("PDF \uC218\uC2DD OCR \uD65C\uC131\uD654 (MFD+MFR ONNX, \uCCAB \uC0AC\uC6A9 \uC2DC \uBAA8\uB378 ~155MB \uC790\uB3D9 \uB2E4\uC6B4\uB85C\uB4DC, CLI --formula-ocr \uB300\uC751)"),
192
+ dedupe_running_headers: z.boolean().optional().describe("HWP5 \uB808\uC774\uC544\uC6C3 \uD45C \uD398\uC774\uC9C0 \uBC18\uBCF5 \uB7EC\uB2DD \uD5E4\uB354 \uC911\uBCF5 \uC81C\uAC70 (\uAE30\uBCF8 off \u2014 \uBD99\uC784\uBCC4 \uC7AC\uBC88\uD638 \uC624\uC0AD\uC81C \uC8FC\uC758, CLI --dedupe-headers \uB300\uC751)"),
193
+ keep_trailing_empty_cols: z.boolean().optional().describe("\uD45C \uC624\uB978\uCABD \uB05D \uBE48 \uC5F4(\uC11C\uC2DD \uC785\uB825\uB780) \uBCF4\uC874 (#47, \uAE30\uBCF8 off: \uD6C4\uD589 \uBE48 \uC5F4 \uD2B8\uB9BC, CLI --keep-empty-cols \uB300\uC751)"),
194
+ keep_empty_paragraphs: z.boolean().optional().describe("\uBE48 \uBB38\uB2E8 \uBCF4\uC874 \u2014 \uBCF8\uBB38\uC740 \uBE48 paragraph \uBE14\uB85D, \uD45C \uC140\uC740 \uBE48 \uC904 (#57, \uAE30\uBCF8 off, CLI --keep-empty-paragraphs \uB300\uC751)"),
195
+ password: z.string().optional().describe("\uC554\uD638\uB85C \uBCF4\uD638\uB41C \uBB38\uC11C\uC758 \uC5F4\uAE30 \uC554\uD638 (#59, HWPX\xB7HWP3\xB7HWP5 \uC9C0\uC6D0. \uD30C\uC2F1\uC774 ENCRYPTED\uB85C \uC2E4\uD328\uD558\uBA74 \uC774 \uC635\uC158\uC73C\uB85C \uC7AC\uC2DC\uB3C4\uD558\uC138\uC694. \uD55C\uCEF4 DRM \uBCF4\uD638 \uBB38\uC11C\uB294 \uD574\uB2F9 \uC5C6\uC74C)"),
196
+ tables: z.boolean().optional().describe("PDF \uD45C \uAC10\uC9C0 (\uAE30\uBCF8 true \u2014 false\uB85C \uB044\uAE30, CLI --no-tables \uB300\uC751). \uD14C\uB450\uB9AC \uBC15\uC2A4\uB97C \uD45C\uB85C \uC624\uC778\uD574 \uC77D\uAE30 \uC21C\uC11C\uAC00 \uB4A4\uC9D1\uD788\uB294 \uBB38\uC11C(2\uB2E8 \uC2DC\uD5D8\uC9C0 \uB4F1)\uC5D0\uC11C \uC790\uC5F0 \uC77D\uAE30\uC21C \uD14D\uC2A4\uD2B8\uB9CC \uBF51\uC2B5\uB2C8\uB2E4 (#64)")
197
+ },
198
+ async ({ file_path, ocr, remove_header_footer, formula_ocr, dedupe_running_headers, keep_trailing_empty_cols, keep_empty_paragraphs, password, tables }) => {
199
+ try {
200
+ const { buffer, resolved } = await readValidatedFile(file_path, MAX_FILE_SIZE, PARSE_EXTENSIONS);
201
+ const format = detectFormat(buffer);
202
+ if (format === "unknown") {
203
+ return {
204
+ content: [{ type: "text", text: `\uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 \uD30C\uC77C \uD615\uC2DD\uC785\uB2C8\uB2E4: ${file_path}` }],
205
+ isError: true
206
+ };
207
+ }
208
+ const result = await parse(buffer, {
209
+ filePath: resolved,
210
+ ...ocr !== void 0 ? { ocr } : {},
211
+ ...remove_header_footer !== void 0 ? { removeHeaderFooter: remove_header_footer } : {},
212
+ ...formula_ocr ? { formulaOcr: true } : {},
213
+ ...dedupe_running_headers ? { dedupeRunningHeaders: true } : {},
214
+ ...keep_trailing_empty_cols ? { keepTrailingEmptyCols: true } : {},
215
+ ...keep_empty_paragraphs ? { keepEmptyParagraphs: true } : {},
216
+ ...password ? { password } : {},
217
+ ...tables === false ? { tables: false } : {}
218
+ });
219
+ if (!result.success) {
220
+ return {
221
+ content: [{ type: "text", text: `\uD30C\uC2F1 \uC2E4\uD328 (${result.fileType}): ${result.error}` }],
222
+ isError: true
223
+ };
224
+ }
225
+ const markdown = result.markdown;
226
+ const meta = [
227
+ `\uD3EC\uB9F7: ${result.fileType.toUpperCase()}`,
228
+ result.pageCount ? `\uD398\uC774\uC9C0: ${result.pageCount}` : null,
229
+ result.metadata?.title ? `\uC81C\uBAA9: ${result.metadata.title}` : null,
230
+ result.metadata?.author ? `\uC791\uC131\uC790: ${result.metadata.author}` : null,
231
+ result.isImageBased ? result.warnings?.some((w) => w.code === "OCR_APPLIED") ? "\uC774\uBBF8\uC9C0 \uAE30\uBC18 PDF (OCR \uC801\uC6A9)" : "\uC774\uBBF8\uC9C0 \uAE30\uBC18 PDF (\uD14D\uC2A4\uD2B8 \uCD94\uCD9C \uBD88\uAC00 \u2014 ocr: true \uB85C \uC7AC\uC2DC\uB3C4 \uAC00\uB2A5)" : null
232
+ ].filter(Boolean).join(" | ");
233
+ const parts = [`[${meta}]`];
234
+ if (result.outline && result.outline.length > 0) {
235
+ const outlineText = result.outline.map((o) => `${" ".repeat(o.level - 1)}- ${o.text}`).join("\n");
236
+ parts.push(`
233
237
  \u{1F4D1} \uBB38\uC11C \uAD6C\uC870:
234
238
  ${outlineText}`);
235
- }
236
- if (result.warnings && result.warnings.length > 0) {
237
- const warnText = result.warnings.map((w) => `- [p${w.page || "?"}] ${w.message}`).join("\n");
238
- parts.push(`
239
+ }
240
+ if (result.warnings && result.warnings.length > 0) {
241
+ const warnText = result.warnings.map((w) => `- [p${w.page || "?"}] ${w.message}`).join("\n");
242
+ parts.push(`
239
243
  \u26A0\uFE0F \uACBD\uACE0:
240
244
  ${warnText}`);
241
- }
242
- parts.push(`
245
+ }
246
+ parts.push(`
243
247
 
244
248
  ${markdown}`);
245
- return {
246
- content: [{ type: "text", text: capResponseText(parts.join("")) }]
247
- };
248
- } catch (err) {
249
- return {
250
- content: [{ type: "text", text: `\uC624\uB958: ${describeError(err)}` }],
251
- isError: true
252
- };
253
- }
254
- }
255
- );
256
- server.tool(
257
- "detect_format",
258
- "\uD30C\uC77C\uC758 \uD3EC\uB9F7\uC744 \uB9E4\uC9C1 \uBC14\uC774\uD2B8\uB85C \uAC10\uC9C0\uD569\uB2C8\uB2E4 (hwpx, hwp, hwp3, hwpml, pdf, xls, xlsx, docx, unknown).",
259
- {
260
- file_path: z.string().min(1).describe("\uAC10\uC9C0\uD560 \uD30C\uC77C\uC758 \uC808\uB300 \uACBD\uB85C")
261
- },
262
- async ({ file_path }) => {
263
- try {
264
- const resolved = safePath(file_path, PARSE_EXTENSIONS);
265
- let format = detectFormatFromHeader(resolved);
266
- if (format === "hwpx") {
267
- const { buffer } = await readValidatedFile(file_path, MAX_FILE_SIZE, PARSE_EXTENSIONS);
268
- format = await detectZipFormat(buffer);
269
- } else if (format === "hwp") {
270
- const { buffer } = await readValidatedFile(file_path, MAX_FILE_SIZE, PARSE_EXTENSIONS);
271
- const ole2Format = detectOle2Format(buffer);
272
- if (ole2Format !== "unknown") format = ole2Format;
273
- }
274
- return {
275
- content: [{ type: "text", text: `${file_path}: ${format}` }]
276
- };
277
- } catch (err) {
278
- return {
279
- content: [{ type: "text", text: `\uC624\uB958: ${describeError(err)}` }],
280
- isError: true
281
- };
282
- }
283
- }
284
- );
285
- server.tool(
286
- "parse_metadata",
287
- "\uBB38\uC11C\uC758 \uBA54\uD0C0\uB370\uC774\uD130(\uC81C\uBAA9, \uC791\uC131\uC790, \uB0A0\uC9DC \uB4F1)\uB9CC \uBE60\uB974\uAC8C \uCD94\uCD9C\uD569\uB2C8\uB2E4. \uC804\uCCB4 \uD30C\uC2F1 \uC5C6\uC774 \uD5E4\uB354/\uB9E4\uB2C8\uD398\uC2A4\uD2B8\uB9CC \uC77D\uC2B5\uB2C8\uB2E4.",
288
- {
289
- file_path: z.string().min(1).describe("\uBA54\uD0C0\uB370\uC774\uD130\uB97C \uCD94\uCD9C\uD560 \uBB38\uC11C \uD30C\uC77C\uC758 \uC808\uB300 \uACBD\uB85C")
290
- },
291
- async ({ file_path }) => {
292
- try {
293
- const resolved = safePath(file_path, PARSE_EXTENSIONS);
294
- const format = detectFormatFromHeader(resolved);
295
- if (format === "unknown") {
296
249
  return {
297
- content: [{ type: "text", text: `\uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 \uD30C\uC77C \uD615\uC2DD\uC785\uB2C8\uB2E4: ${file_path}` }],
250
+ content: [{ type: "text", text: capResponseText(parts.join("")) }]
251
+ };
252
+ } catch (err) {
253
+ return {
254
+ content: [{ type: "text", text: `\uC624\uB958: ${describeError(err)}` }],
298
255
  isError: true
299
256
  };
300
257
  }
301
- const { buffer } = await readValidatedFile(file_path, MAX_METADATA_FILE_SIZE, PARSE_EXTENSIONS);
302
- let metadata;
303
- let effectiveFormat = format;
304
- if (format === "hwpx") {
305
- const zipFormat = await detectZipFormat(buffer);
306
- if (zipFormat === "xlsx" || zipFormat === "docx") effectiveFormat = zipFormat;
307
- } else if (format === "hwp") {
308
- if (detectOle2Format(buffer) === "xls") effectiveFormat = "xls";
309
- }
310
- switch (effectiveFormat) {
311
- case "hwp":
312
- metadata = extractHwp5MetadataOnly(Buffer.from(buffer));
313
- break;
314
- case "hwpx":
315
- metadata = await extractHwpxMetadataOnly(buffer);
316
- break;
317
- case "pdf":
318
- try {
319
- const { extractPdfMetadataOnly } = await import("./parser-BK66Y5JD.js");
320
- metadata = await extractPdfMetadataOnly(buffer);
321
- } catch {
322
- metadata = void 0;
323
- }
324
- break;
325
- case "hwp3":
326
- case "hwpml":
327
- case "xls":
328
- case "xlsx":
329
- case "docx": {
330
- const result = await parse(buffer);
331
- metadata = result.success ? result.metadata : void 0;
332
- break;
333
- }
334
- default:
335
- metadata = void 0;
336
- break;
337
- }
338
- return {
339
- content: [{ type: "text", text: JSON.stringify({ format, ...metadata }, null, 2) }]
340
- };
341
- } catch (err) {
342
- return {
343
- content: [{ type: "text", text: `\uC624\uB958: ${describeError(err)}` }],
344
- isError: true
345
- };
346
258
  }
347
- }
348
- );
349
- server.tool(
350
- "parse_pages",
351
- "\uBB38\uC11C\uC758 \uD2B9\uC815 \uD398\uC774\uC9C0 \uBC94\uC704\uB9CC \uD30C\uC2F1\uD569\uB2C8\uB2E4. PDF\uC640 \uD55C\uCEF4 \uC800\uC7A5\uBCF8 HWP/HWPX\uB294 \uC2E4\uC81C \uD398\uC774\uC9C0 \uAE30\uC900(metadata.pageMode=layout), \uC870\uD310 \uCE90\uC2DC\uAC00 \uC5C6\uB294 \uC0DD\uC131 \uD30C\uC77C\uC740 \uC139\uC158 \uB2E8\uC704 \uADFC\uC0AC(pageMode=section)\uC785\uB2C8\uB2E4.",
352
- {
353
- file_path: z.string().min(1).describe("\uD30C\uC2F1\uD560 \uBB38\uC11C \uD30C\uC77C\uC758 \uC808\uB300 \uACBD\uB85C"),
354
- pages: z.string().min(1).describe("\uD398\uC774\uC9C0 \uBC94\uC704 (\uC608: '1-3', '1,3,5-7')")
355
- },
356
- async ({ file_path, pages }) => {
357
- try {
358
- const resolved = safePath(file_path, PARSE_EXTENSIONS);
359
- const { buffer } = await readValidatedFile(file_path, MAX_FILE_SIZE, PARSE_EXTENSIONS);
360
- const format = detectFormat(buffer);
361
- if (format === "unknown") {
259
+ );
260
+ server2.tool(
261
+ "detect_format",
262
+ "\uD30C\uC77C\uC758 \uD3EC\uB9F7\uC744 \uB9E4\uC9C1 \uBC14\uC774\uD2B8\uC640 \uCEE8\uD14C\uC774\uB108 \uB0B4\uBD80 \uAD6C\uC870\uB85C \uAC10\uC9C0\uD569\uB2C8\uB2E4 (hwpx, hwp, hwp3, hwpml, pdf, xls, xlsx, docx, pptx, image, unknown). PPTX\uB294 \uAC10\uC9C0\uB9CC \uC9C0\uC6D0\uD569\uB2C8\uB2E4.",
263
+ {
264
+ file_path: z.string().min(1).describe("\uAC10\uC9C0\uD560 \uD30C\uC77C\uC758 \uC808\uB300 \uACBD\uB85C")
265
+ },
266
+ async ({ file_path }) => {
267
+ try {
268
+ const resolved = safePath(file_path, PARSE_EXTENSIONS);
269
+ let format = detectFormatFromHeader(resolved);
270
+ if (format === "hwpx") {
271
+ const { buffer } = await readValidatedFile(file_path, MAX_FILE_SIZE, PARSE_EXTENSIONS);
272
+ format = await detectZipFormat(buffer);
273
+ } else if (format === "hwp") {
274
+ const { buffer } = await readValidatedFile(file_path, MAX_FILE_SIZE, PARSE_EXTENSIONS);
275
+ const ole2Format = detectOle2Format(buffer);
276
+ if (ole2Format !== "unknown") format = ole2Format;
277
+ }
362
278
  return {
363
- content: [{ type: "text", text: `\uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 \uD30C\uC77C \uD615\uC2DD\uC785\uB2C8\uB2E4: ${file_path}` }],
364
- isError: true
279
+ content: [{ type: "text", text: `${file_path}: ${format}` }]
365
280
  };
366
- }
367
- const result = await parse(buffer, { pages, filePath: resolved });
368
- if (!result.success) {
281
+ } catch (err) {
369
282
  return {
370
- content: [{ type: "text", text: `\uD30C\uC2F1 \uC2E4\uD328 (${result.fileType}): ${result.error}` }],
283
+ content: [{ type: "text", text: `\uC624\uB958: ${describeError(err)}` }],
371
284
  isError: true
372
285
  };
373
286
  }
374
- const meta = [
375
- `\uD3EC\uB9F7: ${result.fileType.toUpperCase()}`,
376
- `\uBC94\uC704: ${pages}`,
377
- result.pageCount ? `\uD398\uC774\uC9C0: ${result.pageCount}` : null,
378
- result.metadata?.pageMode === "section" ? "\uAE30\uC900: \uC139\uC158 \uADFC\uC0AC" : null
379
- ].filter(Boolean).join(" | ");
380
- return {
381
- content: [{ type: "text", text: `[${meta}]
382
-
383
- ${result.markdown}` }]
384
- };
385
- } catch (err) {
386
- return {
387
- content: [{ type: "text", text: `\uC624\uB958: ${describeError(err)}` }],
388
- isError: true
389
- };
390
287
  }
391
- }
392
- );
393
- server.tool(
394
- "parse_table",
395
- "\uBB38\uC11C\uC5D0\uC11C N\uBC88\uC9F8 \uD14C\uC774\uBE14\uB9CC \uCD94\uCD9C\uD569\uB2C8\uB2E4 (0-based index). \uD14C\uC774\uBE14\uC774 \uC5C6\uAC70\uB098 \uC778\uB371\uC2A4 \uBC94\uC704\uB97C \uCD08\uACFC\uD558\uBA74 \uC624\uB958\uB97C \uBC18\uD658\uD569\uB2C8\uB2E4.",
396
- {
397
- file_path: z.string().min(1).describe("\uD30C\uC2F1\uD560 \uBB38\uC11C \uD30C\uC77C\uC758 \uC808\uB300 \uACBD\uB85C"),
398
- table_index: z.number().int().min(0).describe("\uCD94\uCD9C\uD560 \uD14C\uC774\uBE14 \uC778\uB371\uC2A4 (0\uBD80\uD130 \uC2DC\uC791)")
399
- },
400
- async ({ file_path, table_index }) => {
401
- try {
402
- const { buffer } = await readValidatedFile(file_path, MAX_FILE_SIZE, PARSE_EXTENSIONS);
403
- const format = detectFormat(buffer);
404
- if (format === "unknown") {
288
+ );
289
+ server2.tool(
290
+ "parse_metadata",
291
+ "\uBB38\uC11C\uC758 \uBA54\uD0C0\uB370\uC774\uD130(\uC81C\uBAA9, \uC791\uC131\uC790, \uB0A0\uC9DC \uB4F1)\uB9CC \uBE60\uB974\uAC8C \uCD94\uCD9C\uD569\uB2C8\uB2E4. \uC804\uCCB4 \uD30C\uC2F1 \uC5C6\uC774 \uD5E4\uB354/\uB9E4\uB2C8\uD398\uC2A4\uD2B8\uB9CC \uC77D\uC2B5\uB2C8\uB2E4.",
292
+ {
293
+ file_path: z.string().min(1).describe("\uBA54\uD0C0\uB370\uC774\uD130\uB97C \uCD94\uCD9C\uD560 \uBB38\uC11C \uD30C\uC77C\uC758 \uC808\uB300 \uACBD\uB85C")
294
+ },
295
+ async ({ file_path }) => {
296
+ try {
297
+ const resolved = safePath(file_path, PARSE_EXTENSIONS);
298
+ const format = detectFormatFromHeader(resolved);
299
+ if (format === "unknown") {
300
+ return {
301
+ content: [{ type: "text", text: `\uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 \uD30C\uC77C \uD615\uC2DD\uC785\uB2C8\uB2E4: ${file_path}` }],
302
+ isError: true
303
+ };
304
+ }
305
+ const { buffer } = await readValidatedFile(file_path, MAX_METADATA_FILE_SIZE, PARSE_EXTENSIONS);
306
+ let metadata;
307
+ let effectiveFormat = format;
308
+ if (format === "hwpx") {
309
+ const zipFormat = await detectZipFormat(buffer);
310
+ if (zipFormat !== "unknown") effectiveFormat = zipFormat;
311
+ } else if (format === "hwp") {
312
+ if (detectOle2Format(buffer) === "xls") effectiveFormat = "xls";
313
+ }
314
+ switch (effectiveFormat) {
315
+ case "pptx":
316
+ throw new KordocError("PPTX \uD30C\uC77C\uC740 \uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 \uD30C\uC77C \uD615\uC2DD\uC785\uB2C8\uB2E4.");
317
+ case "hwp":
318
+ metadata = extractHwp5MetadataOnly(Buffer.from(buffer));
319
+ break;
320
+ case "hwpx":
321
+ metadata = await extractHwpxMetadataOnly(buffer);
322
+ break;
323
+ case "pdf":
324
+ try {
325
+ const { extractPdfMetadataOnly } = await import("./parser-SCUTZAIJ.js");
326
+ metadata = await extractPdfMetadataOnly(buffer);
327
+ } catch {
328
+ metadata = void 0;
329
+ }
330
+ break;
331
+ case "hwp3":
332
+ case "hwpml":
333
+ case "xls":
334
+ case "xlsx":
335
+ case "docx": {
336
+ const result = await parse(buffer);
337
+ metadata = result.success ? result.metadata : void 0;
338
+ break;
339
+ }
340
+ default:
341
+ metadata = void 0;
342
+ break;
343
+ }
405
344
  return {
406
- content: [{ type: "text", text: `\uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 \uD30C\uC77C \uD615\uC2DD\uC785\uB2C8\uB2E4: ${file_path}` }],
407
- isError: true
345
+ content: [{ type: "text", text: JSON.stringify({ format: effectiveFormat, ...metadata }, null, 2) }]
408
346
  };
409
- }
410
- const result = await parse(buffer);
411
- if (!result.success) {
347
+ } catch (err) {
412
348
  return {
413
- content: [{ type: "text", text: `\uD30C\uC2F1 \uC2E4\uD328 (${result.fileType}): ${result.error}` }],
349
+ content: [{ type: "text", text: `\uC624\uB958: ${describeError(err)}` }],
414
350
  isError: true
415
351
  };
416
352
  }
417
- const tableBlocks = result.blocks.filter((b) => b.type === "table" && b.table);
418
- if (tableBlocks.length === 0) {
353
+ }
354
+ );
355
+ server2.tool(
356
+ "parse_pages",
357
+ "\uBB38\uC11C\uC758 \uD2B9\uC815 \uD398\uC774\uC9C0 \uBC94\uC704\uB9CC \uD30C\uC2F1\uD569\uB2C8\uB2E4. PDF\uC640 \uD55C\uCEF4 \uC800\uC7A5\uBCF8 HWP/HWPX\uB294 \uC2E4\uC81C \uD398\uC774\uC9C0 \uAE30\uC900(metadata.pageMode=layout), \uC870\uD310 \uCE90\uC2DC\uAC00 \uC5C6\uB294 \uC0DD\uC131 \uD30C\uC77C\uC740 \uC139\uC158 \uB2E8\uC704 \uADFC\uC0AC(pageMode=section)\uC785\uB2C8\uB2E4.",
358
+ {
359
+ file_path: z.string().min(1).describe("\uD30C\uC2F1\uD560 \uBB38\uC11C \uD30C\uC77C\uC758 \uC808\uB300 \uACBD\uB85C"),
360
+ pages: z.string().min(1).describe("\uD398\uC774\uC9C0 \uBC94\uC704 (\uC608: '1-3', '1,3,5-7')")
361
+ },
362
+ async ({ file_path, pages }) => {
363
+ try {
364
+ const resolved = safePath(file_path, PARSE_EXTENSIONS);
365
+ const { buffer } = await readValidatedFile(file_path, MAX_FILE_SIZE, PARSE_EXTENSIONS);
366
+ const format = detectFormat(buffer);
367
+ if (format === "unknown") {
368
+ return {
369
+ content: [{ type: "text", text: `\uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 \uD30C\uC77C \uD615\uC2DD\uC785\uB2C8\uB2E4: ${file_path}` }],
370
+ isError: true
371
+ };
372
+ }
373
+ const result = await parse(buffer, { pages, filePath: resolved });
374
+ if (!result.success) {
375
+ return {
376
+ content: [{ type: "text", text: `\uD30C\uC2F1 \uC2E4\uD328 (${result.fileType}): ${result.error}` }],
377
+ isError: true
378
+ };
379
+ }
380
+ const meta = [
381
+ `\uD3EC\uB9F7: ${result.fileType.toUpperCase()}`,
382
+ `\uBC94\uC704: ${pages}`,
383
+ result.pageCount ? `\uD398\uC774\uC9C0: ${result.pageCount}` : null,
384
+ result.metadata?.pageMode === "section" ? "\uAE30\uC900: \uC139\uC158 \uADFC\uC0AC" : null
385
+ ].filter(Boolean).join(" | ");
419
386
  return {
420
- content: [{ type: "text", text: `\uBB38\uC11C\uC5D0 \uD14C\uC774\uBE14\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.` }],
421
- isError: true
387
+ content: [{ type: "text", text: `[${meta}]
388
+
389
+ ${result.markdown}` }]
422
390
  };
423
- }
424
- if (table_index >= tableBlocks.length) {
391
+ } catch (err) {
425
392
  return {
426
- content: [{ type: "text", text: `\uD14C\uC774\uBE14 \uC778\uB371\uC2A4 \uCD08\uACFC: ${table_index} (\uCD1D ${tableBlocks.length}\uAC1C \uD14C\uC774\uBE14)` }],
393
+ content: [{ type: "text", text: `\uC624\uB958: ${describeError(err)}` }],
427
394
  isError: true
428
395
  };
429
396
  }
430
- const tableBlock = tableBlocks[table_index];
431
- const tableMarkdown = blocksToMarkdown([tableBlock]);
432
- return {
433
- content: [{ type: "text", text: `[\uD14C\uC774\uBE14 #${table_index} / \uCD1D ${tableBlocks.length}\uAC1C]
397
+ }
398
+ );
399
+ server2.tool(
400
+ "parse_table",
401
+ "\uBB38\uC11C\uC5D0\uC11C N\uBC88\uC9F8 \uD14C\uC774\uBE14\uB9CC \uCD94\uCD9C\uD569\uB2C8\uB2E4 (0-based index). \uD14C\uC774\uBE14\uC774 \uC5C6\uAC70\uB098 \uC778\uB371\uC2A4 \uBC94\uC704\uB97C \uCD08\uACFC\uD558\uBA74 \uC624\uB958\uB97C \uBC18\uD658\uD569\uB2C8\uB2E4.",
402
+ {
403
+ file_path: z.string().min(1).describe("\uD30C\uC2F1\uD560 \uBB38\uC11C \uD30C\uC77C\uC758 \uC808\uB300 \uACBD\uB85C"),
404
+ table_index: z.number().int().min(0).describe("\uCD94\uCD9C\uD560 \uD14C\uC774\uBE14 \uC778\uB371\uC2A4 (0\uBD80\uD130 \uC2DC\uC791)")
405
+ },
406
+ async ({ file_path, table_index }) => {
407
+ try {
408
+ const { buffer } = await readValidatedFile(file_path, MAX_FILE_SIZE, PARSE_EXTENSIONS);
409
+ const format = detectFormat(buffer);
410
+ if (format === "unknown") {
411
+ return {
412
+ content: [{ type: "text", text: `\uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 \uD30C\uC77C \uD615\uC2DD\uC785\uB2C8\uB2E4: ${file_path}` }],
413
+ isError: true
414
+ };
415
+ }
416
+ const result = await parse(buffer);
417
+ if (!result.success) {
418
+ return {
419
+ content: [{ type: "text", text: `\uD30C\uC2F1 \uC2E4\uD328 (${result.fileType}): ${result.error}` }],
420
+ isError: true
421
+ };
422
+ }
423
+ const tableBlocks = result.blocks.filter((b) => b.type === "table" && b.table);
424
+ if (tableBlocks.length === 0) {
425
+ return {
426
+ content: [{ type: "text", text: `\uBB38\uC11C\uC5D0 \uD14C\uC774\uBE14\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.` }],
427
+ isError: true
428
+ };
429
+ }
430
+ if (table_index >= tableBlocks.length) {
431
+ return {
432
+ content: [{ type: "text", text: `\uD14C\uC774\uBE14 \uC778\uB371\uC2A4 \uCD08\uACFC: ${table_index} (\uCD1D ${tableBlocks.length}\uAC1C \uD14C\uC774\uBE14)` }],
433
+ isError: true
434
+ };
435
+ }
436
+ const tableBlock = tableBlocks[table_index];
437
+ const tableMarkdown = blocksToMarkdown([tableBlock]);
438
+ return {
439
+ content: [{ type: "text", text: `[\uD14C\uC774\uBE14 #${table_index} / \uCD1D ${tableBlocks.length}\uAC1C]
434
440
 
435
441
  ${tableMarkdown}` }]
436
- };
437
- } catch (err) {
438
- return {
439
- content: [{ type: "text", text: `\uC624\uB958: ${describeError(err)}` }],
440
- isError: true
441
- };
442
- }
443
- }
444
- );
445
- server.tool(
446
- "compare_documents",
447
- "\uB450 \uD55C\uAD6D \uBB38\uC11C \uD30C\uC77C\uC744 \uBE44\uAD50\uD558\uC5EC \uCD94\uAC00/\uC0AD\uC81C/\uBCC0\uACBD\uB41C \uBE14\uB85D\uC744 \uD45C\uC2DC\uD569\uB2C8\uB2E4. \uC2E0\uAD6C\uB300\uC870\uD45C \uC0DD\uC131\uC5D0 \uD65C\uC6A9\uB429\uB2C8\uB2E4. \uD06C\uB85C\uC2A4 \uD3EC\uB9F7(HWP\u2194HWPX) \uBE44\uAD50 \uAC00\uB2A5.",
448
- {
449
- file_path_a: z.string().min(1).describe("\uBE44\uAD50 \uC6D0\uBCF8 \uBB38\uC11C\uC758 \uC808\uB300 \uACBD\uB85C"),
450
- file_path_b: z.string().min(1).describe("\uBE44\uAD50 \uB300\uC0C1 \uBB38\uC11C\uC758 \uC808\uB300 \uACBD\uB85C")
451
- },
452
- async ({ file_path_a, file_path_b }) => {
453
- try {
454
- const { buffer: bufA } = await readValidatedFile(file_path_a);
455
- const { buffer: bufB } = await readValidatedFile(file_path_b);
456
- const result = await compare(bufA, bufB);
457
- const { stats, diffs } = result;
458
- const lines = [
459
- `## \uBB38\uC11C \uBE44\uAD50 \uACB0\uACFC`,
460
- `\uCD94\uAC00: ${stats.added} | \uC0AD\uC81C: ${stats.removed} | \uBCC0\uACBD: ${stats.modified} | \uB3D9\uC77C: ${stats.unchanged}`,
461
- ""
462
- ];
463
- for (const d of diffs) {
464
- const prefix = d.type === "added" ? "+" : d.type === "removed" ? "-" : d.type === "modified" ? "~" : " ";
465
- const text = d.after?.text || d.before?.text || (d.after?.table ? "[\uD14C\uC774\uBE14]" : d.before?.table ? "[\uD14C\uC774\uBE14]" : "");
466
- const sim = d.similarity !== void 0 ? ` (${(d.similarity * 100).toFixed(0)}%)` : "";
467
- lines.push(`${prefix} ${text.substring(0, 200)}${sim}`);
442
+ };
443
+ } catch (err) {
444
+ return {
445
+ content: [{ type: "text", text: `\uC624\uB958: ${describeError(err)}` }],
446
+ isError: true
447
+ };
468
448
  }
469
- return {
470
- content: [{ type: "text", text: lines.join("\n") }]
471
- };
472
- } catch (err) {
473
- return {
474
- content: [{ type: "text", text: `\uC624\uB958: ${describeError(err)}` }],
475
- isError: true
476
- };
477
449
  }
478
- }
479
- );
480
- server.tool(
481
- "parse_form",
482
- "\uD55C\uAD6D \uC11C\uC2DD \uBB38\uC11C\uC5D0\uC11C \uB808\uC774\uBE14-\uAC12 \uC30D\uC744 \uAD6C\uC870\uD654\uB41C JSON\uC73C\uB85C \uCD94\uCD9C\uD569\uB2C8\uB2E4. \uC591\uC2DD/\uC11C\uC2DD \uBB38\uC11C\uC5D0 \uCD5C\uC801\uD654.",
483
- {
484
- file_path: z.string().min(1).describe("\uC11C\uC2DD \uBB38\uC11C \uD30C\uC77C\uC758 \uC808\uB300 \uACBD\uB85C")
485
- },
486
- async ({ file_path }) => {
487
- try {
488
- const { buffer } = await readValidatedFile(file_path);
489
- const result = await parse(buffer, { keepTrailingEmptyCols: true });
490
- if (!result.success) {
450
+ );
451
+ server2.tool(
452
+ "compare_documents",
453
+ "\uB450 \uD55C\uAD6D \uBB38\uC11C \uD30C\uC77C\uC744 \uBE44\uAD50\uD558\uC5EC \uCD94\uAC00/\uC0AD\uC81C/\uBCC0\uACBD\uB41C \uBE14\uB85D\uC744 \uD45C\uC2DC\uD569\uB2C8\uB2E4. \uC2E0\uAD6C\uB300\uC870\uD45C \uC0DD\uC131\uC5D0 \uD65C\uC6A9\uB429\uB2C8\uB2E4. \uD06C\uB85C\uC2A4 \uD3EC\uB9F7(HWP\u2194HWPX) \uBE44\uAD50 \uAC00\uB2A5.",
454
+ {
455
+ file_path_a: z.string().min(1).describe("\uBE44\uAD50 \uC6D0\uBCF8 \uBB38\uC11C\uC758 \uC808\uB300 \uACBD\uB85C"),
456
+ file_path_b: z.string().min(1).describe("\uBE44\uAD50 \uB300\uC0C1 \uBB38\uC11C\uC758 \uC808\uB300 \uACBD\uB85C")
457
+ },
458
+ async ({ file_path_a, file_path_b }) => {
459
+ try {
460
+ const { buffer: bufA } = await readValidatedFile(file_path_a);
461
+ const { buffer: bufB } = await readValidatedFile(file_path_b);
462
+ const result = await compare(bufA, bufB);
463
+ const { stats, diffs } = result;
464
+ const lines = [
465
+ `## \uBB38\uC11C \uBE44\uAD50 \uACB0\uACFC`,
466
+ `\uCD94\uAC00: ${stats.added} | \uC0AD\uC81C: ${stats.removed} | \uBCC0\uACBD: ${stats.modified} | \uB3D9\uC77C: ${stats.unchanged}`,
467
+ ""
468
+ ];
469
+ for (const d of diffs) {
470
+ const prefix = d.type === "added" ? "+" : d.type === "removed" ? "-" : d.type === "modified" ? "~" : " ";
471
+ const text = d.after?.text || d.before?.text || (d.after?.table ? "[\uD14C\uC774\uBE14]" : d.before?.table ? "[\uD14C\uC774\uBE14]" : "");
472
+ const sim = d.similarity !== void 0 ? ` (${(d.similarity * 100).toFixed(0)}%)` : "";
473
+ lines.push(`${prefix} ${text.substring(0, 200)}${sim}`);
474
+ }
491
475
  return {
492
- content: [{ type: "text", text: `\uD30C\uC2F1 \uC2E4\uD328: ${result.error}` }],
476
+ content: [{ type: "text", text: lines.join("\n") }]
477
+ };
478
+ } catch (err) {
479
+ return {
480
+ content: [{ type: "text", text: `\uC624\uB958: ${describeError(err)}` }],
493
481
  isError: true
494
482
  };
495
483
  }
496
- const form = extractFormFields(result.blocks);
497
- return {
498
- content: [{ type: "text", text: JSON.stringify(form, null, 2) }]
499
- };
500
- } catch (err) {
501
- return {
502
- content: [{ type: "text", text: `\uC624\uB958: ${describeError(err)}` }],
503
- isError: true
504
- };
505
484
  }
506
- }
507
- );
485
+ );
486
+ server2.tool(
487
+ "parse_chunks",
488
+ "\uBB38\uC11C\uB97C RAG\uC6A9 \uAD6C\uC870 \uCCAD\uD06C JSON\uC73C\uB85C \uD30C\uC2F1\uD569\uB2C8\uB2E4. \uD5E4\uB529\xB7\uAC1C\uC870\uC2DD \uC704\uACC4(\u25A1\u25CB- / 1.\xB7\uAC00.\xB71))\uAC00 breadcrumb \uACBD\uB85C\uB85C \uBCF4\uC874\uB418\uACE0 \uD45C\uB294 \uB3C5\uB9BD \uCCAD\uD06C\uB85C \uB098\uC635\uB2C8\uB2E4 \u2014 \uC784\uBCA0\uB529\xB7\uC778\uB371\uC2F1 \uC804\uCC98\uB9AC\uC6A9. \uC790\uB974\uAE30(\uD1A0\uD070 \uC0C1\uD55C\xB7\uC624\uBC84\uB7A9)\uB294 \uC18C\uBE44\uC790 \uBAAB\uC785\uB2C8\uB2E4.",
489
+ {
490
+ file_path: z.string().min(1).describe("\uB300\uC0C1 \uBB38\uC11C\uC758 \uC808\uB300 \uACBD\uB85C (HWP/HWPX/PDF/XLSX/DOCX)"),
491
+ granularity: z.enum(["section", "block"]).default("section").describe("section=\uAC19\uC740 breadcrumb \uC544\uB798 \uC5F0\uC18D \uD14D\uC2A4\uD2B8 \uBCD1\uD569(\uAE30\uBCF8), block=IRBlock 1\uAC1C=\uCCAD\uD06C 1\uAC1C"),
492
+ include_table_cells: z.boolean().default(false).describe("\uD45C \uCCAD\uD06C\uC5D0 \uC140 \uD14D\uC2A4\uD2B8 2\uCC28\uC6D0 \uBC30\uC5F4 \uD3EC\uD568 \uC5EC\uBD80")
493
+ },
494
+ async ({ file_path, granularity, include_table_cells }) => {
495
+ try {
496
+ const { buffer } = await readValidatedFile(file_path, MAX_FILE_SIZE, PARSE_EXTENSIONS);
497
+ const parsed = await parse(buffer, { filePath: file_path });
498
+ if (!parsed.success) {
499
+ return { content: [{ type: "text", text: `\uD30C\uC2F1 \uC2E4\uD328: ${parsed.error}` }], isError: true };
500
+ }
501
+ const { blocksToChunks } = await import("./chunks-SX77P5CV.js");
502
+ const chunks = blocksToChunks(parsed.blocks, { granularity, includeTableCells: include_table_cells });
503
+ return { content: [{ type: "text", text: capResponseText(JSON.stringify(chunks, null, 2)) }] };
504
+ } catch (err) {
505
+ return { content: [{ type: "text", text: `\uCCAD\uD06C \uD30C\uC2F1 \uC2E4\uD328: ${describeError(err)}` }], isError: true };
506
+ }
507
+ }
508
+ );
509
+ }
510
+
511
+ // src/mcp/tools-form.ts
512
+ import { z as z2 } from "zod";
513
+ import { readFile as readFile2, writeFile, mkdir, stat as stat2 } from "fs/promises";
514
+ import { extname as extname2, dirname as dirname2 } from "path";
508
515
  function buildFillInputs(fields, formats) {
509
516
  const out = {};
510
517
  for (const [k, v] of Object.entries(fields)) {
@@ -513,705 +520,739 @@ function buildFillInputs(fields, formats) {
513
520
  }
514
521
  return out;
515
522
  }
516
- server.tool(
517
- "fill_form",
518
- "\uD55C\uAD6D \uC11C\uC2DD \uBB38\uC11C\uC758 \uBE48\uCE78\uC744 \uCC44\uC6CC\uC11C \uC0C8 \uBB38\uC11C\uB85C \uCD9C\uB825\uD569\uB2C8\uB2E4. hwpx-preserve\uB97C \uC0AC\uC6A9\uD558\uBA74 \uC6D0\uBCF8 \uC11C\uC2DD(\uD14C\uB450\uB9AC, \uD3F0\uD2B8, \uBCD1\uD569 \uB4F1)\uC744 100% \uC720\uC9C0\uD569\uB2C8\uB2E4. HWPX \uB204\uB984\uD2C0(CLICK_HERE) \uD544\uB4DC\uB294 \uC774\uB984\uC73C\uB85C \uC815\uD655 \uB9E4\uCE6D\uB418\uC5B4 \uC6B0\uC120 \uCC44\uC6CC\uC9D1\uB2C8\uB2E4. file_path \uB300\uC2E0 template\uC73C\uB85C \uB0B4\uC7A5 \uC815\uBD80 \uD45C\uC900 \uAE30\uC548\uBB38 \uC11C\uC2DD\uC744 \uC4F8 \uC218 \uC788\uC2B5\uB2C8\uB2E4 \u2014 gian(\uC77C\uBC18\uAE30\uC548\uBB38, \uBCC4\uC9C0 \uC81C1\uD638\uC11C\uC2DD: \uD589\uC815\uAE30\uAD00\uBA85\xB7\uC218\uC2E0\uC790\xB7\uACBD\uC720\xB7\uC81C\uBAA9\xB7\uBCF8\uBB38\xB7\uBD99\uC784\xB7\uBC1C\uC2E0\uBA85\uC758\xB7\uAE30\uC548\uC790\xB7\uAC80\uD1A0\uC790\xB7\uACB0\uC7AC\uAD8C\uC790 \uB4F1 23\uD544\uB4DC) / gian-simple(\uAC04\uC774\uAE30\uC548\uBB38, \uBCC4\uC9C0 \uC81C2\uD638\uC11C\uC2DD: \uB0B4\uBD80\uACB0\uC7AC\uC6A9 13\uD544\uB4DC).",
519
- {
520
- file_path: z.string().min(1).optional().describe("\uC11C\uC2DD \uD15C\uD50C\uB9BF \uBB38\uC11C\uC758 \uC808\uB300 \uACBD\uB85C (HWP, HWPX, PDF, XLSX, DOCX). template \uC0AC\uC6A9 \uC2DC \uC0DD\uB7B5"),
521
- template: z.enum(["gian", "gian-simple", "\uC77C\uBC18\uAE30\uC548\uBB38", "\uAC04\uC774\uAE30\uC548\uBB38"]).optional().describe("\uB0B4\uC7A5 \uC815\uBD80 \uD45C\uC900 \uC11C\uC2DD \uC774\uB984 \u2014 file_path \uB300\uC2E0 \uC0AC\uC6A9. gian=\uC77C\uBC18\uAE30\uC548\uBB38(\uBCC4\uC9C0 \uC81C1\uD638\uC11C\uC2DD), gian-simple=\uAC04\uC774\uAE30\uC548\uBB38(\uBCC4\uC9C0 \uC81C2\uD638\uC11C\uC2DD)"),
522
- fields: z.record(z.string(), z.string()).describe('\uCC44\uC6B8 \uD544\uB4DC \uB9F5 (\uB77C\uBCA8 \u2192 \uAC12). \uC608: {"\uC131\uBA85": "\uD64D\uAE38\uB3D9", "\uC804\uD654\uBC88\uD638": "010-1234-5678"}'),
523
- formats: z.record(z.string(), z.string()).optional().describe("\uD544\uB4DC\uBCC4 \uAC12 \uC11C\uC2DD (\uB77C\uBCA8 \u2192 \uD3EC\uB9F7). \uC815\uC900\uAC12 \uD558\uB098\uB85C \uC11C\uC2DD\uB9C8\uB2E4 \uB2E4\uB978 \uBAA8\uC591\uC744 \uCC44\uC6B8 \uB54C: date:yy.mm.dd / phone:hyphen\xB7dot\xB7digits / rrn:hyphen\xB7masked / mask:###-## / \uC790\uC720 \uD328\uD134(yyyy\uB144 m\uC6D4 d\uC77C, ###-####-####)"),
524
- require_unique: z.boolean().optional().describe("\uD55C \uD0A4\uAC00 \uC11C\uC2DD\uC758 2\uACF3 \uC774\uC0C1\uC5D0 \uB9E4\uCE6D\uB418\uBA74 \uCC44\uC6B0\uC9C0 \uC54A\uACE0 \uAC70\uBD80 \u2014 \uBC18\uBCF5 \uB77C\uBCA8 \uC591\uC2DD\uC5D0\uC11C \uB0A8\uC758 \uBE14\uB85D \uC624\uC5FC \uBC29\uC9C0 (\uBC30\uC5F4 \uAC12\uC740 \uC608\uC678)"),
525
- mask_values: z.boolean().optional().describe("\uC751\uB2F5\uC5D0 \uAC12 \uB300\uC2E0 \uAE00\uC790\uC218\uB9CC \uD45C\uC2DC \u2014 \uAC1C\uC778\uC815\uBCF4 \uCC44\uC6C0 \uC2DC \uAC12\uC774 \uB300\uD654 \uB85C\uADF8\uC5D0 \uB0A8\uC9C0 \uC54A\uAC8C"),
526
- output_format: z.enum(["markdown", "hwpx", "hwpx-preserve"]).default("hwpx-preserve").describe("\uCD9C\uB825 \uD3EC\uB9F7: hwpx-preserve (\uC6D0\uBCF8 \uC2A4\uD0C0\uC77C \uBCF4\uC874, HWPX \uC804\uC6A9), hwpx (\uC0C8 HWPX \uC0DD\uC131), markdown"),
527
- output_path: z.string().optional().describe("\uCD9C\uB825 \uD30C\uC77C \uC800\uC7A5 \uACBD\uB85C (\uC120\uD0DD). \uC9C0\uC815 \uC2DC \uD30C\uC77C\uB85C \uC800\uC7A5, \uBBF8\uC9C0\uC815 \uC2DC \uD14D\uC2A4\uD2B8\uB85C \uBC18\uD658")
528
- },
529
- async ({ file_path, template, fields, formats, require_unique, mask_values, output_format, output_path }) => {
530
- try {
531
- const outExts = output_format === "markdown" ? /* @__PURE__ */ new Set([".md", ".markdown", ".txt"]) : /* @__PURE__ */ new Set([".hwpx"]);
532
- const outPath = output_path ? safeOutputPath(output_path, outExts) : void 0;
533
- let buffer;
534
- if (template) {
535
- const t = resolveBuiltinTemplate(template);
536
- if (!t) {
523
+ function registerFormTools(server2) {
524
+ server2.tool(
525
+ "parse_form",
526
+ "\uD55C\uAD6D \uC11C\uC2DD \uBB38\uC11C\uC5D0\uC11C \uB808\uC774\uBE14-\uAC12 \uC30D\uC744 \uAD6C\uC870\uD654\uB41C JSON\uC73C\uB85C \uCD94\uCD9C\uD569\uB2C8\uB2E4. \uC591\uC2DD/\uC11C\uC2DD \uBB38\uC11C\uC5D0 \uCD5C\uC801\uD654.",
527
+ {
528
+ file_path: z2.string().min(1).describe("\uC11C\uC2DD \uBB38\uC11C \uD30C\uC77C\uC758 \uC808\uB300 \uACBD\uB85C")
529
+ },
530
+ async ({ file_path }) => {
531
+ try {
532
+ const { buffer } = await readValidatedFile(file_path);
533
+ const result = await parse(buffer, { keepTrailingEmptyCols: true });
534
+ if (!result.success) {
537
535
  return {
538
- content: [{ type: "text", text: `\uC54C \uC218 \uC5C6\uB294 \uB0B4\uC7A5 \uD15C\uD50C\uB9BF: ${template} (\uC0AC\uC6A9 \uAC00\uB2A5: ${BUILTIN_TEMPLATES.map((x) => `${x.id}(${x.aliases[0]})`).join(", ")})` }],
536
+ content: [{ type: "text", text: `\uD30C\uC2F1 \uC2E4\uD328: ${result.error}` }],
539
537
  isError: true
540
538
  };
541
539
  }
542
- buffer = readBuiltinTemplate(t);
543
- } else if (file_path) {
544
- buffer = (await readValidatedFile(file_path)).buffer;
545
- } else {
540
+ const form = extractFormFields(result.blocks);
541
+ return {
542
+ content: [{ type: "text", text: JSON.stringify(form, null, 2) }]
543
+ };
544
+ } catch (err) {
546
545
  return {
547
- content: [{ type: "text", text: "file_path \uB610\uB294 template \uC911 \uD558\uB098\uB97C \uC9C0\uC815\uD574\uC8FC\uC138\uC694" }],
546
+ content: [{ type: "text", text: `\uC624\uB958: ${describeError(err)}` }],
548
547
  isError: true
549
548
  };
550
549
  }
551
- if (output_format === "hwpx-preserve") {
552
- const format = detectFormat(buffer);
553
- let isHwpx = format === "hwpx";
554
- if (isHwpx) {
555
- const zipFormat = await detectZipFormat(buffer);
556
- isHwpx = zipFormat === "hwpx";
557
- }
558
- if (!isHwpx) {
550
+ }
551
+ );
552
+ server2.tool(
553
+ "fill_form",
554
+ "\uD55C\uAD6D \uC11C\uC2DD \uBB38\uC11C\uC758 \uBE48\uCE78\uC744 \uCC44\uC6CC\uC11C \uC0C8 \uBB38\uC11C\uB85C \uCD9C\uB825\uD569\uB2C8\uB2E4. hwpx-preserve\uB97C \uC0AC\uC6A9\uD558\uBA74 \uC6D0\uBCF8 \uC11C\uC2DD(\uD14C\uB450\uB9AC, \uD3F0\uD2B8, \uBCD1\uD569 \uB4F1)\uC744 100% \uC720\uC9C0\uD569\uB2C8\uB2E4. HWPX \uB204\uB984\uD2C0(CLICK_HERE) \uD544\uB4DC\uB294 \uC774\uB984\uC73C\uB85C \uC815\uD655 \uB9E4\uCE6D\uB418\uC5B4 \uC6B0\uC120 \uCC44\uC6CC\uC9D1\uB2C8\uB2E4. file_path \uB300\uC2E0 template\uC73C\uB85C \uB0B4\uC7A5 \uC815\uBD80 \uD45C\uC900 \uAE30\uC548\uBB38 \uC11C\uC2DD\uC744 \uC4F8 \uC218 \uC788\uC2B5\uB2C8\uB2E4 \u2014 gian(\uC77C\uBC18\uAE30\uC548\uBB38, \uBCC4\uC9C0 \uC81C1\uD638\uC11C\uC2DD: \uD589\uC815\uAE30\uAD00\uBA85\xB7\uC218\uC2E0\uC790\xB7\uACBD\uC720\xB7\uC81C\uBAA9\xB7\uBCF8\uBB38\xB7\uBD99\uC784\xB7\uBC1C\uC2E0\uBA85\uC758\xB7\uAE30\uC548\uC790\xB7\uAC80\uD1A0\uC790\xB7\uACB0\uC7AC\uAD8C\uC790 \uB4F1 23\uD544\uB4DC) / gian-simple(\uAC04\uC774\uAE30\uC548\uBB38, \uBCC4\uC9C0 \uC81C2\uD638\uC11C\uC2DD: \uB0B4\uBD80\uACB0\uC7AC\uC6A9 13\uD544\uB4DC).",
555
+ {
556
+ file_path: z2.string().min(1).optional().describe("\uC11C\uC2DD \uD15C\uD50C\uB9BF \uBB38\uC11C\uC758 \uC808\uB300 \uACBD\uB85C (HWP, HWPX, PDF, XLSX, DOCX). template \uC0AC\uC6A9 \uC2DC \uC0DD\uB7B5"),
557
+ template: z2.enum(["gian", "gian-simple", "\uC77C\uBC18\uAE30\uC548\uBB38", "\uAC04\uC774\uAE30\uC548\uBB38"]).optional().describe("\uB0B4\uC7A5 \uC815\uBD80 \uD45C\uC900 \uC11C\uC2DD \uC774\uB984 \u2014 file_path \uB300\uC2E0 \uC0AC\uC6A9. gian=\uC77C\uBC18\uAE30\uC548\uBB38(\uBCC4\uC9C0 \uC81C1\uD638\uC11C\uC2DD), gian-simple=\uAC04\uC774\uAE30\uC548\uBB38(\uBCC4\uC9C0 \uC81C2\uD638\uC11C\uC2DD)"),
558
+ fields: z2.record(z2.string(), z2.string()).describe('\uCC44\uC6B8 \uD544\uB4DC \uB9F5 (\uB77C\uBCA8 \u2192 \uAC12). \uC608: {"\uC131\uBA85": "\uD64D\uAE38\uB3D9", "\uC804\uD654\uBC88\uD638": "010-1234-5678"}'),
559
+ formats: z2.record(z2.string(), z2.string()).optional().describe("\uD544\uB4DC\uBCC4 \uAC12 \uC11C\uC2DD (\uB77C\uBCA8 \u2192 \uD3EC\uB9F7). \uC815\uC900\uAC12 \uD558\uB098\uB85C \uC11C\uC2DD\uB9C8\uB2E4 \uB2E4\uB978 \uBAA8\uC591\uC744 \uCC44\uC6B8 \uB54C: date:yy.mm.dd / phone:hyphen\xB7dot\xB7digits / rrn:hyphen\xB7masked / mask:###-## / \uC790\uC720 \uD328\uD134(yyyy\uB144 m\uC6D4 d\uC77C, ###-####-####)"),
560
+ require_unique: z2.boolean().optional().describe("\uD55C \uD0A4\uAC00 \uC11C\uC2DD\uC758 2\uACF3 \uC774\uC0C1\uC5D0 \uB9E4\uCE6D\uB418\uBA74 \uCC44\uC6B0\uC9C0 \uC54A\uACE0 \uAC70\uBD80 \u2014 \uBC18\uBCF5 \uB77C\uBCA8 \uC591\uC2DD\uC5D0\uC11C \uB0A8\uC758 \uBE14\uB85D \uC624\uC5FC \uBC29\uC9C0 (\uBC30\uC5F4 \uAC12\uC740 \uC608\uC678)"),
561
+ mask_values: z2.boolean().optional().describe("\uC751\uB2F5\uC5D0 \uAC12 \uB300\uC2E0 \uAE00\uC790\uC218\uB9CC \uD45C\uC2DC \u2014 \uAC1C\uC778\uC815\uBCF4 \uCC44\uC6C0 \uC2DC \uAC12\uC774 \uB300\uD654 \uB85C\uADF8\uC5D0 \uB0A8\uC9C0 \uC54A\uAC8C"),
562
+ output_format: z2.enum(["markdown", "hwpx", "hwpx-preserve"]).default("hwpx-preserve").describe("\uCD9C\uB825 \uD3EC\uB9F7: hwpx-preserve (\uC6D0\uBCF8 \uC2A4\uD0C0\uC77C \uBCF4\uC874, HWPX \uC804\uC6A9), hwpx (\uC0C8 HWPX \uC0DD\uC131), markdown"),
563
+ output_path: z2.string().optional().describe("\uCD9C\uB825 \uD30C\uC77C \uC800\uC7A5 \uACBD\uB85C (\uC120\uD0DD). \uC9C0\uC815 \uC2DC \uD30C\uC77C\uB85C \uC800\uC7A5, \uBBF8\uC9C0\uC815 \uC2DC \uD14D\uC2A4\uD2B8\uB85C \uBC18\uD658")
564
+ },
565
+ async ({ file_path, template, fields, formats, require_unique, mask_values, output_format, output_path }) => {
566
+ try {
567
+ const outExts = output_format === "markdown" ? /* @__PURE__ */ new Set([".md", ".markdown", ".txt"]) : /* @__PURE__ */ new Set([".hwpx"]);
568
+ const outPath = output_path ? safeOutputPath(output_path, outExts) : void 0;
569
+ let buffer;
570
+ if (template) {
571
+ const t = resolveBuiltinTemplate(template);
572
+ if (!t) {
573
+ return {
574
+ content: [{ type: "text", text: `\uC54C \uC218 \uC5C6\uB294 \uB0B4\uC7A5 \uD15C\uD50C\uB9BF: ${template} (\uC0AC\uC6A9 \uAC00\uB2A5: ${BUILTIN_TEMPLATES.map((x) => `${x.id}(${x.aliases[0]})`).join(", ")})` }],
575
+ isError: true
576
+ };
577
+ }
578
+ buffer = readBuiltinTemplate(t);
579
+ } else if (file_path) {
580
+ buffer = (await readValidatedFile(file_path)).buffer;
581
+ } else {
559
582
  return {
560
- content: [{ type: "text", text: `hwpx-preserve\uB294 HWPX \uD30C\uC77C\uB9CC \uC9C0\uC6D0\uD569\uB2C8\uB2E4 (\uAC10\uC9C0\uB41C \uD3EC\uB9F7: ${format}). hwpx \uB610\uB294 markdown\uC744 \uC0AC\uC6A9\uD558\uC138\uC694.` }],
583
+ content: [{ type: "text", text: "file_path \uB610\uB294 template \uC911 \uD558\uB098\uB97C \uC9C0\uC815\uD574\uC8FC\uC138\uC694" }],
561
584
  isError: true
562
585
  };
563
586
  }
564
- const inputs = buildFillInputs(fields, formats);
565
- const hwpxResult = require_unique ? await fillWithUniqueGuard(inputs, (vals, blocked) => fillHwpx(buffer, vals, blocked)) : { ...await fillHwpx(buffer, inputs), rejected: [] };
566
- let verifyLine = null;
567
- if (mask_values && hwpxResult.filled.length > 0) {
568
- const reparsed = await parse(Buffer.from(hwpxResult.buffer));
569
- const norm = (s) => s.replace(/\\([\\`*_{}[\]()#+.!|~>-])/g, "$1").replace(/\s+/g, " ");
570
- const normMd = reparsed.success ? norm(reparsed.markdown) : "";
571
- const okCount = reparsed.success ? hwpxResult.filled.filter((f) => f.value !== "" && normMd.includes(norm(f.value))).length : 0;
572
- verifyLine = `\uAC80\uC99D(\uB9C8\uC2A4\uD0B9): ${okCount}/${hwpxResult.filled.length} FILLED \u2014 \uC7AC\uD30C\uC2F1 \uB300\uC870, \uAC12 \uBBF8\uB178\uCD9C`;
573
- }
574
- const summary2 = [
575
- `\uCC44\uC6CC\uC9C4 \uD544\uB4DC: ${hwpxResult.filled.length}\uAC1C (\uC6D0\uBCF8 \uC2A4\uD0C0\uC77C \uBCF4\uC874)`,
576
- hwpxResult.rejected.length > 0 ? `\uBAA8\uD638 \uB77C\uBCA8 \uAC70\uBD80(2\uACF3+ \uB9E4\uCE6D): ${hwpxResult.rejected.join(", ")}` : null,
577
- hwpxResult.unmatched.length > 0 ? `\uB9E4\uCE6D \uC2E4\uD328: ${hwpxResult.unmatched.join(", ")}` : null,
578
- verifyLine
579
- ].filter(Boolean).join(" | ");
580
- const filledList = hwpxResult.filled.map((f) => ` - ${f.label}: ${mask_values ? `[${[...f.value].length}\uC790]` : f.value}`).join("\n");
581
- if (outPath) {
582
- await mkdir(dirname(outPath), { recursive: true });
583
- await writeFile(outPath, Buffer.from(hwpxResult.buffer));
584
- return {
585
- content: [{ type: "text", text: `[${summary2}]
587
+ if (output_format === "hwpx-preserve") {
588
+ let format = detectFormat(buffer);
589
+ if (format === "hwpx") {
590
+ const z5 = await detectZipFormat(buffer);
591
+ if (z5 !== "unknown") format = z5;
592
+ }
593
+ const isHwpx = format === "hwpx";
594
+ if (!isHwpx) {
595
+ return {
596
+ content: [{ type: "text", text: `hwpx-preserve\uB294 HWPX \uD30C\uC77C\uB9CC \uC9C0\uC6D0\uD569\uB2C8\uB2E4 (\uAC10\uC9C0\uB41C \uD3EC\uB9F7: ${format}). hwpx \uB610\uB294 markdown\uC744 \uC0AC\uC6A9\uD558\uC138\uC694.` }],
597
+ isError: true
598
+ };
599
+ }
600
+ const inputs = buildFillInputs(fields, formats);
601
+ const hwpxResult = require_unique ? await fillWithUniqueGuard(inputs, (vals, blocked) => fillHwpx(buffer, vals, blocked)) : { ...await fillHwpx(buffer, inputs), rejected: [] };
602
+ let verifyLine = null;
603
+ if (mask_values && hwpxResult.filled.length > 0) {
604
+ const reparsed = await parse(Buffer.from(hwpxResult.buffer));
605
+ const norm = (s) => s.replace(/\\([\\`*_{}[\]()#+.!|~>-])/g, "$1").replace(/\s+/g, " ");
606
+ const normMd = reparsed.success ? norm(reparsed.markdown) : "";
607
+ const okCount = reparsed.success ? hwpxResult.filled.filter((f) => f.value !== "" && normMd.includes(norm(f.value))).length : 0;
608
+ verifyLine = `\uAC80\uC99D(\uB9C8\uC2A4\uD0B9): ${okCount}/${hwpxResult.filled.length} FILLED \u2014 \uC7AC\uD30C\uC2F1 \uB300\uC870, \uAC12 \uBBF8\uB178\uCD9C`;
609
+ }
610
+ const summary2 = [
611
+ `\uCC44\uC6CC\uC9C4 \uD544\uB4DC: ${hwpxResult.filled.length}\uAC1C (\uC6D0\uBCF8 \uC2A4\uD0C0\uC77C \uBCF4\uC874)`,
612
+ hwpxResult.rejected.length > 0 ? `\uBAA8\uD638 \uB77C\uBCA8 \uAC70\uBD80(2\uACF3+ \uB9E4\uCE6D): ${hwpxResult.rejected.join(", ")}` : null,
613
+ hwpxResult.unmatched.length > 0 ? `\uB9E4\uCE6D \uC2E4\uD328: ${hwpxResult.unmatched.join(", ")}` : null,
614
+ verifyLine
615
+ ].filter(Boolean).join(" | ");
616
+ const filledList = hwpxResult.filled.map((f) => ` - ${f.label}: ${mask_values ? `[${[...f.value].length}\uC790]` : f.value}`).join("\n");
617
+ if (outPath) {
618
+ await mkdir(dirname2(outPath), { recursive: true });
619
+ await writeFile(outPath, Buffer.from(hwpxResult.buffer));
620
+ return {
621
+ content: [{ type: "text", text: `[${summary2}]
586
622
 
587
623
  \uCC44\uC6CC\uC9C4 \uD544\uB4DC:
588
624
  ${filledList}
589
625
 
590
626
  HWPX \uD30C\uC77C \uC800\uC7A5 (\uC6D0\uBCF8 \uC11C\uC2DD \uC720\uC9C0): ${outPath}` }]
591
- };
592
- }
593
- return {
594
- content: [{ type: "text", text: `[${summary2}]
627
+ };
628
+ }
629
+ return {
630
+ content: [{ type: "text", text: `[${summary2}]
595
631
 
596
632
  \uCC44\uC6CC\uC9C4 \uD544\uB4DC:
597
633
  ${filledList}
598
634
 
599
635
  \u26A0\uFE0F output_path\uB97C \uC9C0\uC815\uD558\uBA74 \uC6D0\uBCF8 \uC11C\uC2DD\uC774 \uC720\uC9C0\uB41C HWPX \uD30C\uC77C\uB85C \uC800\uC7A5\uB429\uB2C8\uB2E4.` }]
600
- };
601
- }
602
- const result = await parse(buffer, { keepTrailingEmptyCols: true });
603
- if (!result.success) {
604
- return {
605
- content: [{ type: "text", text: `\uD30C\uC2F1 \uC2E4\uD328: ${result.error}` }],
606
- isError: true
607
- };
608
- }
609
- const formInfo = extractFormFields(result.blocks);
610
- const irInputs = buildFillInputs(fields, formats);
611
- const fillResult = require_unique ? await fillWithUniqueGuard(irInputs, (vals, blocked) => fillFormFields(result.blocks, vals, blocked)) : { ...fillFormFields(result.blocks, irInputs), rejected: [] };
612
- if (fillResult.filled.length === 0 && formInfo.fields.length === 0) {
613
- return {
614
- content: [{ type: "text", text: `\uC11C\uC2DD \uD544\uB4DC\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uC77C\uBC18 \uBB38\uC11C\uC774\uAC70\uB098 \uC11C\uC2DD \uD328\uD134\uC774 \uAC10\uC9C0\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.` }],
615
- isError: true
616
- };
617
- }
618
- const markdown = blocksToMarkdown(fillResult.blocks);
619
- const previewMd = mask_values ? "\u26A0\uFE0F mask_values \uD65C\uC131 \u2014 \uAC1C\uC778\uC815\uBCF4 \uB178\uCD9C \uBC29\uC9C0\uB97C \uC704\uD574 \uBCF8\uBB38\uC744 \uC751\uB2F5\uC5D0 \uD3EC\uD568\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. output_path \uB85C \uD30C\uC77C \uC800\uC7A5 \uD6C4 \uD655\uC778\uD558\uC138\uC694." : markdown;
620
- const summary = [
621
- `\uCC44\uC6CC\uC9C4 \uD544\uB4DC: ${fillResult.filled.length}\uAC1C`,
622
- fillResult.rejected.length > 0 ? `\uBAA8\uD638 \uB77C\uBCA8 \uAC70\uBD80(2\uACF3+ \uB9E4\uCE6D): ${fillResult.rejected.join(", ")}` : null,
623
- fillResult.unmatched.length > 0 ? `\uB9E4\uCE6D \uC2E4\uD328: ${fillResult.unmatched.join(", ")}` : null,
624
- formInfo.fields.length > 0 ? `\uC11C\uC2DD \uD544\uB4DC: ${formInfo.fields.length}\uAC1C (\uD655\uC2E0\uB3C4 ${(formInfo.confidence * 100).toFixed(0)}%)` : null
625
- ].filter(Boolean).join(" | ");
626
- if (output_format === "hwpx") {
627
- const hwpxBuffer = await markdownToHwpx(markdown);
628
- if (outPath) {
629
- await mkdir(dirname(outPath), { recursive: true });
630
- await writeFile(outPath, Buffer.from(hwpxBuffer));
636
+ };
637
+ }
638
+ const result = await parse(buffer, { keepTrailingEmptyCols: true });
639
+ if (!result.success) {
631
640
  return {
632
- content: [{ type: "text", text: `[${summary}]
633
-
634
- HWPX \uD30C\uC77C \uC800\uC7A5: ${outPath}` }]
641
+ content: [{ type: "text", text: `\uD30C\uC2F1 \uC2E4\uD328: ${result.error}` }],
642
+ isError: true
635
643
  };
636
644
  }
637
- return {
638
- content: [{ type: "text", text: `[${summary}]
645
+ const formInfo = extractFormFields(result.blocks);
646
+ const irInputs = buildFillInputs(fields, formats);
647
+ const fillResult = require_unique ? await fillWithUniqueGuard(irInputs, (vals, blocked) => fillFormFields(result.blocks, vals, blocked)) : { ...fillFormFields(result.blocks, irInputs), rejected: [] };
648
+ if (fillResult.filled.length === 0 && formInfo.fields.length === 0) {
649
+ return {
650
+ content: [{ type: "text", text: `\uC11C\uC2DD \uD544\uB4DC\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uC77C\uBC18 \uBB38\uC11C\uC774\uAC70\uB098 \uC11C\uC2DD \uD328\uD134\uC774 \uAC10\uC9C0\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.` }],
651
+ isError: true
652
+ };
653
+ }
654
+ const markdown = blocksToMarkdown(fillResult.blocks);
655
+ const previewMd = mask_values ? "\u26A0\uFE0F mask_values \uD65C\uC131 \u2014 \uAC1C\uC778\uC815\uBCF4 \uB178\uCD9C \uBC29\uC9C0\uB97C \uC704\uD574 \uBCF8\uBB38\uC744 \uC751\uB2F5\uC5D0 \uD3EC\uD568\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. output_path \uB85C \uD30C\uC77C \uC800\uC7A5 \uD6C4 \uD655\uC778\uD558\uC138\uC694." : markdown;
656
+ const summary = [
657
+ `\uCC44\uC6CC\uC9C4 \uD544\uB4DC: ${fillResult.filled.length}\uAC1C`,
658
+ fillResult.rejected.length > 0 ? `\uBAA8\uD638 \uB77C\uBCA8 \uAC70\uBD80(2\uACF3+ \uB9E4\uCE6D): ${fillResult.rejected.join(", ")}` : null,
659
+ fillResult.unmatched.length > 0 ? `\uB9E4\uCE6D \uC2E4\uD328: ${fillResult.unmatched.join(", ")}` : null,
660
+ formInfo.fields.length > 0 ? `\uC11C\uC2DD \uD544\uB4DC: ${formInfo.fields.length}\uAC1C (\uD655\uC2E0\uB3C4 ${(formInfo.confidence * 100).toFixed(0)}%)` : null
661
+ ].filter(Boolean).join(" | ");
662
+ if (output_format === "hwpx") {
663
+ const hwpxBuffer = await markdownToHwpx(markdown);
664
+ if (outPath) {
665
+ await mkdir(dirname2(outPath), { recursive: true });
666
+ await writeFile(outPath, Buffer.from(hwpxBuffer));
667
+ return {
668
+ content: [{ type: "text", text: `[${summary}]
669
+
670
+ HWPX \uD30C\uC77C \uC800\uC7A5: ${outPath}` }]
671
+ };
672
+ }
673
+ return {
674
+ content: [{ type: "text", text: `[${summary}]
639
675
 
640
676
  \u26A0\uFE0F output_path\uB97C \uC9C0\uC815\uD558\uBA74 HWPX \uD30C\uC77C\uB85C \uC800\uC7A5\uB429\uB2C8\uB2E4. \uBBF8\uB9AC\uBCF4\uAE30:
641
677
 
642
678
  ${previewMd}` }]
643
- };
644
- }
645
- if (outPath) {
646
- await mkdir(dirname(outPath), { recursive: true });
647
- await writeFile(outPath, markdown, "utf-8");
648
- return {
649
- content: [{ type: "text", text: `[${summary}]
679
+ };
680
+ }
681
+ if (outPath) {
682
+ await mkdir(dirname2(outPath), { recursive: true });
683
+ await writeFile(outPath, markdown, "utf-8");
684
+ return {
685
+ content: [{ type: "text", text: `[${summary}]
650
686
 
651
687
  \uB9C8\uD06C\uB2E4\uC6B4 \uD30C\uC77C \uC800\uC7A5: ${outPath}
652
688
 
653
689
  ${previewMd}` }]
654
- };
655
- }
656
- return {
657
- content: [{ type: "text", text: `[${summary}]
690
+ };
691
+ }
692
+ return {
693
+ content: [{ type: "text", text: `[${summary}]
658
694
 
659
695
  ${previewMd}` }]
660
- };
661
- } catch (err) {
662
- return {
663
- content: [{ type: "text", text: `\uC624\uB958: ${describeError(err)}` }],
664
- isError: true
665
- };
666
- }
667
- }
668
- );
669
- server.tool(
670
- "place_seal",
671
- '\uB3C4\uC7A5/\uC11C\uBA85 \uC774\uBBF8\uC9C0\uB97C \uC575\uCEE4 \uBB38\uAD6C("(\uC778)"\xB7"\uC11C\uBA85 \uB610\uB294 \uC778" \uB4F1) \uC704\uC5D0 \uBD80\uC720(\uAE00 \uC55E) \uBC30\uCE58\uD569\uB2C8\uB2E4. \uD45C/\uD398\uC774\uC9C0\uB97C \uD0A4\uC6B0\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 (HWPX \uC804\uC6A9).',
672
- {
673
- file_path: z.string().min(1).describe("\uB300\uC0C1 HWPX \uBB38\uC11C\uC758 \uC808\uB300 \uACBD\uB85C"),
674
- image_path: z.string().min(1).describe("\uB3C4\uC7A5/\uC11C\uBA85 \uC774\uBBF8\uC9C0 \uC808\uB300 \uACBD\uB85C (\uD22C\uBA85 \uBC30\uACBD PNG \uAD8C\uC7A5)"),
675
- anchor: z.string().default("(\uC778)").describe("\uC575\uCEE4 \uBB38\uAD6C \u2014 \uC774 \uBB38\uAD6C \uAE30\uC900\uC73C\uB85C \uBC30\uCE58"),
676
- occurrence: z.number().int().min(0).default(0).describe("\uAC19\uC740 \uC575\uCEE4\uAC00 \uC5EC\uB7FF\uC77C \uB54C 0-based \uC120\uD0DD"),
677
- size_mm: z.number().positive().optional().describe("\uB3C4\uC7A5 \uD55C \uBCC0 \uD06C\uAE30 mm (\uAE30\uBCF8: \uC904\uB192\uC774\xD71.6, 7~18 \uD074\uB7A8\uD504)"),
678
- mode: z.enum(["overlap", "right", "auto"]).default("auto").describe("overlap=\uBB38\uAD6C \uC704 \uACB9\uCE68, right=\uBB38\uAD6C \uC624\uB978\uCABD \uC606, auto=\uACF5\uAC04 \uC788\uC73C\uBA74 right"),
679
- dx_mm: z.number().optional().describe("x \uBBF8\uC138\uC870\uC815 mm"),
680
- dy_mm: z.number().optional().describe("y \uBBF8\uC138\uC870\uC815 mm"),
681
- output_path: z.string().min(1).describe("\uCD9C\uB825 HWPX \uC800\uC7A5 \uACBD\uB85C")
682
- },
683
- async ({ file_path, image_path, anchor, occurrence, size_mm, mode, dx_mm, dy_mm, output_path }) => {
684
- try {
685
- const outPath = safeOutputPath(output_path, /* @__PURE__ */ new Set([".hwpx"]));
686
- const { buffer } = await readValidatedFile(file_path);
687
- const format = detectFormat(buffer);
688
- if (format !== "hwpx") {
696
+ };
697
+ } catch (err) {
689
698
  return {
690
- content: [{ type: "text", text: `place_seal \uC740 HWPX \uD30C\uC77C\uB9CC \uC9C0\uC6D0\uD569\uB2C8\uB2E4 (\uAC10\uC9C0\uB41C \uD3EC\uB9F7: ${format}).` }],
699
+ content: [{ type: "text", text: `\uC624\uB958: ${describeError(err)}` }],
691
700
  isError: true
692
701
  };
693
702
  }
694
- const imgResolved = safePath(image_path, IMAGE_EXTENSIONS);
695
- const imgSize = (await stat(imgResolved)).size;
696
- if (imgSize > 500 * 1024 * 1024) {
697
- return { content: [{ type: "text", text: `\uB3C4\uC7A5 \uC774\uBBF8\uC9C0\uAC00 \uB108\uBB34 \uD07D\uB2C8\uB2E4 (${(imgSize / 1024 / 1024).toFixed(0)}MB) \u2014 500MB \uC774\uD558\uC5EC\uC57C \uD569\uB2C8\uB2E4.` }], isError: true };
698
- }
699
- const image = new Uint8Array(await readFile(imgResolved));
700
- const ext = extname(imgResolved).slice(1).toLowerCase();
701
- const { placeSealHwpx } = await import("./seal-JQWSNHRG.js");
702
- const result = await placeSealHwpx(buffer, [{
703
- anchor,
704
- occurrence,
705
- image,
706
- ext,
707
- sizeMm: size_mm,
708
- mode,
709
- dxMm: dx_mm,
710
- dyMm: dy_mm
711
- }]);
712
- await mkdir(dirname(outPath), { recursive: true });
713
- await writeFile(outPath, Buffer.from(result.buffer));
714
- const p0 = result.placed[0];
715
- const warnLines = (p0.warnings ?? []).map((w) => `
703
+ }
704
+ );
705
+ server2.tool(
706
+ "place_seal",
707
+ '\uB3C4\uC7A5/\uC11C\uBA85 \uC774\uBBF8\uC9C0\uB97C \uC575\uCEE4 \uBB38\uAD6C("(\uC778)"\xB7"\uC11C\uBA85 \uB610\uB294 \uC778" \uB4F1) \uC704\uC5D0 \uBD80\uC720(\uAE00 \uC55E) \uBC30\uCE58\uD569\uB2C8\uB2E4. \uD45C/\uD398\uC774\uC9C0\uB97C \uD0A4\uC6B0\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4 (HWPX \uC804\uC6A9).',
708
+ {
709
+ file_path: z2.string().min(1).describe("\uB300\uC0C1 HWPX \uBB38\uC11C\uC758 \uC808\uB300 \uACBD\uB85C"),
710
+ image_path: z2.string().min(1).describe("\uB3C4\uC7A5/\uC11C\uBA85 \uC774\uBBF8\uC9C0 \uC808\uB300 \uACBD\uB85C (\uD22C\uBA85 \uBC30\uACBD PNG \uAD8C\uC7A5)"),
711
+ anchor: z2.string().default("(\uC778)").describe("\uC575\uCEE4 \uBB38\uAD6C \u2014 \uC774 \uBB38\uAD6C \uAE30\uC900\uC73C\uB85C \uBC30\uCE58"),
712
+ occurrence: z2.number().int().min(0).default(0).describe("\uAC19\uC740 \uC575\uCEE4\uAC00 \uC5EC\uB7FF\uC77C \uB54C 0-based \uC120\uD0DD"),
713
+ size_mm: z2.number().positive().optional().describe("\uB3C4\uC7A5 \uD55C \uBCC0 \uD06C\uAE30 mm (\uAE30\uBCF8: \uC904\uB192\uC774\xD71.6, 7~18 \uD074\uB7A8\uD504)"),
714
+ mode: z2.enum(["overlap", "right", "auto"]).default("auto").describe("overlap=\uBB38\uAD6C \uC704 \uACB9\uCE68, right=\uBB38\uAD6C \uC624\uB978\uCABD \uC606, auto=\uACF5\uAC04 \uC788\uC73C\uBA74 right"),
715
+ dx_mm: z2.number().optional().describe("x \uBBF8\uC138\uC870\uC815 mm"),
716
+ dy_mm: z2.number().optional().describe("y \uBBF8\uC138\uC870\uC815 mm"),
717
+ output_path: z2.string().min(1).describe("\uCD9C\uB825 HWPX \uC800\uC7A5 \uACBD\uB85C")
718
+ },
719
+ async ({ file_path, image_path, anchor, occurrence, size_mm, mode, dx_mm, dy_mm, output_path }) => {
720
+ try {
721
+ const outPath = safeOutputPath(output_path, /* @__PURE__ */ new Set([".hwpx"]));
722
+ const { buffer } = await readValidatedFile(file_path);
723
+ const format = detectFormat(buffer);
724
+ if (format !== "hwpx") {
725
+ return {
726
+ content: [{ type: "text", text: `place_seal \uC740 HWPX \uD30C\uC77C\uB9CC \uC9C0\uC6D0\uD569\uB2C8\uB2E4 (\uAC10\uC9C0\uB41C \uD3EC\uB9F7: ${format}).` }],
727
+ isError: true
728
+ };
729
+ }
730
+ const imgResolved = safePath(image_path, IMAGE_EXTENSIONS);
731
+ const imgSize = (await stat2(imgResolved)).size;
732
+ if (imgSize > 500 * 1024 * 1024) {
733
+ return { content: [{ type: "text", text: `\uB3C4\uC7A5 \uC774\uBBF8\uC9C0\uAC00 \uB108\uBB34 \uD07D\uB2C8\uB2E4 (${(imgSize / 1024 / 1024).toFixed(0)}MB) \u2014 500MB \uC774\uD558\uC5EC\uC57C \uD569\uB2C8\uB2E4.` }], isError: true };
734
+ }
735
+ const image = new Uint8Array(await readFile2(imgResolved));
736
+ const ext = extname2(imgResolved).slice(1).toLowerCase();
737
+ const { placeSealHwpx } = await import("./seal-RGUPOLXD.js");
738
+ const result = await placeSealHwpx(buffer, [{
739
+ anchor,
740
+ occurrence,
741
+ image,
742
+ ext,
743
+ sizeMm: size_mm,
744
+ mode,
745
+ dxMm: dx_mm,
746
+ dyMm: dy_mm
747
+ }]);
748
+ await mkdir(dirname2(outPath), { recursive: true });
749
+ await writeFile(outPath, Buffer.from(result.buffer));
750
+ const p0 = result.placed[0];
751
+ const warnLines = (p0.warnings ?? []).map((w) => `
716
752
  \u26A0\uFE0F ${w}`).join("");
717
- return {
718
- content: [{
719
- type: "text",
720
- text: `\uB3C4\uC7A5 \uBC30\uCE58 \uC644\uB8CC: "${p0.anchor}" #${p0.occurrence} \u2192 ${p0.mode} (x ${p0.posXMm}mm, y ${p0.posYMm}mm, ${p0.sizeMm}mm\uAC01, ${p0.entry})
753
+ return {
754
+ content: [{
755
+ type: "text",
756
+ text: `\uB3C4\uC7A5 \uBC30\uCE58 \uC644\uB8CC: "${p0.anchor}" #${p0.occurrence} \u2192 ${p0.mode} (x ${p0.posXMm}mm, y ${p0.posYMm}mm, ${p0.sizeMm}mm\uAC01, ${p0.entry})
721
757
  \uC800\uC7A5: ${outPath}${warnLines}
722
758
  \uD45C/\uD398\uC774\uC9C0 \uBD88\uD655\uC7A5(\uAE00 \uC55E \uBD80\uC720) \u2014 \uD55C\uCEF4\uC5D0\uC11C \uC704\uCE58 \uD655\uC778 \uD6C4 dx_mm/dy_mm \uB85C \uBBF8\uC138\uC870\uC815 \uAC00\uB2A5\uD569\uB2C8\uB2E4.`
723
- }]
724
- };
725
- } catch (err) {
726
- return {
727
- content: [{ type: "text", text: `\uB3C4\uC7A5 \uBC30\uCE58 \uC2E4\uD328: ${describeError(err)}` }],
728
- isError: true
729
- };
730
- }
731
- }
732
- );
733
- server.tool(
734
- "patch_document",
735
- "\uC6D0\uBCF8 HWPX/HWP\uC758 \uC11C\uC2DD(\uAE00\uAF34\xB7\uD45C\xB7\uB3C4\uC7A5\uCE78\xB7\uC774\uBBF8\uC9C0)\uC744 1\uBC14\uC774\uD2B8\uB3C4 \uAC74\uB4DC\uB9AC\uC9C0 \uC54A\uACE0, \uD3B8\uC9D1\uB41C \uB9C8\uD06C\uB2E4\uC6B4\uC758 \uBC14\uB010 \uD14D\uC2A4\uD2B8\uB9CC \uC81C\uC790\uB9AC \uCE58\uD658\uD574 \uC0C8 \uBB38\uC11C\uB85C \uCD9C\uB825\uD569\uB2C8\uB2E4. parse_document\uB85C \uC5BB\uC740 \uB9C8\uD06C\uB2E4\uC6B4\uC744 \uC218\uC815\uD574 \uB118\uAE30\uC138\uC694 \u2014 \uC591\uC2DD \uBE48\uCE78 \uCC44\uC6B0\uAE30\xB7\uBB38\uAD6C \uC218\uC815\uC5D0 \uC801\uD569\uD558\uBA70 \uD55C\uCEF4 \uD55C\uAE00\uC5D0\uC11C \uBCC0\uC870 \uACBD\uACE0 \uC5C6\uC774 \uC5F4\uB9BD\uB2C8\uB2E4. (\uBE14\uB85D \uCD94\uAC00/\uC0AD\uC81C\xB7\uD45C \uAD6C\uC870 \uBCC0\uACBD\uC740 \uBBF8\uC9C0\uC6D0, \uBBF8\uC801\uC6A9 \uD56D\uBAA9\uC740 \uACB0\uACFC\uC5D0 \uBCF4\uACE0)",
736
- {
737
- file_path: z.string().min(1).describe("\uC6D0\uBCF8 \uBB38\uC11C\uC758 \uC808\uB300 \uACBD\uB85C (HWPX \uB610\uB294 HWP 5.x)"),
738
- edited_markdown: z.string().min(1).describe("parse_document \uCD9C\uB825 \uB9C8\uD06C\uB2E4\uC6B4\uC744 \uD3B8\uC9D1\uD55C \uC804\uCCB4 \uB9C8\uD06C\uB2E4\uC6B4. \uBC14\uB010 \uBB38\uB2E8/\uC140 \uD14D\uC2A4\uD2B8\uB9CC \uBC18\uC601\uD558\uACE0 \uBE14\uB85D \uC218\xB7\uC21C\uC11C\uB294 \uC6D0\uBCF8\uACFC \uAC19\uAC8C \uC720\uC9C0\uD558\uC138\uC694"),
739
- output_path: z.string().min(1).describe("\uCD9C\uB825 \uD30C\uC77C \uC800\uC7A5 \uC808\uB300 \uACBD\uB85C (\uC6D0\uBCF8\uACFC \uAC19\uC740 \uD655\uC7A5\uC790: .hwpx \uB610\uB294 .hwp)")
740
- },
741
- async ({ file_path, edited_markdown, output_path }) => {
742
- try {
743
- const out = safeOutputPath(output_path, /* @__PURE__ */ new Set([".hwpx", ".hwp"]));
744
- const { buffer } = await readValidatedFile(file_path);
745
- const format = detectFormat(buffer);
746
- let isHwpx = format === "hwpx";
747
- if (isHwpx) {
748
- const zipFormat = await detectZipFormat(buffer);
749
- isHwpx = zipFormat === "hwpx";
750
- }
751
- if (!isHwpx && format !== "hwp") {
759
+ }]
760
+ };
761
+ } catch (err) {
752
762
  return {
753
- content: [{ type: "text", text: `patch_document\uB294 HWPX \uB610\uB294 HWP 5.x\uB9CC \uC9C0\uC6D0\uD569\uB2C8\uB2E4 (\uAC10\uC9C0\uB41C \uD3EC\uB9F7: ${format}).` }],
763
+ content: [{ type: "text", text: `\uB3C4\uC7A5 \uBC30\uCE58 \uC2E4\uD328: ${describeError(err)}` }],
754
764
  isError: true
755
765
  };
756
766
  }
757
- const original = new Uint8Array(buffer);
758
- const result = isHwpx ? await patchHwpx(original, edited_markdown) : await patchHwp(original, edited_markdown);
759
- if (!result.success || !result.data) {
767
+ }
768
+ );
769
+ server2.tool(
770
+ "patch_document",
771
+ "\uC6D0\uBCF8 HWPX/HWP\uC758 \uC11C\uC2DD(\uAE00\uAF34\xB7\uD45C\xB7\uB3C4\uC7A5\uCE78\xB7\uC774\uBBF8\uC9C0)\uC744 1\uBC14\uC774\uD2B8\uB3C4 \uAC74\uB4DC\uB9AC\uC9C0 \uC54A\uACE0, \uD3B8\uC9D1\uB41C \uB9C8\uD06C\uB2E4\uC6B4\uC758 \uBC14\uB010 \uD14D\uC2A4\uD2B8\uB9CC \uC81C\uC790\uB9AC \uCE58\uD658\uD574 \uC0C8 \uBB38\uC11C\uB85C \uCD9C\uB825\uD569\uB2C8\uB2E4. parse_document\uB85C \uC5BB\uC740 \uB9C8\uD06C\uB2E4\uC6B4\uC744 \uC218\uC815\uD574 \uB118\uAE30\uC138\uC694 \u2014 \uC591\uC2DD \uBE48\uCE78 \uCC44\uC6B0\uAE30\xB7\uBB38\uAD6C \uC218\uC815\uC5D0 \uC801\uD569\uD558\uBA70 \uD55C\uCEF4 \uD55C\uAE00\uC5D0\uC11C \uBCC0\uC870 \uACBD\uACE0 \uC5C6\uC774 \uC5F4\uB9BD\uB2C8\uB2E4. (\uBE14\uB85D \uCD94\uAC00/\uC0AD\uC81C\xB7\uD45C \uAD6C\uC870 \uBCC0\uACBD\uC740 \uBBF8\uC9C0\uC6D0, \uBBF8\uC801\uC6A9 \uD56D\uBAA9\uC740 \uACB0\uACFC\uC5D0 \uBCF4\uACE0)",
772
+ {
773
+ file_path: z2.string().min(1).describe("\uC6D0\uBCF8 \uBB38\uC11C\uC758 \uC808\uB300 \uACBD\uB85C (HWPX \uB610\uB294 HWP 5.x)"),
774
+ edited_markdown: z2.string().min(1).describe("parse_document \uCD9C\uB825 \uB9C8\uD06C\uB2E4\uC6B4\uC744 \uD3B8\uC9D1\uD55C \uC804\uCCB4 \uB9C8\uD06C\uB2E4\uC6B4. \uBC14\uB010 \uBB38\uB2E8/\uC140 \uD14D\uC2A4\uD2B8\uB9CC \uBC18\uC601\uD558\uACE0 \uBE14\uB85D \uC218\xB7\uC21C\uC11C\uB294 \uC6D0\uBCF8\uACFC \uAC19\uAC8C \uC720\uC9C0\uD558\uC138\uC694"),
775
+ output_path: z2.string().min(1).describe("\uCD9C\uB825 \uD30C\uC77C \uC800\uC7A5 \uC808\uB300 \uACBD\uB85C (\uC6D0\uBCF8\uACFC \uAC19\uC740 \uD655\uC7A5\uC790: .hwpx \uB610\uB294 .hwp)")
776
+ },
777
+ async ({ file_path, edited_markdown, output_path }) => {
778
+ try {
779
+ const out = safeOutputPath(output_path, /* @__PURE__ */ new Set([".hwpx", ".hwp"]));
780
+ const { buffer } = await readValidatedFile(file_path);
781
+ let format = detectFormat(buffer);
782
+ if (format === "hwpx") {
783
+ const z5 = await detectZipFormat(buffer);
784
+ if (z5 !== "unknown") format = z5;
785
+ } else if (format === "hwp") {
786
+ const o = detectOle2Format(buffer);
787
+ if (o !== "unknown") format = o;
788
+ }
789
+ const isHwpx = format === "hwpx";
790
+ if (!isHwpx && format !== "hwp") {
791
+ return {
792
+ content: [{ type: "text", text: `patch_document\uB294 HWPX \uB610\uB294 HWP 5.x\uB9CC \uC9C0\uC6D0\uD569\uB2C8\uB2E4 (\uAC10\uC9C0\uB41C \uD3EC\uB9F7: ${format}).` }],
793
+ isError: true
794
+ };
795
+ }
796
+ const original = new Uint8Array(buffer);
797
+ const result = isHwpx ? await patchHwpx(original, edited_markdown) : await patchHwp(original, edited_markdown);
798
+ if (!result.success || !result.data) {
799
+ return {
800
+ content: [{ type: "text", text: `\uD328\uCE58 \uC2E4\uD328: ${result.error ?? "\uC54C \uC218 \uC5C6\uB294 \uC624\uB958"}` }],
801
+ isError: true
802
+ };
803
+ }
804
+ await mkdir(dirname2(out), { recursive: true });
805
+ await writeFile(out, Buffer.from(result.data));
806
+ const v = result.verification?.stats;
807
+ const lossless = v ? v.modified === 0 && v.added === 0 && v.removed === 0 : void 0;
808
+ const lines = [
809
+ `\u2713 ${result.applied}\uAC1C \uBCC0\uACBD \uC801\uC6A9 (${isHwpx ? "HWPX" : "HWP"}, \uC6D0\uBCF8 \uC11C\uC2DD \uBCF4\uC874) \u2192 ${out}`,
810
+ lossless === true ? "\uAC80\uC99D: \uD3B8\uC9D1 \uB0B4\uC6A9\uACFC \uC7AC\uD30C\uC2F1 \uACB0\uACFC \uC644\uC804 \uC77C\uCE58" : lossless === false ? `\uAC80\uC99D \uC794\uCC28: \uC218\uC815 ${v.modified} \xB7 \uCD94\uAC00 ${v.added} \xB7 \uC0AD\uC81C ${v.removed} (\uBC18\uC601 \uC548 \uB41C \uD3B8\uC9D1 \uC788\uC74C)` : null,
811
+ result.skipped.length > 0 ? `\uBBF8\uC801\uC6A9 ${result.skipped.length}\uAC74:
812
+ ` + result.skipped.map((s) => ` - ${s.reason}`).join("\n") : null
813
+ ].filter(Boolean);
814
+ return {
815
+ content: [{ type: "text", text: lines.join("\n") }]
816
+ };
817
+ } catch (err) {
760
818
  return {
761
- content: [{ type: "text", text: `\uD328\uCE58 \uC2E4\uD328: ${result.error ?? "\uC54C \uC218 \uC5C6\uB294 \uC624\uB958"}` }],
819
+ content: [{ type: "text", text: `\uC624\uB958: ${describeError(err)}` }],
762
820
  isError: true
763
821
  };
764
822
  }
765
- await mkdir(dirname(out), { recursive: true });
766
- await writeFile(out, Buffer.from(result.data));
767
- const v = result.verification?.stats;
768
- const lossless = v ? v.modified === 0 && v.added === 0 && v.removed === 0 : void 0;
769
- const lines = [
770
- `\u2713 ${result.applied}\uAC1C \uBCC0\uACBD \uC801\uC6A9 (${isHwpx ? "HWPX" : "HWP"}, \uC6D0\uBCF8 \uC11C\uC2DD \uBCF4\uC874) \u2192 ${out}`,
771
- lossless === true ? "\uAC80\uC99D: \uD3B8\uC9D1 \uB0B4\uC6A9\uACFC \uC7AC\uD30C\uC2F1 \uACB0\uACFC \uC644\uC804 \uC77C\uCE58" : lossless === false ? `\uAC80\uC99D \uC794\uCC28: \uC218\uC815 ${v.modified} \xB7 \uCD94\uAC00 ${v.added} \xB7 \uC0AD\uC81C ${v.removed} (\uBC18\uC601 \uC548 \uB41C \uD3B8\uC9D1 \uC788\uC74C)` : null,
772
- result.skipped.length > 0 ? `\uBBF8\uC801\uC6A9 ${result.skipped.length}\uAC74:
773
- ` + result.skipped.map((s) => ` - ${s.reason}`).join("\n") : null
774
- ].filter(Boolean);
775
- return {
776
- content: [{ type: "text", text: lines.join("\n") }]
777
- };
778
- } catch (err) {
779
- return {
780
- content: [{ type: "text", text: `\uC624\uB958: ${describeError(err)}` }],
781
- isError: true
782
- };
783
823
  }
784
- }
785
- );
786
- server.tool(
787
- "render_document",
788
- "HWPX\xB7HWP \uBB38\uC11C\uB97C \uC2E4\uC81C \uC870\uD310 \uADF8\uB300\uB85C \uB80C\uB354\uD574 PNG/JPEG \uC774\uBBF8\uC9C0\uB85C \uC751\uB2F5\uD558\uAC70\uB098 SVG\xB7HTML\xB7PDF \uD30C\uC77C\uB85C \uC800\uC7A5\uD569\uB2C8\uB2E4. generate_document\xB7fill_form\xB7patch_document\xB7place_seal \uACB0\uACFC\uBB3C\uC744 \uB208\uC73C\uB85C \uD655\uC778\uD558\uB294 \uC6A9\uB3C4 \u2014 \uC0DD\uC131/\uC218\uC815 \uD6C4 \uC774 \uB3C4\uAD6C\uB85C \uB80C\uB354\uD574 \uAE68\uC9D0\xB7\uC798\uB9BC\xB7\uBC30\uCE58\uB97C \uAC80\uC99D\uD558\uACE0 \uB2E4\uC2DC \uACE0\uCE58\uB294 \uB8E8\uD504\uB97C \uAD8C\uC7A5\uD569\uB2C8\uB2E4. \uD55C\uCEF4 \uC800\uC7A5\uBCF8(HWPX\xB7HWP \uBAA8\uB450)\uC740 \uC870\uD310 \uCE90\uC2DC\uB85C \uC815\uD655\uD788, AI \uC0DD\uC131\uBCF8(\uCE90\uC2DC \uC5C6\uC74C)\uC740 \uC21C\uC218 \uC870\uD310 \uC5D4\uC9C4(reflow)\uC73C\uB85C \uB80C\uB354\uB418\uBA70 \uD6C4\uC790\uB294 \uD55C\uCEF4 \uC2E4\uC870\uD310\uC758 \uADFC\uC0AC\uC785\uB2C8\uB2E4 (\uCC38\uACE0\uC6A9 \uBBF8\uB9AC\uBCF4\uAE30).",
789
- {
790
- file_path: z.string().min(1).describe("\uB80C\uB354\uD560 HWPX \uB610\uB294 HWP(5.x) \uD30C\uC77C\uC758 \uC808\uB300 \uACBD\uB85C"),
791
- format: z.enum(["png", "jpeg", "svg", "html", "pdf"]).default("png").describe("png/jpeg=\uC774\uBBF8\uC9C0\uB85C \uC751\uB2F5\uC5D0 \uC9C1\uC811 \uBC18\uD658(output_path \uB294 \uC120\uD0DD) / svg\xB7html\xB7pdf=output_path \uC5D0 \uD30C\uC77C \uC800\uC7A5(\uD544\uC218). svg \uB294 pages \uBBF8\uC9C0\uC815 HWPX \uBA74 \uC804\uCCB4 \uC138\uB85C \uC2A4\uD0DD 1\uD30C\uC77C, \uADF8 \uC678 \uD398\uC774\uC9C0\uBCC4 \uD30C\uC77C. html=\uC790\uAE09\uC790\uC871 \uB808\uC774\uC544\uC6C3 HTML 1\uD30C\uC77C, pdf=Chromium \uD544\uC694"),
792
- output_path: z.string().min(1).optional().describe("\uACB0\uACFC \uC800\uC7A5 \uACBD\uB85C (\uD655\uC7A5\uC790\uB294 format \uACFC \uC77C\uCE58 \u2014 .png/.jpg/.svg/.html/.pdf). \uD398\uC774\uC9C0\uBCC4 \uC0B0\uCD9C\uC740 _page_NNN \uC811\uBBF8"),
793
- highlights: z.array(z.string().min(1)).optional().describe("\uD615\uAD11\uD39C \uD45C\uC2DC\uD560 \uAC80\uC0C9\uC5B4 \uBAA9\uB85D \u2014 \uCC44\uC6B4 \uAC12\xB7\uC218\uC815 \uBB38\uAD6C \uC704\uCE58 \uD655\uC778\uC6A9"),
794
- reflow_mode: z.enum(["keep", "charAll"]).default("keep").describe("reflow \uC904\uBC14\uAFC8: keep=\uC5B4\uC808 \uB2E8\uC704, charAll=\uAE00\uC790 \uB2E8\uC704"),
795
- pages: z.string().min(1).optional().describe("\uD398\uC774\uC9C0 \uC120\uD0DD(1-based: '3', '1-3', '1,3,7-9') \u2014 \uC9C0\uC815\uD558\uBA74 \uD398\uC774\uC9C0\uBCC4 \uC0B0\uCD9C(\uC774\uBBF8\uC9C0 \uC751\uB2F5\uC740 \uCD5C\uB300 8\uCABD). HWPX png/svg \uC5D0\uC11C \uBBF8\uC9C0\uC815\uC774\uBA74 \uC804\uCCB4\uB97C \uC138\uB85C \uC2A4\uD0DD 1\uC7A5\uC73C\uB85C"),
796
- max_width_px: z.number().int().min(200).max(4e3).optional().describe("\uB798\uC2A4\uD130(png/jpeg) \uCD5C\uB300 \uD3ED px \u2014 \uD070 \uBB38\uC11C \uC751\uB2F5 \uBD80\uD53C \uC870\uC808")
797
- },
798
- async ({ file_path, format, output_path, highlights, reflow_mode, pages, max_width_px }) => {
799
- try {
800
- const EXT = { png: [".png"], jpeg: [".jpg", ".jpeg"], svg: [".svg"], html: [".html", ".htm"], pdf: [".pdf"] };
801
- const needsFile = format === "svg" || format === "html" || format === "pdf";
802
- if (needsFile && !output_path) {
803
- return { content: [{ type: "text", text: `format: "${format}" \uC740 output_path(${EXT[format][0]})\uAC00 \uD544\uC218\uC785\uB2C8\uB2E4 \u2014 \uC6D0\uBB38\uC740 \uCEE4\uC11C \uC751\uB2F5\uC5D0 \uC9C1\uC811 \uB2F4\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.` }], isError: true };
804
- }
805
- const outPath = output_path ? safeOutputPath(output_path, new Set(EXT[format])) : void 0;
806
- const { buffer, resolved } = await readValidatedFile(file_path, MAX_FILE_SIZE, /* @__PURE__ */ new Set([".hwpx", ".hwp"]));
807
- const isHwp5 = resolved.toLowerCase().endsWith(".hwp");
808
- if (!isHwp5 && !pages && (format === "png" || format === "svg")) {
809
- const { renderHwpxToSvg } = await import("./render-MLN4ZLVD.js");
810
- const result = await renderHwpxToSvg(buffer, { highlights, reflow: true, reflowMode: reflow_mode });
811
- const summary2 = [
812
- `\uB80C\uB354 \uC644\uB8CC: ${result.pageCount}\uD398\uC774\uC9C0, ${Math.round(result.width)}x${Math.round(result.height)}pt (\uD14D\uC2A4\uD2B8 ${result.stats.texts}\xB7\uC774\uBBF8\uC9C0 ${result.stats.images}\xB7\uD45C ${result.stats.tables})`,
813
- ...result.warnings.map((w) => `\u26A0\uFE0F ${w}`)
824
+ );
825
+ server2.tool(
826
+ "redact_document",
827
+ "\uBB38\uC11C\uC758 \uAC1C\uC778\uC815\uBCF4(\uC8FC\uBBFC\uBC88\uD638\xB7\uC804\uD654\xB7\uC774\uBA54\uC77C\xB7\uCE74\uB4DC\xB7\uACC4\uC88C)\uB97C \uD0D0\uC9C0\uD574 \uC11C\uC2DD \uBCF4\uC874 \uB9C8\uC2A4\uD0B9\uD569\uB2C8\uB2E4. HWPX/HWP\uB294 \uC6D0\uBCF8 \uC11C\uC2DD 1\uBC14\uC774\uD2B8 \uADF8\uB300\uB85C patch, \uADF8 \uC678 \uD3EC\uB9F7\uC740 \uB9C8\uC2A4\uD0B9\uB41C \uB9C8\uD06C\uB2E4\uC6B4\uC73C\uB85C \uCD9C\uB825. \uC790\uB3D9 \uAC80\uCD9C \uBCF4\uC870 \uB3C4\uAD6C \u2014 \uACB0\uACFC \uB9AC\uD3EC\uD2B8\uB97C \uC0AC\uB78C\uC774 \uCD5C\uC885 \uD655\uC778\uD574\uC57C \uD558\uBA70 \uC774\uBBF8\uC9C0 \uC548 \uD14D\uC2A4\uD2B8\uB294 \uD0D0\uC9C0\uD558\uC9C0 \uBABB\uD569\uB2C8\uB2E4. \uB9C8\uC2A4\uD0B9 \uD6C4 render_document\uB85C \uB208\uC73C\uB85C \uD655\uC778\uD558\uB294 \uAC83\uC744 \uAD8C\uC7A5\uD569\uB2C8\uB2E4.",
828
+ {
829
+ file_path: z2.string().min(1).describe("\uB300\uC0C1 \uBB38\uC11C\uC758 \uC808\uB300 \uACBD\uB85C"),
830
+ rules: z2.array(z2.enum(["rrn", "phone", "email", "card", "account", "passport", "driver"])).optional().describe("\uC801\uC6A9 \uB8F0 (\uAE30\uBCF8: rrn\xB7phone\xB7email\xB7card\xB7account \u2014 passport\xB7driver\uB294 opt-in)"),
831
+ mask_char: z2.string().min(1).max(1).optional().describe("\uB9C8\uC2A4\uD06C \uBB38\uC790 1\uAE00\uC790 (\uAE30\uBCF8: \u25CF)"),
832
+ output_path: z2.string().min(1).optional().describe("\uCD9C\uB825 \uACBD\uB85C (HWPX/HWP\uB294 \uAC19\uC740 \uD655\uC7A5\uC790, \uADF8 \uC678\uB294 .md) \u2014 dry_run\uC774 \uC544\uB2C8\uBA74 \uD544\uC218"),
833
+ dry_run: z2.boolean().default(false).describe("\uD0D0\uC9C0 \uB9AC\uD3EC\uD2B8\uB9CC \uBC18\uD658, \uD30C\uC77C \uBBF8\uC0DD\uC131")
834
+ },
835
+ async ({ file_path, rules, mask_char, output_path, dry_run }) => {
836
+ try {
837
+ const { buffer } = await readValidatedFile(file_path);
838
+ const format = detectFormat(buffer);
839
+ const patchable = format === "hwpx" || format === "hwp";
840
+ if (!dry_run && !output_path) {
841
+ return { content: [{ type: "text", text: "output_path\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4 (\uD0D0\uC9C0\uB9CC \uC6D0\uD558\uBA74 dry_run: true)." }], isError: true };
842
+ }
843
+ const outPath = !dry_run ? safeOutputPath(output_path, new Set(patchable ? [format === "hwp" ? ".hwp" : ".hwpx"] : [".md", ".markdown", ".txt"])) : void 0;
844
+ const parsed = await parse(buffer, { filePath: file_path });
845
+ if (!parsed.success) {
846
+ return { content: [{ type: "text", text: `\uD30C\uC2F1 \uC2E4\uD328: ${parsed.error}` }], isError: true };
847
+ }
848
+ const { redactMarkdown } = await import("./redact-7ILEAUTS.js");
849
+ const r = redactMarkdown(parsed.markdown, { rules, maskChar: mask_char });
850
+ const byRule = /* @__PURE__ */ new Map();
851
+ for (const h of r.hits) byRule.set(h.rule, (byRule.get(h.rule) ?? 0) + 1);
852
+ const lines = [
853
+ `\uD0D0\uC9C0: ${r.hits.length}\uAC74 (${[...byRule.entries()].map(([k, v]) => `${k} ${v}`).join(", ") || "\uC5C6\uC74C"})`,
854
+ ...r.hits.map((h) => ` - [${h.rule}] ${h.masked}`)
814
855
  ];
815
- if (format === "svg") {
816
- await mkdir(dirname(outPath), { recursive: true });
817
- await writeFile(outPath, result.svg, "utf-8");
818
- summary2.push(`\uC800\uC7A5: ${outPath}`);
819
- return { content: [{ type: "text", text: summary2.join("\n") }] };
856
+ if (!dry_run && r.hits.length > 0) {
857
+ if (patchable) {
858
+ const original = new Uint8Array(buffer);
859
+ const result = format === "hwp" ? await patchHwp(original, r.text) : await patchHwpx(original, r.text);
860
+ if (!result.success || !result.data) {
861
+ return { content: [{ type: "text", text: `\uB9C8\uC2A4\uD0B9 \uD328\uCE58 \uC2E4\uD328: ${result.error ?? "\uC54C \uC218 \uC5C6\uB294 \uC624\uB958"}` }], isError: true };
862
+ }
863
+ await mkdir(dirname2(outPath), { recursive: true });
864
+ await writeFile(outPath, Buffer.from(result.data));
865
+ lines.push(`\uC800\uC7A5: ${outPath} (\uC6D0\uBCF8 \uC11C\uC2DD \uBCF4\uC874)`);
866
+ if (result.skipped.length > 0) {
867
+ lines.push(`\u26A0\uFE0F \uBBF8\uC801\uC6A9 ${result.skipped.length}\uAC74 \u2014 \uD574\uB2F9 \uC704\uCE58\uB294 \uC6D0\uBB38\uC774 \uB0A8\uC544 \uC788\uC73C\uB2C8 \uBC18\uB4DC\uC2DC \uC218\uB3D9 \uD655\uC778:`);
868
+ for (const s of result.skipped) lines.push(` - ${s.reason}`);
869
+ }
870
+ lines.push("render_document\uB85C \uB9C8\uC2A4\uD0B9 \uACB0\uACFC\uB97C \uB208\uC73C\uB85C \uD655\uC778\uD558\uC138\uC694.");
871
+ } else {
872
+ await mkdir(dirname2(outPath), { recursive: true });
873
+ await writeFile(outPath, r.text, "utf-8");
874
+ lines.push(`\uC800\uC7A5: ${outPath} (${format}\uB294 \uC11C\uC2DD \uBCF4\uC874 \uBBF8\uC9C0\uC6D0 \u2014 \uB9C8\uC2A4\uD0B9\uB41C \uB9C8\uD06C\uB2E4\uC6B4)`);
875
+ }
876
+ } else if (!dry_run) {
877
+ lines.push("\uD0D0\uC9C0 0\uAC74 \u2014 \uCD9C\uB825 \uD30C\uC77C\uC744 \uB9CC\uB4E4\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.");
820
878
  }
821
- const { rasterizeSvg } = await import("./rasterize-33WOOVSM.js");
822
- const raster = await rasterizeSvg(result.svg, result.width, result.height, max_width_px ? { maxWidthPx: max_width_px } : void 0);
879
+ lines.push("\uC8FC\uC758: \uC790\uB3D9 \uAC80\uCD9C\uC740 \uBCF4\uC870 \uC218\uB2E8\uC785\uB2C8\uB2E4. \uC774\uBBF8\uC9C0 \uC18D \uD14D\uC2A4\uD2B8\xB7\uD45C\uAE30 \uBCC0\uD615\uC740 \uB193\uCE60 \uC218 \uC788\uC73C\uB2C8 \uCD5C\uC885 \uACF5\uAC1C \uC804 \uC0AC\uB78C \uAC80\uD1A0\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4.");
880
+ return { content: [{ type: "text", text: capResponseText(lines.join("\n")) }] };
881
+ } catch (err) {
882
+ return { content: [{ type: "text", text: `\uB9C8\uC2A4\uD0B9 \uC2E4\uD328: ${describeError(err)}` }], isError: true };
883
+ }
884
+ }
885
+ );
886
+ }
887
+
888
+ // src/mcp/tools-render.ts
889
+ import { z as z3 } from "zod";
890
+ import { writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
891
+ import { dirname as dirname3, join } from "path";
892
+ function registerRenderTools(server2) {
893
+ server2.tool(
894
+ "render_document",
895
+ "HWPX\xB7HWP \uBB38\uC11C\uB97C \uC2E4\uC81C \uC870\uD310 \uADF8\uB300\uB85C \uB80C\uB354\uD574 PNG/JPEG \uC774\uBBF8\uC9C0\uB85C \uC751\uB2F5\uD558\uAC70\uB098 SVG\xB7HTML\xB7PDF \uD30C\uC77C\uB85C \uC800\uC7A5\uD569\uB2C8\uB2E4. generate_document\xB7fill_form\xB7patch_document\xB7place_seal \uACB0\uACFC\uBB3C\uC744 \uB208\uC73C\uB85C \uD655\uC778\uD558\uB294 \uC6A9\uB3C4 \u2014 \uC0DD\uC131/\uC218\uC815 \uD6C4 \uC774 \uB3C4\uAD6C\uB85C \uB80C\uB354\uD574 \uAE68\uC9D0\xB7\uC798\uB9BC\xB7\uBC30\uCE58\uB97C \uAC80\uC99D\uD558\uACE0 \uB2E4\uC2DC \uACE0\uCE58\uB294 \uB8E8\uD504\uB97C \uAD8C\uC7A5\uD569\uB2C8\uB2E4. \uD55C\uCEF4 \uC800\uC7A5\uBCF8(HWPX\xB7HWP \uBAA8\uB450)\uC740 \uC870\uD310 \uCE90\uC2DC\uB85C \uC815\uD655\uD788, AI \uC0DD\uC131\uBCF8(\uCE90\uC2DC \uC5C6\uC74C)\uC740 \uC21C\uC218 \uC870\uD310 \uC5D4\uC9C4(reflow)\uC73C\uB85C \uB80C\uB354\uB418\uBA70 \uD6C4\uC790\uB294 \uD55C\uCEF4 \uC2E4\uC870\uD310\uC758 \uADFC\uC0AC\uC785\uB2C8\uB2E4 (\uCC38\uACE0\uC6A9 \uBBF8\uB9AC\uBCF4\uAE30).",
896
+ {
897
+ file_path: z3.string().min(1).describe("\uB80C\uB354\uD560 HWPX \uB610\uB294 HWP(5.x) \uD30C\uC77C\uC758 \uC808\uB300 \uACBD\uB85C"),
898
+ format: z3.enum(["png", "jpeg", "svg", "html", "pdf"]).default("png").describe("png/jpeg=\uC774\uBBF8\uC9C0\uB85C \uC751\uB2F5\uC5D0 \uC9C1\uC811 \uBC18\uD658(output_path \uB294 \uC120\uD0DD) / svg\xB7html\xB7pdf=output_path \uC5D0 \uD30C\uC77C \uC800\uC7A5(\uD544\uC218). svg \uB294 pages \uBBF8\uC9C0\uC815 HWPX \uBA74 \uC804\uCCB4 \uC138\uB85C \uC2A4\uD0DD 1\uD30C\uC77C, \uADF8 \uC678 \uD398\uC774\uC9C0\uBCC4 \uD30C\uC77C. html=\uC790\uAE09\uC790\uC871 \uB808\uC774\uC544\uC6C3 HTML 1\uD30C\uC77C, pdf=Chromium \uD544\uC694"),
899
+ output_path: z3.string().min(1).optional().describe("\uACB0\uACFC \uC800\uC7A5 \uACBD\uB85C (\uD655\uC7A5\uC790\uB294 format \uACFC \uC77C\uCE58 \u2014 .png/.jpg/.svg/.html/.pdf). \uD398\uC774\uC9C0\uBCC4 \uC0B0\uCD9C\uC740 _page_NNN \uC811\uBBF8"),
900
+ highlights: z3.array(z3.string().min(1)).optional().describe("\uD615\uAD11\uD39C \uD45C\uC2DC\uD560 \uAC80\uC0C9\uC5B4 \uBAA9\uB85D \u2014 \uCC44\uC6B4 \uAC12\xB7\uC218\uC815 \uBB38\uAD6C \uC704\uCE58 \uD655\uC778\uC6A9"),
901
+ reflow_mode: z3.enum(["keep", "charAll"]).default("keep").describe("reflow \uC904\uBC14\uAFC8: keep=\uC5B4\uC808 \uB2E8\uC704, charAll=\uAE00\uC790 \uB2E8\uC704"),
902
+ pages: z3.string().min(1).optional().describe("\uD398\uC774\uC9C0 \uC120\uD0DD(1-based: '3', '1-3', '1,3,7-9') \u2014 \uC9C0\uC815\uD558\uBA74 \uD398\uC774\uC9C0\uBCC4 \uC0B0\uCD9C(\uC774\uBBF8\uC9C0 \uC751\uB2F5\uC740 \uCD5C\uB300 8\uCABD). HWPX png/svg \uC5D0\uC11C \uBBF8\uC9C0\uC815\uC774\uBA74 \uC804\uCCB4\uB97C \uC138\uB85C \uC2A4\uD0DD 1\uC7A5\uC73C\uB85C"),
903
+ max_width_px: z3.number().int().min(200).max(4e3).optional().describe("\uB798\uC2A4\uD130(png/jpeg) \uCD5C\uB300 \uD3ED px \u2014 \uD070 \uBB38\uC11C \uC751\uB2F5 \uBD80\uD53C \uC870\uC808")
904
+ },
905
+ async ({ file_path, format, output_path, highlights, reflow_mode, pages, max_width_px }) => {
906
+ try {
907
+ const EXT = { png: [".png"], jpeg: [".jpg", ".jpeg"], svg: [".svg"], html: [".html", ".htm"], pdf: [".pdf"] };
908
+ const needsFile = format === "svg" || format === "html" || format === "pdf";
909
+ if (needsFile && !output_path) {
910
+ return { content: [{ type: "text", text: `format: "${format}" \uC740 output_path(${EXT[format][0]})\uAC00 \uD544\uC218\uC785\uB2C8\uB2E4 \u2014 \uC6D0\uBB38\uC740 \uCEE4\uC11C \uC751\uB2F5\uC5D0 \uC9C1\uC811 \uB2F4\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.` }], isError: true };
911
+ }
912
+ const outPath = output_path ? safeOutputPath(output_path, new Set(EXT[format])) : void 0;
913
+ const { buffer, resolved } = await readValidatedFile(file_path, MAX_FILE_SIZE, /* @__PURE__ */ new Set([".hwpx", ".hwp"]));
914
+ const isHwp5 = resolved.toLowerCase().endsWith(".hwp");
915
+ if (!isHwp5 && !pages && (format === "png" || format === "svg")) {
916
+ const { renderHwpxToSvg } = await import("./render-NGP5T24S.js");
917
+ const result = await renderHwpxToSvg(buffer, { highlights, reflow: true, reflowMode: reflow_mode });
918
+ const summary2 = [
919
+ `\uB80C\uB354 \uC644\uB8CC: ${result.pageCount}\uD398\uC774\uC9C0, ${Math.round(result.width)}x${Math.round(result.height)}pt (\uD14D\uC2A4\uD2B8 ${result.stats.texts}\xB7\uC774\uBBF8\uC9C0 ${result.stats.images}\xB7\uD45C ${result.stats.tables})`,
920
+ ...result.warnings.map((w) => `\u26A0\uFE0F ${w}`)
921
+ ];
922
+ if (format === "svg") {
923
+ await mkdir2(dirname3(outPath), { recursive: true });
924
+ await writeFile2(outPath, result.svg, "utf-8");
925
+ summary2.push(`\uC800\uC7A5: ${outPath}`);
926
+ return { content: [{ type: "text", text: summary2.join("\n") }] };
927
+ }
928
+ const { rasterizeSvg } = await import("./rasterize-46RG4ZF6.js");
929
+ const raster = await rasterizeSvg(result.svg, result.width, result.height, max_width_px ? { maxWidthPx: max_width_px } : void 0);
930
+ if (outPath) {
931
+ await mkdir2(dirname3(outPath), { recursive: true });
932
+ await writeFile2(outPath, raster.png);
933
+ summary2.push(`\uC800\uC7A5: ${outPath}`);
934
+ }
935
+ summary2.push(`\uC774\uBBF8\uC9C0 ${raster.widthPx}x${raster.heightPx}px \u2014 \uC798\uB9BC\xB7\uACB9\uCE68\xB7\uBE48\uCE78\xB7\uD398\uC774\uC9C0 \uB118\uCE68\uC774 \uBCF4\uC774\uBA74 \uC6D0\uC778 \uD14D\uC2A4\uD2B8\uB97C \uC218\uC815\uD574 \uB2E4\uC2DC \uC0DD\uC131/\uD328\uCE58\uD558\uC138\uC694.`);
936
+ return {
937
+ content: [
938
+ { type: "image", data: raster.png.toString("base64"), mimeType: "image/png" },
939
+ { type: "text", text: summary2.join("\n") }
940
+ ]
941
+ };
942
+ }
943
+ const { renderDocument } = await import("./render-NGP5T24S.js");
944
+ const { scene, assets } = await renderDocument(buffer, { format, pages, highlights, reflow: true, reflowMode: reflow_mode, maxWidthPx: max_width_px });
945
+ const summary = [
946
+ `\uB80C\uB354 \uC644\uB8CC: \uBB38\uC11C ${scene.pages.length}\uD398\uC774\uC9C0 \uC911 ${assets.filter((a) => a.page !== void 0).length || 1}\uAC74 (\uD14D\uC2A4\uD2B8 ${scene.stats.texts}\xB7\uC774\uBBF8\uC9C0 ${scene.stats.images}\xB7\uD45C ${scene.stats.tables}\xB7\uB3C4\uD615 ${scene.stats.shapes})`,
947
+ ...scene.warnings.map((w) => `\u26A0\uFE0F ${w}`)
948
+ ];
949
+ const suffixed = (p, page) => p.replace(/(\.[^.]+)$/, `_page_${String(page).padStart(3, "0")}$1`);
823
950
  if (outPath) {
824
- await mkdir(dirname(outPath), { recursive: true });
825
- await writeFile(outPath, raster.png);
826
- summary2.push(`\uC800\uC7A5: ${outPath}`);
951
+ await mkdir2(dirname3(outPath), { recursive: true });
952
+ for (const a of assets) {
953
+ const p = a.page === void 0 || assets.length === 1 ? outPath : suffixed(outPath, a.page);
954
+ await writeFile2(p, a.data);
955
+ summary.push(`\uC800\uC7A5: ${p}`);
956
+ }
827
957
  }
828
- summary2.push(`\uC774\uBBF8\uC9C0 ${raster.widthPx}x${raster.heightPx}px \u2014 \uC798\uB9BC\xB7\uACB9\uCE68\xB7\uBE48\uCE78\xB7\uD398\uC774\uC9C0 \uB118\uCE68\uC774 \uBCF4\uC774\uBA74 \uC6D0\uC778 \uD14D\uC2A4\uD2B8\uB97C \uC218\uC815\uD574 \uB2E4\uC2DC \uC0DD\uC131/\uD328\uCE58\uD558\uC138\uC694.`);
829
- return {
830
- content: [
831
- { type: "image", data: raster.png.toString("base64"), mimeType: "image/png" },
832
- { type: "text", text: summary2.join("\n") }
833
- ]
834
- };
835
- }
836
- const { renderDocument } = await import("./render-MLN4ZLVD.js");
837
- const { scene, assets } = await renderDocument(buffer, { format, pages, highlights, reflow: true, reflowMode: reflow_mode, maxWidthPx: max_width_px });
838
- const summary = [
839
- `\uB80C\uB354 \uC644\uB8CC: \uBB38\uC11C ${scene.pages.length}\uD398\uC774\uC9C0 \uC911 ${assets.filter((a) => a.page !== void 0).length || 1}\uAC74 (\uD14D\uC2A4\uD2B8 ${scene.stats.texts}\xB7\uC774\uBBF8\uC9C0 ${scene.stats.images}\xB7\uD45C ${scene.stats.tables}\xB7\uB3C4\uD615 ${scene.stats.shapes})`,
840
- ...scene.warnings.map((w) => `\u26A0\uFE0F ${w}`)
841
- ];
842
- const suffixed = (p, page) => p.replace(/(\.[^.]+)$/, `_page_${String(page).padStart(3, "0")}$1`);
843
- if (outPath) {
844
- await mkdir(dirname(outPath), { recursive: true });
845
- for (const a of assets) {
846
- const p = a.page === void 0 || assets.length === 1 ? outPath : suffixed(outPath, a.page);
847
- await writeFile(p, a.data);
848
- summary.push(`\uC800\uC7A5: ${p}`);
958
+ if (format === "png" || format === "jpeg") {
959
+ const MAX_PAGES = 8;
960
+ const shown = assets.slice(0, MAX_PAGES);
961
+ if (assets.length > MAX_PAGES) summary.push(`\u26A0\uFE0F ${MAX_PAGES}\uCABD\uAE4C\uC9C0\uB9CC \uC751\uB2F5\uC5D0 \uB2F4\uC558\uC2B5\uB2C8\uB2E4 \u2014 pages \uB97C \uC881\uD788\uC138\uC694`);
962
+ return {
963
+ content: [
964
+ ...shown.map((a) => ({ type: "image", data: a.data.toString("base64"), mimeType: format === "jpeg" ? "image/jpeg" : "image/png" })),
965
+ { type: "text", text: summary.join("\n") }
966
+ ]
967
+ };
849
968
  }
850
- }
851
- if (format === "png" || format === "jpeg") {
852
- const MAX_PAGES = 8;
853
- const shown = assets.slice(0, MAX_PAGES);
854
- if (assets.length > MAX_PAGES) summary.push(`\u26A0\uFE0F ${MAX_PAGES}\uCABD\uAE4C\uC9C0\uB9CC \uC751\uB2F5\uC5D0 \uB2F4\uC558\uC2B5\uB2C8\uB2E4 \u2014 pages \uB97C \uC881\uD788\uC138\uC694`);
969
+ return { content: [{ type: "text", text: summary.join("\n") }] };
970
+ } catch (err) {
855
971
  return {
856
- content: [
857
- ...shown.map((a) => ({ type: "image", data: a.data.toString("base64"), mimeType: format === "jpeg" ? "image/jpeg" : "image/png" })),
858
- { type: "text", text: summary.join("\n") }
859
- ]
972
+ content: [{ type: "text", text: `\uB80C\uB354 \uC2E4\uD328: ${describeError(err)}` }],
973
+ isError: true
860
974
  };
861
975
  }
862
- return { content: [{ type: "text", text: summary.join("\n") }] };
863
- } catch (err) {
864
- return {
865
- content: [{ type: "text", text: `\uB80C\uB354 \uC2E4\uD328: ${describeError(err)}` }],
866
- isError: true
867
- };
868
976
  }
869
- }
870
- );
871
- server.tool(
872
- "crop_regions",
873
- "HWPX\xB7HWP \uBB38\uC11C\uB97C \uB80C\uB354\uD574 \uD45C\xB7\uC774\uBBF8\uC9C0\xB7\uBB38\uB2E8\xB7\uB3C4\uD615 \uC601\uC5ED\uC744 \uD398\uC774\uC9C0 \uC774\uBBF8\uC9C0\uC5D0\uC11C \uC798\uB77C \uD30C\uC77C\uB85C \uC800\uC7A5\uD569\uB2C8\uB2E4(render_document \uC640 \uAC19\uC740 \uC870\uD310 \uC5D4\uC9C4, \uD398\uC774\uC9C0 \uB85C\uCEEC pt bbox \uB97C \uC2E4\uBC30\uC728\uB85C \uD658\uC0B0). \uD45C\uAC00 \uC9C4\uC9DC \uB370\uC774\uD130\uD45C\uC778\uC9C0 \uC870\uC9C1\uB3C4\uC778\uC9C0\uB294 \uD310\uB2E8\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4(\uADF8\uAC74 extract_tables) \u2014 \uB80C\uB354\uB7EC\uAC00 \uC544\uB294 \uAC1C\uCCB4\uB97C \uC790\uB97C \uBFD0. \uACB0\uACFC: output_dir/<\uC720\uD615>_<\uBC88\uD638>_page_<\uCABD>.png + regions.json(id\xB7\uC720\uD615\xB7\uD398\uC774\uC9C0\xB7bbox pt).",
874
- {
875
- file_path: z.string().min(1).describe("HWPX \uB610\uB294 HWP(5.x) \uD30C\uC77C \uC808\uB300 \uACBD\uB85C"),
876
- output_dir: z.string().min(1).describe("crop \uD30C\uC77C \uC800\uC7A5 \uB514\uB809\uD1A0\uB9AC"),
877
- target: z.array(z.enum(["table", "image", "paragraph", "shape"])).default(["table"]).describe("\uC798\uB77C\uB0BC \uAC1C\uCCB4 \uC720\uD615"),
878
- format: z.enum(["png", "jpeg"]).default("png"),
879
- pages: z.string().min(1).optional().describe("\uB300\uC0C1 \uD398\uC774\uC9C0(1-based \uBC94\uC704)"),
880
- padding_pt: z.number().min(0).max(100).default(0).describe("bbox \uB458\uB808 \uC5EC\uBC31 pt")
881
- },
882
- async ({ file_path, output_dir, target, format, pages, padding_pt }) => {
883
- try {
884
- const { buffer } = await readValidatedFile(file_path, MAX_FILE_SIZE, /* @__PURE__ */ new Set([".hwpx", ".hwp"]));
885
- const dir = safeOutputPath(join(output_dir, "regions.json"), /* @__PURE__ */ new Set([".json"])).replace(/[\\/]regions\.json$/, "");
886
- const { extractRenderedRegions } = await import("./render-MLN4ZLVD.js");
887
- const regions = await extractRenderedRegions(buffer, { types: target, format, pages, paddingPt: padding_pt, reflow: true });
888
- await mkdir(dir, { recursive: true });
889
- const ext = format === "jpeg" ? "jpg" : "png";
890
- const manifest = [];
891
- for (const r of regions) {
892
- const name = `${r.region.id.replace("-", "_")}_page_${String(r.page).padStart(3, "0")}.${ext}`;
893
- await writeFile(join(dir, name), r.data);
894
- manifest.push({ file: name, id: r.region.id, type: r.region.type, sourceId: r.region.sourceId, parentId: r.region.parentId, page: r.page, bbox: r.bbox, widthPx: r.widthPx, heightPx: r.heightPx });
895
- }
896
- await writeFile(join(dir, "regions.json"), JSON.stringify(manifest, null, 2));
897
- return { content: [{ type: "text", text: `crop ${regions.length}\uAC74 \u2192 ${dir}
898
- ` + manifest.map((m) => `${m.file} p${m.page} (${m.bbox.x},${m.bbox.y} ${m.bbox.width}\xD7${m.bbox.height}pt)${m.sourceId ? ` src=${m.sourceId}` : ""}`).join("\n") }] };
899
- } catch (err) {
900
- return { content: [{ type: "text", text: `crop \uC2E4\uD328: ${describeError(err)}` }], isError: true };
901
- }
902
- }
903
- );
904
- server.tool(
905
- "extract_tables",
906
- "HWPX\xB7HWP \uBB38\uC11C\uC758 \uD45C\uB97C \uCD94\uCD9C\xB7\uBD84\uB958\uD569\uB2C8\uB2E4(#76). \uD45C\uB9C8\uB2E4 semantic-table(\uB370\uC774\uD130\uD45C) / non-tabular-layout(\uC870\uC9C1\uB3C4\xB7\uC5F0\uB77D\uB9DD\xB7\uACB0\uC7AC\uB780\uCC98\uB7FC \uD45C\uB97C \uCE94\uBC84\uC2A4\uB85C \uC4F4 \uAC83) / uncertain \uBD84\uB958\uC640 \uADFC\uAC70 \uC2E0\uD638, \uD398\uC774\uC9C0\xB7bbox(pt, \uB80C\uB354 region \uC870\uC778), \uC815\uCC45(visual)\uC5D0 \uB530\uB978 crop \uC774\uBBF8\uC9C0\uB97C \uB3CC\uB824\uC90D\uB2C8\uB2E4 \u2014 \uC870\uC9C1\uB3C4\uB294 \uC774\uBBF8\uC9C0\uB85C, \uB370\uC774\uD130\uD45C\uB294 \uC140 \uAD6C\uC870\uB85C \uB118\uAE30\uB294 \uBA40\uD2F0\uBAA8\uB2EC \uD30C\uC774\uD504\uB77C\uC778\uC6A9. \uD734\uB9AC\uC2A4\uD2F1(\uB124\uD2B8\uC6CC\uD06C\xB7LLM \uC5C6\uC74C), \uC911\uCCA9\uD45C\uB3C4 \uB3C5\uB9BD \uBD84\uB958. crop \uC740 output_dir \uC5D0 \uC800\uC7A5\uD558\uACE0 8\uC7A5\uAE4C\uC9C0 \uC751\uB2F5\uC5D0 \uD568\uAED8 \uB2F4\uC2B5\uB2C8\uB2E4.",
907
- {
908
- file_path: z.string().min(1).describe("HWPX \uB610\uB294 HWP(5.x) \uD30C\uC77C \uC808\uB300 \uACBD\uB85C"),
909
- output_dir: z.string().min(1).optional().describe("crop \uD30C\uC77C\xB7tables.json \uC800\uC7A5 \uB514\uB809\uD1A0\uB9AC (visual \uC774 none \uC774 \uC544\uB2C8\uBA74 \uD544\uC218)"),
910
- visual: z.enum(["none", "non-tabular", "non-tabular-and-uncertain", "all"]).default("none").describe("crop \uB300\uC0C1: none=\uBD84\uB958\xB7bbox \uB9CC / non-tabular=\uC870\uC9C1\uB3C4\uB958\uB9CC / non-tabular-and-uncertain(\uAD8C\uC7A5) / all"),
911
- format: z.enum(["png", "jpeg"]).default("png"),
912
- padding_pt: z.number().min(0).max(100).default(0).describe("crop \uB458\uB808 \uC5EC\uBC31 pt"),
913
- cells: z.boolean().default(false).describe("\uC751\uB2F5 JSON \uC5D0 \uC140 \uD14D\uC2A4\uD2B8 \uACA9\uC790 \uD3EC\uD568")
914
- },
915
- async ({ file_path, output_dir, visual, format, padding_pt, cells }) => {
916
- try {
917
- if (visual !== "none" && !output_dir) {
918
- return { content: [{ type: "text", text: "visual \uC774 none \uC774 \uC544\uB2C8\uBA74 crop \uD30C\uC77C\uC744 \uC800\uC7A5\uD560 output_dir \uC774 \uD544\uC694\uD569\uB2C8\uB2E4." }], isError: true };
919
- }
920
- const { buffer } = await readValidatedFile(file_path, MAX_FILE_SIZE, /* @__PURE__ */ new Set([".hwpx", ".hwp"]));
921
- const dir = output_dir ? safeOutputPath(join(output_dir, "tables.json"), /* @__PURE__ */ new Set([".json"])).replace(/[\\/]tables\.json$/, "") : void 0;
922
- const { extractTables } = await import("./visual-VZ7QGAUC.js");
923
- const tables = await extractTables(buffer, { policy: visual, format, paddingPt: padding_pt });
924
- if (dir) await mkdir(dir, { recursive: true });
925
- const ext = format === "jpeg" ? "jpg" : "png";
926
- const images = [];
927
- const report = [];
928
- for (const t of tables) {
929
- const crops = [];
930
- for (const c of t.crops) {
931
- const name = `${t.id.replace(/[^\w.-]/g, "_")}_page_${String(c.page).padStart(3, "0")}.${ext}`;
932
- if (dir) await writeFile(join(dir, name), c.data);
933
- if (images.length < 8) images.push({ type: "image", data: c.data.toString("base64"), mimeType: c.mimeType });
934
- crops.push({ file: dir ? name : void 0, page: c.page, bbox: c.bbox });
977
+ );
978
+ server2.tool(
979
+ "crop_regions",
980
+ "HWPX\xB7HWP \uBB38\uC11C\uB97C \uB80C\uB354\uD574 \uD45C\xB7\uC774\uBBF8\uC9C0\xB7\uBB38\uB2E8\xB7\uB3C4\uD615 \uC601\uC5ED\uC744 \uD398\uC774\uC9C0 \uC774\uBBF8\uC9C0\uC5D0\uC11C \uC798\uB77C \uD30C\uC77C\uB85C \uC800\uC7A5\uD569\uB2C8\uB2E4(render_document \uC640 \uAC19\uC740 \uC870\uD310 \uC5D4\uC9C4, \uD398\uC774\uC9C0 \uB85C\uCEEC pt bbox \uB97C \uC2E4\uBC30\uC728\uB85C \uD658\uC0B0). \uD45C\uAC00 \uC9C4\uC9DC \uB370\uC774\uD130\uD45C\uC778\uC9C0 \uC870\uC9C1\uB3C4\uC778\uC9C0\uB294 \uD310\uB2E8\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4(\uADF8\uAC74 extract_tables) \u2014 \uB80C\uB354\uB7EC\uAC00 \uC544\uB294 \uAC1C\uCCB4\uB97C \uC790\uB97C \uBFD0. \uACB0\uACFC: output_dir/<\uC720\uD615>_<\uBC88\uD638>_page_<\uCABD>.png + regions.json(id\xB7\uC720\uD615\xB7\uD398\uC774\uC9C0\xB7bbox pt).",
981
+ {
982
+ file_path: z3.string().min(1).describe("HWPX \uB610\uB294 HWP(5.x) \uD30C\uC77C \uC808\uB300 \uACBD\uB85C"),
983
+ output_dir: z3.string().min(1).describe("crop \uD30C\uC77C \uC800\uC7A5 \uB514\uB809\uD1A0\uB9AC"),
984
+ target: z3.array(z3.enum(["table", "image", "paragraph", "shape"])).default(["table"]).describe("\uC798\uB77C\uB0BC \uAC1C\uCCB4 \uC720\uD615"),
985
+ format: z3.enum(["png", "jpeg"]).default("png"),
986
+ pages: z3.string().min(1).optional().describe("\uB300\uC0C1 \uD398\uC774\uC9C0(1-based \uBC94\uC704)"),
987
+ padding_pt: z3.number().min(0).max(100).default(0).describe("bbox \uB458\uB808 \uC5EC\uBC31 pt")
988
+ },
989
+ async ({ file_path, output_dir, target, format, pages, padding_pt }) => {
990
+ try {
991
+ const { buffer } = await readValidatedFile(file_path, MAX_FILE_SIZE, /* @__PURE__ */ new Set([".hwpx", ".hwp"]));
992
+ const dir = safeOutputPath(join(output_dir, "regions.json"), /* @__PURE__ */ new Set([".json"])).replace(/[\\/]regions\.json$/, "");
993
+ const { extractRenderedRegions } = await import("./render-NGP5T24S.js");
994
+ const regions = await extractRenderedRegions(buffer, { types: target, format, pages, paddingPt: padding_pt, reflow: true });
995
+ await mkdir2(dir, { recursive: true });
996
+ const ext = format === "jpeg" ? "jpg" : "png";
997
+ const manifest = [];
998
+ for (const r of regions) {
999
+ const name = `${r.region.id.replace("-", "_")}_page_${String(r.page).padStart(3, "0")}.${ext}`;
1000
+ await writeFile2(join(dir, name), r.data);
1001
+ manifest.push({ file: name, id: r.region.id, type: r.region.type, sourceId: r.region.sourceId, parentId: r.region.parentId, page: r.page, bbox: r.bbox, widthPx: r.widthPx, heightPx: r.heightPx });
935
1002
  }
936
- report.push({
937
- id: t.id,
938
- sourceId: t.sourceId,
939
- page: t.page,
940
- classification: t.classification,
941
- table: { rows: t.table.rows, cols: t.table.cols, hasHeader: t.table.hasHeader, caption: t.table.caption, ...cells ? { cells: t.table.cells.map((r) => r.map((c) => c.text)) } : {} },
942
- regions: t.regions,
943
- crops,
944
- warnings: t.warnings
945
- });
1003
+ await writeFile2(join(dir, "regions.json"), JSON.stringify(manifest, null, 2));
1004
+ return { content: [{ type: "text", text: `crop ${regions.length}\uAC74 \u2192 ${dir}
1005
+ ` + manifest.map((m) => `${m.file} p${m.page} (${m.bbox.x},${m.bbox.y} ${m.bbox.width}\xD7${m.bbox.height}pt)${m.sourceId ? ` src=${m.sourceId}` : ""}`).join("\n") }] };
1006
+ } catch (err) {
1007
+ return { content: [{ type: "text", text: `crop \uC2E4\uD328: ${describeError(err)}` }], isError: true };
946
1008
  }
947
- const json = JSON.stringify(report, null, 2);
948
- if (dir) await writeFile(join(dir, "tables.json"), json + "\n");
949
- const kinds = report.reduce((m, r) => {
950
- m[r.classification.kind] = (m[r.classification.kind] ?? 0) + 1;
951
- return m;
952
- }, {});
953
- const head = `\uD45C ${report.length}\uAC1C (${Object.entries(kinds).map(([k, v]) => `${k} ${v}`).join(", ") || "\uC5C6\uC74C"}) crop ${report.reduce((n, r) => n + r.crops.length, 0)}\uAC74${dir ? ` \u2192 ${dir}` : ""}${images.length < report.reduce((n, r) => n + r.crops.length, 0) ? " (\uC751\uB2F5\uC5D0\uB294 8\uC7A5\uAE4C\uC9C0)" : ""}`;
954
- return { content: [...images, { type: "text", text: `${head}
955
- ${json}` }] };
956
- } catch (err) {
957
- return { content: [{ type: "text", text: `\uD45C \uCD94\uCD9C \uC2E4\uD328: ${describeError(err)}` }], isError: true };
958
1009
  }
959
- }
960
- );
961
- server.tool(
962
- "redact_document",
963
- "\uBB38\uC11C\uC758 \uAC1C\uC778\uC815\uBCF4(\uC8FC\uBBFC\uBC88\uD638\xB7\uC804\uD654\xB7\uC774\uBA54\uC77C\xB7\uCE74\uB4DC\xB7\uACC4\uC88C)\uB97C \uD0D0\uC9C0\uD574 \uC11C\uC2DD \uBCF4\uC874 \uB9C8\uC2A4\uD0B9\uD569\uB2C8\uB2E4. HWPX/HWP\uB294 \uC6D0\uBCF8 \uC11C\uC2DD 1\uBC14\uC774\uD2B8 \uADF8\uB300\uB85C patch, \uADF8 \uC678 \uD3EC\uB9F7\uC740 \uB9C8\uC2A4\uD0B9\uB41C \uB9C8\uD06C\uB2E4\uC6B4\uC73C\uB85C \uCD9C\uB825. \uC790\uB3D9 \uAC80\uCD9C \uBCF4\uC870 \uB3C4\uAD6C \u2014 \uACB0\uACFC \uB9AC\uD3EC\uD2B8\uB97C \uC0AC\uB78C\uC774 \uCD5C\uC885 \uD655\uC778\uD574\uC57C \uD558\uBA70 \uC774\uBBF8\uC9C0 \uC548 \uD14D\uC2A4\uD2B8\uB294 \uD0D0\uC9C0\uD558\uC9C0 \uBABB\uD569\uB2C8\uB2E4. \uB9C8\uC2A4\uD0B9 \uD6C4 render_document\uB85C \uB208\uC73C\uB85C \uD655\uC778\uD558\uB294 \uAC83\uC744 \uAD8C\uC7A5\uD569\uB2C8\uB2E4.",
964
- {
965
- file_path: z.string().min(1).describe("\uB300\uC0C1 \uBB38\uC11C\uC758 \uC808\uB300 \uACBD\uB85C"),
966
- rules: z.array(z.enum(["rrn", "phone", "email", "card", "account", "passport", "driver"])).optional().describe("\uC801\uC6A9 \uB8F0 (\uAE30\uBCF8: rrn\xB7phone\xB7email\xB7card\xB7account \u2014 passport\xB7driver\uB294 opt-in)"),
967
- mask_char: z.string().min(1).max(1).optional().describe("\uB9C8\uC2A4\uD06C \uBB38\uC790 1\uAE00\uC790 (\uAE30\uBCF8: \u25CF)"),
968
- output_path: z.string().min(1).optional().describe("\uCD9C\uB825 \uACBD\uB85C (HWPX/HWP\uB294 \uAC19\uC740 \uD655\uC7A5\uC790, \uADF8 \uC678\uB294 .md) \u2014 dry_run\uC774 \uC544\uB2C8\uBA74 \uD544\uC218"),
969
- dry_run: z.boolean().default(false).describe("\uD0D0\uC9C0 \uB9AC\uD3EC\uD2B8\uB9CC \uBC18\uD658, \uD30C\uC77C \uBBF8\uC0DD\uC131")
970
- },
971
- async ({ file_path, rules, mask_char, output_path, dry_run }) => {
972
- try {
973
- const { buffer } = await readValidatedFile(file_path);
974
- const format = detectFormat(buffer);
975
- const patchable = format === "hwpx" || format === "hwp";
976
- if (!dry_run && !output_path) {
977
- return { content: [{ type: "text", text: "output_path\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4 (\uD0D0\uC9C0\uB9CC \uC6D0\uD558\uBA74 dry_run: true)." }], isError: true };
978
- }
979
- const outPath = !dry_run ? safeOutputPath(output_path, new Set(patchable ? [format === "hwp" ? ".hwp" : ".hwpx"] : [".md", ".markdown", ".txt"])) : void 0;
980
- const parsed = await parse(buffer, { filePath: file_path });
981
- if (!parsed.success) {
982
- return { content: [{ type: "text", text: `\uD30C\uC2F1 \uC2E4\uD328: ${parsed.error}` }], isError: true };
983
- }
984
- const { redactMarkdown } = await import("./redact-7ILEAUTS.js");
985
- const r = redactMarkdown(parsed.markdown, { rules, maskChar: mask_char });
986
- const byRule = /* @__PURE__ */ new Map();
987
- for (const h of r.hits) byRule.set(h.rule, (byRule.get(h.rule) ?? 0) + 1);
988
- const lines = [
989
- `\uD0D0\uC9C0: ${r.hits.length}\uAC74 (${[...byRule.entries()].map(([k, v]) => `${k} ${v}`).join(", ") || "\uC5C6\uC74C"})`,
990
- ...r.hits.map((h) => ` - [${h.rule}] ${h.masked}`)
991
- ];
992
- if (!dry_run && r.hits.length > 0) {
993
- if (patchable) {
994
- const original = new Uint8Array(buffer);
995
- const result = format === "hwp" ? await patchHwp(original, r.text) : await patchHwpx(original, r.text);
996
- if (!result.success || !result.data) {
997
- return { content: [{ type: "text", text: `\uB9C8\uC2A4\uD0B9 \uD328\uCE58 \uC2E4\uD328: ${result.error ?? "\uC54C \uC218 \uC5C6\uB294 \uC624\uB958"}` }], isError: true };
998
- }
999
- await mkdir(dirname(outPath), { recursive: true });
1000
- await writeFile(outPath, Buffer.from(result.data));
1001
- lines.push(`\uC800\uC7A5: ${outPath} (\uC6D0\uBCF8 \uC11C\uC2DD \uBCF4\uC874)`);
1002
- if (result.skipped.length > 0) {
1003
- lines.push(`\u26A0\uFE0F \uBBF8\uC801\uC6A9 ${result.skipped.length}\uAC74 \u2014 \uD574\uB2F9 \uC704\uCE58\uB294 \uC6D0\uBB38\uC774 \uB0A8\uC544 \uC788\uC73C\uB2C8 \uBC18\uB4DC\uC2DC \uC218\uB3D9 \uD655\uC778:`);
1004
- for (const s of result.skipped) lines.push(` - ${s.reason}`);
1010
+ );
1011
+ server2.tool(
1012
+ "extract_tables",
1013
+ "HWPX\xB7HWP \uBB38\uC11C\uC758 \uD45C\uB97C \uCD94\uCD9C\xB7\uBD84\uB958\uD569\uB2C8\uB2E4(#76). \uD45C\uB9C8\uB2E4 semantic-table(\uB370\uC774\uD130\uD45C) / non-tabular-layout(\uC870\uC9C1\uB3C4\xB7\uC5F0\uB77D\uB9DD\xB7\uACB0\uC7AC\uB780\uCC98\uB7FC \uD45C\uB97C \uCE94\uBC84\uC2A4\uB85C \uC4F4 \uAC83) / uncertain \uBD84\uB958\uC640 \uADFC\uAC70 \uC2E0\uD638, \uD398\uC774\uC9C0\xB7bbox(pt, \uB80C\uB354 region \uC870\uC778), \uC815\uCC45(visual)\uC5D0 \uB530\uB978 crop \uC774\uBBF8\uC9C0\uB97C \uB3CC\uB824\uC90D\uB2C8\uB2E4 \u2014 \uC870\uC9C1\uB3C4\uB294 \uC774\uBBF8\uC9C0\uB85C, \uB370\uC774\uD130\uD45C\uB294 \uC140 \uAD6C\uC870\uB85C \uB118\uAE30\uB294 \uBA40\uD2F0\uBAA8\uB2EC \uD30C\uC774\uD504\uB77C\uC778\uC6A9. \uD734\uB9AC\uC2A4\uD2F1(\uB124\uD2B8\uC6CC\uD06C\xB7LLM \uC5C6\uC74C), \uC911\uCCA9\uD45C\uB3C4 \uB3C5\uB9BD \uBD84\uB958. crop \uC740 output_dir \uC5D0 \uC800\uC7A5\uD558\uACE0 8\uC7A5\uAE4C\uC9C0 \uC751\uB2F5\uC5D0 \uD568\uAED8 \uB2F4\uC2B5\uB2C8\uB2E4.",
1014
+ {
1015
+ file_path: z3.string().min(1).describe("HWPX \uB610\uB294 HWP(5.x) \uD30C\uC77C \uC808\uB300 \uACBD\uB85C"),
1016
+ output_dir: z3.string().min(1).optional().describe("crop \uD30C\uC77C\xB7tables.json \uC800\uC7A5 \uB514\uB809\uD1A0\uB9AC (visual \uC774 none \uC774 \uC544\uB2C8\uBA74 \uD544\uC218)"),
1017
+ visual: z3.enum(["none", "non-tabular", "non-tabular-and-uncertain", "all"]).default("none").describe("crop \uB300\uC0C1: none=\uBD84\uB958\xB7bbox \uB9CC / non-tabular=\uC870\uC9C1\uB3C4\uB958\uB9CC / non-tabular-and-uncertain(\uAD8C\uC7A5) / all"),
1018
+ format: z3.enum(["png", "jpeg"]).default("png"),
1019
+ padding_pt: z3.number().min(0).max(100).default(0).describe("crop \uB458\uB808 \uC5EC\uBC31 pt"),
1020
+ cells: z3.boolean().default(false).describe("\uC751\uB2F5 JSON \uC5D0 \uC140 \uD14D\uC2A4\uD2B8 \uACA9\uC790 \uD3EC\uD568")
1021
+ },
1022
+ async ({ file_path, output_dir, visual, format, padding_pt, cells }) => {
1023
+ try {
1024
+ if (visual !== "none" && !output_dir) {
1025
+ return { content: [{ type: "text", text: "visual \uC774 none \uC774 \uC544\uB2C8\uBA74 crop \uD30C\uC77C\uC744 \uC800\uC7A5\uD560 output_dir \uC774 \uD544\uC694\uD569\uB2C8\uB2E4." }], isError: true };
1026
+ }
1027
+ const { buffer } = await readValidatedFile(file_path, MAX_FILE_SIZE, /* @__PURE__ */ new Set([".hwpx", ".hwp"]));
1028
+ const dir = output_dir ? safeOutputPath(join(output_dir, "tables.json"), /* @__PURE__ */ new Set([".json"])).replace(/[\\/]tables\.json$/, "") : void 0;
1029
+ const { extractTables } = await import("./visual-NZLYIYEM.js");
1030
+ const tables = await extractTables(buffer, { policy: visual, format, paddingPt: padding_pt });
1031
+ if (dir) await mkdir2(dir, { recursive: true });
1032
+ const ext = format === "jpeg" ? "jpg" : "png";
1033
+ const images = [];
1034
+ const report = [];
1035
+ for (const t of tables) {
1036
+ const crops = [];
1037
+ for (const c of t.crops) {
1038
+ const name = `${t.id.replace(/[^\w.-]/g, "_")}_page_${String(c.page).padStart(3, "0")}.${ext}`;
1039
+ if (dir) await writeFile2(join(dir, name), c.data);
1040
+ if (images.length < 8) images.push({ type: "image", data: c.data.toString("base64"), mimeType: c.mimeType });
1041
+ crops.push({ file: dir ? name : void 0, page: c.page, bbox: c.bbox });
1005
1042
  }
1006
- lines.push("render_document\uB85C \uB9C8\uC2A4\uD0B9 \uACB0\uACFC\uB97C \uB208\uC73C\uB85C \uD655\uC778\uD558\uC138\uC694.");
1007
- } else {
1008
- await mkdir(dirname(outPath), { recursive: true });
1009
- await writeFile(outPath, r.text, "utf-8");
1010
- lines.push(`\uC800\uC7A5: ${outPath} (${format}\uB294 \uC11C\uC2DD \uBCF4\uC874 \uBBF8\uC9C0\uC6D0 \u2014 \uB9C8\uC2A4\uD0B9\uB41C \uB9C8\uD06C\uB2E4\uC6B4)`);
1043
+ report.push({
1044
+ id: t.id,
1045
+ sourceId: t.sourceId,
1046
+ page: t.page,
1047
+ classification: t.classification,
1048
+ table: { rows: t.table.rows, cols: t.table.cols, hasHeader: t.table.hasHeader, caption: t.table.caption, ...cells ? { cells: t.table.cells.map((r) => r.map((c) => c.text)) } : {} },
1049
+ regions: t.regions,
1050
+ crops,
1051
+ warnings: t.warnings
1052
+ });
1011
1053
  }
1012
- } else if (!dry_run) {
1013
- lines.push("\uD0D0\uC9C0 0\uAC74 \u2014 \uCD9C\uB825 \uD30C\uC77C\uC744 \uB9CC\uB4E4\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.");
1014
- }
1015
- lines.push("\uC8FC\uC758: \uC790\uB3D9 \uAC80\uCD9C\uC740 \uBCF4\uC870 \uC218\uB2E8\uC785\uB2C8\uB2E4. \uC774\uBBF8\uC9C0 \uC18D \uD14D\uC2A4\uD2B8\xB7\uD45C\uAE30 \uBCC0\uD615\uC740 \uB193\uCE60 \uC218 \uC788\uC73C\uB2C8 \uCD5C\uC885 \uACF5\uAC1C \uC804 \uC0AC\uB78C \uAC80\uD1A0\uAC00 \uD544\uC694\uD569\uB2C8\uB2E4.");
1016
- return { content: [{ type: "text", text: capResponseText(lines.join("\n")) }] };
1017
- } catch (err) {
1018
- return { content: [{ type: "text", text: `\uB9C8\uC2A4\uD0B9 \uC2E4\uD328: ${describeError(err)}` }], isError: true };
1019
- }
1020
- }
1021
- );
1022
- server.tool(
1023
- "parse_chunks",
1024
- "\uBB38\uC11C\uB97C RAG\uC6A9 \uAD6C\uC870 \uCCAD\uD06C JSON\uC73C\uB85C \uD30C\uC2F1\uD569\uB2C8\uB2E4. \uD5E4\uB529\xB7\uAC1C\uC870\uC2DD \uC704\uACC4(\u25A1\u25CB- / 1.\xB7\uAC00.\xB71))\uAC00 breadcrumb \uACBD\uB85C\uB85C \uBCF4\uC874\uB418\uACE0 \uD45C\uB294 \uB3C5\uB9BD \uCCAD\uD06C\uB85C \uB098\uC635\uB2C8\uB2E4 \u2014 \uC784\uBCA0\uB529\xB7\uC778\uB371\uC2F1 \uC804\uCC98\uB9AC\uC6A9. \uC790\uB974\uAE30(\uD1A0\uD070 \uC0C1\uD55C\xB7\uC624\uBC84\uB7A9)\uB294 \uC18C\uBE44\uC790 \uBAAB\uC785\uB2C8\uB2E4.",
1025
- {
1026
- file_path: z.string().min(1).describe("\uB300\uC0C1 \uBB38\uC11C\uC758 \uC808\uB300 \uACBD\uB85C (HWP/HWPX/PDF/XLSX/DOCX)"),
1027
- granularity: z.enum(["section", "block"]).default("section").describe("section=\uAC19\uC740 breadcrumb \uC544\uB798 \uC5F0\uC18D \uD14D\uC2A4\uD2B8 \uBCD1\uD569(\uAE30\uBCF8), block=IRBlock 1\uAC1C=\uCCAD\uD06C 1\uAC1C"),
1028
- include_table_cells: z.boolean().default(false).describe("\uD45C \uCCAD\uD06C\uC5D0 \uC140 \uD14D\uC2A4\uD2B8 2\uCC28\uC6D0 \uBC30\uC5F4 \uD3EC\uD568 \uC5EC\uBD80")
1029
- },
1030
- async ({ file_path, granularity, include_table_cells }) => {
1031
- try {
1032
- const { buffer } = await readValidatedFile(file_path, MAX_FILE_SIZE, PARSE_EXTENSIONS);
1033
- const parsed = await parse(buffer, { filePath: file_path });
1034
- if (!parsed.success) {
1035
- return { content: [{ type: "text", text: `\uD30C\uC2F1 \uC2E4\uD328: ${parsed.error}` }], isError: true };
1054
+ const json = JSON.stringify(report, null, 2);
1055
+ if (dir) await writeFile2(join(dir, "tables.json"), json + "\n");
1056
+ const kinds = report.reduce((m, r) => {
1057
+ m[r.classification.kind] = (m[r.classification.kind] ?? 0) + 1;
1058
+ return m;
1059
+ }, {});
1060
+ const head = `\uD45C ${report.length}\uAC1C (${Object.entries(kinds).map(([k, v]) => `${k} ${v}`).join(", ") || "\uC5C6\uC74C"}) crop ${report.reduce((n, r) => n + r.crops.length, 0)}\uAC74${dir ? ` \u2192 ${dir}` : ""}${images.length < report.reduce((n, r) => n + r.crops.length, 0) ? " (\uC751\uB2F5\uC5D0\uB294 8\uC7A5\uAE4C\uC9C0)" : ""}`;
1061
+ return { content: [...images, { type: "text", text: `${head}
1062
+ ${json}` }] };
1063
+ } catch (err) {
1064
+ return { content: [{ type: "text", text: `\uD45C \uCD94\uCD9C \uC2E4\uD328: ${describeError(err)}` }], isError: true };
1036
1065
  }
1037
- const { blocksToChunks } = await import("./chunks-PEHRXW2P.js");
1038
- const chunks = blocksToChunks(parsed.blocks, { granularity, includeTableCells: include_table_cells });
1039
- return { content: [{ type: "text", text: capResponseText(JSON.stringify(chunks, null, 2)) }] };
1040
- } catch (err) {
1041
- return { content: [{ type: "text", text: `\uCCAD\uD06C \uD30C\uC2F1 \uC2E4\uD328: ${describeError(err)}` }], isError: true };
1042
1066
  }
1043
- }
1044
- );
1045
- server.tool(
1046
- "extract_profile",
1047
- '\uCC38\uC870 HWPX \uBB38\uC11C\uC5D0\uC11C \uD45C \uC11C\uC2DD \uD504\uB85C\uD544(\uD14C\uB450\uB9AC\xB7\uC74C\uC601\xB7\uC5F4 \uB108\uBE44\xB7\uC140 \uAE00\uAF34)\uC744 JSON\uC73C\uB85C \uCD94\uCD9C\uD569\uB2C8\uB2E4. \uCD94\uCD9C\uD55C \uD504\uB85C\uD544\uC744 generate_document\uC758 profile_path\uB85C \uB118\uAE30\uBA74 \uC6D0\uBCF8 \uBB38\uC11C \uC5C6\uC774 \uAC19\uC740 \uD45C \uC11C\uC2DD\uC744 \uC7AC\uD604\uD569\uB2C8\uB2E4 \u2014 "\uC774 \uBB38\uC11C \uD45C \uC11C\uC2DD \uADF8\uB300\uB85C \uB9CC\uB4E4\uC5B4\uC918" \uC6CC\uD06C\uD50C\uB85C\uC758 1\uB2E8\uACC4.',
1048
- {
1049
- hwpx_path: z.string().min(1).describe("\uC11C\uC2DD\uC744 \uCD94\uCD9C\uD560 \uCC38\uC870 HWPX \uD30C\uC77C\uC758 \uC808\uB300 \uACBD\uB85C"),
1050
- output_path: z.string().min(1).describe("\uD504\uB85C\uD544 JSON \uCD9C\uB825 \uC808\uB300 \uACBD\uB85C (.json \uAD8C\uC7A5)")
1051
- },
1052
- async ({ hwpx_path, output_path }) => {
1053
- try {
1054
- const out = safeOutputPath(output_path, PROFILE_EXTENSIONS);
1055
- const { buffer } = await readValidatedFile(hwpx_path);
1056
- const { hwpxToProfile } = await import("./-SLX6IB6Y.js");
1057
- const profile = await hwpxToProfile(buffer);
1058
- await mkdir(dirname(out), { recursive: true });
1059
- await writeFile(out, JSON.stringify(profile, null, 2));
1060
- return {
1061
- content: [{ type: "text", text: `\uC11C\uC2DD \uD504\uB85C\uD544 \uCD94\uCD9C \uC644\uB8CC: \uD45C ${profile.tables.length}\uAC1C \u2192 ${out}
1067
+ );
1068
+ }
1069
+
1070
+ // src/mcp/tools-generate.ts
1071
+ import { z as z4 } from "zod";
1072
+ import { readFile as readFile3, writeFile as writeFile3, mkdir as mkdir3, realpath as realpath3 } from "fs/promises";
1073
+ import { resolve as resolve2, dirname as dirname4, join as join2 } from "path";
1074
+ function registerGenerateTools(server2) {
1075
+ server2.tool(
1076
+ "extract_profile",
1077
+ '\uCC38\uC870 HWPX \uBB38\uC11C\uC5D0\uC11C \uD45C \uC11C\uC2DD \uD504\uB85C\uD544(\uD14C\uB450\uB9AC\xB7\uC74C\uC601\xB7\uC5F4 \uB108\uBE44\xB7\uC140 \uAE00\uAF34)\uC744 JSON\uC73C\uB85C \uCD94\uCD9C\uD569\uB2C8\uB2E4. \uCD94\uCD9C\uD55C \uD504\uB85C\uD544\uC744 generate_document\uC758 profile_path\uB85C \uB118\uAE30\uBA74 \uC6D0\uBCF8 \uBB38\uC11C \uC5C6\uC774 \uAC19\uC740 \uD45C \uC11C\uC2DD\uC744 \uC7AC\uD604\uD569\uB2C8\uB2E4 \u2014 "\uC774 \uBB38\uC11C \uD45C \uC11C\uC2DD \uADF8\uB300\uB85C \uB9CC\uB4E4\uC5B4\uC918" \uC6CC\uD06C\uD50C\uB85C\uC758 1\uB2E8\uACC4.',
1078
+ {
1079
+ hwpx_path: z4.string().min(1).describe("\uC11C\uC2DD\uC744 \uCD94\uCD9C\uD560 \uCC38\uC870 HWPX \uD30C\uC77C\uC758 \uC808\uB300 \uACBD\uB85C"),
1080
+ output_path: z4.string().min(1).describe("\uD504\uB85C\uD544 JSON \uCD9C\uB825 \uC808\uB300 \uACBD\uB85C (.json \uAD8C\uC7A5)")
1081
+ },
1082
+ async ({ hwpx_path, output_path }) => {
1083
+ try {
1084
+ const out = safeOutputPath(output_path, PROFILE_EXTENSIONS);
1085
+ const { buffer } = await readValidatedFile(hwpx_path);
1086
+ const { hwpxToProfile } = await import("./-AQKXLFIM.js");
1087
+ const profile = await hwpxToProfile(buffer);
1088
+ await mkdir3(dirname4(out), { recursive: true });
1089
+ await writeFile3(out, JSON.stringify(profile, null, 2));
1090
+ return {
1091
+ content: [{ type: "text", text: `\uC11C\uC2DD \uD504\uB85C\uD544 \uCD94\uCD9C \uC644\uB8CC: \uD45C ${profile.tables.length}\uAC1C \u2192 ${out}
1062
1092
  (generate_document\uC758 profile_path\uB85C \uC0AC\uC6A9)` }]
1063
- };
1064
- } catch (err) {
1065
- return {
1066
- content: [{ type: "text", text: `\uC624\uB958: ${describeError(err)}` }],
1067
- isError: true
1068
- };
1069
- }
1070
- }
1071
- );
1072
- server.tool(
1073
- "generate_document",
1074
- "\uB9C8\uD06C\uB2E4\uC6B4\uC744 HWPX \uD55C\uAE00 \uBB38\uC11C\uB85C \uC0DD\uC131\uD569\uB2C8\uB2E4. \"\uBCF4\uACE0\uC11C\uB85C/\uACF5\uBB38\uC11C\uB85C/\uAC1C\uC870\uC2DD\uC73C\uB85C/\uACC4\uD68D\uC11C\uB85C \uBF51\uC544\uC918\xB7\uB9CC\uB4E4\uC5B4\uC918\" \uC694\uCCAD\uC774 \uC774 \uB3C4\uAD6C\uC785\uB2C8\uB2E4. \uD504\uB9AC\uC14B \uB9E4\uD551: \uC815\uBD80 \uD45C\uC900 \uBCF4\uACE0\uC11C(\uD45C\uC9C0\xB7\uBAA9\uCC28\xB7\uB85C\uB9C8\uC22B\uC790 \uC7A5\uD5E4\uB354 \uC790\uB3D9)='\uAC1C\uC870\uC2DD', \uC911\uC559\uBD80\uCC98 \uC5C5\uBB34\uBCF4\uACE0\xB7\uAD6D\uD68C \uC11C\uBA74\uBCF4\uACE0(\uC7AC\uACBD\uBD80 \uC2E4\uCE21: \uC7A5 \uB760\xB7\uC808 \uC22B\uC790\uCE78\xB7\uC18C\uC81C\uBAA9 \uBC15\uC2A4\xB7\u2460 \uD56D\uBAA9 \uB760\xB7\uC5F0\uB178\uB791 \uC131\uACFC \uC694\uC57D\uBC15\uC2A4\xB7\uBCC4\uCCA8 \uB760, \u25A1 \uB4A4 (\uD0A4\uC6CC\uB4DC) \uD30C\uB791)='\uC5C5\uBB34\uBCF4\uACE0', \uAE30\uC548\uBB38\xB7\uC2DC\uD589\uBB38\xB7\uC54C\uB9BC\uACF5\uBB38='\uAE30\uC548\uBB38', 1\uD398\uC774\uC9C0 \uC694\uC57D\uBCF4\uACE0\uC11C='\uBCF4\uACE0\uC11C', \uCD94\uC9C4\uACC4\uD68D='\uACC4\uD68D\uC11C'. \uD45C\uB294 \uC2E4\uCE21 \uC815\uBD80 \uC11C\uC2DD(\uD5E4\uB354 \uC74C\uC601+\uC774\uC911\uC120\xB7\uC678\uACFD \uAD75\uC740\uC120\xB7\uB0B4\uC6A9 \uBE44\uB840 \uC5F4\uD3ED), \uCABD\uBC88\uD638\xB7\uACB0\uC7AC\uB780\xB7'\uB05D.' \uD45C\uC2DC \uC9C0\uC6D0. \u26A0 \uC0DD\uC131 \uC804 \uD655\uC778 \uAD8C\uC7A5: \uBB38\uC11C\uC885\uB958(\uBCF4\uACE0\uC11C/\uAE30\uC548\uBB38)\xB7\uC81C\uBAA9\xB7\uAE30\uAD00\uBA85(org)\xB7\uB0A0\uC9DC\xB7\uBAA9\uCC28 \uC5EC\uBD80\uAC00 \uBD88\uBA85\uD655\uD558\uBA74 \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uBB3C\uC5B4\uBCF4\uC138\uC694 \u2014 \uC5C9\uB6B1\uD55C \uD504\uB9AC\uC14B \uC120\uD0DD\uC774 \uAC00\uC7A5 \uD754\uD55C \uC624\uC0DD\uC131 \uC6D0\uC778. \uB9C8\uD06C\uB2E4\uC6B4 \uADDC\uCE59(v4.13 \uC11C\uC6B8 \uC2E4\uACB0\uC7AC \uC2E4\uCE21 \uC704\uACC4): #(h1)=\uBB38\uC11C \uC81C\uBAA9, ##(h2)=\uC7A5(\uBCF4\uACE0\uC11C \u2160 \u2161 / \uAE30\uC548\uBB38\xB7\uD1B5\uC9C0\uB294 \uBC95\uC815 1.), ###(h3)=\u25A1 \uB300\uD56D\uBAA9(\uAE30\uC548\uBB38 \uAC00.), \uADF8 \uC544\uB798 \uB9AC\uC2A4\uD2B8=\u3147 \u2192 - \u2192 \u318D(\uAE30\uC548\uBB38 \uAC00. \u2192 1) \u2192 \uAC00)), \uBCF8\uBB38\uC5D0 \u25A1/\u3147/-/1./\uAC00. \uB97C \uC9C1\uC811 \uC368\uB3C4 \uAC19\uC740 \uC704\uACC4\uB85C \uC815\uADDC\uD654, \u203B\uC2DC\uC791\xB7'\uCD9C\uCC98:'\xB7'\uC790\uB8CC:'=\uCC38\uACE0(13pt), \uC81C\uBAA9 \uC9C1\uD6C4 \uC778\uC6A9\uBB38(>)=\uBCF4\uACE0\uC11C \uC694\uC57D\uBC15\uC2A4 \u2014 \u2605\uBCF4\uACE0\uC11C\uB294 \uBC18\uB4DC\uC2DC \uC81C\uBAA9 \uC9C1\uD6C4 `> \u2026\uD558\uACE0\uC790 \uD568` \uD55C \uBB38\uC7A5(\uC27C\uD45C \uD5C8\uC6A9\xB7\uBD80\uD638 \uC5C6\uC74C\xB73\uC904 \uC774\uB0B4 \uC57D 90~100\uC790)\uB85C \uBCF4\uACE0 \uBAA9\uC801\uC744 \uB123\uC744 \uAC83(\uC5C6\uAC70\uB098 3\uC904 \uCD08\uACFC\uBA74 \uACBD\uACE0), <right>\uD14D\uC2A4\uD2B8</right>=\uC6B0\uCE21\uC815\uB82C. \u25A1\xB7\uC81C\uBAA9\uC740 \uD55C \uC904\uC5D0 \uC790\uB3D9 \uCD95\uC18C, \uB458\uC9F8 \uC904 20% \uC774\uB0B4 \uACE0\uC544 \uC904\uC740 \uC790\uAC04 \uCD95\uC18C\uB85C \uD55C \uC904. \uBC95\uB839 \uCF54\uB4DC (282791)\xB7KOSIS \uD45C ID DT_\u2026\xB7(\uBC95\uC815\uB3D9\uCF54\uB4DC \u2026)\xB7\u25CB\u25CB MCP \uC870\uD68C \uAC19\uC740 \uB0B4\uBD80 \uC2DD\uBCC4\uC790\xB7\uB3C4\uAD6C \uC5B8\uAE09\uC740 \uC790\uB3D9 \uC81C\uAC70 \u2014 \uCD9C\uCC98\uB294 \uAE30\uAD00\xB7\uC790\uB8CC\uBA85\uB9CC \uC4F8 \uAC83. (\uC6D0\uBCF8 \uC11C\uC2DD \uBCF4\uC874 \uC81C\uC790\uB9AC \uC218\uC815\uC740 patch_document, \uC11C\uC2DD \uBE48\uCE78 \uCC44\uC6B0\uAE30\uB294 fill_form)",
1075
- {
1076
- markdown: z.string().min(1).describe("HWPX\uB85C \uBCC0\uD658\uD560 \uB9C8\uD06C\uB2E4\uC6B4 \uC804\uBB38. \uD45C\uB294 GFM \uBB38\uBC95 \uC0AC\uC6A9 (\uC608: '| \uC774\uB984 | \uBD80\uC11C |\\n| --- | --- |\\n| \uD64D\uAE38\uB3D9 | \uAE30\uD68D\uD300 |')"),
1077
- output_path: z.string().min(1).describe("\uCD9C\uB825 HWPX \uD30C\uC77C\uC758 \uC808\uB300 \uACBD\uB85C (.hwpx \uAD8C\uC7A5)"),
1078
- profile_path: z.string().optional().describe("\uC11C\uC2DD \uD504\uB85C\uD544 JSON \uACBD\uB85C (extract_profile\uB85C \uCD94\uCD9C) \u2014 \uCC38\uC870 \uBB38\uC11C\uC758 \uD45C \uD14C\uB450\uB9AC\xB7\uC74C\uC601\xB7\uC5F4\uD3ED\xB7\uC140 \uAE00\uAF34\uC744 \uC7AC\uD604. \uD45C \uD589\xB7\uC5F4 \uC218\uC640 \uCCAB \uC140 \uD14D\uC2A4\uD2B8\uAC00 \uC77C\uCE58\uD558\uB294 \uD45C\uC5D0\uB9CC \uC801\uC6A9"),
1079
- // 값 집합·범위는 gongmun-surface SSOT에서 파생 (CLI와 드리프트 불가 — v4.0.4 영역1-1)
1080
- preset: z.enum(Object.keys(PRESET_ALIAS)).optional().describe("\uACF5\uBB38\uC11C \uD504\uB9AC\uC14B \u2014 \uC9C0\uC815 \uC2DC \uD55C\uAD6D \uD589\uC815 \uACF5\uBB38\uC11C \uD45C\uC900 \uC11C\uC2DD \uC801\uC6A9. '\uAC1C\uC870\uC2DD'=\uC815\uBD80 \uD45C\uC900 \uAC1C\uC870\uC2DD \uBCF4\uACE0\uC11C(\uD45C\uC9C0\xB7\uBAA9\uCC28\xB7\uB85C\uB9C8\uC22B\uC790 \uC7A5 \uD5E4\uB354 \uC790\uB3D9 + \u25A1\u25CB-\u203B \uBD80\uD638\uBCC4 \uD3F0\uD2B8), '\uC5C5\uBB34\uBCF4\uACE0'=\uC911\uC559\uBD80\uCC98 \uC5C5\uBB34\uBCF4\uACE0(##=\u2160 \uC7A5 \uB760, ###=\uD30C\uB780 \uC22B\uC790\uCE78 \uC808, ####=\uB0A8\uC0C9 \uC18C\uC81C\uBAA9 \uBC15\uC2A4, #####=\u2460 \uD558\uB298\uC0C9 \uD56D\uBAA9 \uB760, > \u25AA\u2026=\uC5F0\uB178\uB791 \uC131\uACFC \uC694\uC57D\uBC15\uC2A4, ## \uBCC4\uCCA8 \u2026=\uBCC4\uCCA8 \uB760, \u2776\u21D2 \uBD80\uD638 \uBCF4\uC874, \uD568\uCD08\uB86C\uBC14\uD0D5 15\xB7\uAC01\uC8FC \uB9D1\uC740 \uACE0\uB515 12), '\uBCF4\uB3C4\uC790\uB8CC'=\uBA38\uB9AC\uBC15\uC2A4+\uC81C\uBAA9 25pt+\u25A1\u2192\u3147\u2192*(\uAC01\uC8FC) \uCCB4\uACC4. \uBBF8\uC9C0\uC815 \uC2DC \uBC94\uC6A9 \uB9C8\uD06C\uB2E4\uC6B4 \uBCC0\uD658"),
1081
- font: z.enum(BODY_FONTS).optional().describe("\uBCF8\uBB38 \uAE00\uAF34(\uACF5\uBB38\uC11C \uBAA8\uB4DC): myeongjo=\uBA85\uC870 \uACC4\uC5F4(\uAC1C\uC870\uC2DD\xB7\uBCF4\uACE0\uC11C\xB7\uACC4\uD68D\uC11C\uB294 \uC2E4\uCE21 \uD734\uBA3C\uBA85\uC870, \uADF8 \uC678 \uD568\uCD08\uB86C\uBC14\uD0D5), gothic=\uB9D1\uC740 \uACE0\uB515"),
1082
- body_pt: z.number().int().min(BODY_PT_RANGE.min).max(BODY_PT_RANGE.max).optional().describe("\uBCF8\uBB38 \uAE00\uC790 \uD06C\uAE30(pt, \uACF5\uBB38\uC11C \uBAA8\uB4DC). \uAE30\uBCF8: \uAE30\uC548\uBB38 12, \uBCF4\uACE0\uC11C\xB7\uACC4\uD68D\uC11C\xB7\uD1B5\uC9C0 15"),
1083
- line_spacing: z.number().int().min(LINE_SPACING_RANGE.min).max(LINE_SPACING_RANGE.max).optional().describe("\uBCF8\uBB38 \uC904\uAC04\uACA9(%, \uACF5\uBB38\uC11C \uBAA8\uB4DC). \uAE30\uBCF8: \uD504\uB9AC\uC14B\uBCC4 \uC2E4\uCE21\uAC12(\uAE30\uC548\uBB38 160, \uD68C\uC758\uB85D 130 \uB4F1)"),
1084
- org: z.string().optional().describe("\uD45C\uC9C0 \uAE30\uAD00\uBA85 (\uD45C\uC9C0\uB97C \uCF1C\uB294 \uBAA8\uB4E0 \uD504\uB9AC\uC14B\uC5D0\uC11C \uC0AC\uC6A9 \uAC00\uB2A5). \uBBF8\uC9C0\uC815 \uC2DC \uD45C\uC9C0\uC5D0 \uAE30\uAD00\uBA85 \uC0DD\uB7B5"),
1085
- date: z.string().optional().describe("\uD45C\uC9C0 \uB0A0\uC9DC ('YYYY. M. D.' \uD45C\uAE30 \uAD8C\uC7A5). \uBBF8\uC9C0\uC815 \uC2DC \uC624\uB298 \uB0A0\uC9DC"),
1086
- toc: z.boolean().optional().describe("\uBAA9\uCC28 \uD398\uC774\uC9C0 \uC0DD\uC131 \uC5EC\uBD80 \u2014 h2 \uBAA9\uB85D\uC744 \u2160 \u2161 \u2162 \uC7A5\uC73C\uB85C \uC790\uB3D9 \uAD6C\uC131. \uC804 \uD504\uB9AC\uC14B \uC0AC\uC6A9 \uAC00\uB2A5(\uBCF4\uB3C4\uC790\uB8CC \uC81C\uC678). \uBBF8\uC9C0\uC815 \uC2DC \uAC1C\uC870\uC2DD \uD504\uB9AC\uC14B\uB9CC \uCF1C\uC9D0"),
1087
- cover: z.boolean().optional().describe("\uD45C\uC9C0 \uD398\uC774\uC9C0 \uC0DD\uC131 \uC5EC\uBD80 \u2014 \uCCAB h1\uC744 \uC81C\uBAA9\uC73C\uB85C \uD30C\uB791 \uC7A5\uC2DD \uD45C\uC9C0. \uC804 \uD504\uB9AC\uC14B \uC0AC\uC6A9 \uAC00\uB2A5(\uBCF4\uB3C4\uC790\uB8CC \uC81C\uC678 \u2014 \uBA38\uB9AC\uBC15\uC2A4 \uC11C\uC2DD\uACFC \uC591\uB9BD \uBD88\uAC00). \uBBF8\uC9C0\uC815 \uC2DC \uAC1C\uC870\uC2DD \uD504\uB9AC\uC14B\uB9CC \uCF1C\uC9D0 (org/date \uC9C0\uC815 \uC2DC \uC790\uB3D9 \uCF1C\uC9D0)"),
1088
- approval: z.array(z.string()).max(APPROVAL_MAX).optional().describe("\uACB0\uC7AC\uB780 \uC9C1\uC704 \uB77C\uBCA8 (\uCD5C\uB300 6\uAC1C, \uC608: ['\uB2F4\uB2F9','\uD300\uC7A5','\uACFC\uC7A5']) \u2014 \uBB38\uC11C \uCD5C\uC0C1\uB2E8 \uC6B0\uCE21\uC5D0 \uC11C\uBA85 \uACF5\uB780 \uACB0\uC7AC \uD45C \uC0DD\uC131"),
1089
- page_numbers: z.boolean().optional().describe("\uCABD\uBC88\uD638(\uD558\uB2E8 \uC911\uC559 '- 1 -', \uD45C\uC9C0\xB7\uBAA9\uCC28 \uCE74\uC6B4\uD2B8 \uC81C\uC678). \uBBF8\uC9C0\uC815 \uC2DC \uAC1C\uC870\uC2DD\xB7\uBCF4\uACE0\uC11C\xB7\uACC4\uD68D\uC11C \uCF1C\uC9D0"),
1090
- end_mark: z.boolean().optional().describe("\uBCF8\uBB38 \uB05D '\uB05D.' \uD45C\uC2DC (\uD589\uC815\uC5C5\uBB34\uADDC\uC815). \uBBF8\uC9C0\uC815 \uC2DC \uAE30\uC548\uBB38\uB9CC \uCF1C\uC9D0, \uBCF8\uBB38\uC774 \uC774\uBBF8 '\uB05D.'\uC73C\uB85C \uB05D\uB098\uBA74 \uC911\uBCF5 \uC0DD\uC131 \uC548 \uD568"),
1091
- body_title_box: z.boolean().optional().describe("\uBCF8\uBB38 \uCCAB \uD398\uC774\uC9C0 \uC81C\uBAA9 \uBC18\uBCF5 \uBC15\uC2A4 (\uAC1C\uC870\uC2DD \uC2E4\uCE21 \uAD00\uD589). \uBBF8\uC9C0\uC815 \uC2DC \uAC1C\uC870\uC2DD+\uD45C\uC9C0 \uC870\uD569\uC5D0\uC11C \uCF1C\uC9D0"),
1092
- h2_marker: z.enum(H2_MARKERS).optional().describe("h2 \uC7A5 \uC81C\uBAA9 \uD45C\uAE30: band=\uB85C\uB9C8\uC790 \uCC44\uC6C0 \uCE78+\uC81C\uBAA9 \uB760 \uD45C(\uBCF4\uACE0\uC11C\xB7\uACC4\uD68D\uC11C \uAE30\uBCF8), roman='\u2160. \uC81C\uBAA9' \uD14D\uC2A4\uD2B8, number='1. \uC81C\uBAA9'(\uD1B5\uC9C0 \uAE30\uBCF8), box=\uC7A5 \uC5C6\uC774 \u25A1 \uB300\uD56D\uBAA9\uC73C\uB85C, none=\uBC88\uD638 \uC5C6\uC74C. \uAE30\uC548\uBB38 \uBCF8\uBB38\uC758 h2\uB294 \uD56D\uC0C1 \uBC95\uC815 '1.' \uD56D\uBAA9"),
1093
- band_color: z.string().regex(/^#[0-9A-Fa-f]{6}$/).optional().describe("\uB760 \uC81C\uBAA9 \uBC88\uD638\uCE78 \uCC44\uC6C0\uC0C9 #RRGGBB (\uAE30\uBCF8 #003366 \uC2E4\uCE21 \uCD5C\uB2E4. \uAD50\uC721\uCCAD\uD615 \uBC1D\uC740 \uB760\uB294 #DFE6F7 + band_text_color #000000)"),
1094
- band_text_color: z.string().regex(/^#[0-9A-Fa-f]{6}$/).optional().describe("\uB760 \uC81C\uBAA9 \uBC88\uD638 \uAE00\uC790\uC0C9 #RRGGBB (\uAE30\uBCF8 #FFFFFF)"),
1095
- summary: z.string().optional().describe("\uBCF4\uACE0\uC11C \uC694\uC57D \uBC15\uC2A4 \u2014 \uC81C\uBAA9\uD45C \uC544\uB798 #DFE6F7 \uC74C\uC601 \uC0C1\uC790(\uC11C\uC6B8 \uC2E4\uACB0\uC7AC \uAD00\uD589). \uB9C8\uD06C\uB2E4\uC6B4 \uC81C\uBAA9 \uC9C1\uD6C4 \uC778\uC6A9\uBB38(> \u2026)\uC73C\uB85C\uB3C4 \uC9C0\uC815 \uAC00\uB2A5"),
1096
- doc_info: z.object(Object.fromEntries(DOC_INFO_KEYS.map((k) => [k, z.string().optional()]))).optional().describe("\uBCF4\uACE0\uC11C \uD45C\uC9C0 \uBB38\uC11C\uC815\uBCF4\uD45C \u2014 docNum=\uBB38\uC11C\uBC88\uD638/date=\uACB0\uC7AC\uC77C\uC790/disclosure=\uACF5\uAC1C\uC5EC\uBD80/policyNo=\uBC29\uCE68\uBC88\uD638 (cover=true\uC640 \uD568\uAED8)"),
1097
- dept: z.string().optional().describe("\uD45C\uC9C0 \uBD80\uC11C\uBA85 \u2014 \uAE30\uAD00\uBA85 \uC544\uB798 '(\uC2A4\uB9C8\uD2B8\uB3C4\uC2DC\uACFC)' (cover\uC640 \uD568\uAED8)"),
1098
- cover_label: z.string().optional().describe("\uD45C\uC9C0 \uC6B0\uC0C1\uB2E8 \uCDE8\uAE09 \uD45C\uC2DC \u2014 '\uB300\uC678\uC8FC\uC758'\xB7'\uBE44\uACF5\uAC1C' \uBE68\uAC04 \uD14C\uB450\uB9AC \uBC15\uC2A4 (\uC5C5\uBB34\uBCF4\uACE0 \uD504\uB9AC\uC14B \uC2E4\uCE21, cover\uC640 \uD568\uAED8)"),
1099
- fonts: z.object(Object.fromEntries(FONT_ROLE_KEYS.map((k) => [k, z.string().optional()]))).optional().describe("\uC694\uC18C\uBCC4 \uAE00\uAF34 \uC624\uBC84\uB77C\uC774\uB4DC(\uACF5\uBB38\uC11C \uBAA8\uB4DC) \u2014 body=\uBCF8\uBB38(\u25CB\xB7-)/heading=\uC81C\uBAA9 \uACC4\uC5F4(\u25A1\xB7\uC7A5\uD5E4\uB354\xB7\uD45C\uC9C0\xB7\uBAA9\uCC28)/ref=\u203B \uCC38\uACE0/table=\uD45C \uC140. \uAC1C\uC870\uC2DD\xB7\uBCF4\uACE0\uC11C\xB7\uACC4\uD68D\uC11C\uB294 \uB124 \uC5ED\uD560 \uC804\uBD80, \uADF8 \uC678 \uD504\uB9AC\uC14B\uC740 body\uB9CC \uC801\uC6A9"),
1100
- sizes: z.object(Object.fromEntries(SIZE_KEYS.map((k) => [k, z.number().min(SIZE_PT_RANGE.min).max(SIZE_PT_RANGE.max).optional()]))).optional().describe("\uAC1C\uC870\uC2DD \uC694\uC18C\uBCC4 \uAE00\uC790 \uD06C\uAE30(pt) \uC624\uBC84\uB77C\uC774\uB4DC \u2014 dae=\u25A1/cham=\u203B/chapter=\uC7A5\uD5E4\uB354/coverTitle\xB7coverSub=\uD45C\uC9C0/tocLabel\xB7tocRoman\xB7tocItem=\uBAA9\uCC28/table=\uD45C \uC140/bodyTitle=\uBCF8\uBB38 \uCCAB \uD398\uC774\uC9C0 \uC81C\uBAA9 \uBC18\uBCF5 \uBC15\uC2A4. \uBBF8\uC9C0\uC815 \uC694\uC18C\uB294 body_pt \uBE44\uB840 \uAE30\uBCF8\uAC12"),
1101
- levels: z.record(z.string().regex(/^[0-7]$/), z.object(Object.fromEntries(LEVEL_STYLE_KEYS.map((k) => [k, k === "pt" ? z.number().min(SIZE_PT_RANGE.min).max(SIZE_PT_RANGE.max).optional() : k === "bold" ? z.boolean().optional() : z.string().optional()])))).optional().describe('\uD56D\uBAA9\uBD80\uD638 \uB2E8\uACC4\uBCC4 \uC704\uACC4 \uD0C0\uC774\uD3EC(\uACF5\uBB38\uC11C \uBAA8\uB4DC, v4.12.3) \u2014 {"0":{font:"HY\uACAC\uACE0\uB515",pt:17,bold:true},"1":{font:"\uD55C\uCEF4\uB3CB\uC6C0",bold:true}} \uAF34(depth 0~7). \uC2E4\uCE21: \uC804\uC790\uACB0\uC7AC \uAE30\uC548\uBB38 \u25A1/\u3147/- \uACC4\uC5F4\uC740 \u25A1=HY\uACAC\uACE0\uB515 +2~3pt \uAD75\uAC8C\xB7\u3147=\uD55C\uCEF4\uB3CB\uC6C0 \uAD75\uAC8C\xB7-=\uD734\uBA3C\uBA85\uC870 \uBCF8\uBB38. \uBC95\uC815 8\uB2E8\uACC4(1. \uAC00.)\uB294 \uBCF8\uBB38 \uB3D9\uC77C\uC774 \uAD00\uD589\uC774\uB77C \uAE30\uBCF8 \uBBF8\uC801\uC6A9'),
1102
- bullet2: z.enum(BULLET2_CHARS).optional().describe("2\uB2E8\uACC4 \uD56D\uBAA9\uBD80\uD638 \u2014 '\u3147'(\uC774\uC751, \uC804\uC790\uACB0\uC7AC \uAE30\uC548\uBB38\xB7\uACF5\uACE0\uBB38 \uC2E4\uCE21 \uC9C0\uBC30) / '\u25CB'(\uC6D0, \uBCF4\uACE0\uC11C \uC591\uC2DD). \uBBF8\uC9C0\uC815 \uC2DC \uD1B5\uC9C0\xB7\uBCF4\uB3C4\uC790\uB8CC \u3147, \uADF8 \uC678 \u25CB"),
1103
- suppress_single: z.boolean().optional().describe("\uB2E8\uC77C \uD615\uC81C \uD56D\uBAA9 \uBD80\uD638 \uC0DD\uB7B5(\uD3B8\uB78C \uADDC\uC815, \uBC95\uC815 \uBC88\uD638 standard \uC804\uC6A9 \u2014 \uBD88\uB9BF \uCCB4\uACC4\uC778 \uBCF4\uACE0\uC11C\xB7\uACC4\uD68D\uC11C\xB7\uAC1C\uC870\uC2DD\xB7\uBCF4\uB3C4\uC790\uB8CC\uC5D4 \uBB34\uD6A8). \uAE30\uBCF8 false \u2014 \uD558\uB098\uBFD0\uC778 \uD56D\uBAA9\uC5D0\uB3C4 \uBD80\uD638(1. \uAC00.)\uB97C \uBD80\uC5EC (\uBD80\uD638 \uC5C6\uB294 \uACC4\uB2E8 \uB4E4\uC5EC\uC4F0\uAE30\uAC00 \uC2E4\uBB34 \uB208\uC5D0 \uC5B4\uC0C9)"),
1104
- doc_head: z.object(Object.fromEntries(DOC_HEAD_KEYS.map((k) => [k, z.string().optional()]))).optional().describe("\uAE30\uC548\uBB38 \uB450\uBB38\uD45C(\uBCC4\uC9C0 \uC81C1\uD638\uC11C\uC2DD\xB7\uC11C\uC6B8 \uC2E4\uACB0\uC7AC 6\uD589 \uD45C) \u2014 org=\uD589\uC815\uAE30\uAD00\uBA85(\uAD74\uB9BC 20pt bold \uC790\uAC04\uB744\uC6C0)/slogan=\uC6D0\uD6C8(\uC0C1\uB2E8 10pt)/to=\uC218\uC2E0('\uB0B4\uBD80\uACB0\uC7AC'\uBA74 \uBC1C\uC2E0\uBA85\uC758 \uC0DD\uB7B5)/via=\uACBD\uC720/title=\uC81C\uBAA9(\uBBF8\uC9C0\uC815 \uC2DC \uCCAB h1). \uAE30\uC548\uBB38 \uD504\uB9AC\uC14B \uC804\uC6A9"),
1105
- doc_foot: z.object(Object.fromEntries(DOC_FOOT_KEYS.map((k) => [k, z.string().optional()]))).optional().describe("\uAE30\uC548\uBB38 \uACB0\uBB38\uD45C \u2014 sender=\uBC1C\uC2E0\uBA85\uC758(18pt bold \uC911\uC559)/drafter\xB7reviewer\xB7approver='\uC9C1\uC704 \uC131\uBA85' \uACB0\uC7AC\uC120/cooperator=\uD611\uC870\uC790/recipients=\uC218\uC2E0\uC790 \uBAA9\uB85D/docNum=\uC2DC\uD589 '\uACFC-\uBC88\uD638 (\uB0A0\uC9DC)'/receive=\uC811\uC218/zip\xB7address\xB7site/phone\xB7fax\xB7email/disclosure=\uACF5\uAC1C\uAD6C\uBD84. \uAE30\uC548\uBB38 \uD504\uB9AC\uC14B \uC804\uC6A9"),
1106
- report_info: z.string().optional().describe("\uBCF4\uACE0\uC11C: \uC81C\uBAA9\uD45C \uC544\uB798 \uB2F4\uB2F9\uC790 \uD589 '(2026. 9. 6., \uC2A4\uB9C8\uD2B8\uB3C4\uC2DC\uACFC \uD64D\uAE38\uB3D9, \u260E450-1234)' / \uAE30\uC548\uBB38: \uC6B0\uC0C1\uB2E8 12pt \uBCF4\uACE0\uC815\uBCF4 \uD589"),
1107
- notice_head: z.object(Object.fromEntries(NOTICE_HEAD_KEYS.map((k) => [k, z.string().optional()]))).optional().describe("\uACF5\uACE0\uBB38 \uB450\uBB38\xB7\uACB0\uBB38 \u2014 no=\uACF5\uACE0\uBC88\uD638(\uBCF8\uBB38 \uC704 bold)/date=\uB0A0\uC9DC(\uBCF8\uBB38 \uC544\uB798 \uC6B0\uCE21)/sender=\uBC1C\uC2E0\uBA85\uC758(\uC6B0\uCE21 bold). \uD1B5\uC9C0 \uD504\uB9AC\uC14B \uC804\uC6A9"),
1108
- press: z.object({
1109
- release: z.string().optional(),
1110
- distribute: z.string().optional(),
1111
- sub: z.array(z.string()).optional(),
1112
- contact: z.object(Object.fromEntries(PRESS_CONTACT_KEYS.map((k) => [k, z.string().optional()]))).optional()
1113
- }).optional().describe("\uBCF4\uB3C4\uC790\uB8CC \uC635\uC158 \u2014 release=\uBCF4\uB3C4\uC2DC\uC810/distribute=\uBC30\uD3EC\uC77C(\uBA38\uB9AC\uBC15\uC2A4)/sub=\uBD80\uC81C \uBC30\uC5F4('- \u2026 -')/contact=\uB2F4\uB2F9 \uBD80\uC11C\xB7\uB2F4\uB2F9\uC790\xB7\uC5F0\uB77D\uCC98 \uD45C"),
1114
- paper: z.enum(["A4", "A3", "B4", "B5", "Letter"]).optional().describe("\uC6A9\uC9C0 \uD06C\uAE30 (v4.5.0). \uAE30\uBCF8 A4"),
1115
- landscape: z.boolean().optional().describe("\uC6A9\uC9C0 \uAC00\uB85C \uBC29\uD5A5 (v4.5.0). \uAE30\uBCF8 \uC138\uB85C"),
1116
- columns: z.number().int().min(1).max(8).optional().describe("\uB2E4\uB2E8 \uAC1C\uC218 (v4.5.0). \uAE30\uBCF8 1\uB2E8"),
1117
- header: z.string().optional().describe("\uBA38\uB9AC\uB9D0 \uD14D\uC2A4\uD2B8 \u2014 \uBAA8\uB4E0 \uCABD \uC0C1\uB2E8, \uC778\uB77C\uC778 \uB9C8\uD06C\uB2E4\uC6B4 \uD5C8\uC6A9 (v4.5.0)"),
1118
- footer: z.string().optional().describe("\uAF2C\uB9AC\uB9D0 \uD14D\uC2A4\uD2B8 \u2014 \uBAA8\uB4E0 \uCABD \uD558\uB2E8 (v4.5.0)"),
1119
- image_dir: z.string().optional().describe("\uB9C8\uD06C\uB2E4\uC6B4 \uC774\uBBF8\uC9C0 \uCC38\uC870(![](x.png))\uB97C \uC774 \uB514\uB809\uD1A0\uB9AC\uC5D0\uC11C \uC77D\uC5B4 \uC2E4\uB370\uC774\uD130 \uC784\uBCA0\uB4DC (v4.5.0, PNG/JPEG/GIF/BMP). \uBBF8\uC9C0\uC815 \uC2DC \uCC38\uC870\uB9CC placeholder\uB85C \uBCF4\uC874")
1120
- },
1121
- async ({ markdown, output_path, profile_path, preset, font, body_pt, line_spacing, org, date, toc, cover, approval, page_numbers, end_mark, body_title_box, h2_marker, band_color, band_text_color, summary, doc_info, dept, cover_label, fonts, sizes, levels, bullet2, suppress_single, doc_head, doc_foot, report_info, notice_head, press, paper, landscape, columns, header, footer, image_dir }) => {
1122
- try {
1123
- let gongmun;
1124
- if (preset) {
1125
- gongmun = buildGongmunOptions({
1126
- preset: PRESET_ALIAS[preset],
1127
- font,
1128
- bodyPt: body_pt,
1129
- lineSpacing: line_spacing,
1130
- org,
1131
- date,
1132
- cover,
1133
- toc,
1134
- approval,
1135
- pageNumbers: page_numbers,
1136
- endMark: end_mark,
1137
- bodyTitleBox: body_title_box,
1138
- h2Marker: h2_marker,
1139
- bandColor: band_color,
1140
- bandTextColor: band_text_color,
1141
- fonts,
1142
- sizes,
1143
- levels,
1144
- bullet2,
1145
- suppressSingle: suppress_single,
1146
- docHead: doc_head,
1147
- docFoot: doc_foot,
1148
- reportInfo: report_info,
1149
- noticeHead: notice_head,
1150
- press,
1151
- summary,
1152
- docInfo: doc_info,
1153
- dept,
1154
- coverLabel: cover_label
1155
- });
1156
- }
1157
- let profile;
1158
- if (profile_path) {
1159
- const { parseFormatProfileJson } = await import("./profile-io-JZ3FDHDK.js");
1160
- profile = parseFormatProfileJson(await readFile(safePath(profile_path, PROFILE_EXTENSIONS), "utf-8"));
1093
+ };
1094
+ } catch (err) {
1095
+ return {
1096
+ content: [{ type: "text", text: `\uC624\uB958: ${describeError(err)}` }],
1097
+ isError: true
1098
+ };
1161
1099
  }
1162
- const out = safeOutputPath(output_path, /* @__PURE__ */ new Set([".hwpx"]));
1163
- const page = paper || landscape || columns || header || footer ? {
1164
- ...paper ? { size: paper } : {},
1165
- ...landscape ? { orientation: "landscape" } : {},
1166
- ...columns ? { columns } : {},
1167
- ...header ? { header } : {},
1168
- ...footer ? { footer } : {}
1169
- } : void 0;
1170
- let images;
1171
- if (image_dir) {
1172
- const dir = await realpath(resolve(image_dir));
1173
- assertWithinRoot(dir);
1174
- for (const m of markdown.matchAll(/!\[[^\]]*\]\(([^)\s]+)\)/g)) {
1175
- const url = m[1];
1176
- if (!/^[A-Za-z0-9._-]+\.[A-Za-z0-9]+$/.test(url) || url.includes("..")) continue;
1177
- try {
1178
- images ??= {};
1179
- images[url] = new Uint8Array(await readFile(join(dir, url)));
1180
- } catch {
1100
+ }
1101
+ );
1102
+ server2.tool(
1103
+ "generate_document",
1104
+ "\uB9C8\uD06C\uB2E4\uC6B4\uC744 HWPX \uD55C\uAE00 \uBB38\uC11C\uB85C \uC0DD\uC131\uD569\uB2C8\uB2E4. \"\uBCF4\uACE0\uC11C\uB85C/\uACF5\uBB38\uC11C\uB85C/\uAC1C\uC870\uC2DD\uC73C\uB85C/\uACC4\uD68D\uC11C\uB85C \uBF51\uC544\uC918\xB7\uB9CC\uB4E4\uC5B4\uC918\" \uC694\uCCAD\uC774 \uC774 \uB3C4\uAD6C\uC785\uB2C8\uB2E4. \uD504\uB9AC\uC14B \uB9E4\uD551: \uC815\uBD80 \uD45C\uC900 \uBCF4\uACE0\uC11C(\uD45C\uC9C0\xB7\uBAA9\uCC28\xB7\uB85C\uB9C8\uC22B\uC790 \uC7A5\uD5E4\uB354 \uC790\uB3D9)='\uAC1C\uC870\uC2DD', \uC911\uC559\uBD80\uCC98 \uC5C5\uBB34\uBCF4\uACE0\xB7\uAD6D\uD68C \uC11C\uBA74\uBCF4\uACE0(\uC7AC\uACBD\uBD80 \uC2E4\uCE21: \uC7A5 \uB760\xB7\uC808 \uC22B\uC790\uCE78\xB7\uC18C\uC81C\uBAA9 \uBC15\uC2A4\xB7\u2460 \uD56D\uBAA9 \uB760\xB7\uC5F0\uB178\uB791 \uC131\uACFC \uC694\uC57D\uBC15\uC2A4\xB7\uBCC4\uCCA8 \uB760, \u25A1 \uB4A4 (\uD0A4\uC6CC\uB4DC) \uD30C\uB791)='\uC5C5\uBB34\uBCF4\uACE0', \uAE30\uC548\uBB38\xB7\uC2DC\uD589\uBB38\xB7\uC54C\uB9BC\uACF5\uBB38='\uAE30\uC548\uBB38', 1\uD398\uC774\uC9C0 \uC694\uC57D\uBCF4\uACE0\uC11C='\uBCF4\uACE0\uC11C', \uCD94\uC9C4\uACC4\uD68D='\uACC4\uD68D\uC11C'. \uD45C\uB294 \uC2E4\uCE21 \uC815\uBD80 \uC11C\uC2DD(\uD5E4\uB354 \uC74C\uC601+\uC774\uC911\uC120\xB7\uC678\uACFD \uAD75\uC740\uC120\xB7\uB0B4\uC6A9 \uBE44\uB840 \uC5F4\uD3ED), \uCABD\uBC88\uD638\xB7\uACB0\uC7AC\uB780\xB7'\uB05D.' \uD45C\uC2DC \uC9C0\uC6D0. \u26A0 \uC0DD\uC131 \uC804 \uD655\uC778 \uAD8C\uC7A5: \uBB38\uC11C\uC885\uB958(\uBCF4\uACE0\uC11C/\uAE30\uC548\uBB38)\xB7\uC81C\uBAA9\xB7\uAE30\uAD00\uBA85(org)\xB7\uB0A0\uC9DC\xB7\uBAA9\uCC28 \uC5EC\uBD80\uAC00 \uBD88\uBA85\uD655\uD558\uBA74 \uC0AC\uC6A9\uC790\uC5D0\uAC8C \uBB3C\uC5B4\uBCF4\uC138\uC694 \u2014 \uC5C9\uB6B1\uD55C \uD504\uB9AC\uC14B \uC120\uD0DD\uC774 \uAC00\uC7A5 \uD754\uD55C \uC624\uC0DD\uC131 \uC6D0\uC778. \uB9C8\uD06C\uB2E4\uC6B4 \uADDC\uCE59(v4.13 \uC11C\uC6B8 \uC2E4\uACB0\uC7AC \uC2E4\uCE21 \uC704\uACC4): #(h1)=\uBB38\uC11C \uC81C\uBAA9, ##(h2)=\uC7A5(\uBCF4\uACE0\uC11C \u2160 \u2161 / \uAE30\uC548\uBB38\xB7\uD1B5\uC9C0\uB294 \uBC95\uC815 1.), ###(h3)=\u25A1 \uB300\uD56D\uBAA9(\uAE30\uC548\uBB38 \uAC00.), \uADF8 \uC544\uB798 \uB9AC\uC2A4\uD2B8=\u3147 \u2192 - \u2192 \u318D(\uAE30\uC548\uBB38 \uAC00. \u2192 1) \u2192 \uAC00)), \uBCF8\uBB38\uC5D0 \u25A1/\u3147/-/1./\uAC00. \uB97C \uC9C1\uC811 \uC368\uB3C4 \uAC19\uC740 \uC704\uACC4\uB85C \uC815\uADDC\uD654, \u203B\uC2DC\uC791\xB7'\uCD9C\uCC98:'\xB7'\uC790\uB8CC:'=\uCC38\uACE0(13pt), \uC81C\uBAA9 \uC9C1\uD6C4 \uC778\uC6A9\uBB38(>)=\uBCF4\uACE0\uC11C \uC694\uC57D\uBC15\uC2A4 \u2014 \u2605\uBCF4\uACE0\uC11C\uB294 \uBC18\uB4DC\uC2DC \uC81C\uBAA9 \uC9C1\uD6C4 `> \u2026\uD558\uACE0\uC790 \uD568` \uD55C \uBB38\uC7A5(\uC27C\uD45C \uD5C8\uC6A9\xB7\uBD80\uD638 \uC5C6\uC74C\xB73\uC904 \uC774\uB0B4 \uC57D 90~100\uC790)\uB85C \uBCF4\uACE0 \uBAA9\uC801\uC744 \uB123\uC744 \uAC83(\uC5C6\uAC70\uB098 3\uC904 \uCD08\uACFC\uBA74 \uACBD\uACE0), <right>\uD14D\uC2A4\uD2B8</right>=\uC6B0\uCE21\uC815\uB82C. \u25A1\xB7\uC81C\uBAA9\uC740 \uD55C \uC904\uC5D0 \uC790\uB3D9 \uCD95\uC18C, \uBCF8\uBB38\uC740 \uC5B4\uC808 \uB2E8\uC704 \uC904\uBC14\uAFC8(\uB0B1\uB9D0\xB7\uB0A0\uC9DC\uAC00 \uC904 \uB05D\uC5D0\uC11C \uC548 \uCABC\uAC1C\uC9D0)\xB7\uBD80\uD638 \uB4A4 \uD0ED \uC815\uB82C(\uB458\uC9F8 \uC904\uC774 \uCCAB \uC904 \uB0B4\uC6A9\uACFC \uAC19\uC740 \uC704\uCE58), \uB9C8\uC9C0\uB9C9 \uC904\uC774 \uC9E7\uC740 \uACE0\uC544 \uC904\uACFC \uACF5\uBC31\uC774 \uD06C\uAC8C \uBC8C\uC5B4\uC9C0\uB294 \uC904\uC740 \uC790\uAC04 \uCD95\uC18C\uB85C \uC815\uB9AC, \uACE7\uC740\uB530\uC634\uD45C\uB294 \u2018\u2019\u201C\u201D\uB85C. \uBC95\uB839 \uCF54\uB4DC (282791)\xB7KOSIS \uD45C ID DT_\u2026\xB7(\uBC95\uC815\uB3D9\uCF54\uB4DC \u2026)\xB7\u25CB\u25CB MCP \uC870\uD68C \uAC19\uC740 \uB0B4\uBD80 \uC2DD\uBCC4\uC790\xB7\uB3C4\uAD6C \uC5B8\uAE09\uC740 \uC790\uB3D9 \uC81C\uAC70 \u2014 \uCD9C\uCC98\uB294 \uAE30\uAD00\xB7\uC790\uB8CC\uBA85\uB9CC \uC4F8 \uAC83. (\uC6D0\uBCF8 \uC11C\uC2DD \uBCF4\uC874 \uC81C\uC790\uB9AC \uC218\uC815\uC740 patch_document, \uC11C\uC2DD \uBE48\uCE78 \uCC44\uC6B0\uAE30\uB294 fill_form)",
1105
+ {
1106
+ markdown: z4.string().min(1).describe("HWPX\uB85C \uBCC0\uD658\uD560 \uB9C8\uD06C\uB2E4\uC6B4 \uC804\uBB38. \uD45C\uB294 GFM \uBB38\uBC95 \uC0AC\uC6A9 (\uC608: '| \uC774\uB984 | \uBD80\uC11C |\\n| --- | --- |\\n| \uD64D\uAE38\uB3D9 | \uAE30\uD68D\uD300 |')"),
1107
+ output_path: z4.string().min(1).describe("\uCD9C\uB825 HWPX \uD30C\uC77C\uC758 \uC808\uB300 \uACBD\uB85C (.hwpx \uAD8C\uC7A5)"),
1108
+ profile_path: z4.string().optional().describe("\uC11C\uC2DD \uD504\uB85C\uD544 JSON \uACBD\uB85C (extract_profile\uB85C \uCD94\uCD9C) \u2014 \uCC38\uC870 \uBB38\uC11C\uC758 \uD45C \uD14C\uB450\uB9AC\xB7\uC74C\uC601\xB7\uC5F4\uD3ED\xB7\uC140 \uAE00\uAF34\uC744 \uC7AC\uD604. \uD45C \uD589\xB7\uC5F4 \uC218\uC640 \uCCAB \uC140 \uD14D\uC2A4\uD2B8\uAC00 \uC77C\uCE58\uD558\uB294 \uD45C\uC5D0\uB9CC \uC801\uC6A9"),
1109
+ // 값 집합·범위는 gongmun-surface SSOT에서 파생 (CLI와 드리프트 불가 — v4.0.4 영역1-1)
1110
+ preset: z4.enum(Object.keys(PRESET_ALIAS)).optional().describe("\uACF5\uBB38\uC11C \uD504\uB9AC\uC14B \u2014 \uC9C0\uC815 \uC2DC \uD55C\uAD6D \uD589\uC815 \uACF5\uBB38\uC11C \uD45C\uC900 \uC11C\uC2DD \uC801\uC6A9. '\uAC1C\uC870\uC2DD'=\uC815\uBD80 \uD45C\uC900 \uAC1C\uC870\uC2DD \uBCF4\uACE0\uC11C(\uD45C\uC9C0\xB7\uBAA9\uCC28\xB7\uB85C\uB9C8\uC22B\uC790 \uC7A5 \uD5E4\uB354 \uC790\uB3D9 + \u25A1\u25CB-\u203B \uBD80\uD638\uBCC4 \uD3F0\uD2B8), '\uC5C5\uBB34\uBCF4\uACE0'=\uC911\uC559\uBD80\uCC98 \uC5C5\uBB34\uBCF4\uACE0(##=\u2160 \uC7A5 \uB760, ###=\uD30C\uB780 \uC22B\uC790\uCE78 \uC808, ####=\uB0A8\uC0C9 \uC18C\uC81C\uBAA9 \uBC15\uC2A4, #####=\u2460 \uD558\uB298\uC0C9 \uD56D\uBAA9 \uB760, > \u25AA\u2026=\uC5F0\uB178\uB791 \uC131\uACFC \uC694\uC57D\uBC15\uC2A4, ## \uBCC4\uCCA8 \u2026=\uBCC4\uCCA8 \uB760, \u2776\u21D2 \uBD80\uD638 \uBCF4\uC874, \uD568\uCD08\uB86C\uBC14\uD0D5 15\xB7\uAC01\uC8FC \uB9D1\uC740 \uACE0\uB515 12), '\uBCF4\uB3C4\uC790\uB8CC'=\uBA38\uB9AC\uBC15\uC2A4+\uC81C\uBAA9 25pt+\u25A1\u2192\u3147\u2192*(\uAC01\uC8FC) \uCCB4\uACC4. \uBBF8\uC9C0\uC815 \uC2DC \uBC94\uC6A9 \uB9C8\uD06C\uB2E4\uC6B4 \uBCC0\uD658"),
1111
+ font: z4.enum(BODY_FONTS).optional().describe("\uBCF8\uBB38 \uAE00\uAF34(\uACF5\uBB38\uC11C \uBAA8\uB4DC): myeongjo=\uBA85\uC870 \uACC4\uC5F4(\uAC1C\uC870\uC2DD\xB7\uBCF4\uACE0\uC11C\xB7\uACC4\uD68D\uC11C\uB294 \uC2E4\uCE21 \uD734\uBA3C\uBA85\uC870, \uADF8 \uC678 \uD568\uCD08\uB86C\uBC14\uD0D5), gothic=\uB9D1\uC740 \uACE0\uB515"),
1112
+ body_pt: z4.number().int().min(BODY_PT_RANGE.min).max(BODY_PT_RANGE.max).optional().describe("\uBCF8\uBB38 \uAE00\uC790 \uD06C\uAE30(pt, \uACF5\uBB38\uC11C \uBAA8\uB4DC). \uAE30\uBCF8: \uAE30\uC548\uBB38 12, \uBCF4\uACE0\uC11C\xB7\uACC4\uD68D\uC11C\xB7\uD1B5\uC9C0 15"),
1113
+ line_spacing: z4.number().int().min(LINE_SPACING_RANGE.min).max(LINE_SPACING_RANGE.max).optional().describe("\uBCF8\uBB38 \uC904\uAC04\uACA9(%, \uACF5\uBB38\uC11C \uBAA8\uB4DC). \uAE30\uBCF8: \uD504\uB9AC\uC14B\uBCC4 \uC2E4\uCE21\uAC12(\uAE30\uC548\uBB38 160, \uD68C\uC758\uB85D 130 \uB4F1)"),
1114
+ org: z4.string().optional().describe("\uD45C\uC9C0 \uAE30\uAD00\uBA85 (\uD45C\uC9C0\uB97C \uCF1C\uB294 \uBAA8\uB4E0 \uD504\uB9AC\uC14B\uC5D0\uC11C \uC0AC\uC6A9 \uAC00\uB2A5). \uBBF8\uC9C0\uC815 \uC2DC \uD45C\uC9C0\uC5D0 \uAE30\uAD00\uBA85 \uC0DD\uB7B5"),
1115
+ date: z4.string().optional().describe("\uD45C\uC9C0 \uB0A0\uC9DC ('YYYY. M. D.' \uD45C\uAE30 \uAD8C\uC7A5). \uBBF8\uC9C0\uC815 \uC2DC \uC624\uB298 \uB0A0\uC9DC"),
1116
+ toc: z4.boolean().optional().describe("\uBAA9\uCC28 \uD398\uC774\uC9C0 \uC0DD\uC131 \uC5EC\uBD80 \u2014 h2 \uBAA9\uB85D\uC744 \u2160 \u2161 \u2162 \uC7A5\uC73C\uB85C \uC790\uB3D9 \uAD6C\uC131. \uC804 \uD504\uB9AC\uC14B \uC0AC\uC6A9 \uAC00\uB2A5(\uBCF4\uB3C4\uC790\uB8CC \uC81C\uC678). \uBBF8\uC9C0\uC815 \uC2DC \uAC1C\uC870\uC2DD \uD504\uB9AC\uC14B\uB9CC \uCF1C\uC9D0"),
1117
+ cover: z4.boolean().optional().describe("\uD45C\uC9C0 \uD398\uC774\uC9C0 \uC0DD\uC131 \uC5EC\uBD80 \u2014 \uCCAB h1\uC744 \uC81C\uBAA9\uC73C\uB85C \uD30C\uB791 \uC7A5\uC2DD \uD45C\uC9C0. \uC804 \uD504\uB9AC\uC14B \uC0AC\uC6A9 \uAC00\uB2A5(\uBCF4\uB3C4\uC790\uB8CC \uC81C\uC678 \u2014 \uBA38\uB9AC\uBC15\uC2A4 \uC11C\uC2DD\uACFC \uC591\uB9BD \uBD88\uAC00). \uBBF8\uC9C0\uC815 \uC2DC \uAC1C\uC870\uC2DD \uD504\uB9AC\uC14B\uB9CC \uCF1C\uC9D0 (org/date \uC9C0\uC815 \uC2DC \uC790\uB3D9 \uCF1C\uC9D0)"),
1118
+ approval: z4.array(z4.string()).max(APPROVAL_MAX).optional().describe("\uACB0\uC7AC\uB780 \uC9C1\uC704 \uB77C\uBCA8 (\uCD5C\uB300 6\uAC1C, \uC608: ['\uB2F4\uB2F9','\uD300\uC7A5','\uACFC\uC7A5']) \u2014 \uBB38\uC11C \uCD5C\uC0C1\uB2E8 \uC6B0\uCE21\uC5D0 \uC11C\uBA85 \uACF5\uB780 \uACB0\uC7AC \uD45C \uC0DD\uC131"),
1119
+ page_numbers: z4.boolean().optional().describe("\uCABD\uBC88\uD638(\uD558\uB2E8 \uC911\uC559 '- 1 -', \uD45C\uC9C0\xB7\uBAA9\uCC28 \uCE74\uC6B4\uD2B8 \uC81C\uC678). \uBBF8\uC9C0\uC815 \uC2DC \uAC1C\uC870\uC2DD\xB7\uBCF4\uACE0\uC11C\xB7\uACC4\uD68D\uC11C \uCF1C\uC9D0"),
1120
+ end_mark: z4.boolean().optional().describe("\uBCF8\uBB38 \uB05D '\uB05D.' \uD45C\uC2DC (\uD589\uC815\uC5C5\uBB34\uADDC\uC815). \uBBF8\uC9C0\uC815 \uC2DC \uAE30\uC548\uBB38\uB9CC \uCF1C\uC9D0, \uBCF8\uBB38\uC774 \uC774\uBBF8 '\uB05D.'\uC73C\uB85C \uB05D\uB098\uBA74 \uC911\uBCF5 \uC0DD\uC131 \uC548 \uD568"),
1121
+ body_title_box: z4.boolean().optional().describe("\uBCF8\uBB38 \uCCAB \uD398\uC774\uC9C0 \uC81C\uBAA9 \uBC18\uBCF5 \uBC15\uC2A4 (\uAC1C\uC870\uC2DD \uC2E4\uCE21 \uAD00\uD589). \uBBF8\uC9C0\uC815 \uC2DC \uAC1C\uC870\uC2DD+\uD45C\uC9C0 \uC870\uD569\uC5D0\uC11C \uCF1C\uC9D0"),
1122
+ h2_marker: z4.enum(H2_MARKERS).optional().describe("h2 \uC7A5 \uC81C\uBAA9 \uD45C\uAE30: band=\uB85C\uB9C8\uC790 \uCC44\uC6C0 \uCE78+\uC81C\uBAA9 \uB760 \uD45C(\uBCF4\uACE0\uC11C\xB7\uACC4\uD68D\uC11C \uAE30\uBCF8), roman='\u2160. \uC81C\uBAA9' \uD14D\uC2A4\uD2B8, number='1. \uC81C\uBAA9'(\uD1B5\uC9C0 \uAE30\uBCF8), box=\uC7A5 \uC5C6\uC774 \u25A1 \uB300\uD56D\uBAA9\uC73C\uB85C, none=\uBC88\uD638 \uC5C6\uC74C. \uAE30\uC548\uBB38 \uBCF8\uBB38\uC758 h2\uB294 \uD56D\uC0C1 \uBC95\uC815 '1.' \uD56D\uBAA9"),
1123
+ band_color: z4.string().regex(/^#[0-9A-Fa-f]{6}$/).optional().describe("\uB760 \uC81C\uBAA9 \uBC88\uD638\uCE78 \uCC44\uC6C0\uC0C9 #RRGGBB (\uAE30\uBCF8 #003366 \uC2E4\uCE21 \uCD5C\uB2E4. \uAD50\uC721\uCCAD\uD615 \uBC1D\uC740 \uB760\uB294 #DFE6F7 + band_text_color #000000)"),
1124
+ band_text_color: z4.string().regex(/^#[0-9A-Fa-f]{6}$/).optional().describe("\uB760 \uC81C\uBAA9 \uBC88\uD638 \uAE00\uC790\uC0C9 #RRGGBB (\uAE30\uBCF8 #FFFFFF)"),
1125
+ summary: z4.string().optional().describe("\uBCF4\uACE0\uC11C \uC694\uC57D \uBC15\uC2A4 \u2014 \uC81C\uBAA9\uD45C \uC544\uB798 #DFE6F7 \uC74C\uC601 \uC0C1\uC790(\uC11C\uC6B8 \uC2E4\uACB0\uC7AC \uAD00\uD589). \uB9C8\uD06C\uB2E4\uC6B4 \uC81C\uBAA9 \uC9C1\uD6C4 \uC778\uC6A9\uBB38(> \u2026)\uC73C\uB85C\uB3C4 \uC9C0\uC815 \uAC00\uB2A5"),
1126
+ doc_info: z4.object(Object.fromEntries(DOC_INFO_KEYS.map((k) => [k, z4.string().optional()]))).optional().describe("\uBCF4\uACE0\uC11C \uD45C\uC9C0 \uBB38\uC11C\uC815\uBCF4\uD45C \u2014 docNum=\uBB38\uC11C\uBC88\uD638/date=\uACB0\uC7AC\uC77C\uC790/disclosure=\uACF5\uAC1C\uC5EC\uBD80/policyNo=\uBC29\uCE68\uBC88\uD638 (cover=true\uC640 \uD568\uAED8)"),
1127
+ dept: z4.string().optional().describe("\uD45C\uC9C0 \uBD80\uC11C\uBA85 \u2014 \uAE30\uAD00\uBA85 \uC544\uB798 '(\uC2A4\uB9C8\uD2B8\uB3C4\uC2DC\uACFC)' (cover\uC640 \uD568\uAED8)"),
1128
+ cover_label: z4.string().optional().describe("\uD45C\uC9C0 \uC6B0\uC0C1\uB2E8 \uCDE8\uAE09 \uD45C\uC2DC \u2014 '\uB300\uC678\uC8FC\uC758'\xB7'\uBE44\uACF5\uAC1C' \uBE68\uAC04 \uD14C\uB450\uB9AC \uBC15\uC2A4 (\uC5C5\uBB34\uBCF4\uACE0 \uD504\uB9AC\uC14B \uC2E4\uCE21, cover\uC640 \uD568\uAED8)"),
1129
+ fonts: z4.object(Object.fromEntries(FONT_ROLE_KEYS.map((k) => [k, z4.string().optional()]))).optional().describe("\uC694\uC18C\uBCC4 \uAE00\uAF34 \uC624\uBC84\uB77C\uC774\uB4DC(\uACF5\uBB38\uC11C \uBAA8\uB4DC) \u2014 body=\uBCF8\uBB38(\u25CB\xB7-)/heading=\uC81C\uBAA9 \uACC4\uC5F4(\u25A1\xB7\uC7A5\uD5E4\uB354\xB7\uD45C\uC9C0\xB7\uBAA9\uCC28)/ref=\u203B \uCC38\uACE0/table=\uD45C \uC140. \uAC1C\uC870\uC2DD\xB7\uBCF4\uACE0\uC11C\xB7\uACC4\uD68D\uC11C\uB294 \uB124 \uC5ED\uD560 \uC804\uBD80, \uADF8 \uC678 \uD504\uB9AC\uC14B\uC740 body\uB9CC \uC801\uC6A9"),
1130
+ sizes: z4.object(Object.fromEntries(SIZE_KEYS.map((k) => [k, z4.number().min(SIZE_PT_RANGE.min).max(SIZE_PT_RANGE.max).optional()]))).optional().describe("\uAC1C\uC870\uC2DD \uC694\uC18C\uBCC4 \uAE00\uC790 \uD06C\uAE30(pt) \uC624\uBC84\uB77C\uC774\uB4DC \u2014 dae=\u25A1/cham=\u203B/chapter=\uC7A5\uD5E4\uB354/coverTitle\xB7coverSub=\uD45C\uC9C0/tocLabel\xB7tocRoman\xB7tocItem=\uBAA9\uCC28/table=\uD45C \uC140/bodyTitle=\uBCF8\uBB38 \uCCAB \uD398\uC774\uC9C0 \uC81C\uBAA9 \uBC18\uBCF5 \uBC15\uC2A4. \uBBF8\uC9C0\uC815 \uC694\uC18C\uB294 body_pt \uBE44\uB840 \uAE30\uBCF8\uAC12"),
1131
+ levels: z4.record(z4.string().regex(/^[0-7]$/), z4.object(Object.fromEntries(LEVEL_STYLE_KEYS.map((k) => [k, k === "pt" ? z4.number().min(SIZE_PT_RANGE.min).max(SIZE_PT_RANGE.max).optional() : k === "bold" ? z4.boolean().optional() : z4.string().optional()])))).optional().describe('\uD56D\uBAA9\uBD80\uD638 \uB2E8\uACC4\uBCC4 \uC704\uACC4 \uD0C0\uC774\uD3EC(\uACF5\uBB38\uC11C \uBAA8\uB4DC, v4.12.3) \u2014 {"0":{font:"HY\uACAC\uACE0\uB515",pt:17,bold:true},"1":{font:"\uD55C\uCEF4\uB3CB\uC6C0",bold:true}} \uAF34(depth 0~7). \uC2E4\uCE21: \uC804\uC790\uACB0\uC7AC \uAE30\uC548\uBB38 \u25A1/\u3147/- \uACC4\uC5F4\uC740 \u25A1=HY\uACAC\uACE0\uB515 +2~3pt \uAD75\uAC8C\xB7\u3147=\uD55C\uCEF4\uB3CB\uC6C0 \uAD75\uAC8C\xB7-=\uD734\uBA3C\uBA85\uC870 \uBCF8\uBB38. \uBC95\uC815 8\uB2E8\uACC4(1. \uAC00.)\uB294 \uBCF8\uBB38 \uB3D9\uC77C\uC774 \uAD00\uD589\uC774\uB77C \uAE30\uBCF8 \uBBF8\uC801\uC6A9'),
1132
+ bullet2: z4.enum(BULLET2_CHARS).optional().describe("2\uB2E8\uACC4 \uD56D\uBAA9\uBD80\uD638 \u2014 '\u3147'(\uC774\uC751, \uC804\uC790\uACB0\uC7AC \uAE30\uC548\uBB38\xB7\uACF5\uACE0\uBB38 \uC2E4\uCE21 \uC9C0\uBC30) / '\u25CB'(\uC6D0, \uBCF4\uACE0\uC11C \uC591\uC2DD). \uBBF8\uC9C0\uC815 \uC2DC \uD1B5\uC9C0\xB7\uBCF4\uB3C4\uC790\uB8CC \u3147, \uADF8 \uC678 \u25CB"),
1133
+ suppress_single: z4.boolean().optional().describe("\uB2E8\uC77C \uD615\uC81C \uD56D\uBAA9 \uBD80\uD638 \uC0DD\uB7B5(\uD3B8\uB78C \uADDC\uC815, \uBC95\uC815 \uBC88\uD638 standard \uC804\uC6A9 \u2014 \uBD88\uB9BF \uCCB4\uACC4\uC778 \uBCF4\uACE0\uC11C\xB7\uACC4\uD68D\uC11C\xB7\uAC1C\uC870\uC2DD\xB7\uBCF4\uB3C4\uC790\uB8CC\uC5D4 \uBB34\uD6A8). \uAE30\uBCF8 false \u2014 \uD558\uB098\uBFD0\uC778 \uD56D\uBAA9\uC5D0\uB3C4 \uBD80\uD638(1. \uAC00.)\uB97C \uBD80\uC5EC (\uBD80\uD638 \uC5C6\uB294 \uACC4\uB2E8 \uB4E4\uC5EC\uC4F0\uAE30\uAC00 \uC2E4\uBB34 \uB208\uC5D0 \uC5B4\uC0C9)"),
1134
+ doc_head: z4.object(Object.fromEntries(DOC_HEAD_KEYS.map((k) => [k, z4.string().optional()]))).optional().describe("\uAE30\uC548\uBB38 \uB450\uBB38\uD45C(\uBCC4\uC9C0 \uC81C1\uD638\uC11C\uC2DD\xB7\uC11C\uC6B8 \uC2E4\uACB0\uC7AC 6\uD589 \uD45C) \u2014 org=\uD589\uC815\uAE30\uAD00\uBA85(\uAD74\uB9BC 20pt bold \uC790\uAC04\uB744\uC6C0)/slogan=\uC6D0\uD6C8(\uC0C1\uB2E8 10pt)/to=\uC218\uC2E0('\uB0B4\uBD80\uACB0\uC7AC'\uBA74 \uBC1C\uC2E0\uBA85\uC758 \uC0DD\uB7B5)/via=\uACBD\uC720/title=\uC81C\uBAA9(\uBBF8\uC9C0\uC815 \uC2DC \uCCAB h1). \uAE30\uC548\uBB38 \uD504\uB9AC\uC14B \uC804\uC6A9"),
1135
+ doc_foot: z4.object(Object.fromEntries(DOC_FOOT_KEYS.map((k) => [k, z4.string().optional()]))).optional().describe("\uAE30\uC548\uBB38 \uACB0\uBB38\uD45C \u2014 sender=\uBC1C\uC2E0\uBA85\uC758(18pt bold \uC911\uC559)/drafter\xB7reviewer\xB7approver='\uC9C1\uC704 \uC131\uBA85' \uACB0\uC7AC\uC120/cooperator=\uD611\uC870\uC790/recipients=\uC218\uC2E0\uC790 \uBAA9\uB85D/docNum=\uC2DC\uD589 '\uACFC-\uBC88\uD638 (\uB0A0\uC9DC)'/receive=\uC811\uC218/zip\xB7address\xB7site/phone\xB7fax\xB7email/disclosure=\uACF5\uAC1C\uAD6C\uBD84. \uAE30\uC548\uBB38 \uD504\uB9AC\uC14B \uC804\uC6A9"),
1136
+ report_info: z4.string().optional().describe("\uBCF4\uACE0\uC11C: \uC81C\uBAA9\uD45C \uC544\uB798 \uB2F4\uB2F9\uC790 \uD589 '(2026. 9. 6., \uC2A4\uB9C8\uD2B8\uB3C4\uC2DC\uACFC \uD64D\uAE38\uB3D9, \u260E450-1234)' / \uAE30\uC548\uBB38: \uC6B0\uC0C1\uB2E8 12pt \uBCF4\uACE0\uC815\uBCF4 \uD589"),
1137
+ notice_head: z4.object(Object.fromEntries(NOTICE_HEAD_KEYS.map((k) => [k, z4.string().optional()]))).optional().describe("\uACF5\uACE0\uBB38 \uB450\uBB38\xB7\uACB0\uBB38 \u2014 no=\uACF5\uACE0\uBC88\uD638(\uBCF8\uBB38 \uC704 bold)/date=\uB0A0\uC9DC(\uBCF8\uBB38 \uC544\uB798 \uC6B0\uCE21)/sender=\uBC1C\uC2E0\uBA85\uC758(\uC6B0\uCE21 bold). \uD1B5\uC9C0 \uD504\uB9AC\uC14B \uC804\uC6A9"),
1138
+ press: z4.object({
1139
+ release: z4.string().optional(),
1140
+ distribute: z4.string().optional(),
1141
+ sub: z4.array(z4.string()).optional(),
1142
+ contact: z4.object(Object.fromEntries(PRESS_CONTACT_KEYS.map((k) => [k, z4.string().optional()]))).optional()
1143
+ }).optional().describe("\uBCF4\uB3C4\uC790\uB8CC \uC635\uC158 \u2014 release=\uBCF4\uB3C4\uC2DC\uC810/distribute=\uBC30\uD3EC\uC77C(\uBA38\uB9AC\uBC15\uC2A4)/sub=\uBD80\uC81C \uBC30\uC5F4('- \u2026 -')/contact=\uB2F4\uB2F9 \uBD80\uC11C\xB7\uB2F4\uB2F9\uC790\xB7\uC5F0\uB77D\uCC98 \uD45C"),
1144
+ paper: z4.enum(["A4", "A3", "B4", "B5", "Letter"]).optional().describe("\uC6A9\uC9C0 \uD06C\uAE30 (v4.5.0). \uAE30\uBCF8 A4"),
1145
+ landscape: z4.boolean().optional().describe("\uC6A9\uC9C0 \uAC00\uB85C \uBC29\uD5A5 (v4.5.0). \uAE30\uBCF8 \uC138\uB85C"),
1146
+ columns: z4.number().int().min(1).max(8).optional().describe("\uB2E4\uB2E8 \uAC1C\uC218 (v4.5.0). \uAE30\uBCF8 1\uB2E8"),
1147
+ header: z4.string().optional().describe("\uBA38\uB9AC\uB9D0 \uD14D\uC2A4\uD2B8 \u2014 \uBAA8\uB4E0 \uCABD \uC0C1\uB2E8, \uC778\uB77C\uC778 \uB9C8\uD06C\uB2E4\uC6B4 \uD5C8\uC6A9 (v4.5.0)"),
1148
+ footer: z4.string().optional().describe("\uAF2C\uB9AC\uB9D0 \uD14D\uC2A4\uD2B8 \u2014 \uBAA8\uB4E0 \uCABD \uD558\uB2E8 (v4.5.0)"),
1149
+ image_dir: z4.string().optional().describe("\uB9C8\uD06C\uB2E4\uC6B4 \uC774\uBBF8\uC9C0 \uCC38\uC870(![](x.png))\uB97C \uC774 \uB514\uB809\uD1A0\uB9AC\uC5D0\uC11C \uC77D\uC5B4 \uC2E4\uB370\uC774\uD130 \uC784\uBCA0\uB4DC (v4.5.0, PNG/JPEG/GIF/BMP). \uBBF8\uC9C0\uC815 \uC2DC \uCC38\uC870\uB9CC placeholder\uB85C \uBCF4\uC874")
1150
+ },
1151
+ async ({ markdown, output_path, profile_path, preset, font, body_pt, line_spacing, org, date, toc, cover, approval, page_numbers, end_mark, body_title_box, h2_marker, band_color, band_text_color, summary, doc_info, dept, cover_label, fonts, sizes, levels, bullet2, suppress_single, doc_head, doc_foot, report_info, notice_head, press, paper, landscape, columns, header, footer, image_dir }) => {
1152
+ try {
1153
+ let gongmun;
1154
+ if (preset) {
1155
+ gongmun = buildGongmunOptions({
1156
+ preset: PRESET_ALIAS[preset],
1157
+ font,
1158
+ bodyPt: body_pt,
1159
+ lineSpacing: line_spacing,
1160
+ org,
1161
+ date,
1162
+ cover,
1163
+ toc,
1164
+ approval,
1165
+ pageNumbers: page_numbers,
1166
+ endMark: end_mark,
1167
+ bodyTitleBox: body_title_box,
1168
+ h2Marker: h2_marker,
1169
+ bandColor: band_color,
1170
+ bandTextColor: band_text_color,
1171
+ fonts,
1172
+ sizes,
1173
+ levels,
1174
+ bullet2,
1175
+ suppressSingle: suppress_single,
1176
+ docHead: doc_head,
1177
+ docFoot: doc_foot,
1178
+ reportInfo: report_info,
1179
+ noticeHead: notice_head,
1180
+ press,
1181
+ summary,
1182
+ docInfo: doc_info,
1183
+ dept,
1184
+ coverLabel: cover_label
1185
+ });
1186
+ }
1187
+ let profile;
1188
+ if (profile_path) {
1189
+ const { parseFormatProfileJson } = await import("./profile-io-LBM263K7.js");
1190
+ profile = parseFormatProfileJson(await readFile3(safePath(profile_path, PROFILE_EXTENSIONS), "utf-8"));
1191
+ }
1192
+ const out = safeOutputPath(output_path, /* @__PURE__ */ new Set([".hwpx"]));
1193
+ const page = paper || landscape || columns || header || footer ? {
1194
+ ...paper ? { size: paper } : {},
1195
+ ...landscape ? { orientation: "landscape" } : {},
1196
+ ...columns ? { columns } : {},
1197
+ ...header ? { header } : {},
1198
+ ...footer ? { footer } : {}
1199
+ } : void 0;
1200
+ let images;
1201
+ if (image_dir) {
1202
+ const dir = await realpath3(resolve2(image_dir));
1203
+ assertWithinRoot(dir);
1204
+ for (const m of markdown.matchAll(/!\[[^\]]*\]\(([^)\s]+)\)/g)) {
1205
+ const url = m[1];
1206
+ if (!/^[A-Za-z0-9._-]+\.[A-Za-z0-9]+$/.test(url) || url.includes("..")) continue;
1207
+ try {
1208
+ images ??= {};
1209
+ images[url] = new Uint8Array(await readFile3(join2(dir, url)));
1210
+ } catch {
1211
+ }
1181
1212
  }
1182
1213
  }
1183
- }
1184
- const genWarnings = [];
1185
- const buf = await markdownToHwpx(markdown, gongmun || profile || page || images ? {
1186
- ...gongmun ? { gongmun } : {},
1187
- ...profile ? { profile } : {},
1188
- ...page ? { page } : {},
1189
- ...images ? { images } : {},
1190
- warnings: genWarnings
1191
- } : void 0);
1192
- await mkdir(dirname(out), { recursive: true });
1193
- await writeFile(out, Buffer.from(buf));
1194
- const mode = gongmun ? `\uACF5\uBB38\uC11C:${gongmun.preset}` : "\uBC94\uC6A9";
1195
- const tableCount = (markdown.match(/^\s*\|.*\|\s*$/gm) || []).length > 0 ? `, \uD45C \uD3EC\uD568` : "";
1196
- const fontWarns = gongmun?.fonts ? unknownFontWarnings(gongmun.fonts) : [];
1197
- if (gongmun) fontWarns.push(...incompatibleGongmunWarnings(gongmun));
1198
- if (gongmun) fontWarns.push(...gongmunLintWarnings(markdown, 5));
1199
- if (gongmun && usesGaejosikMunche(gongmun.preset)) fontWarns.push(...muncheLintWarnings(markdown, 5));
1200
- fontWarns.push(...genWarnings);
1201
- const warnText = fontWarns.length ? `
1214
+ const genWarnings = [];
1215
+ const buf = await markdownToHwpx(markdown, gongmun || profile || page || images ? {
1216
+ ...gongmun ? { gongmun } : {},
1217
+ ...profile ? { profile } : {},
1218
+ ...page ? { page } : {},
1219
+ ...images ? { images } : {},
1220
+ warnings: genWarnings
1221
+ } : void 0);
1222
+ await mkdir3(dirname4(out), { recursive: true });
1223
+ await writeFile3(out, Buffer.from(buf));
1224
+ const mode = gongmun ? `\uACF5\uBB38\uC11C:${gongmun.preset}` : "\uBC94\uC6A9";
1225
+ const tableCount = (markdown.match(/^\s*\|.*\|\s*$/gm) || []).length > 0 ? `, \uD45C \uD3EC\uD568` : "";
1226
+ const fontWarns = gongmun?.fonts ? unknownFontWarnings(gongmun.fonts) : [];
1227
+ if (gongmun) fontWarns.push(...incompatibleGongmunWarnings(gongmun));
1228
+ if (gongmun) fontWarns.push(...gongmunLintWarnings(markdown, 5));
1229
+ if (gongmun && usesGaejosikMunche(gongmun.preset)) fontWarns.push(...muncheLintWarnings(markdown, 5));
1230
+ fontWarns.push(...genWarnings);
1231
+ const warnText = fontWarns.length ? `
1202
1232
  \u26A0 ${fontWarns.join("\n\u26A0 ")}` : "";
1203
- return {
1204
- content: [{ type: "text", text: `\u2713 HWPX \uC0DD\uC131 (${mode}${tableCount}) \u2192 ${out}
1233
+ return {
1234
+ content: [{ type: "text", text: `\u2713 HWPX \uC0DD\uC131 (${mode}${tableCount}) \u2192 ${out}
1205
1235
  \uD06C\uAE30: ${(buf.byteLength / 1024).toFixed(1)}KB${warnText}` }]
1206
- };
1207
- } catch (err) {
1208
- return {
1209
- content: [{ type: "text", text: `\uC624\uB958: ${describeError(err)}` }],
1210
- isError: true
1211
- };
1236
+ };
1237
+ } catch (err) {
1238
+ return {
1239
+ content: [{ type: "text", text: `\uC624\uB958: ${describeError(err)}` }],
1240
+ isError: true
1241
+ };
1242
+ }
1212
1243
  }
1213
- }
1214
- );
1244
+ );
1245
+ }
1246
+
1247
+ // src/mcp.ts
1248
+ var server = new McpServer({
1249
+ name: "kordoc",
1250
+ version: VERSION
1251
+ });
1252
+ registerParseTools(server);
1253
+ registerFormTools(server);
1254
+ registerRenderTools(server);
1255
+ registerGenerateTools(server);
1215
1256
  var serverStarted = false;
1216
1257
  async function startMcpServer() {
1217
1258
  if (serverStarted) return;
@@ -1228,7 +1269,7 @@ async function startMcpServer() {
1228
1269
  var autoStart = true;
1229
1270
  try {
1230
1271
  const entry = process.argv[1];
1231
- if (entry && pathToFileURL(realpathSync(entry)).href !== import.meta.url) autoStart = false;
1272
+ if (entry && pathToFileURL(realpathSync2(entry)).href !== import.meta.url) autoStart = false;
1232
1273
  } catch {
1233
1274
  }
1234
1275
  if (autoStart) startMcpServer().catch((err) => {