@rscc/common-core 0.1.2 → 0.2.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/dist/index.cjs CHANGED
@@ -23,35 +23,69 @@ __export(index_exports, {
23
23
  ApiError: () => ApiError,
24
24
  BulkheadFullError: () => BulkheadFullError,
25
25
  ResultCode: () => ResultCode,
26
+ WEBHOOK_SIGNATURE_HEADER: () => WEBHOOK_SIGNATURE_HEADER,
27
+ abbreviateAmount: () => abbreviateAmount,
28
+ ageByYear: () => ageByYear,
29
+ ageInsurance: () => ageInsurance,
30
+ ageMan: () => ageMan,
31
+ attachJosa: () => attachJosa,
32
+ buildListQuery: () => buildListQuery,
33
+ bulkFailures: () => bulkFailures,
34
+ classifyPhoneNumber: () => classifyPhoneNumber,
35
+ composeHangul: () => composeHangul,
26
36
  createApiClient: () => createApiClient,
37
+ createBulkResultBuilder: () => createBulkResultBuilder,
27
38
  createBulkhead: () => createBulkhead,
39
+ createBusinessDays: () => createBusinessDays,
28
40
  createCircuitBreaker: () => createCircuitBreaker,
41
+ createFeatureFlags: () => createFeatureFlags,
29
42
  createTokenBucket: () => createTokenBucket,
30
43
  createTtlCache: () => createTtlCache,
31
44
  decodeJwtPayload: () => decodeJwtPayload,
45
+ decomposeHangul: () => decomposeHangul,
46
+ formatPhoneNumber: () => formatPhoneNumber,
47
+ generateIdempotencyKey: () => generateIdempotencyKey,
32
48
  getTokenExpiry: () => getTokenExpiry,
49
+ isBulkResult: () => isBulkResult,
33
50
  isChosungQuery: () => isChosungQuery,
51
+ isForeignerRrn: () => isForeignerRrn,
34
52
  isRetryableStatus: () => isRetryableStatus,
35
53
  isTokenExpired: () => isTokenExpired,
36
54
  isValidBusinessNumber: () => isValidBusinessNumber,
37
55
  isValidCorporateNumber: () => isValidCorporateNumber,
56
+ isValidRrn: () => isValidRrn,
38
57
  isValidationErrorData: () => isValidationErrorData,
58
+ kindsForExtension: () => kindsForExtension,
39
59
  maskCardNumber: () => maskCardNumber,
40
60
  maskEmail: () => maskEmail,
41
61
  maskName: () => maskName,
42
62
  maskPhone: () => maskPhone,
43
63
  maskSecret: () => maskSecret,
64
+ matchesHangul: () => matchesHangul,
44
65
  normalizeBusinessNumber: () => normalizeBusinessNumber,
66
+ normalizePhoneNumber: () => normalizePhoneNumber,
67
+ normalizeRrn: () => normalizeRrn,
68
+ parseFlag: () => parseFlag,
45
69
  parseRetryAfterMs: () => parseRetryAfterMs,
46
70
  parseSseFrame: () => parseSseFrame,
47
71
  parseWireDateTime: () => parseWireDateTime,
72
+ pickJosa: () => pickJosa,
48
73
  readSseStream: () => readSseStream,
49
74
  retry: () => retry,
75
+ rrnBirthDate: () => rrnBirthDate,
76
+ rrnChecksumOkLegacy: () => rrnChecksumOkLegacy,
50
77
  sanitizeLogValue: () => sanitizeLogValue,
78
+ signWebhook: () => signWebhook,
79
+ sniffFile: () => sniffFile,
51
80
  stripZone: () => stripZone,
52
81
  toChosung: () => toChosung,
82
+ toE164: () => toE164,
83
+ toFormalNotation: () => toFormalNotation,
84
+ toKoreanWords: () => toKoreanWords,
53
85
  toWireDate: () => toWireDate,
54
- toWireDateTime: () => toWireDateTime
86
+ toWireDateTime: () => toWireDateTime,
87
+ validateUpload: () => validateUpload,
88
+ verifyWebhook: () => verifyWebhook
55
89
  });
56
90
  module.exports = __toCommonJS(index_exports);
57
91
 
@@ -77,6 +111,18 @@ var ResultCode = {
77
111
  INTERNAL_SERVER_ERROR: "500"
78
112
  };
79
113
 
114
+ // src/idempotency.ts
115
+ var FALLBACK_GROUPS = [8, 4, 4, 4, 12];
116
+ function generateIdempotencyKey() {
117
+ const c = globalThis.crypto;
118
+ if (c && typeof c.randomUUID === "function") return c.randomUUID();
119
+ return FALLBACK_GROUPS.map((len) => {
120
+ let group = "";
121
+ for (let i = 0; i < len; i++) group += Math.floor(Math.random() * 16).toString(16);
122
+ return group;
123
+ }).join("-");
124
+ }
125
+
80
126
  // src/retry.ts
81
127
  var RETRYABLE_STATUS = /* @__PURE__ */ new Set([408, 429, 500, 502, 503, 504]);
82
128
  function isRetryableStatus(status) {
@@ -170,6 +216,7 @@ var ApiError = class extends Error {
170
216
  }
171
217
  };
172
218
  var DEFAULT_RETRY_METHODS = ["GET", "HEAD", "OPTIONS", "PUT", "DELETE"];
219
+ var DEFAULT_IDEMPOTENCY_METHODS = ["POST", "PATCH"];
173
220
  var MAX_RAW_TEXT_LENGTH = 2048;
