hangul-toolkit 0.1.0
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.
- package/LICENSE +21 -0
- package/README.md +128 -0
- package/dist/index.cjs +480 -0
- package/dist/index.d.cts +141 -0
- package/dist/index.d.ts +141 -0
- package/dist/index.js +434 -0
- package/package.json +58 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
// src/syllable.ts
|
|
2
|
+
var HANGUL_BASE = 44032;
|
|
3
|
+
var CHOSEONG_COUNT = 19;
|
|
4
|
+
var JUNGSEONG_COUNT = 21;
|
|
5
|
+
var JONGSEONG_COUNT = 28;
|
|
6
|
+
var CHOSEONG_LIST = [
|
|
7
|
+
"\u3131",
|
|
8
|
+
"\u3132",
|
|
9
|
+
"\u3134",
|
|
10
|
+
"\u3137",
|
|
11
|
+
"\u3138",
|
|
12
|
+
"\u3139",
|
|
13
|
+
"\u3141",
|
|
14
|
+
"\u3142",
|
|
15
|
+
"\u3143",
|
|
16
|
+
"\u3145",
|
|
17
|
+
"\u3146",
|
|
18
|
+
"\u3147",
|
|
19
|
+
"\u3148",
|
|
20
|
+
"\u3149",
|
|
21
|
+
"\u314A",
|
|
22
|
+
"\u314B",
|
|
23
|
+
"\u314C",
|
|
24
|
+
"\u314D",
|
|
25
|
+
"\u314E"
|
|
26
|
+
];
|
|
27
|
+
var JUNGSEONG_LIST = [
|
|
28
|
+
"\u314F",
|
|
29
|
+
"\u3150",
|
|
30
|
+
"\u3151",
|
|
31
|
+
"\u3152",
|
|
32
|
+
"\u3153",
|
|
33
|
+
"\u3154",
|
|
34
|
+
"\u3155",
|
|
35
|
+
"\u3156",
|
|
36
|
+
"\u3157",
|
|
37
|
+
"\u3158",
|
|
38
|
+
"\u3159",
|
|
39
|
+
"\u315A",
|
|
40
|
+
"\u315B",
|
|
41
|
+
"\u315C",
|
|
42
|
+
"\u315D",
|
|
43
|
+
"\u315E",
|
|
44
|
+
"\u315F",
|
|
45
|
+
"\u3160",
|
|
46
|
+
"\u3161",
|
|
47
|
+
"\u3162",
|
|
48
|
+
"\u3163"
|
|
49
|
+
];
|
|
50
|
+
var JONGSEONG_LIST = [
|
|
51
|
+
"",
|
|
52
|
+
"\u3131",
|
|
53
|
+
"\u3132",
|
|
54
|
+
"\u3133",
|
|
55
|
+
"\u3134",
|
|
56
|
+
"\u3135",
|
|
57
|
+
"\u3136",
|
|
58
|
+
"\u3137",
|
|
59
|
+
"\u3139",
|
|
60
|
+
"\u313A",
|
|
61
|
+
"\u313B",
|
|
62
|
+
"\u313C",
|
|
63
|
+
"\u313D",
|
|
64
|
+
"\u313E",
|
|
65
|
+
"\u313F",
|
|
66
|
+
"\u3140",
|
|
67
|
+
"\u3141",
|
|
68
|
+
"\u3142",
|
|
69
|
+
"\u3144",
|
|
70
|
+
"\u3145",
|
|
71
|
+
"\u3146",
|
|
72
|
+
"\u3147",
|
|
73
|
+
"\u3148",
|
|
74
|
+
"\u314A",
|
|
75
|
+
"\u314B",
|
|
76
|
+
"\u314C",
|
|
77
|
+
"\u314D",
|
|
78
|
+
"\u314E"
|
|
79
|
+
];
|
|
80
|
+
var CHO_SET = new Set(CHOSEONG_LIST);
|
|
81
|
+
var JUNG_SET = new Set(JUNGSEONG_LIST);
|
|
82
|
+
var JONG_SET = new Set(JONGSEONG_LIST.filter((j) => j !== ""));
|
|
83
|
+
var COMPLEX_JUNGSEONG = {
|
|
84
|
+
"\u3158": "\u3157\u314F",
|
|
85
|
+
"\u3159": "\u3157\u3150",
|
|
86
|
+
"\u315A": "\u3157\u3163",
|
|
87
|
+
"\u315D": "\u315C\u3153",
|
|
88
|
+
"\u315E": "\u315C\u3154",
|
|
89
|
+
"\u315F": "\u315C\u3163",
|
|
90
|
+
"\u3162": "\u3161\u3163"
|
|
91
|
+
};
|
|
92
|
+
var COMPLEX_JONGSEONG = {
|
|
93
|
+
"\u3133": "\u3131\u3145",
|
|
94
|
+
"\u3135": "\u3134\u3148",
|
|
95
|
+
"\u3136": "\u3134\u314E",
|
|
96
|
+
"\u313A": "\u3139\u3131",
|
|
97
|
+
"\u313B": "\u3139\u3141",
|
|
98
|
+
"\u313C": "\u3139\u3142",
|
|
99
|
+
"\u313D": "\u3139\u3145",
|
|
100
|
+
"\u313E": "\u3139\u314C",
|
|
101
|
+
"\u313F": "\u3139\u314D",
|
|
102
|
+
"\u3140": "\u3139\u314E",
|
|
103
|
+
"\u3144": "\u3142\u3145"
|
|
104
|
+
};
|
|
105
|
+
var REVERSE_JUNGSEONG = Object.fromEntries(
|
|
106
|
+
Object.entries(COMPLEX_JUNGSEONG).map(([k, v]) => [v, k])
|
|
107
|
+
);
|
|
108
|
+
var REVERSE_JONGSEONG = Object.fromEntries(
|
|
109
|
+
Object.entries(COMPLEX_JONGSEONG).map(([k, v]) => [v, k])
|
|
110
|
+
);
|
|
111
|
+
function isHangulSyllable(char) {
|
|
112
|
+
if (!char || char.length !== 1) return false;
|
|
113
|
+
const code = char.charCodeAt(0);
|
|
114
|
+
return code >= HANGUL_BASE && code < HANGUL_BASE + CHOSEONG_COUNT * JUNGSEONG_COUNT * JONGSEONG_COUNT;
|
|
115
|
+
}
|
|
116
|
+
function decompose(char) {
|
|
117
|
+
if (!isHangulSyllable(char)) {
|
|
118
|
+
throw new Error(`decompose: '${char}' is not a Hangul syllable`);
|
|
119
|
+
}
|
|
120
|
+
const offset = char.charCodeAt(0) - HANGUL_BASE;
|
|
121
|
+
const choseongIndex = Math.floor(offset / (JUNGSEONG_COUNT * JONGSEONG_COUNT));
|
|
122
|
+
const jungseongIndex = Math.floor(offset % (JUNGSEONG_COUNT * JONGSEONG_COUNT) / JONGSEONG_COUNT);
|
|
123
|
+
const jongseongIndex = offset % JONGSEONG_COUNT;
|
|
124
|
+
return {
|
|
125
|
+
choseong: CHOSEONG_LIST[choseongIndex],
|
|
126
|
+
jungseong: JUNGSEONG_LIST[jungseongIndex],
|
|
127
|
+
jongseong: jongseongIndex === 0 ? null : JONGSEONG_LIST[jongseongIndex]
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function compose(choseong, jungseong, jongseong) {
|
|
131
|
+
const choIndex = CHOSEONG_LIST.indexOf(choseong);
|
|
132
|
+
const jungIndex = JUNGSEONG_LIST.indexOf(jungseong);
|
|
133
|
+
const jongIndex = jongseong ? JONGSEONG_LIST.indexOf(jongseong) : 0;
|
|
134
|
+
if (choIndex < 0 || jungIndex < 0 || jongIndex < 0) {
|
|
135
|
+
throw new Error(`compose: invalid jamo combination (${choseong}, ${jungseong}, ${jongseong ?? ""})`);
|
|
136
|
+
}
|
|
137
|
+
return String.fromCharCode(HANGUL_BASE + (choIndex * JUNGSEONG_COUNT + jungIndex) * JONGSEONG_COUNT + jongIndex);
|
|
138
|
+
}
|
|
139
|
+
function hasBatchim(char) {
|
|
140
|
+
if (!char) return false;
|
|
141
|
+
const last = char[char.length - 1];
|
|
142
|
+
if (!isHangulSyllable(last)) return false;
|
|
143
|
+
return (last.charCodeAt(0) - HANGUL_BASE) % JONGSEONG_COUNT !== 0;
|
|
144
|
+
}
|
|
145
|
+
function disassemble(text) {
|
|
146
|
+
let result = "";
|
|
147
|
+
for (const char of text) {
|
|
148
|
+
if (!isHangulSyllable(char)) {
|
|
149
|
+
result += char;
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
const { choseong, jungseong, jongseong } = decompose(char);
|
|
153
|
+
result += choseong;
|
|
154
|
+
result += COMPLEX_JUNGSEONG[jungseong] ?? jungseong;
|
|
155
|
+
if (jongseong) {
|
|
156
|
+
result += COMPLEX_JONGSEONG[jongseong] ?? jongseong;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return result;
|
|
160
|
+
}
|
|
161
|
+
function assemble(jamo) {
|
|
162
|
+
const chars = [...jamo];
|
|
163
|
+
let result = "";
|
|
164
|
+
let i = 0;
|
|
165
|
+
while (i < chars.length) {
|
|
166
|
+
const cho = chars[i];
|
|
167
|
+
if (!CHO_SET.has(cho)) {
|
|
168
|
+
result += cho;
|
|
169
|
+
i += 1;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
let jung = "";
|
|
173
|
+
let jungLen = 0;
|
|
174
|
+
const v1 = chars[i + 1] ?? "";
|
|
175
|
+
const v2 = chars[i + 2] ?? "";
|
|
176
|
+
if (v1 && v2 && REVERSE_JUNGSEONG[v1 + v2]) {
|
|
177
|
+
jung = REVERSE_JUNGSEONG[v1 + v2];
|
|
178
|
+
jungLen = 2;
|
|
179
|
+
} else if (v1 && JUNG_SET.has(v1)) {
|
|
180
|
+
jung = v1;
|
|
181
|
+
jungLen = 1;
|
|
182
|
+
}
|
|
183
|
+
if (!jung) {
|
|
184
|
+
result += cho;
|
|
185
|
+
i += 1;
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
let jong;
|
|
189
|
+
let jongLen = 0;
|
|
190
|
+
const c1 = chars[i + 1 + jungLen] ?? "";
|
|
191
|
+
const c2 = chars[i + 2 + jungLen] ?? "";
|
|
192
|
+
const afterCompound = chars[i + 3 + jungLen] ?? "";
|
|
193
|
+
if (c1 && c2 && REVERSE_JONGSEONG[c1 + c2] && !JUNG_SET.has(afterCompound)) {
|
|
194
|
+
jong = REVERSE_JONGSEONG[c1 + c2];
|
|
195
|
+
jongLen = 2;
|
|
196
|
+
} else if (c1 && JONG_SET.has(c1) && !JUNG_SET.has(c2)) {
|
|
197
|
+
jong = c1;
|
|
198
|
+
jongLen = 1;
|
|
199
|
+
}
|
|
200
|
+
result += compose(cho, jung, jong);
|
|
201
|
+
i += 1 + jungLen + jongLen;
|
|
202
|
+
}
|
|
203
|
+
return result;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// src/josa.ts
|
|
207
|
+
var JOSA_PAIRS = {
|
|
208
|
+
"\uC740/\uB294": ["\uC740", "\uB294"],
|
|
209
|
+
"\uC774/\uAC00": ["\uC774", "\uAC00"],
|
|
210
|
+
"\uC744/\uB97C": ["\uC744", "\uB97C"],
|
|
211
|
+
"\uACFC/\uC640": ["\uACFC", "\uC640"],
|
|
212
|
+
"\uC73C\uB85C/\uB85C": ["\uC73C\uB85C", "\uB85C"],
|
|
213
|
+
"\uC774\uB098/\uB098": ["\uC774\uB098", "\uB098"],
|
|
214
|
+
"\uC774\uB791/\uB791": ["\uC774\uB791", "\uB791"],
|
|
215
|
+
"\uC544/\uC57C": ["\uC544", "\uC57C"],
|
|
216
|
+
"\uC774\uC5EC/\uC5EC": ["\uC774\uC5EC", "\uC5EC"],
|
|
217
|
+
"\uC774\uC5D0\uC694/\uC608\uC694": ["\uC774\uC5D0\uC694", "\uC608\uC694"],
|
|
218
|
+
"\uC774\uC5C8/\uC600": ["\uC774\uC5C8", "\uC600"],
|
|
219
|
+
"\uC774\uACE0/\uACE0": ["\uC774\uACE0", "\uACE0"],
|
|
220
|
+
"\uC774\uB4E0/\uB4E0": ["\uC774\uB4E0", "\uB4E0"],
|
|
221
|
+
"\uC774\uC57C\uB9D0\uB85C/\uC57C\uB9D0\uB85C": ["\uC774\uC57C\uB9D0\uB85C", "\uC57C\uB9D0\uB85C"],
|
|
222
|
+
"\uC774\uB098\uB9C8/\uB098\uB9C8": ["\uC774\uB098\uB9C8", "\uB098\uB9C8"],
|
|
223
|
+
"\uC774\uC57C/\uC57C": ["\uC774\uC57C", "\uC57C"]
|
|
224
|
+
};
|
|
225
|
+
var DIGIT_HAS_BATCHIM = [true, true, false, true, false, false, true, true, true, false];
|
|
226
|
+
var DIGIT_ENDS_WITH_RIEUL = [false, true, false, false, false, false, false, true, true, false];
|
|
227
|
+
function lastCharInfo(word) {
|
|
228
|
+
const last = word[word.length - 1];
|
|
229
|
+
if (last >= "0" && last <= "9") {
|
|
230
|
+
const d = last.charCodeAt(0) - 48;
|
|
231
|
+
return { hasBatchim: DIGIT_HAS_BATCHIM[d], endsWithRieul: DIGIT_ENDS_WITH_RIEUL[d] };
|
|
232
|
+
}
|
|
233
|
+
if (isHangulSyllable(last)) {
|
|
234
|
+
const jongIndex = (last.charCodeAt(0) - 44032) % 28;
|
|
235
|
+
return {
|
|
236
|
+
hasBatchim: jongIndex !== 0,
|
|
237
|
+
endsWithRieul: jongIndex === 8
|
|
238
|
+
// 'ㄹ'
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
return { hasBatchim: false, endsWithRieul: false };
|
|
242
|
+
}
|
|
243
|
+
function pickJosa(word, pair) {
|
|
244
|
+
const particles = JOSA_PAIRS[pair];
|
|
245
|
+
if (!particles) {
|
|
246
|
+
throw new Error(
|
|
247
|
+
`pickJosa: unsupported particle pair '${pair}'. Supported: ${Object.keys(JOSA_PAIRS).join(", ")}`
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
if (!word) return particles[1];
|
|
251
|
+
const { hasBatchim: bat, endsWithRieul } = lastCharInfo(word);
|
|
252
|
+
if (pair === "\uC73C\uB85C/\uB85C") {
|
|
253
|
+
return !bat || endsWithRieul ? "\uB85C" : "\uC73C\uB85C";
|
|
254
|
+
}
|
|
255
|
+
return bat ? particles[0] : particles[1];
|
|
256
|
+
}
|
|
257
|
+
function josa(word, pair) {
|
|
258
|
+
const text = String(word);
|
|
259
|
+
return text + pickJosa(text, pair);
|
|
260
|
+
}
|
|
261
|
+
function supportedJosaPairs() {
|
|
262
|
+
return Object.keys(JOSA_PAIRS);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// src/number.ts
|
|
266
|
+
var DIGITS = ["", "\uC77C", "\uC774", "\uC0BC", "\uC0AC", "\uC624", "\uC721", "\uCE60", "\uD314", "\uAD6C"];
|
|
267
|
+
var SMALL_UNITS = ["", "\uC2ED", "\uBC31", "\uCC9C"];
|
|
268
|
+
var BIG_UNITS = ["", "\uB9CC", "\uC5B5", "\uC870", "\uACBD", "\uD574"];
|
|
269
|
+
function fourDigitsToKorean(n) {
|
|
270
|
+
const str = String(n);
|
|
271
|
+
let result = "";
|
|
272
|
+
for (let i = 0; i < str.length; i++) {
|
|
273
|
+
const digit = Number(str[i]);
|
|
274
|
+
if (digit === 0) continue;
|
|
275
|
+
const unit = SMALL_UNITS[str.length - 1 - i];
|
|
276
|
+
if (digit === 1 && unit !== "") {
|
|
277
|
+
result += unit;
|
|
278
|
+
} else {
|
|
279
|
+
result += DIGITS[digit] + unit;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return result;
|
|
283
|
+
}
|
|
284
|
+
function numberToKorean(num) {
|
|
285
|
+
if (!Number.isSafeInteger(num)) {
|
|
286
|
+
throw new RangeError("numberToKorean: value must be a safe integer");
|
|
287
|
+
}
|
|
288
|
+
if (num === 0) return "\uC601";
|
|
289
|
+
let prefix = "";
|
|
290
|
+
let n = num;
|
|
291
|
+
if (n < 0) {
|
|
292
|
+
prefix = "\uB9C8\uC774\uB108\uC2A4 ";
|
|
293
|
+
n = -n;
|
|
294
|
+
}
|
|
295
|
+
const groups = [];
|
|
296
|
+
while (n > 0) {
|
|
297
|
+
groups.push(n % 1e4);
|
|
298
|
+
n = Math.floor(n / 1e4);
|
|
299
|
+
}
|
|
300
|
+
if (groups.length > BIG_UNITS.length) {
|
|
301
|
+
throw new RangeError(`numberToKorean: numbers up to ${BIG_UNITS.length - 1} * 10^4 scale are supported`);
|
|
302
|
+
}
|
|
303
|
+
let result = "";
|
|
304
|
+
for (let i = groups.length - 1; i >= 0; i--) {
|
|
305
|
+
const group = groups[i];
|
|
306
|
+
if (group === 0) continue;
|
|
307
|
+
if (group === 1 && i > 0) {
|
|
308
|
+
result += "\uC77C" + BIG_UNITS[i];
|
|
309
|
+
} else {
|
|
310
|
+
result += fourDigitsToKorean(group) + BIG_UNITS[i];
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return prefix + result;
|
|
314
|
+
}
|
|
315
|
+
function formatWon(amount) {
|
|
316
|
+
return numberToKorean(amount) + "\uC6D0";
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// src/pii.ts
|
|
320
|
+
var DEFAULT_TYPES = ["rrn", "phone", "email", "card", "driverLicense"];
|
|
321
|
+
var PATTERN_PRIORITY = ["rrn", "driverLicense", "card", "phone", "email", "account"];
|
|
322
|
+
var PATTERNS = {
|
|
323
|
+
// 주민등록번호 / 외국인등록번호
|
|
324
|
+
rrn: /\b\d{6}-?[1-4]\d{6}\b/g,
|
|
325
|
+
// 운전면허번호
|
|
326
|
+
driverLicense: /\b\d{2}-\d{2}-\d{6}-\d{2}\b/g,
|
|
327
|
+
// 신용카드 번호
|
|
328
|
+
card: /\b\d{4}[- ]\d{4}[- ]\d{4}[- ]\d{4}\b/g,
|
|
329
|
+
// 휴대폰(010~019) + 일반전화(02, 031~064)
|
|
330
|
+
phone: /\b0(?:1[016789]|2|[3-6]\d)-?\d{3,4}-?\d{4}\b/g,
|
|
331
|
+
email: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g,
|
|
332
|
+
// 계좌번호 (오탐 가능성이 있어 기본 비활성)
|
|
333
|
+
account: /\b\d{3,6}-\d{2,6}-\d{2,7}\b/g
|
|
334
|
+
};
|
|
335
|
+
function collectMatches(text, types) {
|
|
336
|
+
const matches = [];
|
|
337
|
+
for (const type of PATTERN_PRIORITY) {
|
|
338
|
+
if (!types.includes(type)) continue;
|
|
339
|
+
const pattern = new RegExp(PATTERNS[type].source, PATTERNS[type].flags);
|
|
340
|
+
let m;
|
|
341
|
+
while ((m = pattern.exec(text)) !== null) {
|
|
342
|
+
matches.push({ type, value: m[0], start: m.index, end: m.index + m[0].length });
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
const accepted = [];
|
|
346
|
+
const sorted = matches.sort(
|
|
347
|
+
(a, b) => PATTERN_PRIORITY.indexOf(a.type) - PATTERN_PRIORITY.indexOf(b.type) || a.start - b.start
|
|
348
|
+
);
|
|
349
|
+
for (const match of sorted) {
|
|
350
|
+
const overlaps = accepted.some((a) => match.start < a.end && a.start < match.end);
|
|
351
|
+
if (!overlaps) accepted.push(match);
|
|
352
|
+
}
|
|
353
|
+
return accepted.sort((a, b) => a.start - b.start);
|
|
354
|
+
}
|
|
355
|
+
function detectPII(text, types = [...PATTERN_PRIORITY]) {
|
|
356
|
+
return collectMatches(text, types);
|
|
357
|
+
}
|
|
358
|
+
function maskPII(text, options = {}) {
|
|
359
|
+
const { types = DEFAULT_TYPES, maskChar = "*", keepLast = 0 } = options;
|
|
360
|
+
const matches = collectMatches(text, types);
|
|
361
|
+
let result = text;
|
|
362
|
+
for (let i = matches.length - 1; i >= 0; i--) {
|
|
363
|
+
const { start, end, value } = matches[i];
|
|
364
|
+
const alnumCount = (value.match(/[A-Za-z0-9]/g) ?? []).length;
|
|
365
|
+
const keep = Math.max(0, Math.min(keepLast, alnumCount));
|
|
366
|
+
let seen = 0;
|
|
367
|
+
const masked = [...value].map((ch) => {
|
|
368
|
+
if (!/[A-Za-z0-9]/.test(ch)) return ch;
|
|
369
|
+
seen += 1;
|
|
370
|
+
return seen > alnumCount - keep ? ch : maskChar;
|
|
371
|
+
}).join("");
|
|
372
|
+
result = result.slice(0, start) + masked + result.slice(end);
|
|
373
|
+
}
|
|
374
|
+
return result;
|
|
375
|
+
}
|
|
376
|
+
function maskName(name, maskChar = "*") {
|
|
377
|
+
const chars = [...name.trim()];
|
|
378
|
+
if (chars.length <= 1) return name;
|
|
379
|
+
if (chars.length === 2) return chars[0] + maskChar;
|
|
380
|
+
return chars[0] + maskChar.repeat(chars.length - 2) + chars[chars.length - 1];
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// src/validate.ts
|
|
384
|
+
function isValidRRN(input) {
|
|
385
|
+
const digits = input.replace(/-/g, "");
|
|
386
|
+
if (!/^\d{13}$/.test(digits)) return false;
|
|
387
|
+
const month = Number(digits.slice(2, 4));
|
|
388
|
+
const day = Number(digits.slice(4, 6));
|
|
389
|
+
if (month < 1 || month > 12 || day < 1 || day > 31) return false;
|
|
390
|
+
const weights = [2, 3, 4, 5, 6, 7, 8, 9, 2, 3, 4, 5];
|
|
391
|
+
let sum = 0;
|
|
392
|
+
for (let i = 0; i < 12; i++) {
|
|
393
|
+
sum += Number(digits[i]) * weights[i];
|
|
394
|
+
}
|
|
395
|
+
const check = (11 - sum % 11) % 10;
|
|
396
|
+
return check === Number(digits[12]);
|
|
397
|
+
}
|
|
398
|
+
function isValidBusinessNumber(input) {
|
|
399
|
+
const digits = input.replace(/-/g, "");
|
|
400
|
+
if (!/^\d{10}$/.test(digits)) return false;
|
|
401
|
+
const weights = [1, 3, 7, 1, 3, 7, 1, 3];
|
|
402
|
+
let sum = 0;
|
|
403
|
+
for (let i = 0; i < 8; i++) {
|
|
404
|
+
sum += Number(digits[i]) * weights[i];
|
|
405
|
+
}
|
|
406
|
+
sum += Math.floor(Number(digits[8]) * 5 / 10);
|
|
407
|
+
const check = (10 - sum % 10) % 10;
|
|
408
|
+
return check === Number(digits[9]);
|
|
409
|
+
}
|
|
410
|
+
function isValidPhoneNumber(input) {
|
|
411
|
+
return /^0(?:1[016789]|2|[3-6]\d)-?\d{3,4}-?\d{4}$/.test(input);
|
|
412
|
+
}
|
|
413
|
+
export {
|
|
414
|
+
CHOSEONG_LIST,
|
|
415
|
+
JONGSEONG_LIST,
|
|
416
|
+
JUNGSEONG_LIST,
|
|
417
|
+
assemble,
|
|
418
|
+
compose,
|
|
419
|
+
decompose,
|
|
420
|
+
detectPII,
|
|
421
|
+
disassemble,
|
|
422
|
+
formatWon,
|
|
423
|
+
hasBatchim,
|
|
424
|
+
isHangulSyllable,
|
|
425
|
+
isValidBusinessNumber,
|
|
426
|
+
isValidPhoneNumber,
|
|
427
|
+
isValidRRN,
|
|
428
|
+
josa,
|
|
429
|
+
maskName,
|
|
430
|
+
maskPII,
|
|
431
|
+
numberToKorean,
|
|
432
|
+
pickJosa,
|
|
433
|
+
supportedJosaPairs
|
|
434
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "hangul-toolkit",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "한국어 텍스트 처리 유틸리티 — 조사 선택, 자소 분해/조합, 숫자→한글 변환, 개인정보(PII) 마스킹 / Korean text utilities: josa picker, syllable disassemble/assemble, number-to-Korean, PII masking for LLM preprocessing",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"require": "./dist/index.cjs"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"sideEffects": false,
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"README.md",
|
|
20
|
+
"LICENSE"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsup src/index.ts --format esm,cjs --dts --clean",
|
|
24
|
+
"test": "vitest run",
|
|
25
|
+
"test:watch": "vitest",
|
|
26
|
+
"prepublishOnly": "npm run build && npm test"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"hangul",
|
|
30
|
+
"korean",
|
|
31
|
+
"한글",
|
|
32
|
+
"한국어",
|
|
33
|
+
"josa",
|
|
34
|
+
"조사",
|
|
35
|
+
"pii",
|
|
36
|
+
"masking",
|
|
37
|
+
"개인정보",
|
|
38
|
+
"llm",
|
|
39
|
+
"korean-text",
|
|
40
|
+
"nlp"
|
|
41
|
+
],
|
|
42
|
+
"license": "MIT",
|
|
43
|
+
"author": "feelyday",
|
|
44
|
+
"repository": {
|
|
45
|
+
"type": "git",
|
|
46
|
+
"url": "git+https://github.com/feelyday/hangul-toolkit.git"
|
|
47
|
+
},
|
|
48
|
+
"bugs": "https://github.com/feelyday/hangul-toolkit/issues",
|
|
49
|
+
"homepage": "https://github.com/feelyday/hangul-toolkit#readme",
|
|
50
|
+
"engines": {
|
|
51
|
+
"node": ">=18"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"tsup": "^8.3.5",
|
|
55
|
+
"typescript": "^5.7.0",
|
|
56
|
+
"vitest": "^3.0.0"
|
|
57
|
+
}
|
|
58
|
+
}
|