kordoc 4.14.2 → 4.14.3

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 (151) hide show
  1. package/README.md +38 -5
  2. package/dist/{-AQKXLFIM.js → -7XBPXCXN.js} +13 -11
  3. package/dist/{chunk-FOJZNMMK.js → chunk-2LQGSDZ6.js} +1 -1
  4. package/dist/chunk-2LQGSDZ6.js.map +1 -0
  5. package/dist/chunk-4R67INKT.js +1002 -0
  6. package/dist/chunk-4R67INKT.js.map +1 -0
  7. package/dist/{chunk-ILU76UF3.js → chunk-4RJ5D7MT.js} +2 -2
  8. package/dist/chunk-4RJ5D7MT.js.map +1 -0
  9. package/dist/{chunk-EQZI77PP.cjs → chunk-5VVS2QIJ.cjs} +3 -3
  10. package/dist/{chunk-EQZI77PP.cjs.map → chunk-5VVS2QIJ.cjs.map} +1 -1
  11. package/dist/{chunk-3DUADAY2.js → chunk-A275ZQLX.js} +209 -87
  12. package/dist/chunk-A275ZQLX.js.map +1 -0
  13. package/dist/{chunk-EISO3POT.js → chunk-CVEERPMP.js} +788 -194
  14. package/dist/chunk-CVEERPMP.js.map +1 -0
  15. package/dist/{chunk-ZWD734EY.js → chunk-D6KQKMU3.js} +2 -2
  16. package/dist/{chunk-QOMNEWCQ.cjs → chunk-EBZUZC6F.cjs} +305 -31
  17. package/dist/chunk-EBZUZC6F.cjs.map +1 -0
  18. package/dist/{chunk-HR2XNERB.cjs → chunk-EMGF6EEV.cjs} +2 -2
  19. package/dist/{chunk-HR2XNERB.cjs.map → chunk-EMGF6EEV.cjs.map} +1 -1
  20. package/dist/{chunk-GDW4YP7K.js → chunk-FAZR34CA.js} +2 -2
  21. package/dist/{chunk-TNCKOL2O.js → chunk-GTINVM5K.js} +2 -2
  22. package/dist/{chunk-L7DMSMAG.cjs → chunk-HX4UUF7Y.cjs} +1 -1
  23. package/dist/chunk-HX4UUF7Y.cjs.map +1 -0
  24. package/dist/{chunk-UET462HB.js → chunk-I4NNYWIR.js} +2 -2
  25. package/dist/chunk-IH56ZISC.js +620 -0
  26. package/dist/chunk-IH56ZISC.js.map +1 -0
  27. package/dist/{chunk-EDNJLUSE.js → chunk-IHZEFW4L.js} +2 -2
  28. package/dist/chunk-IW2CB755.cjs +1002 -0
  29. package/dist/chunk-IW2CB755.cjs.map +1 -0
  30. package/dist/{chunk-ZWOFQRIB.js → chunk-JCVIOLBF.js} +3 -3
  31. package/dist/{chunk-JQEFP45L.js → chunk-L5EH2NVO.js} +2 -2
  32. package/dist/{chunk-247ANM72.cjs → chunk-NOAQRCV6.cjs} +219 -97
  33. package/dist/chunk-NOAQRCV6.cjs.map +1 -0
  34. package/dist/{chunk-7IAQ4HOK.js → chunk-NQCUAGLR.js} +1 -1
  35. package/dist/chunk-NQCUAGLR.js.map +1 -0
  36. package/dist/{chunk-NS55SM7A.js → chunk-O2PGQNYE.js} +2 -2
  37. package/dist/{chunk-QC5T6D4J.js → chunk-QGW7NMTX.js} +299 -25
  38. package/dist/chunk-QGW7NMTX.js.map +1 -0
  39. package/dist/{chunk-A7QC2ZVP.js → chunk-UVVEBI22.js} +209 -87
  40. package/dist/chunk-UVVEBI22.js.map +1 -0
  41. package/dist/chunk-VJHAUMCF.js +949 -0
  42. package/dist/chunk-VJHAUMCF.js.map +1 -0
  43. package/dist/{chunk-C7FQBSON.js → chunk-VNFJPTU4.js} +2 -2
  44. package/dist/{chunk-VQ5W4MYO.js → chunk-VYOGXNQ3.js} +298 -24
  45. package/dist/chunk-VYOGXNQ3.js.map +1 -0
  46. package/dist/{chunk-DOGPOY4B.js → chunk-WB5LW52Z.js} +2 -2
  47. package/dist/{chunk-TSFS5VKD.js → chunk-YGBZABI4.js} +332 -141
  48. package/dist/chunk-YGBZABI4.js.map +1 -0
  49. package/dist/chunks-I7TEMJXH.js +10 -0
  50. package/dist/cli.js +228 -97
  51. package/dist/cli.js.map +1 -1
  52. package/dist/{formula-PJJ6S3Z2.cjs → formula-CPO2TZGB.cjs} +9 -9
  53. package/dist/{formula-PJJ6S3Z2.cjs.map → formula-CPO2TZGB.cjs.map} +1 -1
  54. package/dist/{formula-5S7WWXGH.js → formula-NOIDZEBA.js} +4 -4
  55. package/dist/{formula-JGFTXWYT.js → formula-X44GV7TN.js} +3 -3
  56. package/dist/{image-ocr-WCAYXLOA.js → image-ocr-72RAR4WR.js} +34 -16
  57. package/dist/image-ocr-72RAR4WR.js.map +1 -0
  58. package/dist/{image-ocr-FLIL7MGN.js → image-ocr-R6W6A75P.js} +32 -14
  59. package/dist/image-ocr-R6W6A75P.js.map +1 -0
  60. package/dist/{image-ocr-ZTYAGV3W.cjs → image-ocr-SUGT64ZM.cjs} +36 -18
  61. package/dist/image-ocr-SUGT64ZM.cjs.map +1 -0
  62. package/dist/index.cjs +2416 -1184
  63. package/dist/index.cjs.map +1 -1
  64. package/dist/index.d.cts +62 -25
  65. package/dist/index.d.ts +62 -25
  66. package/dist/index.js +1853 -621
  67. package/dist/index.js.map +1 -1
  68. package/dist/mcp.js +74 -53
  69. package/dist/mcp.js.map +1 -1
  70. package/dist/{model-bundle-FSPZZ2EM.js → model-bundle-57CLEBCO.js} +5 -5
  71. package/dist/{models-CJPAQK5R.js → models-7ELIFTD7.js} +5 -5
  72. package/dist/{parser-SCUTZAIJ.js → parser-4NKAYWAX.js} +261 -241
  73. package/dist/parser-4NKAYWAX.js.map +1 -0
  74. package/dist/{parser-A4TMPJAM.js → parser-HWFQRSUQ.js} +261 -241
  75. package/dist/parser-HWFQRSUQ.js.map +1 -0
  76. package/dist/{parser-VCXE4I5A.cjs → parser-RANGXGBD.cjs} +289 -269
  77. package/dist/parser-RANGXGBD.cjs.map +1 -0
  78. package/dist/pdf-ocr-BNC34MNU.js +13 -0
  79. package/dist/pdf-ocr-QX2GB33Q.js +16 -0
  80. package/dist/pdf-ocr-VHSFACH7.cjs +14 -0
  81. package/dist/{pdf-ocr-MBZZ57LD.cjs.map → pdf-ocr-VHSFACH7.cjs.map} +1 -1
  82. package/dist/{profile-io-LBM263K7.js → profile-io-V52PL5ZO.js} +3 -3
  83. package/dist/{rasterize-46RG4ZF6.js → rasterize-ZIGLJDOG.js} +3 -3
  84. package/dist/redact-TIQEQI3M.js +18 -0
  85. package/dist/redact-doc-KXZ3TZAG.js +1216 -0
  86. package/dist/redact-doc-KXZ3TZAG.js.map +1 -0
  87. package/dist/{render-NGP5T24S.js → render-U5PE6V2E.js} +7 -7
  88. package/dist/seal-DNXJBILR.js +10 -0
  89. package/dist/{setup-AU4OCHNH.js → setup-YRDM4ODP.js} +3 -3
  90. package/dist/visual-5XWMA4EV.js +19 -0
  91. package/dist/{watch-MKPUCPXM.js → watch-5IOLBATT.js} +12 -12
  92. package/package.json +2 -2
  93. package/dist/chunk-247ANM72.cjs.map +0 -1
  94. package/dist/chunk-3DUADAY2.js.map +0 -1
  95. package/dist/chunk-422427XJ.cjs +0 -571
  96. package/dist/chunk-422427XJ.cjs.map +0 -1
  97. package/dist/chunk-7IAQ4HOK.js.map +0 -1
  98. package/dist/chunk-A7QC2ZVP.js.map +0 -1
  99. package/dist/chunk-DZIXKL7E.js +0 -145
  100. package/dist/chunk-DZIXKL7E.js.map +0 -1
  101. package/dist/chunk-EISO3POT.js.map +0 -1
  102. package/dist/chunk-EP7KOI2V.js +0 -571
  103. package/dist/chunk-EP7KOI2V.js.map +0 -1
  104. package/dist/chunk-FOJZNMMK.js.map +0 -1
  105. package/dist/chunk-ILU76UF3.js.map +0 -1
  106. package/dist/chunk-KVWV4ULP.js +0 -516
  107. package/dist/chunk-KVWV4ULP.js.map +0 -1
  108. package/dist/chunk-L7DMSMAG.cjs.map +0 -1
  109. package/dist/chunk-QC5T6D4J.js.map +0 -1
  110. package/dist/chunk-QOMNEWCQ.cjs.map +0 -1
  111. package/dist/chunk-TSFS5VKD.js.map +0 -1
  112. package/dist/chunk-VQ5W4MYO.js.map +0 -1
  113. package/dist/chunks-SX77P5CV.js +0 -10
  114. package/dist/image-ocr-FLIL7MGN.js.map +0 -1
  115. package/dist/image-ocr-WCAYXLOA.js.map +0 -1
  116. package/dist/image-ocr-ZTYAGV3W.cjs.map +0 -1
  117. package/dist/parser-A4TMPJAM.js.map +0 -1
  118. package/dist/parser-SCUTZAIJ.js.map +0 -1
  119. package/dist/parser-VCXE4I5A.cjs.map +0 -1
  120. package/dist/pdf-ocr-76SAUXMZ.js +0 -13
  121. package/dist/pdf-ocr-CPU55CVF.js +0 -16
  122. package/dist/pdf-ocr-MBZZ57LD.cjs +0 -14
  123. package/dist/redact-7ILEAUTS.js +0 -12
  124. package/dist/seal-RGUPOLXD.js +0 -10
  125. package/dist/visual-NZLYIYEM.js +0 -19
  126. /package/dist/{-AQKXLFIM.js.map → -7XBPXCXN.js.map} +0 -0
  127. /package/dist/{chunk-ZWD734EY.js.map → chunk-D6KQKMU3.js.map} +0 -0
  128. /package/dist/{chunk-GDW4YP7K.js.map → chunk-FAZR34CA.js.map} +0 -0
  129. /package/dist/{chunk-TNCKOL2O.js.map → chunk-GTINVM5K.js.map} +0 -0
  130. /package/dist/{chunk-UET462HB.js.map → chunk-I4NNYWIR.js.map} +0 -0
  131. /package/dist/{chunk-EDNJLUSE.js.map → chunk-IHZEFW4L.js.map} +0 -0
  132. /package/dist/{chunk-ZWOFQRIB.js.map → chunk-JCVIOLBF.js.map} +0 -0
  133. /package/dist/{chunk-JQEFP45L.js.map → chunk-L5EH2NVO.js.map} +0 -0
  134. /package/dist/{chunk-NS55SM7A.js.map → chunk-O2PGQNYE.js.map} +0 -0
  135. /package/dist/{chunk-C7FQBSON.js.map → chunk-VNFJPTU4.js.map} +0 -0
  136. /package/dist/{chunk-DOGPOY4B.js.map → chunk-WB5LW52Z.js.map} +0 -0
  137. /package/dist/{chunks-SX77P5CV.js.map → chunks-I7TEMJXH.js.map} +0 -0
  138. /package/dist/{formula-5S7WWXGH.js.map → formula-NOIDZEBA.js.map} +0 -0
  139. /package/dist/{formula-JGFTXWYT.js.map → formula-X44GV7TN.js.map} +0 -0
  140. /package/dist/{model-bundle-FSPZZ2EM.js.map → model-bundle-57CLEBCO.js.map} +0 -0
  141. /package/dist/{models-CJPAQK5R.js.map → models-7ELIFTD7.js.map} +0 -0
  142. /package/dist/{pdf-ocr-76SAUXMZ.js.map → pdf-ocr-BNC34MNU.js.map} +0 -0
  143. /package/dist/{pdf-ocr-CPU55CVF.js.map → pdf-ocr-QX2GB33Q.js.map} +0 -0
  144. /package/dist/{profile-io-LBM263K7.js.map → profile-io-V52PL5ZO.js.map} +0 -0
  145. /package/dist/{rasterize-46RG4ZF6.js.map → rasterize-ZIGLJDOG.js.map} +0 -0
  146. /package/dist/{redact-7ILEAUTS.js.map → redact-TIQEQI3M.js.map} +0 -0
  147. /package/dist/{render-NGP5T24S.js.map → render-U5PE6V2E.js.map} +0 -0
  148. /package/dist/{seal-RGUPOLXD.js.map → seal-DNXJBILR.js.map} +0 -0
  149. /package/dist/{setup-AU4OCHNH.js.map → setup-YRDM4ODP.js.map} +0 -0
  150. /package/dist/{visual-NZLYIYEM.js.map → visual-5XWMA4EV.js.map} +0 -0
  151. /package/dist/{watch-MKPUCPXM.js.map → watch-5IOLBATT.js.map} +0 -0