174
221
  function joinUrl(baseUrl, path) {
175
222
  if (path === "") return baseUrl;
@@ -207,9 +254,14 @@ function createApiClient(config) {
207
254
  async function requestWithMeta(path, init = {}) {
208
255
  const sentTraceId = generateTraceId();
209
256
  const method = (init.method ?? "GET").toUpperCase();
257
+ const idempotencyHeader = config.idempotency?.header ?? "Idempotency-Key";
258
+ const idempotencyKey = config.idempotency && (config.idempotency.methods ?? DEFAULT_IDEMPOTENCY_METHODS).includes(method) ? generateIdempotencyKey() : null;
210
259
  const attemptOnce = async () => {
211
260
  const headers = new Headers(init.headers);
212
261
  headers.set(traceIdHeader, sentTraceId);
262
+ if (idempotencyKey !== null && !headers.has(idempotencyHeader)) {
263
+ headers.set(idempotencyHeader, idempotencyKey);
264
+ }
213
265
  const isFormData = typeof FormData !== "undefined" && init.body instanceof FormData;
214
266
  if (!headers.has("Content-Type") && !isFormData) {
215
267
  headers.set("Content-Type", "application/json");
@@ -276,8 +328,8 @@ function createApiClient(config) {
276
328
  return { data: json.data ?? void 0, traceId, status: response.status, response };
277
329
  }
278
330
  if (!response.ok) {
279
- const j = json;
280
- const message = typeof j?.message === "string" && j.message || typeof j?.error === "string" && j.error || `API \uC624\uB958: ${response.status} ${response.statusText}`;
331
+ const j2 = json;
332
+ const message = typeof j2?.message === "string" && j2.message || typeof j2?.error === "string" && j2.error || `API \uC624\uB958: ${response.status} ${response.statusText}`;
281
333
  fail(String(response.status), message);
282
334
  }
283
335
  return { data: json, traceId, status: response.status, response };
@@ -825,38 +877,929 @@ function isValidCorporateNumber(value) {
825
877
  const check = (10 - sum % 10) % 10;
826
878
  return check === digitAt(digits, 12);
827
879
  }
880
+
881
+ // src/webhook.ts
882
+ var WEBHOOK_SIGNATURE_HEADER = "X-Rscc-Signature";
883
+ var DEFAULT_TOLERANCE_SECONDS = 300;
884
+ var encoder = new TextEncoder();
885
+ function toBytes(payload) {
886
+ return typeof payload === "string" ? encoder.encode(payload) : payload;
887
+ }
888
+ async function computeSignatureHex(secret, timestampSeconds, payload) {
889
+ const body = toBytes(payload);
890
+ const prefix = encoder.encode(`${timestampSeconds}.`);
891
+ const message = new Uint8Array(prefix.length + body.length);
892
+ message.set(prefix, 0);
893
+ message.set(body, prefix.length);
894
+ const key = await globalThis.crypto.subtle.importKey(
895
+ "raw",
896
+ encoder.encode(secret),
897
+ { name: "HMAC", hash: "SHA-256" },
898
+ false,
899
+ ["sign"]
900
+ );
901
+ const signature = await globalThis.crypto.subtle.sign("HMAC", key, message);
902
+ let hex = "";
903
+ for (const byte of new Uint8Array(signature)) hex += byte.toString(16).padStart(2, "0");
904
+ return hex;
905
+ }
906
+ function timingSafeHexEquals(a, b) {
907
+ if (a.length !== b.length) return false;
908
+ let diff = 0;
909
+ for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
910
+ return diff === 0;
911
+ }
912
+ async function signWebhook(secret, payload, timestampSeconds) {
913
+ const hex = await computeSignatureHex(secret, timestampSeconds, payload);
914
+ return `t=${timestampSeconds},v1=${hex}`;
915
+ }
916
+ async function verifyWebhook(opts) {
917
+ if (opts.header == null || opts.header === "") return false;
918
+ const secrets = typeof opts.secrets === "string" ? [opts.secrets] : opts.secrets;
919
+ if (secrets.length === 0) return false;
920
+ const timestamps = [];
921
+ const candidates = [];
922
+ for (const element of opts.header.split(",")) {
923
+ const eq = element.indexOf("=");
924
+ if (eq < 0) continue;
925
+ const key = element.slice(0, eq).trim();
926
+ const value = element.slice(eq + 1).trim();
927
+ if (key === "t") timestamps.push(value);
928
+ else if (key === "v1") candidates.push(value);
929
+ }
930
+ if (timestamps.length !== 1 || candidates.length === 0) return false;
931
+ if (!/^[0-9]+$/.test(timestamps[0])) return false;
932
+ const t = Number(timestamps[0]);
933
+ const now = opts.nowSeconds ?? Math.floor(Date.now() / 1e3);
934
+ const tolerance = opts.toleranceSeconds ?? DEFAULT_TOLERANCE_SECONDS;
935
+ if (Math.abs(now - t) > tolerance) return false;
936
+ let matched = false;
937
+ for (const secret of secrets) {
938
+ const expected = await computeSignatureHex(secret, t, opts.payload);
939
+ for (const candidate of candidates) {
940
+ if (timingSafeHexEquals(expected, candidate)) matched = true;
941
+ }
942
+ }
943
+ return matched;
944
+ }
945
+
946
+ // src/bulk.ts
947
+ function isBulkResult(value) {
948
+ if (typeof value !== "object" || value === null) return false;
949
+ const v = value;
950
+ if (typeof v.total !== "number" || typeof v.succeeded !== "number" || typeof v.failed !== "number") {
951
+ return false;
952
+ }
953
+ if (!Array.isArray(v.items)) return false;
954
+ return v.items.every(
955
+ (item) => typeof item === "object" && item !== null && typeof item.index === "number"
956
+ // 각 요소 index 필수 (BK-04)
957
+ );
958
+ }
959
+ function bulkFailures(result) {
960
+ return result.items.filter((item) => item.code !== void 0);
961
+ }
962
+ function createBulkResultBuilder() {
963
+ let succeeded = 0;
964
+ let failed = 0;
965
+ const items = [];
966
+ const builder = {
967
+ success(index, id) {
968
+ succeeded += 1;
969
+ if (id !== void 0) items.push({ index, id });
970
+ return builder;
971
+ },
972
+ failure(index, code, message) {
973
+ failed += 1;
974
+ items.push({ index, code, message });
975
+ return builder;
976
+ },
977
+ build() {
978
+ return { total: succeeded + failed, succeeded, failed, items: [...items] };
979
+ }
980
+ };
981
+ return builder;
982
+ }
983
+
984
+ // src/upload.ts
985
+ function bytesAt(head, offset, bytes) {
986
+ if (head.length < offset + bytes.length) return false;
987
+ for (let i = 0; i < bytes.length; i++) {
988
+ if (head[offset + i] !== bytes[i]) return false;
989
+ }
990
+ return true;
991
+ }
992
+ var HWP3_SIGNATURE = [..."HWP Document File"].map((ch) => ch.charCodeAt(0));
993
+ var SNIFF_TABLE = [
994
+ { kind: "png", matches: (h) => bytesAt(h, 0, [137, 80, 78, 71, 13, 10, 26, 10]) },
995
+ // 3바이트 — JFIF/EXIF/원시 모두 커버.
996
+ { kind: "jpeg", matches: (h) => bytesAt(h, 0, [255, 216, 255]) },
997
+ // "GIF87a" / "GIF89a"
998
+ {
999
+ kind: "gif",
1000
+ matches: (h) => bytesAt(h, 0, [71, 73, 70, 56, 55, 97]) || bytesAt(h, 0, [71, 73, 70, 56, 57, 97])
1001
+ },
1002
+ // RIFF 컨테이너 — 4~7 바이트(크기)는 임의, 8~11 이 "WEBP" 여야 한다.
1003
+ {
1004
+ kind: "webp",
1005
+ matches: (h) => bytesAt(h, 0, [82, 73, 70, 70]) && bytesAt(h, 8, [87, 69, 66, 80])
1006
+ },
1007
+ // "%PDF-"
1008
+ { kind: "pdf", matches: (h) => bytesAt(h, 0, [37, 80, 68, 70, 45]) },
1009
+ // PK♥♦ 외 2종 — docx/xlsx/pptx/hwpx 도 ZIP (컨테이너 수준 판정).
1010
+ {
1011
+ kind: "zip",
1012
+ matches: (h) => bytesAt(h, 0, [80, 75, 3, 4]) || bytesAt(h, 0, [80, 75, 5, 6]) || bytesAt(h, 0, [80, 75, 7, 8])
1013
+ },
1014
+ // MS 복합문서 — hwp(5.0)/doc/xls/ppt (컨테이너 수준 판정).
1015
+ { kind: "cfbf", matches: (h) => bytesAt(h, 0, [208, 207, 17, 224, 161, 177, 26, 225]) },
1016
+ { kind: "hwp3", matches: (h) => bytesAt(h, 0, HWP3_SIGNATURE) }
1017
+ ];
1018
+ var EXTENSION_KINDS = {
1019
+ png: ["png"],
1020
+ jpg: ["jpeg"],
1021
+ jpeg: ["jpeg"],
1022
+ gif: ["gif"],
1023
+ webp: ["webp"],
1024
+ pdf: ["pdf"],
1025
+ zip: ["zip"],
1026
+ docx: ["zip"],
1027
+ xlsx: ["zip"],
1028
+ pptx: ["zip"],
1029
+ hwpx: ["zip"],
1030
+ doc: ["cfbf"],
1031
+ xls: ["cfbf"],
1032
+ ppt: ["cfbf"],
1033
+ hwp: ["cfbf", "hwp3"]
1034
+ // HWP 5.0 = CFBF 컨테이너, 3.x = 자체 시그니처
1035
+ };
1036
+ function sniffFile(head) {
1037
+ const bytes = head instanceof Uint8Array ? head : new Uint8Array(head);
1038
+ for (const { kind, matches } of SNIFF_TABLE) {
1039
+ if (matches(bytes)) return kind;
1040
+ }
1041
+ return null;
1042
+ }
1043
+ function kindsForExtension(ext) {
1044
+ const normalized = (ext.startsWith(".") ? ext.slice(1) : ext).toLowerCase();
1045
+ return new Set(EXTENSION_KINDS[normalized] ?? []);
1046
+ }
1047
+ function extensionOf(fileName) {
1048
+ const dot = fileName.lastIndexOf(".");
1049
+ if (dot < 0 || dot === fileName.length - 1) return "";
1050
+ return fileName.slice(dot + 1).toLowerCase();
1051
+ }
1052
+ function validateUpload(input) {
1053
+ if (input.size > input.maxSizeBytes) {
1054
+ return { ok: false, reason: "size", message: "\uC5C5\uB85C\uB4DC \uAC00\uB2A5\uD55C \uCD5C\uB300 \uD06C\uAE30\uB97C \uCD08\uACFC\uD588\uC2B5\uB2C8\uB2E4." };
1055
+ }
1056
+ const ext = extensionOf(input.fileName);
1057
+ const allowed = input.allowedExtensions.map(
1058
+ (e) => (e.startsWith(".") ? e.slice(1) : e).toLowerCase()
1059
+ );
1060
+ if (ext === "" || !allowed.includes(ext)) {
1061
+ return { ok: false, reason: "extension", message: `\uD5C8\uC6A9\uB418\uC9C0 \uC54A\uB294 \uD30C\uC77C \uD615\uC2DD\uC785\uB2C8\uB2E4: ${ext}` };
1062
+ }
1063
+ const extra = input.extraMappings?.[ext];
1064
+ const kinds = extra !== void 0 ? new Set(extra) : kindsForExtension(ext);
1065
+ const kind = sniffFile(input.head);
1066
+ if (kind === null || !kinds.has(kind)) {
1067
+ return {
1068
+ ok: false,
1069
+ reason: "content-mismatch",
1070
+ message: "\uD30C\uC77C \uB0B4\uC6A9\uC774 \uD655\uC7A5\uC790\uC640 \uC77C\uCE58\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4."
1071
+ };
1072
+ }
1073
+ return { ok: true };
1074
+ }
1075
+
1076
+ // src/query.ts
1077
+ var RESERVED_FILTER_NAMES = /* @__PURE__ */ new Set(["page", "size", "sort"]);
1078
+ function buildListQuery(options) {
1079
+ const params = new URLSearchParams();
1080
+ if (options.page !== void 0) params.append("page", String(options.page));
1081
+ if (options.size !== void 0) params.append("size", String(options.size));
1082
+ for (const { field, direction } of options.sort ?? []) {
1083
+ params.append("sort", direction === void 0 ? field : `${field},${direction}`);
1084
+ }
1085
+ for (const [name, value] of Object.entries(options.filters ?? {})) {
1086
+ if (RESERVED_FILTER_NAMES.has(name)) {
1087
+ throw new RangeError(`\uC608\uC57D\uB41C \uD30C\uB77C\uBBF8\uD130 \uC774\uB984\uC740 \uD544\uD130\uB85C \uC4F8 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${name}`);
1088
+ }
1089
+ if (value === null || value === void 0) continue;
1090
+ const values = Array.isArray(value) ? value : [value];
1091
+ for (const v of values) params.append(name, String(v));
1092
+ }
1093
+ return params;
1094
+ }
1095
+
1096
+ // src/featureFlags.ts
1097
+ var TRUE_VALUES = /* @__PURE__ */ new Set(["true", "1", "on", "yes"]);
1098
+ function parseFlag(value) {
1099
+ if (typeof value === "boolean") return value;
1100
+ if (value == null) return false;
1101
+ return TRUE_VALUES.has(value.trim().toLowerCase());
1102
+ }
1103
+ function createFeatureFlags(source) {
1104
+ return {
1105
+ isEnabled(key, defaultValue = false) {
1106
+ const value = Object.prototype.hasOwnProperty.call(source, key) ? source[key] : void 0;
1107
+ if (value === void 0) return defaultValue;
1108
+ return parseFlag(value);
1109
+ }
1110
+ };
1111
+ }
1112
+
1113
+ // src/rrn.ts
1114
+ var SEPARATOR_PATTERN2 = /[-\t ]/g;
1115
+ var RRN_DIGITS_PATTERN = /^[0-9]{13}$/;
1116
+ var LEGACY_WEIGHTS = [2, 3, 4, 5, 6, 7, 8, 9, 2, 3, 4, 5];
1117
+ var digitAt2 = (digits, i) => digits.charCodeAt(i) - 48;
1118
+ var isLeapYear = (year) => year % 4 === 0 && year % 100 !== 0 || year % 400 === 0;
1119
+ var DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
1120
+ function centuryOf(genderCode) {
1121
+ if (genderCode === 1 || genderCode === 2 || genderCode === 5 || genderCode === 6) return 1900;
1122
+ if (genderCode === 3 || genderCode === 4 || genderCode === 7 || genderCode === 8) return 2e3;
1123
+ return null;
1124
+ }
1125
+ function parseRrn(value) {
1126
+ if (value == null) return null;
1127
+ const digits = normalizeRrn(value);
1128
+ if (!RRN_DIGITS_PATTERN.test(digits)) return null;
1129
+ const century = centuryOf(digitAt2(digits, 6));
1130
+ if (century === null) return null;
1131
+ const year = century + digitAt2(digits, 0) * 10 + digitAt2(digits, 1);
1132
+ const month = digitAt2(digits, 2) * 10 + digitAt2(digits, 3);
1133
+ const day = digitAt2(digits, 4) * 10 + digitAt2(digits, 5);
1134
+ if (month < 1 || month > 12) return null;
1135
+ const maxDay = month === 2 && isLeapYear(year) ? 29 : DAYS_IN_MONTH[month - 1];
1136
+ if (day < 1 || day > maxDay) return null;
1137
+ return { digits, year, month, day };
1138
+ }
1139
+ function normalizeRrn(value) {
1140
+ if (value == null) return value;
1141
+ return value.replace(SEPARATOR_PATTERN2, "");
1142
+ }
1143
+ function isValidRrn(value) {
1144
+ return parseRrn(value) !== null;
1145
+ }
1146
+ function isForeignerRrn(value) {
1147
+ const parsed = parseRrn(value);
1148
+ if (parsed === null) return false;
1149
+ const genderCode = digitAt2(parsed.digits, 6);
1150
+ return genderCode >= 5 && genderCode <= 8;
1151
+ }
1152
+ function rrnChecksumOkLegacy(value) {
1153
+ const parsed = parseRrn(value);
1154
+ if (parsed === null) return false;
1155
+ const { digits } = parsed;
1156
+ let sum = 0;
1157
+ for (const [i, weight] of LEGACY_WEIGHTS.entries()) sum += digitAt2(digits, i) * weight;
1158
+ let check = (11 - sum % 11) % 10;
1159
+ const genderCode = digitAt2(digits, 6);
1160
+ if (genderCode >= 5 && genderCode <= 8) check = (check + 2) % 10;
1161
+ return check === digitAt2(digits, 12);
1162
+ }
1163
+ function rrnBirthDate(value) {
1164
+ const parsed = parseRrn(value);
1165
+ if (parsed === null) return null;
1166
+ const pad2 = (n) => String(n).padStart(2, "0");
1167
+ return `${parsed.year}-${pad2(parsed.month)}-${pad2(parsed.day)}`;
1168
+ }
1169
+
1170
+ // src/josa.ts
1171
+ var HANGUL_BASE2 = 44032;
1172
+ var HANGUL_END2 = 55203;
1173
+ var JONG_RIEUL = 8;
1174
+ var DIGIT_READINGS = "\uC601\uC77C\uC774\uC0BC\uC0AC\uC624\uC721\uCE60\uD314\uAD6C";
1175
+ var JOSA_TABLE = {
1176
+ "\uC740/\uB294": ["\uC740", "\uB294", "\uC740(\uB294)"],
1177
+ "\uC774/\uAC00": ["\uC774", "\uAC00", "\uC774(\uAC00)"],
1178
+ "\uC744/\uB97C": ["\uC744", "\uB97C", "\uC744(\uB97C)"],
1179
+ "\uACFC/\uC640": ["\uACFC", "\uC640", "\uACFC(\uC640)"],
1180
+ "(\uC73C)\uB85C": ["\uC73C\uB85C", "\uB85C", "(\uC73C)\uB85C"],
1181
+ "\uC544/\uC57C": ["\uC544", "\uC57C", "\uC544(\uC57C)"]
1182
+ };
1183
+ function lastJongOf(word) {
1184
+ let end = word.length;
1185
+ while (end > 0 && (word[end - 1] === " " || word[end - 1] === " ")) end--;
1186
+ if (end === 0) return null;
1187
+ let c = word.codePointAt(end - 1);
1188
+ if (c >= 56320 && c <= 57343 && end >= 2) c = word.codePointAt(end - 2);
1189
+ if (c >= 48 && c <= 57) {
1190
+ c = DIGIT_READINGS.codePointAt(c - 48);
1191
+ }
1192
+ if (c >= HANGUL_BASE2 && c <= HANGUL_END2) return (c - HANGUL_BASE2) % 28;
1193
+ return null;
1194
+ }
1195
+ function pickJosa(word, josa) {
1196
+ const entry = JOSA_TABLE[josa];
1197
+ if (entry === void 0) throw new RangeError(`\uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 \uC870\uC0AC \uC30D\uC785\uB2C8\uB2E4: ${josa}`);
1198
+ const jong = lastJongOf(word);
1199
+ if (jong === null) return entry[2];
1200
+ if (josa === "(\uC73C)\uB85C") return jong === 0 || jong === JONG_RIEUL ? "\uB85C" : "\uC73C\uB85C";
1201
+ return jong > 0 ? entry[0] : entry[1];
1202
+ }
1203
+ function attachJosa(word, josa) {
1204
+ let end = word.length;
1205
+ while (end > 0 && (word[end - 1] === " " || word[end - 1] === " ")) end--;
1206
+ return word.slice(0, end) + pickJosa(word, josa);
1207
+ }
1208
+
1209
+ // src/age.ts
1210
+ var DATE_PATTERN = /^(\d{4})-(\d{2})-(\d{2})$/;
1211
+ var isLeapYear2 = (year) => year % 4 === 0 && year % 100 !== 0 || year % 400 === 0;
1212
+ var DAYS_IN_MONTH2 = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
1213
+ function parseDate(value, label) {
1214
+ const m = typeof value === "string" ? value.match(DATE_PATTERN) : null;
1215
+ if (!m) throw new RangeError(`${label}\uC740(\uB294) "YYYY-MM-DD" \uD615\uC2DD\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4: ${value}`);
1216
+ const year = Number(m[1]);
1217
+ const month = Number(m[2]);
1218
+ const day = Number(m[3]);
1219
+ const maxDay = month >= 1 && month <= 12 ? month === 2 && isLeapYear2(year) ? 29 : DAYS_IN_MONTH2[month - 1] : 0;
1220
+ if (month < 1 || month > 12 || day < 1 || day > maxDay) {
1221
+ throw new RangeError(`\uC2E4\uC874\uD558\uC9C0 \uC54A\uB294 \uB0A0\uC9DC\uC785\uB2C8\uB2E4: ${value}`);
1222
+ }
1223
+ return { year, month, day };
1224
+ }
1225
+ var monthDayBefore = (a, b) => a.month < b.month || a.month === b.month && a.day < b.day;
1226
+ function parsePair(birth, on) {
1227
+ const b = parseDate(birth, "\uC0DD\uC77C");
1228
+ const o = parseDate(on, "\uAE30\uC900\uC77C");
1229
+ if (o.year < b.year || o.year === b.year && (o.month < b.month || o.month === b.month && o.day < b.day)) {
1230
+ throw new RangeError(`\uAE30\uC900\uC77C(${on})\uC774 \uC0DD\uC77C(${birth})\uBCF4\uB2E4 \uC55E\uC124 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4`);
1231
+ }
1232
+ return [b, o];
1233
+ }
1234
+ function ageMan(birth, on) {
1235
+ const [b, o] = parsePair(birth, on);
1236
+ return o.year - b.year - (monthDayBefore(o, b) ? 1 : 0);
1237
+ }
1238
+ function ageByYear(birth, on) {
1239
+ const [b, o] = parsePair(birth, on);
1240
+ return o.year - b.year;
1241
+ }
1242
+ function ageInsurance(birth, on) {
1243
+ const [b, o] = parsePair(birth, on);
1244
+ const months = (o.year - b.year) * 12 + (o.month - b.month) - (o.day < b.day ? 1 : 0);
1245
+ return Math.floor((months + 6) / 12);
1246
+ }
1247
+
1248
+ // src/phone.ts
1249
+ var SEPARATOR_PATTERN3 = /[-\t ().]/g;
1250
+ var AREA_CODES = /* @__PURE__ */ new Set([
1251
+ "031",
1252
+ "032",
1253
+ "033",
1254
+ "041",
1255
+ "042",
1256
+ "043",
1257
+ "044",
1258
+ "051",
1259
+ "052",
1260
+ "053",
1261
+ "054",
1262
+ "055",
1263
+ "061",
1264
+ "062",
1265
+ "063",
1266
+ "064"
1267
+ ]);
1268
+ var MOBILE_PREFIXES = /* @__PURE__ */ new Set(["011", "016", "017", "018", "019"]);
1269
+ var DIGITS_ONLY = /^[0-9]+$/;
1270
+ function normalizePhoneNumber(value) {
1271
+ if (value == null) return value;
1272
+ const stripped = value.replace(SEPARATOR_PATTERN3, "");
1273
+ if (stripped.startsWith("+82")) {
1274
+ const rest = stripped.slice(3);
1275
+ return rest.startsWith("0") ? rest : "0" + rest;
1276
+ }
1277
+ return stripped;
1278
+ }
1279
+ function classifyPhoneNumber(value) {
1280
+ const digits = normalizePhoneNumber(value);
1281
+ if (digits == null || !DIGITS_ONLY.test(digits)) return "unknown";
1282
+ const len = digits.length;
1283
+ const p3 = digits.slice(0, 3);
1284
+ if (p3 === "010") return len === 11 ? "mobile" : "unknown";
1285
+ if (MOBILE_PREFIXES.has(p3)) return len === 10 || len === 11 ? "mobile" : "unknown";
1286
+ if (p3 === "070") return len === 11 ? "voip" : "unknown";
1287
+ if (p3 === "012") return len === 11 || len === 12 ? "m2m" : "unknown";
1288
+ if (/^050[0-9]/.test(digits)) return len === 11 || len === 12 ? "safe" : "unknown";
1289
+ if (digits.startsWith("02")) return len === 9 || len === 10 ? "landline" : "unknown";
1290
+ if (AREA_CODES.has(p3)) return len === 10 || len === 11 ? "landline" : "unknown";
1291
+ return "unknown";
1292
+ }
1293
+ function formatPhoneNumber(value) {
1294
+ const digits = normalizePhoneNumber(value);
1295
+ if (digits == null) return digits;
1296
+ const type = classifyPhoneNumber(digits);
1297
+ if (type === "unknown") return digits;
1298
+ const len = digits.length;
1299
+ if (digits.startsWith("02")) {
1300
+ return `${digits.slice(0, 2)}-${digits.slice(2, len - 4)}-${digits.slice(len - 4)}`;
1301
+ }
1302
+ if (type === "safe") {
1303
+ return `${digits.slice(0, 4)}-${digits.slice(4, len - 4)}-${digits.slice(len - 4)}`;
1304
+ }
1305
+ if (type === "m2m" && len === 12) return digits;
1306
+ return `${digits.slice(0, 3)}-${digits.slice(3, len - 4)}-${digits.slice(len - 4)}`;
1307
+ }
1308
+ function toE164(value) {
1309
+ const digits = normalizePhoneNumber(value);
1310
+ if (digits == null || classifyPhoneNumber(digits) === "unknown") return null;
1311
+ return "+82" + digits.slice(1);
1312
+ }
1313
+
1314
+ // src/money.ts
1315
+ var LIMIT = 10n ** 20n;
1316
+ var TRILLION = 10n ** 12n;
1317
+ var HUNDRED_MILLION = 10n ** 8n;
1318
+ var TEN_THOUSAND = 10n ** 4n;
1319
+ var DIGIT_WORDS = ["", "\uC77C", "\uC774", "\uC0BC", "\uC0AC", "\uC624", "\uC721", "\uCE60", "\uD314", "\uAD6C"];
1320
+ var PLACE_WORDS = ["", "\uC2ED", "\uBC31", "\uCC9C"];
1321
+ var GROUP_WORDS = ["", "\uB9CC", "\uC5B5", "\uC870", "\uACBD"];
1322
+ function toBigInt(amount) {
1323
+ let value;
1324
+ if (typeof amount === "bigint") {
1325
+ value = amount;
1326
+ } else {
1327
+ if (!Number.isSafeInteger(amount)) {
1328
+ throw new RangeError(`\uAE08\uC561\uC740 \uC548\uC804 \uC815\uC218\uC5EC\uC57C \uD569\uB2C8\uB2E4: ${amount}`);
1329
+ }
1330
+ value = BigInt(amount);
1331
+ }
1332
+ if (value >= LIMIT || value <= -LIMIT) {
1333
+ throw new RangeError(`\uC9C0\uC6D0 \uBC94\uC704(|amount| < 10^20)\uB97C \uCD08\uACFC\uD588\uC2B5\uB2C8\uB2E4: ${value}`);
1334
+ }
1335
+ return value;
1336
+ }
1337
+ function groupWords(group, explicitOne) {
1338
+ let out = "";
1339
+ for (let place = 3; place >= 0; place--) {
1340
+ const d = Math.floor(group / 10 ** place) % 10;
1341
+ if (d === 0) continue;
1342
+ if (d === 1 && place > 0 && !explicitOne) {
1343
+ out += PLACE_WORDS[place];
1344
+ } else {
1345
+ out += DIGIT_WORDS[d] + PLACE_WORDS[place];
1346
+ }
1347
+ }
1348
+ return out;
1349
+ }
1350
+ function wordsOfPositive(abs, explicitOne) {
1351
+ const groups = [];
1352
+ let rest = abs;
1353
+ while (rest > 0n) {
1354
+ groups.push(Number(rest % 10000n));
1355
+ rest /= 10000n;
1356
+ }
1357
+ let out = "";
1358
+ for (let gi = groups.length - 1; gi >= 0; gi--) {
1359
+ const group = groups[gi];
1360
+ if (group === 0) continue;
1361
+ const body = group === 1 && gi > 0 ? "\uC77C" : groupWords(group, explicitOne);
1362
+ out += body + GROUP_WORDS[gi];
1363
+ }
1364
+ return out;
1365
+ }
1366
+ var withCommas = (digits) => digits.replace(/\B(?=(\d{3})+$)/g, ",");
1367
+ function toKoreanWords(amount) {
1368
+ const value = toBigInt(amount);
1369
+ if (value === 0n) return "\uC601";
1370
+ if (value < 0n) return "\uB9C8\uC774\uB108\uC2A4 " + wordsOfPositive(-value, false);
1371
+ return wordsOfPositive(value, false);
1372
+ }
1373
+ function toFormalNotation(amount) {
1374
+ const value = toBigInt(amount);
1375
+ if (value < 0n) throw new RangeError(`\uACF5\uBB38\uC11C \uD45C\uAE30\uB294 \uC74C\uC218\uB97C \uC9C0\uC6D0\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4: ${value}`);
1376
+ if (value === 0n) return "\uAE08\uC601\uC6D0\u6574";
1377
+ return "\uAE08" + wordsOfPositive(value, true) + "\uC6D0\u6574";
1378
+ }
1379
+ function scaledOneDecimal(abs, unit) {
1380
+ const intPart = abs / unit;
1381
+ const tenth = abs % unit * 10n / unit;
1382
+ const intStr = withCommas(intPart.toString());
1383
+ return tenth > 0n ? `${intStr}.${tenth}` : intStr;
1384
+ }
1385
+ function abbreviateAmount(amount) {
1386
+ const value = toBigInt(amount);
1387
+ const sign = value < 0n ? "-" : "";
1388
+ const abs = value < 0n ? -value : value;
1389
+ if (abs >= TRILLION) return sign + scaledOneDecimal(abs, TRILLION) + "\uC870";
1390
+ if (abs >= HUNDRED_MILLION) return sign + scaledOneDecimal(abs, HUNDRED_MILLION) + "\uC5B5";
1391
+ if (abs >= TEN_THOUSAND) return sign + withCommas((abs / TEN_THOUSAND).toString()) + "\uB9CC";
1392
+ return sign + withCommas(abs.toString());
1393
+ }
1394
+
1395
+ // src/businessDays.ts
1396
+ var DATE_PATTERN2 = /^(\d{4})-(\d{2})-(\d{2})$/;
1397
+ var MS_PER_DAY = 864e5;
1398
+ var isLeapYear3 = (year) => year % 4 === 0 && year % 100 !== 0 || year % 400 === 0;
1399
+ var DAYS_IN_MONTH3 = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
1400
+ function toEpochDay(value) {
1401
+ const m = typeof value === "string" ? value.match(DATE_PATTERN2) : null;
1402
+ if (!m) throw new RangeError(`\uB0A0\uC9DC\uB294 "YYYY-MM-DD" \uD615\uC2DD\uC774\uC5B4\uC57C \uD569\uB2C8\uB2E4: ${value}`);
1403
+ const year = Number(m[1]);
1404
+ const month = Number(m[2]);
1405
+ const day = Number(m[3]);
1406
+ const maxDay = month >= 1 && month <= 12 ? month === 2 && isLeapYear3(year) ? 29 : DAYS_IN_MONTH3[month - 1] : 0;
1407
+ if (month < 1 || month > 12 || day < 1 || day > maxDay) {
1408
+ throw new RangeError(`\uC2E4\uC874\uD558\uC9C0 \uC54A\uB294 \uB0A0\uC9DC\uC785\uB2C8\uB2E4: ${value}`);
1409
+ }
1410
+ return Date.UTC(year, month - 1, day) / MS_PER_DAY;
1411
+ }
1412
+ function fromEpochDay(epochDay) {
1413
+ const d = new Date(epochDay * MS_PER_DAY);
1414
+ const pad2 = (n) => String(n).padStart(2, "0");
1415
+ return `${d.getUTCFullYear()}-${pad2(d.getUTCMonth() + 1)}-${pad2(d.getUTCDate())}`;
1416
+ }
1417
+ function isWeekend(epochDay) {
1418
+ const dow = ((epochDay + 4) % 7 + 7) % 7;
1419
+ return dow === 0 || dow === 6;
1420
+ }
1421
+ function createBusinessDays(options) {
1422
+ const holidaySet = /* @__PURE__ */ new Set();
1423
+ for (const holiday of options.holidays) holidaySet.add(toEpochDay(holiday));
1424
+ const isBusinessEpochDay = (epochDay) => !isWeekend(epochDay) && !holidaySet.has(epochDay);
1425
+ const stepUntilBusiness = (epochDay, step) => {
1426
+ let day = epochDay + step;
1427
+ while (!isBusinessEpochDay(day)) day += step;
1428
+ return day;
1429
+ };
1430
+ return {
1431
+ isBusinessDay(date) {
1432
+ return isBusinessEpochDay(toEpochDay(date));
1433
+ },
1434
+ addBusinessDays(date, n) {
1435
+ let day = toEpochDay(date);
1436
+ if (!Number.isInteger(n)) throw new RangeError(`n \uC740 \uC815\uC218\uC5EC\uC57C \uD569\uB2C8\uB2E4: ${n}`);
1437
+ const step = n < 0 ? -1 : 1;
1438
+ for (let remaining = Math.abs(n); remaining > 0; remaining--) {
1439
+ day = stepUntilBusiness(day, step);
1440
+ }
1441
+ return fromEpochDay(day);
1442
+ },
1443
+ nextBusinessDay(date) {
1444
+ return fromEpochDay(stepUntilBusiness(toEpochDay(date), 1));
1445
+ },
1446
+ previousBusinessDay(date) {
1447
+ return fromEpochDay(stepUntilBusiness(toEpochDay(date), -1));
1448
+ },
1449
+ countBusinessDays(start, endExclusive) {
1450
+ const startDay = toEpochDay(start);
1451
+ const endDay = toEpochDay(endExclusive);
1452
+ if (startDay > endDay) {
1453
+ throw new RangeError(`start(${start})\uAC00 end(${endExclusive})\uBCF4\uB2E4 \uB4A4\uC77C \uC218 \uC5C6\uC2B5\uB2C8\uB2E4`);
1454
+ }
1455
+ let count = 0;
1456
+ for (let day = startDay; day < endDay; day++) {
1457
+ if (isBusinessEpochDay(day)) count++;
1458
+ }
1459
+ return count;
1460
+ }
1461
+ };
1462
+ }
1463
+
1464
+ // src/jamo.ts
1465
+ var HANGUL_BASE3 = 44032;
1466
+ var HANGUL_END3 = 55203;
1467
+ var CHO_DIV2 = 21 * 28;
1468
+ var COMPAT_CONS_START2 = 12593;
1469
+ var COMPAT_CONS_END2 = 12622;
1470
+ var COMPAT_VOWEL_START2 = 12623;
1471
+ var COMPAT_VOWEL_END2 = 12643;
1472
+ var j = String.fromCharCode;
1473
+ var CHO_CODEPOINTS = [
1474
+ 12593,
1475
+ 12594,
1476
+ 12596,
1477
+ 12599,
1478
+ 12600,
1479
+ 12601,
1480
+ 12609,
1481
+ 12610,
1482
+ 12611,
1483
+ 12613,
1484
+ 12614,
1485
+ 12615,
1486
+ 12616,
1487
+ 12617,
1488
+ 12618,
1489
+ 12619,
1490
+ 12620,
1491
+ 12621,
1492
+ 12622
1493
+ ];
1494
+ var JUNG_DECOMP = [
1495
+ j(12623),
1496
+ j(12624),
1497
+ j(12625),
1498
+ j(12626),
1499
+ j(12627),
1500
+ j(12628),
1501
+ j(12629),
1502
+ j(12630),
1503
+ j(12631),
1504
+ // ㅗ
1505
+ j(12631) + j(12623),
1506
+ // ㅘ→ㅗㅏ
1507
+ j(12631) + j(12624),
1508
+ // ㅙ→ㅗㅐ
1509
+ j(12631) + j(12643),
1510
+ // ㅚ→ㅗㅣ
1511
+ j(12635),
1512
+ // ㅛ
1513
+ j(12636),
1514
+ // ㅜ
1515
+ j(12636) + j(12627),
1516
+ // ㅝ→ㅜㅓ
1517
+ j(12636) + j(12628),
1518
+ // ㅞ→ㅜㅔ
1519
+ j(12636) + j(12643),
1520
+ // ㅟ→ㅜㅣ
1521
+ j(12640),
1522
+ // ㅠ
1523
+ j(12641),
1524
+ // ㅡ
1525
+ j(12641) + j(12643),
1526
+ // ㅢ→ㅡㅣ
1527
+ j(12643)
1528
+ // ㅣ
1529
+ ];
1530
+ var JONG_DECOMP = [
1531
+ "",
1532
+ j(12593),
1533
+ // ㄱ
1534
+ j(12594),
1535
+ // ㄲ (비분해)
1536
+ j(12593) + j(12613),
1537
+ // ㄳ→ㄱㅅ
1538
+ j(12596),
1539
+ // ㄴ
1540
+ j(12596) + j(12616),
1541
+ // ㄵ→ㄴㅈ
1542
+ j(12596) + j(12622),
1543
+ // ㄶ→ㄴㅎ
1544
+ j(12599),
1545
+ // ㄷ
1546
+ j(12601),
1547
+ // ㄹ
1548
+ j(12601) + j(12593),
1549
+ // ㄺ→ㄹㄱ
1550
+ j(12601) + j(12609),
1551
+ // ㄻ→ㄹㅁ
1552
+ j(12601) + j(12610),
1553
+ // ㄼ→ㄹㅂ
1554
+ j(12601) + j(12613),
1555
+ // ㄽ→ㄹㅅ
1556
+ j(12601) + j(12620),
1557
+ // ㄾ→ㄹㅌ
1558
+ j(12601) + j(12621),
1559
+ // ㄿ→ㄹㅍ
1560
+ j(12601) + j(12622),
1561
+ // ㅀ→ㄹㅎ
1562
+ j(12609),
1563
+ // ㅁ
1564
+ j(12610),
1565
+ // ㅂ
1566
+ j(12610) + j(12613),
1567
+ // ㅄ→ㅂㅅ
1568
+ j(12613),
1569
+ // ㅅ
1570
+ j(12614),
1571
+ // ㅆ (비분해)
1572
+ j(12615),
1573
+ // ㅇ
1574
+ j(12616),
1575
+ // ㅈ
1576
+ j(12618),
1577
+ // ㅊ
1578
+ j(12619),
1579
+ // ㅋ
1580
+ j(12620),
1581
+ // ㅌ
1582
+ j(12621),
1583
+ // ㅍ
1584
+ j(12622)
1585
+ // ㅎ
1586
+ ];
1587
+ var CHO_INDEX = new Map(CHO_CODEPOINTS.map((cp, idx) => [cp, idx]));
1588
+ var JONG_INDEX = /* @__PURE__ */ new Map([
1589
+ [12593, 1],
1590
+ [12594, 2],
1591
+ [12595, 3],
1592
+ [12596, 4],
1593
+ [12597, 5],
1594
+ [12598, 6],
1595
+ [12599, 7],
1596
+ [12601, 8],
1597
+ [12602, 9],
1598
+ [12603, 10],
1599
+ [12604, 11],
1600
+ [12605, 12],
1601
+ [12606, 13],
1602
+ [12607, 14],
1603
+ [12608, 15],
1604
+ [12609, 16],
1605
+ [12610, 17],
1606
+ [12612, 18],
1607
+ [12613, 19],
1608
+ [12614, 20],
1609
+ [12615, 21],
1610
+ [12616, 22],
1611
+ [12618, 23],
1612
+ [12619, 24],
1613
+ [12620, 25],
1614
+ [12621, 26],
1615
+ [12622, 27]
1616
+ ]);
1617
+ var VOWEL_COMBINE = /* @__PURE__ */ new Map([
1618
+ [12631 << 16 | 12623, 12632],
1619
+ // ㅗ+ㅏ→ㅘ
1620
+ [12631 << 16 | 12624, 12633],
1621
+ // ㅗ+ㅐ→ㅙ
1622
+ [12631 << 16 | 12643, 12634],
1623
+ // ㅗ+ㅣ→ㅚ
1624
+ [12636 << 16 | 12627, 12637],
1625
+ // ㅜ+ㅓ→ㅝ
1626
+ [12636 << 16 | 12628, 12638],
1627
+ // ㅜ+ㅔ→ㅞ
1628
+ [12636 << 16 | 12643, 12639],
1629
+ // ㅜ+ㅣ→ㅟ
1630
+ [12641 << 16 | 12643, 12642]
1631
+ // ㅡ+ㅣ→ㅢ
1632
+ ]);
1633
+ var JONG_COMBINE = /* @__PURE__ */ new Map([
1634
+ [12593 << 16 | 12613, 12595],
1635
+ // ㄱ+ㅅ→ㄳ
1636
+ [12596 << 16 | 12616, 12597],
1637
+ // ㄴ+ㅈ→ㄵ
1638
+ [12596 << 16 | 12622, 12598],
1639
+ // ㄴ+ㅎ→ㄶ
1640
+ [12601 << 16 | 12593, 12602],
1641
+ // ㄹ+ㄱ→ㄺ
1642
+ [12601 << 16 | 12609, 12603],
1643
+ // ㄹ+ㅁ→ㄻ
1644
+ [12601 << 16 | 12610, 12604],
1645
+ // ㄹ+ㅂ→ㄼ
1646
+ [12601 << 16 | 12613, 12605],
1647
+ // ㄹ+ㅅ→ㄽ
1648
+ [12601 << 16 | 12620, 12606],
1649
+ // ㄹ+ㅌ→ㄾ
1650
+ [12601 << 16 | 12621, 12607],
1651
+ // ㄹ+ㅍ→ㄿ
1652
+ [12601 << 16 | 12622, 12608],
1653
+ // ㄹ+ㅎ→ㅀ
1654
+ [12610 << 16 | 12613, 12612]
1655
+ // ㅂ+ㅅ→ㅄ
1656
+ ]);
1657
+ var isVowelCp = (cp) => cp >= COMPAT_VOWEL_START2 && cp <= COMPAT_VOWEL_END2;
1658
+ function decomposeHangul(s) {
1659
+ if (!s) return "";
1660
+ let out = "";
1661
+ for (const ch of s) {
1662
+ const cp = ch.codePointAt(0);
1663
+ if (cp >= HANGUL_BASE3 && cp <= HANGUL_END3) {
1664
+ const idx = cp - HANGUL_BASE3;
1665
+ out += j(CHO_CODEPOINTS[Math.floor(idx / CHO_DIV2)]);
1666
+ out += JUNG_DECOMP[Math.floor(idx / 28) % 21];
1667
+ out += JONG_DECOMP[idx % 28];
1668
+ } else {
1669
+ out += ch;
1670
+ }
1671
+ }
1672
+ return out;
1673
+ }
1674
+ function composeHangul(s) {
1675
+ const chars = Array.from(s);
1676
+ const n = chars.length;
1677
+ const cpAt = (idx) => idx >= 0 && idx < n ? chars[idx].codePointAt(0) : -1;
1678
+ let out = "";
1679
+ let i = 0;
1680
+ while (i < n) {
1681
+ const choIdx = CHO_INDEX.get(cpAt(i));
1682
+ if (choIdx === void 0 || !isVowelCp(cpAt(i + 1))) {
1683
+ out += chars[i];
1684
+ i++;
1685
+ continue;
1686
+ }
1687
+ i++;
1688
+ let jungCp = cpAt(i);
1689
+ i++;
1690
+ const combinedVowel = VOWEL_COMBINE.get(jungCp << 16 | cpAt(i));
1691
+ if (combinedVowel !== void 0) {
1692
+ jungCp = combinedVowel;
1693
+ i++;
1694
+ }
1695
+ let jongIdx = 0;
1696
+ const jong1Cp = cpAt(i);
1697
+ const jong1Idx = JONG_INDEX.get(jong1Cp);
1698
+ if (jong1Idx !== void 0 && !isVowelCp(cpAt(i + 1))) {
1699
+ jongIdx = jong1Idx;
1700
+ i++;
1701
+ const combinedJong = JONG_COMBINE.get(jong1Cp << 16 | cpAt(i));
1702
+ if (combinedJong !== void 0 && !isVowelCp(cpAt(i + 1))) {
1703
+ jongIdx = JONG_INDEX.get(combinedJong);
1704
+ i++;
1705
+ }
1706
+ }
1707
+ out += j(HANGUL_BASE3 + choIdx * CHO_DIV2 + (jungCp - COMPAT_VOWEL_START2) * 28 + jongIdx);
1708
+ }
1709
+ return out;
1710
+ }
1711
+ function matchesHangul(query, target) {
1712
+ const q = Array.from(query);
1713
+ const t = Array.from(target);
1714
+ if (q.length === 0) return true;
1715
+ if (q.length > t.length) return false;
1716
+ for (let i = 0; i < q.length; i++) {
1717
+ const qc = q[i];
1718
+ const tc = t[i];
1719
+ if (i === q.length - 1) {
1720
+ const qd = decomposeHangul(qc.toLowerCase());
1721
+ const td = decomposeHangul(tc.toLowerCase());
1722
+ if (!td.startsWith(qd)) return false;
1723
+ continue;
1724
+ }
1725
+ if (qc === tc) continue;
1726
+ const qcp = qc.codePointAt(0);
1727
+ const tcp = tc.codePointAt(0);
1728
+ if (qcp >= COMPAT_CONS_START2 && qcp <= COMPAT_CONS_END2 && tcp >= HANGUL_BASE3 && tcp <= HANGUL_END3) {
1729
+ if (qcp === CHO_CODEPOINTS[Math.floor((tcp - HANGUL_BASE3) / CHO_DIV2)]) continue;
1730
+ return false;
1731
+ }
1732
+ if (qc.toLowerCase() === tc.toLowerCase()) continue;
1733
+ return false;
1734
+ }
1735
+ return true;
1736
+ }
828
1737
  // Annotate the CommonJS export names for ESM import in node:
829
1738
  0 && (module.exports = {
830
1739
  ApiError,
831
1740
  BulkheadFullError,
832
1741
  ResultCode,
1742
+ WEBHOOK_SIGNATURE_HEADER,
1743
+ abbreviateAmount,
1744
+ ageByYear,
1745
+ ageInsurance,
1746
+ ageMan,
1747
+ attachJosa,
1748
+ buildListQuery,
1749
+ bulkFailures,
1750
+ classifyPhoneNumber,
1751
+ composeHangul,
833
1752
  createApiClient,
1753
+ createBulkResultBuilder,
834
1754
  createBulkhead,
1755
+ createBusinessDays,
835
1756
  createCircuitBreaker,
1757
+ createFeatureFlags,
836
1758
  createTokenBucket,
837
1759
  createTtlCache,
838
1760
  decodeJwtPayload,
1761
+ decomposeHangul,
1762
+ formatPhoneNumber,
1763
+ generateIdempotencyKey,
839
1764
  getTokenExpiry,
1765
+ isBulkResult,
840
1766
  isChosungQuery,
1767
+ isForeignerRrn,
841
1768
  isRetryableStatus,
842
1769
  isTokenExpired,
843
1770
  isValidBusinessNumber,
844
1771
  isValidCorporateNumber,
1772
+ isValidRrn,
845
1773
  isValidationErrorData,
1774
+ kindsForExtension,
846
1775
  maskCardNumber,
847
1776
  maskEmail,
848
1777
  maskName,
849
1778
  maskPhone,
850
1779
  maskSecret,
1780
+ matchesHangul,
851
1781
  normalizeBusinessNumber,
1782
+ normalizePhoneNumber,
1783
+ normalizeRrn,
1784
+ parseFlag,
852
1785
  parseRetryAfterMs,
853
1786
  parseSseFrame,
854
1787
  parseWireDateTime,
1788
+ pickJosa,
855
1789
  readSseStream,
856
1790
  retry,
1791
+ rrnBirthDate,
1792
+ rrnChecksumOkLegacy,
857
1793
  sanitizeLogValue,
1794
+ signWebhook,
1795
+ sniffFile,
858
1796
  stripZone,
859
1797
  toChosung,
1798
+ toE164,
1799
+ toFormalNotation,
1800
+ toKoreanWords,
860
1801
  toWireDate,
861
- toWireDateTime
1802
+ toWireDateTime,
1803
+ validateUpload,
1804
+ verifyWebhook
862
1805
  });