@@ -1,145 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- // src/redact.ts
4
- var DEFAULT_REDACT_RULES = [
5
- "rrn",
6
- "phone",
7
- "email",
8
- "card",
9
- "account"
10
- ];
11
- var RULE_PRIORITY = [
12
- "rrn",
13
- "email",
14
- "card",
15
- "phone",
16
- "driver",
17
- "account",
18
- "passport"
19
- ];
20
- function luhnValid(digits) {
21
- let sum = 0;
22
- for (let i = 0; i < digits.length; i++) {
23
- let d = digits.charCodeAt(digits.length - 1 - i) - 48;
24
- if (i % 2 === 1) {
25
- d *= 2;
26
- if (d > 9) d -= 9;
27
- }
28
- sum += d;
29
- }
30
- return sum % 10 === 0;
31
- }
32
- function birthdateValid(front6) {
33
- const mm = Number(front6.slice(2, 4));
34
- const dd = Number(front6.slice(4, 6));
35
- return mm >= 1 && mm <= 12 && dd >= 1 && dd <= 31;
36
- }
37
- var RULES = {
38
- // 주민/외국인등록번호 — 뒷자리 첫 숫자 1-8 + 생년월일 유효성으로 오탐 축소.
39
- // 유니코드 대시 변형(‐ ‑ – —)은 rrn만 허용. 앞 6자리 유지, 뒤 7자리 전부 마스크.
40
- rrn: {
41
- pattern: /(?<!\d)(\d{6})([-‐‑–—])([1-8]\d{6})(?!\d)/g,
42
- validate: (m) => birthdateValid(m[1]),
43
- mask: (m, mc) => m[1] + m[2] + mc.repeat(7)
44
- },
45
- // 이메일 — 로컬파트 첫 글자만 남기고 마스크, 도메인 유지
46
- email: {
47
- pattern: /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g,
48
- mask: (m, mc) => {
49
- const at = m[0].indexOf("@");
50
- return m[0][0] + mc.repeat(at - 1) + m[0].slice(at);
51
- }
52
- },
53
- // 카드번호 — 구분자 필수(무구분 16자리는 오탐 높아 제외), 동일 구분자 강제(\2),
54
- // Luhn 체크. 가운데 8자리 마스크.
55
- card: {
56
- pattern: /(?<!\d)(\d{4})([- ])(\d{4})\2(\d{4})\2(\d{4})(?!\d)/g,
57
- validate: (m) => luhnValid(m[1] + m[3] + m[4] + m[5]),
58
- mask: (m, mc) => m[1] + m[2] + mc.repeat(4) + m[2] + mc.repeat(4) + m[2] + m[5]
59
- },
60
- // 전화번호 — 휴대폰(01[016789])·서울(02)·지역(0[3-6]\d)·인터넷(070)은 구분자
61
- // -·.·공백 또는 무구분(동일 구분자 강제), 대표번호(15xx/16xx/18xx)는 구분자 필수.
62
- // 가운데 자리만 마스크 (대표번호는 뒤 4자리). 선행 [\d-] 금지 — 계좌 부분매치 방지.
63
- phone: {
64
- pattern: /(?<![\d-])(?:(01[016789]|070|02|0[3-6]\d)([-. ]?)(\d{3,4})\2(\d{4})|(1[568]\d{2})([-. ])(\d{4}))(?!\d)/g,
65
- mask: (m, mc) => m[1] !== void 0 ? m[1] + m[2] + mc.repeat(m[3].length) + m[2] + m[4] : m[5] + m[6] + mc.repeat(4)
66
- },
67
- // 운전면허 (기본 OFF) — 신형 12자리만 (지역명 2글자 선행 구버전은 스킵). 뒷 8자리 마스크.
68
- driver: {
69
- pattern: /(?<![\d-])(\d{2})-(\d{2})-\d{6}-\d{2}(?!-?\d)/g,
70
- mask: (m, mc) => m[1] + "-" + m[2] + "-" + mc.repeat(6) + "-" + mc.repeat(2)
71
- },
72
- // 계좌번호 — 3~4그룹 + 총 자릿수 10~16. rrn·card·phone과 겹치면 그쪽 우선.
73
- // 마지막 그룹 빼고 전부 마스크. 사업자등록번호(3-2-5, 10자리)도 걸린다 — 계약상 의도
74
- // (테스트로 명시). 날짜(2026-07-16)는 8자리라 총자릿수 검증에서 탈락.
75
- account: {
76
- pattern: /(?<!\d)(?<!\d-)\d{2,6}(?:-\d{2,6}){1,2}-\d{2,8}(?!-?\d)/g,
77
- validate: (m) => {
78
- const digits = m[0].replace(/-/g, "").length;
79
- return digits >= 10 && digits <= 16;
80
- },
81
- mask: (m, mc) => {
82
- const parts = m[0].split("-");
83
- return parts.map((p, i) => i === parts.length - 1 ? p : mc.repeat(p.length)).join("-");
84
- }
85
- },
86
- // 여권번호 (기본 OFF) — 단어 경계, 첫 글자만 남기고 전부 마스크
87
- passport: {
88
- pattern: /\b([MSRODG])\d{8}(?![0-9A-Za-z])/g,
89
- mask: (m, mc) => m[1] + mc.repeat(8)
90
- }
91
- };
92
- function redactText(text, options) {
93
- const maskChar = options?.maskChar ?? "\u25CF";
94
- if (maskChar.length !== 1 || /[0-9A-Za-z]/.test(maskChar)) {
95
- throw new Error(`maskChar\uB294 \uC601\uC22B\uC790\uAC00 \uC544\uB2CC 1\uAE00\uC790\uC5EC\uC57C \uD568: ${JSON.stringify(maskChar)}`);
96
- }
97
- const enabled = options?.rules ?? DEFAULT_REDACT_RULES;
98
- const hits = [];
99
- if (text === "" || enabled.length === 0) return { text, hits };
100
- const occupied = [];
101
- for (const rule of RULE_PRIORITY) {
102
- if (!enabled.includes(rule)) continue;
103
- const def = RULES[rule];
104
- for (const m of text.matchAll(def.pattern)) {
105
- if (def.validate && !def.validate(m)) continue;
106
- const start = m.index;
107
- const end = start + m[0].length;
108
- if (occupied.some((o) => start < o.end && end > o.start)) continue;
109
- occupied.push({ start, end });
110
- hits.push({ rule, masked: def.mask(m, maskChar), index: start, length: m[0].length });
111
- }
112
- }
113
- hits.sort((a, b) => a.index - b.index);
114
- let out = "";
115
- let cursor = 0;
116
- for (const h of hits) {
117
- out += text.slice(cursor, h.index) + h.masked;
118
- cursor = h.index + h.length;
119
- }
120
- out += text.slice(cursor);
121
- return { text: out, hits };
122
- }
123
- function redactMarkdown(markdown, options) {
124
- const lines = markdown.split("\n");
125
- const hits = [];
126
- let offset = 0;
127
- const outLines = lines.map((line) => {
128
- if (line.includes("data:image/")) {
129
- offset += line.length + 1;
130
- return line;
131
- }
132
- const r = redactText(line, options);
133
- for (const h of r.hits) hits.push({ ...h, index: h.index + offset });
134
- offset += line.length + 1;
135
- return r.text;
136
- });
137
- return { text: outLines.join("\n"), hits };
138
- }
139
-
140
- export {
141
- DEFAULT_REDACT_RULES,
142
- redactText,
143
- redactMarkdown
144
- };
145
- //# sourceMappingURL=chunk-DZIXKL7E.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/redact.ts"],"sourcesContent":["/**\n * 한국 공문서 PII(개인정보) 탐지·마스킹 순수 로직.\n *\n * 텍스트 in → 마스킹된 텍스트 + 히트 리포트 out. 문서 파싱/patch는 기존 인프라가\n * 담당하고 여기서는 텍스트만 다룬다 (CLI/MCP 배선은 별도).\n *\n * 원칙:\n * - 서식 보존 마스킹 — 자릿수·구분자를 유지해 마스킹 전후 길이가 동일\n * - 히트 리포트에 원본 PII를 절대 담지 않는다 (`masked` 필드만 존재)\n * - 룰 우선순위 겹침 처리 — 우선순위순으로 매치를 수집하고, 이미 점유된\n * 구간과 겹치는 하위 룰 매치는 스킵 (RULE_PRIORITY 참조)\n * - 정규식은 모듈 로드 시 1회 컴파일 (matchAll은 내부 클론이라 lastIndex 안전)\n */\n\nexport type RedactRule = \"rrn\" | \"phone\" | \"email\" | \"card\" | \"account\" | \"passport\" | \"driver\"\n\nexport interface RedactHit {\n rule: RedactRule\n /** 마스킹 후 문자열 — 원본 PII는 리포트에 담지 않는다 */\n masked: string\n /** 원문 내 시작 오프셋 (UTF-16 단위) */\n index: number\n /** 매치 길이 (서식 보존이라 마스킹 전후 동일) */\n length: number\n}\n\nexport interface RedactTextResult {\n text: string\n hits: RedactHit[]\n}\n\nexport interface RedactOptions {\n /** 적용할 룰 (기본: DEFAULT_REDACT_RULES — passport·driver는 기본 OFF) */\n rules?: RedactRule[]\n /** 마스크 문자 — 1글자(UTF-16 1단위), 영숫자 금지. 기본 \"●\" (마크다운 특수문자 아님) */\n maskChar?: string\n}\n\n/** 기본 적용 룰 — passport(여권)·driver(운전면허)는 오탐 여지가 있어 opt-in */\nexport const DEFAULT_REDACT_RULES: readonly RedactRule[] = [\n \"rrn\",\n \"phone\",\n \"email\",\n \"card\",\n \"account\",\n]\n\n/**\n * 룰 우선순위 (앞이 높음). 스펙 요구는 rrn > card > phone > account —\n * 나머지는 포섭 관계로 배치: email을 phone 앞에(로컬파트 숫자에 전화 패턴 오탐 방지),\n * driver를 account 앞에(면허번호 12자리 4그룹이 계좌 패턴에 포섭됨).\n */\nconst RULE_PRIORITY: readonly RedactRule[] = [\n \"rrn\",\n \"email\",\n \"card\",\n \"phone\",\n \"driver\",\n \"account\",\n \"passport\",\n]\n\ninterface RuleDef {\n pattern: RegExp\n /** 오탐 필터 — false면 매치 스킵 */\n validate?: (m: RegExpMatchArray) => boolean\n /** 매치 → 같은 길이의 마스킹 문자열 */\n mask: (m: RegExpMatchArray, mc: string) => string\n}\n\n/** Luhn 체크섬 (카드번호 오탐 축소) */\nfunction luhnValid(digits: string): boolean {\n let sum = 0\n for (let i = 0; i < digits.length; i++) {\n let d = digits.charCodeAt(digits.length - 1 - i) - 48\n if (i % 2 === 1) {\n d *= 2\n if (d > 9) d -= 9\n }\n sum += d\n }\n return sum % 10 === 0\n}\n\n/** 주민번호 앞 6자리(YYMMDD)의 월 01-12, 일 01-31 검증 */\nfunction birthdateValid(front6: string): boolean {\n const mm = Number(front6.slice(2, 4))\n const dd = Number(front6.slice(4, 6))\n return mm >= 1 && mm <= 12 && dd >= 1 && dd <= 31\n}\n\n// 전체 정규식은 모듈 로드 시 1회 컴파일\nconst RULES: Record<RedactRule, RuleDef> = {\n // 주민/외국인등록번호 — 뒷자리 첫 숫자 1-8 + 생년월일 유효성으로 오탐 축소.\n // 유니코드 대시 변형(‐ ‑ – —)은 rrn만 허용. 앞 6자리 유지, 뒤 7자리 전부 마스크.\n rrn: {\n pattern: /(?<!\\d)(\\d{6})([-‐‑–—])([1-8]\\d{6})(?!\\d)/g,\n validate: (m) => birthdateValid(m[1]),\n mask: (m, mc) => m[1] + m[2] + mc.repeat(7),\n },\n // 이메일 — 로컬파트 첫 글자만 남기고 마스크, 도메인 유지\n email: {\n pattern: /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}/g,\n mask: (m, mc) => {\n const at = m[0].indexOf(\"@\")\n return m[0][0] + mc.repeat(at - 1) + m[0].slice(at)\n },\n },\n // 카드번호 — 구분자 필수(무구분 16자리는 오탐 높아 제외), 동일 구분자 강제(\\2),\n // Luhn 체크. 가운데 8자리 마스크.\n card: {\n pattern: /(?<!\\d)(\\d{4})([- ])(\\d{4})\\2(\\d{4})\\2(\\d{4})(?!\\d)/g,\n validate: (m) => luhnValid(m[1] + m[3] + m[4] + m[5]),\n mask: (m, mc) => m[1] + m[2] + mc.repeat(4) + m[2] + mc.repeat(4) + m[2] + m[5],\n },\n // 전화번호 — 휴대폰(01[016789])·서울(02)·지역(0[3-6]\\d)·인터넷(070)은 구분자\n // -·.·공백 또는 무구분(동일 구분자 강제), 대표번호(15xx/16xx/18xx)는 구분자 필수.\n // 가운데 자리만 마스크 (대표번호는 뒤 4자리). 선행 [\\d-] 금지 — 계좌 부분매치 방지.\n phone: {\n pattern:\n /(?<![\\d-])(?:(01[016789]|070|02|0[3-6]\\d)([-. ]?)(\\d{3,4})\\2(\\d{4})|(1[568]\\d{2})([-. ])(\\d{4}))(?!\\d)/g,\n mask: (m, mc) =>\n m[1] !== undefined\n ? m[1] + m[2] + mc.repeat(m[3].length) + m[2] + m[4]\n : m[5] + m[6] + mc.repeat(4),\n },\n // 운전면허 (기본 OFF) — 신형 12자리만 (지역명 2글자 선행 구버전은 스킵). 뒷 8자리 마스크.\n driver: {\n pattern: /(?<![\\d-])(\\d{2})-(\\d{2})-\\d{6}-\\d{2}(?!-?\\d)/g,\n mask: (m, mc) => m[1] + \"-\" + m[2] + \"-\" + mc.repeat(6) + \"-\" + mc.repeat(2),\n },\n // 계좌번호 — 3~4그룹 + 총 자릿수 10~16. rrn·card·phone과 겹치면 그쪽 우선.\n // 마지막 그룹 빼고 전부 마스크. 사업자등록번호(3-2-5, 10자리)도 걸린다 — 계약상 의도\n // (테스트로 명시). 날짜(2026-07-16)는 8자리라 총자릿수 검증에서 탈락.\n account: {\n pattern: /(?<!\\d)(?<!\\d-)\\d{2,6}(?:-\\d{2,6}){1,2}-\\d{2,8}(?!-?\\d)/g,\n validate: (m) => {\n const digits = m[0].replace(/-/g, \"\").length\n return digits >= 10 && digits <= 16\n },\n mask: (m, mc) => {\n const parts = m[0].split(\"-\")\n return parts.map((p, i) => (i === parts.length - 1 ? p : mc.repeat(p.length))).join(\"-\")\n },\n },\n // 여권번호 (기본 OFF) — 단어 경계, 첫 글자만 남기고 전부 마스크\n passport: {\n pattern: /\\b([MSRODG])\\d{8}(?![0-9A-Za-z])/g,\n mask: (m, mc) => m[1] + mc.repeat(8),\n },\n}\n\n/**\n * 텍스트에서 PII를 탐지해 서식 보존 마스킹.\n *\n * @param text - 대상 텍스트 (마크다운 포함)\n * @param options - 룰 선택·마스크 문자 (기본: rrn·phone·email·card·account, \"●\")\n * @returns 마스킹된 텍스트 + 히트 리포트 (index 오름차순, 원본 PII 미포함)\n */\nexport function redactText(text: string, options?: RedactOptions): RedactTextResult {\n const maskChar = options?.maskChar ?? \"●\"\n if (maskChar.length !== 1 || /[0-9A-Za-z]/.test(maskChar)) {\n throw new Error(`maskChar는 영숫자가 아닌 1글자여야 함: ${JSON.stringify(maskChar)}`)\n }\n\n const enabled = options?.rules ?? DEFAULT_REDACT_RULES\n const hits: RedactHit[] = []\n if (text === \"\" || enabled.length === 0) return { text, hits }\n\n // 우선순위순으로 매치 수집 — 점유 구간과 겹치는 하위 룰 매치는 스킵\n const occupied: Array<{ start: number; end: number }> = []\n for (const rule of RULE_PRIORITY) {\n if (!enabled.includes(rule)) continue\n const def = RULES[rule]\n for (const m of text.matchAll(def.pattern)) {\n if (def.validate && !def.validate(m)) continue\n const start = m.index as number\n const end = start + m[0].length\n if (occupied.some((o) => start < o.end && end > o.start)) continue\n occupied.push({ start, end })\n hits.push({ rule, masked: def.mask(m, maskChar), index: start, length: m[0].length })\n }\n }\n\n hits.sort((a, b) => a.index - b.index)\n\n let out = \"\"\n let cursor = 0\n for (const h of hits) {\n out += text.slice(cursor, h.index) + h.masked\n cursor = h.index + h.length\n }\n out += text.slice(cursor)\n return { text: out, hits }\n}\n\n/**\n * 마크다운 문서 전용 래퍼 — base64 이미지(data URI) 라인은 마스킹에서 제외한다.\n * base64 페이로드의 숫자열이 phone 등에 오탐되면 이미지가 깨지기 때문.\n * hits의 index는 문서 전체 기준 절대 오프셋으로 환산된다.\n */\nexport function redactMarkdown(markdown: string, options?: RedactOptions): RedactTextResult {\n const lines = markdown.split(\"\\n\")\n const hits: RedactHit[] = []\n let offset = 0\n const outLines = lines.map((line) => {\n if (line.includes(\"data:image/\")) {\n offset += line.length + 1\n return line\n }\n const r = redactText(line, options)\n for (const h of r.hits) hits.push({ ...h, index: h.index + offset })\n offset += line.length + 1\n return r.text\n })\n return { text: outLines.join(\"\\n\"), hits }\n}\n"],"mappings":"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","names":[]}