@jthong/util 0.0.0-beta-20260522002050
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/README.md +71 -0
- package/dist/array/index.cjs +46 -0
- package/dist/array/index.cjs.map +1 -0
- package/dist/array/index.d.ts +91 -0
- package/dist/array/index.d.ts.map +1 -0
- package/dist/array/index.js +40 -0
- package/dist/array/index.js.map +1 -0
- package/dist/fn/index.cjs +81 -0
- package/dist/fn/index.cjs.map +1 -0
- package/dist/fn/index.d.ts +149 -0
- package/dist/fn/index.d.ts.map +1 -0
- package/dist/fn/index.js +73 -0
- package/dist/fn/index.js.map +1 -0
- package/dist/index.cjs +300 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +266 -0
- package/dist/index.js.map +1 -0
- package/dist/object/index.cjs +28 -0
- package/dist/object/index.cjs.map +1 -0
- package/dist/object/index.d.ts +44 -0
- package/dist/object/index.d.ts.map +1 -0
- package/dist/object/index.js +24 -0
- package/dist/object/index.js.map +1 -0
- package/dist/regex/index.cjs +82 -0
- package/dist/regex/index.cjs.map +1 -0
- package/dist/regex/index.d.ts +209 -0
- package/dist/regex/index.d.ts.map +1 -0
- package/dist/regex/index.js +74 -0
- package/dist/regex/index.js.map +1 -0
- package/dist/string/index.cjs +25 -0
- package/dist/string/index.cjs.map +1 -0
- package/dist/string/index.d.ts +94 -0
- package/dist/string/index.d.ts.map +1 -0
- package/dist/string/index.js +18 -0
- package/dist/string/index.js.map +1 -0
- package/package.json +63 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __export = (target, all) => {
|
|
5
|
+
for (var name in all)
|
|
6
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
// src/string/index.ts
|
|
10
|
+
var string_exports = {};
|
|
11
|
+
__export(string_exports, {
|
|
12
|
+
camelToKebab: () => camelToKebab,
|
|
13
|
+
capitalize: () => capitalize,
|
|
14
|
+
formatNumber: () => formatNumber,
|
|
15
|
+
kebabToCamel: () => kebabToCamel,
|
|
16
|
+
mask: () => mask,
|
|
17
|
+
truncate: () => truncate
|
|
18
|
+
});
|
|
19
|
+
var capitalize = (str) => str.length === 0 ? str : str[0].toUpperCase() + str.slice(1);
|
|
20
|
+
var camelToKebab = (str) => str.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`).replace(/^-/, "");
|
|
21
|
+
var kebabToCamel = (str) => str.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
22
|
+
var truncate = (str, max, suffix = "...") => str.length <= max ? str : str.slice(0, Math.max(0, max - suffix.length)) + suffix;
|
|
23
|
+
var formatNumber = (n, locale = "en-US") => new Intl.NumberFormat(locale).format(n);
|
|
24
|
+
var mask = (str, opts = {}) => {
|
|
25
|
+
const start = Math.max(0, opts.start ?? 1);
|
|
26
|
+
const end = Math.max(0, opts.end ?? 0);
|
|
27
|
+
const char = opts.char ?? "*";
|
|
28
|
+
if (str.length <= start + end) return str;
|
|
29
|
+
const masked = char.repeat(str.length - start - end);
|
|
30
|
+
return str.slice(0, start) + masked + str.slice(str.length - end);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// src/array/index.ts
|
|
34
|
+
var array_exports = {};
|
|
35
|
+
__export(array_exports, {
|
|
36
|
+
chunk: () => chunk,
|
|
37
|
+
groupBy: () => groupBy,
|
|
38
|
+
range: () => range,
|
|
39
|
+
sortBy: () => sortBy,
|
|
40
|
+
unique: () => unique
|
|
41
|
+
});
|
|
42
|
+
var chunk = (arr, size) => {
|
|
43
|
+
if (size <= 0) return [];
|
|
44
|
+
const result = [];
|
|
45
|
+
for (let i = 0; i < arr.length; i += size) {
|
|
46
|
+
result.push(arr.slice(i, i + size));
|
|
47
|
+
}
|
|
48
|
+
return result;
|
|
49
|
+
};
|
|
50
|
+
var unique = (arr) => Array.from(new Set(arr));
|
|
51
|
+
var groupBy = (arr, keyFn) => {
|
|
52
|
+
const result = {};
|
|
53
|
+
for (const item of arr) {
|
|
54
|
+
const key = keyFn(item);
|
|
55
|
+
(result[key] ??= []).push(item);
|
|
56
|
+
}
|
|
57
|
+
return result;
|
|
58
|
+
};
|
|
59
|
+
var sortBy = (arr, keyFn, order = "asc") => {
|
|
60
|
+
const sign = order === "asc" ? 1 : -1;
|
|
61
|
+
return [...arr].sort((a, b) => {
|
|
62
|
+
const ka = keyFn(a);
|
|
63
|
+
const kb = keyFn(b);
|
|
64
|
+
if (ka < kb) return -1 * sign;
|
|
65
|
+
if (ka > kb) return 1 * sign;
|
|
66
|
+
return 0;
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
var range = (start, end, step = 1) => {
|
|
70
|
+
const [from, to] = end === void 0 ? [0, start] : [start, end];
|
|
71
|
+
const result = [];
|
|
72
|
+
for (let i = from; step > 0 ? i < to : i > to; i += step) {
|
|
73
|
+
result.push(i);
|
|
74
|
+
}
|
|
75
|
+
return result;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// src/object/index.ts
|
|
79
|
+
var object_exports = {};
|
|
80
|
+
__export(object_exports, {
|
|
81
|
+
isPlainObject: () => isPlainObject,
|
|
82
|
+
omit: () => omit,
|
|
83
|
+
pick: () => pick
|
|
84
|
+
});
|
|
85
|
+
var pick = (obj, keys) => {
|
|
86
|
+
const result = {};
|
|
87
|
+
for (const key of keys) {
|
|
88
|
+
if (key in obj) result[key] = obj[key];
|
|
89
|
+
}
|
|
90
|
+
return result;
|
|
91
|
+
};
|
|
92
|
+
var omit = (obj, keys) => {
|
|
93
|
+
const result = { ...obj };
|
|
94
|
+
for (const key of keys) {
|
|
95
|
+
delete result[key];
|
|
96
|
+
}
|
|
97
|
+
return result;
|
|
98
|
+
};
|
|
99
|
+
var isPlainObject = (value) => {
|
|
100
|
+
if (typeof value !== "object" || value === null) return false;
|
|
101
|
+
const proto = Object.getPrototypeOf(value);
|
|
102
|
+
return proto === null || proto === Object.prototype;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// src/fn/index.ts
|
|
106
|
+
var fn_exports = {};
|
|
107
|
+
__export(fn_exports, {
|
|
108
|
+
debounce: () => debounce,
|
|
109
|
+
memoize: () => memoize,
|
|
110
|
+
once: () => once,
|
|
111
|
+
retry: () => retry,
|
|
112
|
+
sleep: () => sleep,
|
|
113
|
+
throttle: () => throttle,
|
|
114
|
+
withTimeout: () => withTimeout
|
|
115
|
+
});
|
|
116
|
+
var debounce = (fn, ms) => {
|
|
117
|
+
let timer;
|
|
118
|
+
return (...args) => {
|
|
119
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
120
|
+
timer = setTimeout(() => fn(...args), ms);
|
|
121
|
+
};
|
|
122
|
+
};
|
|
123
|
+
var throttle = (fn, ms) => {
|
|
124
|
+
let last = 0;
|
|
125
|
+
return (...args) => {
|
|
126
|
+
const now = Date.now();
|
|
127
|
+
if (now - last >= ms) {
|
|
128
|
+
last = now;
|
|
129
|
+
fn(...args);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
};
|
|
133
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
134
|
+
var once = (fn) => {
|
|
135
|
+
let called = false;
|
|
136
|
+
let result;
|
|
137
|
+
return (...args) => {
|
|
138
|
+
if (!called) {
|
|
139
|
+
called = true;
|
|
140
|
+
result = fn(...args);
|
|
141
|
+
}
|
|
142
|
+
return result;
|
|
143
|
+
};
|
|
144
|
+
};
|
|
145
|
+
var memoize = (fn, keyFn = (...args) => JSON.stringify(args)) => {
|
|
146
|
+
const cache = /* @__PURE__ */ new Map();
|
|
147
|
+
return (...args) => {
|
|
148
|
+
const key = keyFn(...args);
|
|
149
|
+
const cached = cache.get(key);
|
|
150
|
+
if (cached !== void 0 || cache.has(key)) return cached;
|
|
151
|
+
const result = fn(...args);
|
|
152
|
+
cache.set(key, result);
|
|
153
|
+
return result;
|
|
154
|
+
};
|
|
155
|
+
};
|
|
156
|
+
var retry = async (fn, opts = {}) => {
|
|
157
|
+
const { retries = 3, delay = 100, backoff = 2, onError } = opts;
|
|
158
|
+
let lastError;
|
|
159
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
160
|
+
try {
|
|
161
|
+
return await fn();
|
|
162
|
+
} catch (err) {
|
|
163
|
+
lastError = err;
|
|
164
|
+
onError?.(err, attempt);
|
|
165
|
+
if (attempt < retries) {
|
|
166
|
+
await sleep(delay * Math.pow(backoff, attempt));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
throw lastError;
|
|
171
|
+
};
|
|
172
|
+
var withTimeout = (promise, ms, errorMessage = "Operation timed out") => {
|
|
173
|
+
let timeoutId;
|
|
174
|
+
const timeout = new Promise((_, reject) => {
|
|
175
|
+
timeoutId = setTimeout(() => reject(new Error(errorMessage)), ms);
|
|
176
|
+
});
|
|
177
|
+
return Promise.race([
|
|
178
|
+
promise.finally(() => {
|
|
179
|
+
if (timeoutId !== void 0) clearTimeout(timeoutId);
|
|
180
|
+
}),
|
|
181
|
+
timeout
|
|
182
|
+
]);
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
// src/regex/index.ts
|
|
186
|
+
var regex_exports = {};
|
|
187
|
+
__export(regex_exports, {
|
|
188
|
+
COMMON: () => COMMON,
|
|
189
|
+
KR: () => KR,
|
|
190
|
+
REGEX: () => REGEX,
|
|
191
|
+
createRegexHelper: () => createRegexHelper,
|
|
192
|
+
format: () => format,
|
|
193
|
+
matches: () => matches,
|
|
194
|
+
regexFor: () => regexFor
|
|
195
|
+
});
|
|
196
|
+
var COMMON = {
|
|
197
|
+
/** 단순한 이메일 검증 (공백 없는 local@domain.tld). */
|
|
198
|
+
EMAIL: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
|
|
199
|
+
/** http/https URL. */
|
|
200
|
+
URL: /^https?:\/\/[^\s/$.?#][^\s]*$/i,
|
|
201
|
+
/** UUID v1~v5. */
|
|
202
|
+
UUID: /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,
|
|
203
|
+
/** 영문 + 숫자만 (특수문자/공백 불가). */
|
|
204
|
+
ALPHANUMERIC: /^[a-zA-Z0-9]+$/
|
|
205
|
+
};
|
|
206
|
+
var KR = {
|
|
207
|
+
/** 한글 이름 2~17자. */
|
|
208
|
+
NAME: /^[가-힣]{2,17}$/,
|
|
209
|
+
/** 휴대폰번호 (하이픈 포함). */
|
|
210
|
+
PHONE: /^(01[016-9])-(\d{3,4})-(\d{4})$/,
|
|
211
|
+
/** 휴대폰번호 (하이픈 없음, 숫자만). */
|
|
212
|
+
PHONE_DIGITS: /^(01[016-9])(\d{3,4})(\d{4})$/,
|
|
213
|
+
/** 지역 유선번호 (하이픈 포함). */
|
|
214
|
+
REGION_TEL: /^(0(?:2|3[1-3]|4[1-4]|5[1-5]|6[1-4]))-(\d{3,4})-(\d{4})$/,
|
|
215
|
+
/** 지역 유선번호 (하이픈 없음). */
|
|
216
|
+
REGION_TEL_DIGITS: /^(0(?:2|3[1-3]|4[1-4]|5[1-5]|6[1-4]))(\d{3,4})(\d{4})$/,
|
|
217
|
+
/** 인터넷 전화 070 (하이픈 포함). */
|
|
218
|
+
INTERNET_TEL: /^(070)-(\d{3,4})-(\d{4})$/,
|
|
219
|
+
/** 인터넷 전화 070 (하이픈 없음). */
|
|
220
|
+
INTERNET_TEL_DIGITS: /^(070)(\d{3,4})(\d{4})$/,
|
|
221
|
+
/** 안심번호 050X (하이픈 포함). */
|
|
222
|
+
SAFETY_TEL: /^(050[1-9])-(\d{3,4})-(\d{4})$/,
|
|
223
|
+
/** 안심번호 050X (하이픈 없음). */
|
|
224
|
+
SAFETY_TEL_DIGITS: /^(050[1-9])(\d{3,4})(\d{4})$/
|
|
225
|
+
};
|
|
226
|
+
var REGEX = {
|
|
227
|
+
COMMON,
|
|
228
|
+
KR
|
|
229
|
+
};
|
|
230
|
+
var regexFor = (registry, path) => {
|
|
231
|
+
const segments = path.split(".");
|
|
232
|
+
let current = registry;
|
|
233
|
+
for (const segment of segments) {
|
|
234
|
+
if (current === null || typeof current !== "object" || current instanceof RegExp) {
|
|
235
|
+
throw new Error(
|
|
236
|
+
`regexFor: cannot traverse path "${path}" \u2014 "${segment}" is not a navigable object`
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
current = current[segment];
|
|
240
|
+
if (current === void 0) {
|
|
241
|
+
throw new Error(
|
|
242
|
+
`regexFor: path "${path}" not found \u2014 missing segment "${segment}"`
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if (!(current instanceof RegExp)) {
|
|
247
|
+
throw new Error(
|
|
248
|
+
`regexFor: path "${path}" does not resolve to a RegExp (got ${typeof current})`
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
return current;
|
|
252
|
+
};
|
|
253
|
+
var matches = (registry, path, input) => regexFor(registry, path).test(input);
|
|
254
|
+
var format = (registry, path, input, replacement) => {
|
|
255
|
+
const regex = regexFor(registry, path);
|
|
256
|
+
if (!regex.test(input)) return null;
|
|
257
|
+
return typeof replacement === "string" ? input.replace(regex, replacement) : input.replace(regex, replacement);
|
|
258
|
+
};
|
|
259
|
+
var createRegexHelper = (registry) => ({
|
|
260
|
+
registry,
|
|
261
|
+
regexFor: (path) => regexFor(registry, path),
|
|
262
|
+
matches: (path, input) => matches(registry, path, input),
|
|
263
|
+
format: (path, input, replacement) => format(registry, path, input, replacement)
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
exports.COMMON = COMMON;
|
|
267
|
+
exports.KR = KR;
|
|
268
|
+
exports.REGEX = REGEX;
|
|
269
|
+
exports.array = array_exports;
|
|
270
|
+
exports.camelToKebab = camelToKebab;
|
|
271
|
+
exports.capitalize = capitalize;
|
|
272
|
+
exports.chunk = chunk;
|
|
273
|
+
exports.createRegexHelper = createRegexHelper;
|
|
274
|
+
exports.debounce = debounce;
|
|
275
|
+
exports.fn = fn_exports;
|
|
276
|
+
exports.format = format;
|
|
277
|
+
exports.formatNumber = formatNumber;
|
|
278
|
+
exports.groupBy = groupBy;
|
|
279
|
+
exports.isPlainObject = isPlainObject;
|
|
280
|
+
exports.kebabToCamel = kebabToCamel;
|
|
281
|
+
exports.mask = mask;
|
|
282
|
+
exports.matches = matches;
|
|
283
|
+
exports.memoize = memoize;
|
|
284
|
+
exports.object = object_exports;
|
|
285
|
+
exports.omit = omit;
|
|
286
|
+
exports.once = once;
|
|
287
|
+
exports.pick = pick;
|
|
288
|
+
exports.range = range;
|
|
289
|
+
exports.regex = regex_exports;
|
|
290
|
+
exports.regexFor = regexFor;
|
|
291
|
+
exports.retry = retry;
|
|
292
|
+
exports.sleep = sleep;
|
|
293
|
+
exports.sortBy = sortBy;
|
|
294
|
+
exports.string = string_exports;
|
|
295
|
+
exports.throttle = throttle;
|
|
296
|
+
exports.truncate = truncate;
|
|
297
|
+
exports.unique = unique;
|
|
298
|
+
exports.withTimeout = withTimeout;
|
|
299
|
+
//# sourceMappingURL=index.cjs.map
|
|
300
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/string/index.ts","../src/array/index.ts","../src/object/index.ts","../src/fn/index.ts","../src/regex/index.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * 문자열의 첫 글자를 대문자로 변환합니다.\n *\n * @param str - 변환할 문자열\n * @returns 첫 글자가 대문자가 된 문자열. 빈 문자열은 그대로 반환됩니다.\n * @example\n * ```ts\n * capitalize(\"hello\"); // \"Hello\"\n * capitalize(\"\"); // \"\"\n * ```\n */\nexport const capitalize = (str: string): string =>\n str.length === 0 ? str : str[0]!.toUpperCase() + str.slice(1);\n\n/**\n * camelCase 문자열을 kebab-case로 변환합니다.\n *\n * @param str - camelCase 문자열\n * @returns kebab-case로 변환된 문자열\n * @example\n * ```ts\n * camelToKebab(\"backgroundColor\"); // \"background-color\"\n * camelToKebab(\"HTMLParser\"); // \"h-t-m-l-parser\"\n * ```\n */\nexport const camelToKebab = (str: string): string =>\n str.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`).replace(/^-/, \"\");\n\n/**\n * kebab-case 문자열을 camelCase로 변환합니다.\n *\n * @param str - kebab-case 문자열\n * @returns camelCase로 변환된 문자열\n * @example\n * ```ts\n * kebabToCamel(\"background-color\"); // \"backgroundColor\"\n * kebabToCamel(\"data-test-id\"); // \"dataTestId\"\n * ```\n */\nexport const kebabToCamel = (str: string): string =>\n str.replace(/-([a-z])/g, (_, c: string) => c.toUpperCase());\n\n/**\n * 문자열이 최대 길이를 초과하면 잘라내고 접미사를 붙입니다.\n *\n * @param str - 원본 문자열\n * @param max - 결과의 최대 길이 (접미사 포함)\n * @param suffix - 잘렸을 때 붙일 접미사 (기본값 `\"...\"`)\n * @returns 최대 길이 이하면 원본, 초과하면 잘린 문자열 + 접미사\n * @example\n * ```ts\n * truncate(\"hello world\", 8); // \"hello...\"\n * truncate(\"hi\", 5); // \"hi\" (변경 없음)\n * truncate(\"hello world\", 8, \"…\"); // \"hello w…\"\n * ```\n */\nexport const truncate = (str: string, max: number, suffix = \"...\"): string =>\n str.length <= max ? str : str.slice(0, Math.max(0, max - suffix.length)) + suffix;\n\n/**\n * 숫자에 천 단위 구분자를 추가합니다. `Intl.NumberFormat` 기반이라 음수/소수/로케일을 모두 지원합니다.\n *\n * @param n - 포맷할 숫자\n * @param locale - BCP 47 로케일 태그 (기본값 `\"en-US\"`)\n * @returns 천 단위 구분자가 들어간 문자열\n * @example\n * ```ts\n * formatNumber(1234567); // \"1,234,567\"\n * formatNumber(-1234.56); // \"-1,234.56\"\n * formatNumber(1234567, \"de-DE\"); // \"1.234.567\" (독일은 점이 구분자)\n * ```\n */\nexport const formatNumber = (n: number, locale = \"en-US\"): string =>\n new Intl.NumberFormat(locale).format(n);\n\n/**\n * {@link mask} 함수의 옵션.\n */\nexport interface MaskOptions {\n /** 앞에서 보존할 문자 수. 기본값 `1`. */\n start?: number;\n /** 뒤에서 보존할 문자 수. 기본값 `0`. */\n end?: number;\n /** 가릴 때 사용할 문자. 기본값 `\"*\"`. */\n char?: string;\n}\n\n/**\n * 문자열의 중간 일부를 마스킹 문자로 가립니다. 이메일/전화번호 등 민감 정보 표시에 유용합니다.\n *\n * 문자열이 `start + end`보다 짧으면 원본을 그대로 반환합니다.\n *\n * @param str - 원본 문자열\n * @param opts - 마스킹 옵션 ({@link MaskOptions} 참고)\n * @returns 마스킹된 문자열\n * @example\n * ```ts\n * mask(\"01012345678\", { start: 3, end: 4 }); // \"010****5678\"\n * mask(\"jthong@gmail.com\", { start: 1, end: 10 }); // \"j*****@gmail.com\"\n * mask(\"secret\", { start: 0, end: 0, char: \"#\" }); // \"######\"\n * mask(\"hi\", { start: 1, end: 1 }); // \"hi\" (너무 짧아 변경 없음)\n * ```\n */\nexport const mask = (str: string, opts: MaskOptions = {}): string => {\n const start = Math.max(0, opts.start ?? 1);\n const end = Math.max(0, opts.end ?? 0);\n const char = opts.char ?? \"*\";\n if (str.length <= start + end) return str;\n const masked = char.repeat(str.length - start - end);\n return str.slice(0, start) + masked + str.slice(str.length - end);\n};\n","/**\n * 배열을 지정한 크기로 분할합니다. 마지막 청크는 크기보다 작을 수 있습니다.\n *\n * @param arr - 분할할 배열\n * @param size - 각 청크의 크기 (양수)\n * @returns 청크들의 배열. `size <= 0`이면 빈 배열을 반환합니다.\n * @example\n * ```ts\n * chunk([1, 2, 3, 4, 5], 2); // [[1, 2], [3, 4], [5]]\n * chunk([1, 2, 3], 5); // [[1, 2, 3]]\n * chunk([1, 2, 3], 0); // []\n * ```\n */\nexport const chunk = <T>(arr: readonly T[], size: number): T[][] => {\n if (size <= 0) return [];\n const result: T[][] = [];\n for (let i = 0; i < arr.length; i += size) {\n result.push(arr.slice(i, i + size));\n }\n return result;\n};\n\n/**\n * 배열에서 중복 요소를 제거합니다. 등호 비교는 `Set`을 사용하므로 원시값/참조값 모두에 동작하지만,\n * 객체는 참조 동일성으로만 비교됩니다.\n *\n * @param arr - 원본 배열\n * @returns 중복이 제거된 새 배열 (입력 순서 유지)\n * @example\n * ```ts\n * unique([1, 2, 2, 3, 1]); // [1, 2, 3]\n * unique([\"a\", \"b\", \"a\"]); // [\"a\", \"b\"]\n * ```\n */\nexport const unique = <T>(arr: readonly T[]): T[] => Array.from(new Set(arr));\n\n/**\n * 키 함수가 반환한 값으로 배열을 그룹화합니다.\n *\n * @param arr - 그룹화할 배열\n * @param keyFn - 각 요소에서 그룹 키를 추출하는 함수\n * @returns 키별로 묶인 객체. 키는 `keyFn`의 반환값.\n * @example\n * ```ts\n * const users = [\n * { name: \"Alice\", role: \"admin\" },\n * { name: \"Bob\", role: \"user\" },\n * { name: \"Carol\", role: \"admin\" },\n * ];\n * groupBy(users, (u) => u.role);\n * // {\n * // admin: [{ name: \"Alice\", role: \"admin\" }, { name: \"Carol\", role: \"admin\" }],\n * // user: [{ name: \"Bob\", role: \"user\" }],\n * // }\n * ```\n */\nexport const groupBy = <T, K extends PropertyKey>(\n arr: readonly T[],\n keyFn: (item: T) => K,\n): Record<K, T[]> => {\n const result = {} as Record<K, T[]>;\n for (const item of arr) {\n const key = keyFn(item);\n (result[key] ??= []).push(item);\n }\n return result;\n};\n\ntype Comparable = string | number | bigint | boolean | Date;\n\n/**\n * 키 함수가 반환한 값을 기준으로 배열을 정렬합니다. **입력 배열은 변경되지 않습니다** (새 배열 반환).\n *\n * @param arr - 정렬할 배열\n * @param keyFn - 정렬 기준 값을 추출하는 함수 (`string` | `number` | `bigint` | `boolean` | `Date` 반환)\n * @param order - `\"asc\"` (오름차순, 기본값) 또는 `\"desc\"` (내림차순)\n * @returns 정렬된 새 배열\n * @example\n * ```ts\n * sortBy([{ n: 3 }, { n: 1 }, { n: 2 }], (i) => i.n);\n * // [{ n: 1 }, { n: 2 }, { n: 3 }]\n *\n * sortBy([\"banana\", \"apple\", \"cherry\"], (s) => s);\n * // [\"apple\", \"banana\", \"cherry\"]\n *\n * sortBy([1, 3, 2], (n) => n, \"desc\");\n * // [3, 2, 1]\n * ```\n */\nexport const sortBy = <T>(\n arr: readonly T[],\n keyFn: (item: T) => Comparable,\n order: \"asc\" | \"desc\" = \"asc\",\n): T[] => {\n const sign = order === \"asc\" ? 1 : -1;\n return [...arr].sort((a, b) => {\n const ka = keyFn(a);\n const kb = keyFn(b);\n if (ka < kb) return -1 * sign;\n if (ka > kb) return 1 * sign;\n return 0;\n });\n};\n\n/**\n * 정수 범위 배열을 생성합니다. Python의 `range()`와 같은 시맨틱.\n *\n * - 인자 1개: `[0, start)` 범위\n * - 인자 2개: `[start, end)` 범위\n * - 인자 3개: `[start, end)` 범위, `step` 간격\n *\n * @param start - 시작값 (또는 종료값, 인자 1개일 때)\n * @param end - 종료값 (배제, exclusive)\n * @param step - 증가폭. 기본값 `1`. 음수면 감소.\n * @returns 정수 배열\n * @example\n * ```ts\n * range(3); // [0, 1, 2]\n * range(2, 5); // [2, 3, 4]\n * range(0, 10, 3); // [0, 3, 6, 9]\n * range(5, 0, -1); // [5, 4, 3, 2, 1]\n * ```\n */\nexport const range = (start: number, end?: number, step = 1): number[] => {\n const [from, to] = end === undefined ? [0, start] : [start, end];\n const result: number[] = [];\n for (let i = from; step > 0 ? i < to : i > to; i += step) {\n result.push(i);\n }\n return result;\n};\n","/**\n * 객체에서 지정한 키만 추려낸 새 객체를 만듭니다. 원본은 변경되지 않습니다.\n *\n * @param obj - 원본 객체\n * @param keys - 보존할 키들의 배열\n * @returns `keys`에 명시된 속성만 가진 새 객체\n * @example\n * ```ts\n * pick({ a: 1, b: 2, c: 3 }, [\"a\", \"c\"]); // { a: 1, c: 3 }\n * ```\n */\nexport const pick = <T extends object, K extends keyof T>(\n obj: T,\n keys: readonly K[],\n): Pick<T, K> => {\n const result = {} as Pick<T, K>;\n for (const key of keys) {\n if (key in obj) result[key] = obj[key];\n }\n return result;\n};\n\n/**\n * 객체에서 지정한 키를 제외한 새 객체를 만듭니다. 원본은 변경되지 않습니다.\n *\n * @param obj - 원본 객체\n * @param keys - 제외할 키들의 배열\n * @returns `keys`에 명시된 속성을 뺀 새 객체\n * @example\n * ```ts\n * omit({ a: 1, b: 2, c: 3 }, [\"b\"]); // { a: 1, c: 3 }\n * ```\n */\nexport const omit = <T extends object, K extends keyof T>(\n obj: T,\n keys: readonly K[],\n): Omit<T, K> => {\n const result = { ...obj };\n for (const key of keys) {\n delete result[key];\n }\n return result;\n};\n\n/**\n * 값이 **순수 객체**(plain object)인지 판별합니다.\n * `null`, 배열, 클래스 인스턴스, `Date` 등은 모두 `false`를 반환합니다.\n *\n * SSR/Node 환경에서도 안전하게 동작합니다 (브라우저 API 의존성 없음).\n *\n * @param value - 검사할 값\n * @returns `{}` 또는 `Object.create(null)`로 만든 객체면 `true`\n * @example\n * ```ts\n * isPlainObject({}); // true\n * isPlainObject({ a: 1 }); // true\n * isPlainObject(Object.create(null)); // true\n * isPlainObject([]); // false\n * isPlainObject(new Date()); // false\n * isPlainObject(null); // false\n * ```\n */\nexport const isPlainObject = (value: unknown): value is Record<string, unknown> => {\n if (typeof value !== \"object\" || value === null) return false;\n const proto = Object.getPrototypeOf(value);\n return proto === null || proto === Object.prototype;\n};\n","/**\n * 함수 호출을 지연시켜, 마지막 호출 후 `ms` 밀리초가 지나야 실제로 실행되도록 합니다.\n * 검색 입력, 윈도우 리사이즈 등 \"연속 입력이 멈춘 뒤\"에 한 번만 실행하고 싶을 때 사용합니다.\n *\n * @param fn - 디바운스할 함수\n * @param ms - 대기 시간 (밀리초)\n * @returns 디바운스가 적용된 새 함수\n * @example\n * ```ts\n * const onResize = debounce(() => console.log(\"resized\"), 200);\n * window.addEventListener(\"resize\", onResize);\n * // 사용자가 리사이즈를 멈춘 뒤 200ms 후에 한 번만 출력\n * ```\n */\nexport const debounce = <Args extends unknown[]>(\n fn: (...args: Args) => void,\n ms: number,\n): ((...args: Args) => void) => {\n let timer: ReturnType<typeof setTimeout> | undefined;\n return (...args: Args) => {\n if (timer !== undefined) clearTimeout(timer);\n timer = setTimeout(() => fn(...args), ms);\n };\n};\n\n/**\n * `ms` 밀리초마다 최대 한 번씩만 함수가 실행되도록 제한합니다.\n * 스크롤/마우스 무브 등 너무 자주 호출되는 이벤트의 빈도를 줄일 때 사용합니다.\n *\n * @param fn - 스로틀할 함수\n * @param ms - 최소 호출 간격 (밀리초)\n * @returns 스로틀이 적용된 새 함수\n * @example\n * ```ts\n * const onScroll = throttle(() => console.log(\"scroll\"), 100);\n * window.addEventListener(\"scroll\", onScroll);\n * // 100ms에 최대 1번만 출력\n * ```\n */\nexport const throttle = <Args extends unknown[]>(\n fn: (...args: Args) => void,\n ms: number,\n): ((...args: Args) => void) => {\n let last = 0;\n return (...args: Args) => {\n const now = Date.now();\n if (now - last >= ms) {\n last = now;\n fn(...args);\n }\n };\n};\n\n/**\n * 지정한 시간만큼 대기하는 Promise를 반환합니다.\n *\n * @param ms - 대기 시간 (밀리초)\n * @returns `ms` 이후 resolve되는 Promise\n * @example\n * ```ts\n * async function task() {\n * console.log(\"start\");\n * await sleep(1000);\n * console.log(\"1초 뒤\");\n * }\n * ```\n */\nexport const sleep = (ms: number): Promise<void> =>\n new Promise((resolve) => setTimeout(resolve, ms));\n\n/**\n * 첫 호출의 결과를 저장하고, 이후 호출 시 인자에 관계없이 같은 결과를 반환합니다.\n * 초기화 함수나 싱글톤 생성 등 \"한 번만 실행되어야 하는 작업\"에 유용합니다.\n *\n * @param fn - 한 번만 실행할 함수\n * @returns 첫 호출 결과를 캐싱하는 래퍼 함수\n * @example\n * ```ts\n * const init = once(() => {\n * console.log(\"initialized\");\n * return { ready: true };\n * });\n *\n * init(); // \"initialized\" 출력 후 { ready: true } 반환\n * init(); // 출력 없음, 같은 객체 반환\n * ```\n */\nexport const once = <Args extends unknown[], R>(\n fn: (...args: Args) => R,\n): ((...args: Args) => R) => {\n let called = false;\n let result: R;\n return (...args: Args): R => {\n if (!called) {\n called = true;\n result = fn(...args);\n }\n return result;\n };\n};\n\n/**\n * 함수의 결과를 인자별로 캐싱합니다. 동일한 인자로 다시 호출되면 캐시된 결과를 반환합니다.\n *\n * 기본 캐시 키는 `JSON.stringify(args)`로 생성되므로 객체 인자도 값 비교가 가능합니다.\n * 필요하면 `keyFn`을 직접 지정해 키 생성 로직을 커스터마이즈하세요.\n *\n * @param fn - 결과를 캐싱할 함수\n * @param keyFn - 인자에서 캐시 키를 만드는 함수 (기본값: `JSON.stringify(args)`)\n * @returns 결과 캐싱이 적용된 래퍼 함수\n * @example\n * ```ts\n * const slow = memoize((n: number) => {\n * console.log(\"compute\", n);\n * return n * 2;\n * });\n *\n * slow(2); // \"compute 2\" 출력 후 4\n * slow(2); // 출력 없음, 4\n * slow(3); // \"compute 3\" 출력 후 6\n *\n * // 커스텀 키: 객체 id 기준 캐싱\n * const byId = memoize(\n * (user: { id: number; name: string }) => fetchProfile(user.id),\n * (user) => String(user.id),\n * );\n * ```\n */\nexport const memoize = <Args extends unknown[], R>(\n fn: (...args: Args) => R,\n keyFn: (...args: Args) => string = (...args) => JSON.stringify(args),\n): ((...args: Args) => R) => {\n const cache = new Map<string, R>();\n return (...args: Args): R => {\n const key = keyFn(...args);\n const cached = cache.get(key);\n if (cached !== undefined || cache.has(key)) return cached as R;\n const result = fn(...args);\n cache.set(key, result);\n return result;\n };\n};\n\n/**\n * {@link retry} 함수의 옵션.\n */\nexport interface RetryOptions {\n /** 추가 재시도 횟수. 총 시도 횟수는 `retries + 1`. 기본값 `3`. */\n retries?: number;\n /** 첫 재시도 전 대기 시간(ms). 기본값 `100`. */\n delay?: number;\n /** 매 재시도마다 대기 시간에 곱해질 배수 (지수 백오프). 기본값 `2`. */\n backoff?: number;\n /** 매 실패마다 호출되는 콜백. `attempt`는 0부터 시작하는 시도 인덱스. */\n onError?: (error: unknown, attempt: number) => void;\n}\n\n/**\n * 함수를 호출하다가 예외가 나면 지수 백오프로 재시도합니다. 동기/비동기 함수 모두 지원합니다.\n *\n * 모든 시도가 실패하면 마지막 에러를 throw합니다.\n *\n * @param fn - 실행할 함수 (동기 또는 비동기)\n * @param opts - 재시도 옵션 ({@link RetryOptions} 참고)\n * @returns 함수의 결과를 담은 Promise\n * @example\n * ```ts\n * // 기본 옵션: 최대 3회 재시도, 100ms부터 2배씩 백오프 (100, 200, 400ms)\n * const data = await retry(() => fetch(\"/api/data\").then((r) => r.json()));\n *\n * // 커스텀 옵션\n * await retry(unstableTask, {\n * retries: 5,\n * delay: 200,\n * backoff: 1.5,\n * onError: (err, attempt) => console.warn(`시도 ${attempt + 1} 실패`, err),\n * });\n * ```\n */\nexport const retry = async <T>(\n fn: () => Promise<T> | T,\n opts: RetryOptions = {},\n): Promise<T> => {\n const { retries = 3, delay = 100, backoff = 2, onError } = opts;\n let lastError: unknown;\n for (let attempt = 0; attempt <= retries; attempt++) {\n try {\n return await fn();\n } catch (err) {\n lastError = err;\n onError?.(err, attempt);\n if (attempt < retries) {\n await sleep(delay * Math.pow(backoff, attempt));\n }\n }\n }\n throw lastError;\n};\n\n/**\n * Promise에 타임아웃을 부여합니다. 지정한 시간 안에 settle되지 않으면 reject됩니다.\n *\n * 원본 Promise가 먼저 끝나면 내부 타이머는 자동으로 해제되므로 타이머 누수가 없습니다.\n *\n * @param promise - 타임아웃을 걸 Promise\n * @param ms - 제한 시간 (밀리초)\n * @param errorMessage - 타임아웃 시 던질 에러 메시지 (기본값 `\"Operation timed out\"`)\n * @returns 원본 결과 또는 타임아웃 에러로 settle되는 Promise\n * @example\n * ```ts\n * try {\n * const data = await withTimeout(fetch(\"/slow-api\"), 3000);\n * } catch (err) {\n * // 3초 안에 안 오면 여기로 떨어짐\n * }\n *\n * await withTimeout(longTask(), 5000, \"작업이 너무 오래 걸립니다\");\n * ```\n */\nexport const withTimeout = <T>(\n promise: Promise<T>,\n ms: number,\n errorMessage = \"Operation timed out\",\n): Promise<T> => {\n let timeoutId: ReturnType<typeof setTimeout> | undefined;\n const timeout = new Promise<never>((_, reject) => {\n timeoutId = setTimeout(() => reject(new Error(errorMessage)), ms);\n });\n return Promise.race([\n promise.finally(() => {\n if (timeoutId !== undefined) clearTimeout(timeoutId);\n }),\n timeout,\n ]);\n};\n","/**\n * 사용자 정의 정규식 맵 타입. 키 K들의 union으로 `{ [key]: RegExp }` 형태를 만듭니다.\n *\n * @example\n * ```ts\n * type MyKeys = \"POSTAL_CODE\" | \"BUSINESS_NUMBER\";\n * const myRegex: CustomRegexMap<MyKeys> = {\n * POSTAL_CODE: /^\\d{5}$/,\n * BUSINESS_NUMBER: /^\\d{3}-\\d{2}-\\d{5}$/,\n * };\n * ```\n */\nexport type CustomRegexMap<K extends string> = {\n [key in K]: RegExp;\n};\n\n/**\n * 정규식 registry의 모양. 임의 깊이로 중첩 가능합니다.\n * 각 leaf는 `RegExp`, 중간 노드는 동일한 형태의 객체입니다.\n */\nexport type RegexRegistry = {\n readonly [key: string]: RegExp | RegexRegistry;\n};\n\n/**\n * registry에서 RegExp까지의 경로를 dot-notation 문자열 union으로 만듭니다.\n *\n * 모던 TS는 재귀 깊이를 자체적으로 안전하게 다루므로, 깊이 제한 튜플 트릭 없이도 동작합니다.\n *\n * @example\n * ```ts\n * const R = { KR: { PHONE: /.../ }, COMMON: { EMAIL: /.../ } } as const;\n * type P = RegexPath<typeof R>; // \"KR.PHONE\" | \"COMMON.EMAIL\"\n * ```\n */\nexport type RegexPath<T> = T extends RegExp\n ? never\n : T extends object\n ? {\n [K in keyof T & string]: T[K] extends RegExp\n ? K\n : T[K] extends object\n ? `${K}.${RegexPath<T[K]> & string}`\n : never;\n }[keyof T & string]\n : never;\n\n/**\n * `String.replace`의 두 번째 인자 타입. 문자열 패턴 또는 함수 replacer.\n *\n * 함수 replacer는 전체 매치 문자열과 각 capture group을 인자로 받습니다\n * (이어서 offset, 원본 문자열, named groups 객체가 옵니다).\n * 네이티브 `String.prototype.replace`의 lib 시그니처와 동일하게 맞춰 두었습니다.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type RegexReplacer = string | ((substring: string, ...args: any[]) => string);\n\n// ─────────────────────────────────────────────\n// Built-in registry\n// ─────────────────────────────────────────────\n\n/**\n * 모든 서비스/지역 공통 정규식.\n */\nexport const COMMON = {\n /** 단순한 이메일 검증 (공백 없는 local@domain.tld). */\n EMAIL: /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/,\n /** http/https URL. */\n URL: /^https?:\\/\\/[^\\s/$.?#][^\\s]*$/i,\n /** UUID v1~v5. */\n UUID: /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,\n /** 영문 + 숫자만 (특수문자/공백 불가). */\n ALPHANUMERIC: /^[a-zA-Z0-9]+$/,\n} as const;\n\n/**\n * 한국 환경에서 자주 쓰는 정규식.\n *\n * - 휴대폰 접두사는 010/011/016/017/018/019 만 유효.\n * - `_DIGITS` 접미 패턴은 하이픈이 없는 숫자 11자리 형식.\n */\nexport const KR = {\n /** 한글 이름 2~17자. */\n NAME: /^[가-힣]{2,17}$/,\n /** 휴대폰번호 (하이픈 포함). */\n PHONE: /^(01[016-9])-(\\d{3,4})-(\\d{4})$/,\n /** 휴대폰번호 (하이픈 없음, 숫자만). */\n PHONE_DIGITS: /^(01[016-9])(\\d{3,4})(\\d{4})$/,\n /** 지역 유선번호 (하이픈 포함). */\n REGION_TEL: /^(0(?:2|3[1-3]|4[1-4]|5[1-5]|6[1-4]))-(\\d{3,4})-(\\d{4})$/,\n /** 지역 유선번호 (하이픈 없음). */\n REGION_TEL_DIGITS: /^(0(?:2|3[1-3]|4[1-4]|5[1-5]|6[1-4]))(\\d{3,4})(\\d{4})$/,\n /** 인터넷 전화 070 (하이픈 포함). */\n INTERNET_TEL: /^(070)-(\\d{3,4})-(\\d{4})$/,\n /** 인터넷 전화 070 (하이픈 없음). */\n INTERNET_TEL_DIGITS: /^(070)(\\d{3,4})(\\d{4})$/,\n /** 안심번호 050X (하이픈 포함). */\n SAFETY_TEL: /^(050[1-9])-(\\d{3,4})-(\\d{4})$/,\n /** 안심번호 050X (하이픈 없음). */\n SAFETY_TEL_DIGITS: /^(050[1-9])(\\d{3,4})(\\d{4})$/,\n} as const;\n\n/**\n * 기본 제공 정규식 registry. `RegexRegistry`를 만족합니다.\n *\n * 사용자가 자신의 패턴을 추가하려면 스프레드로 확장하세요:\n * ```ts\n * const MY_REGEX = {\n * ...REGEX,\n * MY_DOMAIN: { POSTAL: /^\\d{5}$/ },\n * } as const;\n * ```\n */\nexport const REGEX = {\n COMMON,\n KR,\n} as const;\n\n// ─────────────────────────────────────────────\n// Low-level helpers\n// ─────────────────────────────────────────────\n\n/**\n * registry에서 dot-notation 경로로 `RegExp`를 꺼냅니다.\n *\n * 경로가 존재하지 않거나 RegExp가 아닌 곳을 가리키면 에러를 throw합니다.\n *\n * @param registry - 검색할 정규식 registry\n * @param path - 점으로 구분된 경로 (예: `\"KR.PHONE\"`)\n * @returns 경로가 가리키는 `RegExp`\n * @throws 경로가 잘못되었거나 RegExp가 아닌 경우\n * @example\n * ```ts\n * regexFor(REGEX, \"KR.PHONE\"); // /^(01[016-9])-(\\d{3,4})-(\\d{4})$/\n * ```\n */\nexport const regexFor = <T extends RegexRegistry>(\n registry: T,\n path: RegexPath<T>,\n): RegExp => {\n const segments = (path as string).split(\".\");\n let current: unknown = registry;\n for (const segment of segments) {\n if (current === null || typeof current !== \"object\" || current instanceof RegExp) {\n throw new Error(\n `regexFor: cannot traverse path \"${path as string}\" — \"${segment}\" is not a navigable object`,\n );\n }\n current = (current as Record<string, unknown>)[segment];\n if (current === undefined) {\n throw new Error(\n `regexFor: path \"${path as string}\" not found — missing segment \"${segment}\"`,\n );\n }\n }\n if (!(current instanceof RegExp)) {\n throw new Error(\n `regexFor: path \"${path as string}\" does not resolve to a RegExp (got ${typeof current})`,\n );\n }\n return current;\n};\n\n/**\n * 입력 문자열이 경로의 정규식과 매치되는지 검사합니다.\n *\n * @param registry - 정규식 registry\n * @param path - 경로\n * @param input - 검사할 문자열\n * @returns 매치 여부\n * @example\n * ```ts\n * matches(REGEX, \"KR.PHONE\", \"010-1234-5678\"); // true\n * matches(REGEX, \"KR.PHONE\", \"010-12-5678\"); // false\n * ```\n */\nexport const matches = <T extends RegexRegistry>(\n registry: T,\n path: RegexPath<T>,\n input: string,\n): boolean => regexFor(registry, path).test(input);\n\n/**\n * 입력 문자열을 경로의 정규식으로 치환합니다. 매치되지 않으면 `null`을 반환합니다.\n *\n * 원본 코드와 달리 매치 실패 시 입력 그대로 반환하지 않고 `null`을 돌려주므로, 호출 측이 실패를 명시적으로 처리할 수 있습니다.\n *\n * @param registry - 정규식 registry\n * @param path - 경로\n * @param input - 변환할 문자열\n * @param replacement - 치환 패턴 (문자열 또는 함수, `String.replace`와 동일)\n * @returns 치환된 문자열, 매치 안 되면 `null`\n * @example\n * ```ts\n * format(REGEX, \"KR.PHONE_DIGITS\", \"01012345678\", \"$1-$2-$3\");\n * // \"010-1234-5678\"\n *\n * format(REGEX, \"KR.PHONE_DIGITS\", \"abc\", \"$1-$2-$3\");\n * // null\n * ```\n */\nexport const format = <T extends RegexRegistry>(\n registry: T,\n path: RegexPath<T>,\n input: string,\n replacement: RegexReplacer,\n): string | null => {\n const regex = regexFor(registry, path);\n if (!regex.test(input)) return null;\n return typeof replacement === \"string\"\n ? input.replace(regex, replacement)\n : input.replace(regex, replacement);\n};\n\n// ─────────────────────────────────────────────\n// Ergonomic factory\n// ─────────────────────────────────────────────\n\n/**\n * {@link createRegexHelper}로 만들어지는 헬퍼의 메서드 모음.\n */\nexport interface RegexHelper<T extends RegexRegistry> {\n /** 경로로 RegExp 조회. {@link regexFor}와 동일. */\n regexFor: (path: RegexPath<T>) => RegExp;\n /** 입력이 경로의 정규식과 매치되는지. {@link matches}와 동일. */\n matches: (path: RegexPath<T>, input: string) => boolean;\n /** 정규식으로 치환. {@link format}와 동일, 매치 안 되면 `null`. */\n format: (\n path: RegexPath<T>,\n input: string,\n replacement: RegexReplacer,\n ) => string | null;\n /** 원본 registry 참조 (불변). */\n readonly registry: T;\n}\n\n/**\n * registry에 묶인 헬퍼 객체를 만듭니다. 매번 registry를 인자로 넘기는 번거로움을 없애는 용도.\n *\n * @param registry - 정규식 registry (`REGEX` 또는 커스텀)\n * @returns registry에 바인딩된 헬퍼\n * @example\n * ```ts\n * import { REGEX, createRegexHelper } from \"@jthong/util/regex\";\n *\n * const r = createRegexHelper(REGEX);\n * r.matches(\"KR.PHONE\", \"010-1234-5678\"); // true\n * r.format(\"KR.PHONE_DIGITS\", \"01012345678\", \"$1-$2-$3\"); // \"010-1234-5678\"\n *\n * // 커스텀 registry\n * const MY = { CUSTOM: { ZIP: /^\\d{5}$/ } } as const;\n * const my = createRegexHelper(MY);\n * my.matches(\"CUSTOM.ZIP\", \"06234\"); // true\n * ```\n */\nexport const createRegexHelper = <T extends RegexRegistry>(\n registry: T,\n): RegexHelper<T> => ({\n registry,\n regexFor: (path) => regexFor(registry, path),\n matches: (path, input) => matches(registry, path, input),\n format: (path, input, replacement) => format(registry, path, input, replacement),\n});\n"]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export * from "./string/index.js";
|
|
2
|
+
export * from "./array/index.js";
|
|
3
|
+
export * from "./object/index.js";
|
|
4
|
+
export * from "./fn/index.js";
|
|
5
|
+
export * from "./regex/index.js";
|
|
6
|
+
import * as string from "./string/index.js";
|
|
7
|
+
import * as array from "./array/index.js";
|
|
8
|
+
import * as object from "./object/index.js";
|
|
9
|
+
import * as fn from "./fn/index.js";
|
|
10
|
+
import * as regex from "./regex/index.js";
|
|
11
|
+
export { string, array, object, fn, regex };
|
|
12
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,kBAAkB,CAAC;AACjC,cAAc,mBAAmB,CAAC;AAClC,cAAc,eAAe,CAAC;AAC9B,cAAc,kBAAkB,CAAC;AAEjC,OAAO,KAAK,MAAM,MAAM,mBAAmB,CAAC;AAC5C,OAAO,KAAK,KAAK,MAAM,kBAAkB,CAAC;AAC1C,OAAO,KAAK,MAAM,MAAM,mBAAmB,CAAC;AAC5C,OAAO,KAAK,EAAE,MAAM,eAAe,CAAC;AACpC,OAAO,KAAK,KAAK,MAAM,kBAAkB,CAAC;AAE1C,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __export = (target, all) => {
|
|
3
|
+
for (var name in all)
|
|
4
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
// src/string/index.ts
|
|
8
|
+
var string_exports = {};
|
|
9
|
+
__export(string_exports, {
|
|
10
|
+
camelToKebab: () => camelToKebab,
|
|
11
|
+
capitalize: () => capitalize,
|
|
12
|
+
formatNumber: () => formatNumber,
|
|
13
|
+
kebabToCamel: () => kebabToCamel,
|
|
14
|
+
mask: () => mask,
|
|
15
|
+
truncate: () => truncate
|
|
16
|
+
});
|
|
17
|
+
var capitalize = (str) => str.length === 0 ? str : str[0].toUpperCase() + str.slice(1);
|
|
18
|
+
var camelToKebab = (str) => str.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`).replace(/^-/, "");
|
|
19
|
+
var kebabToCamel = (str) => str.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
20
|
+
var truncate = (str, max, suffix = "...") => str.length <= max ? str : str.slice(0, Math.max(0, max - suffix.length)) + suffix;
|
|
21
|
+
var formatNumber = (n, locale = "en-US") => new Intl.NumberFormat(locale).format(n);
|
|
22
|
+
var mask = (str, opts = {}) => {
|
|
23
|
+
const start = Math.max(0, opts.start ?? 1);
|
|
24
|
+
const end = Math.max(0, opts.end ?? 0);
|
|
25
|
+
const char = opts.char ?? "*";
|
|
26
|
+
if (str.length <= start + end) return str;
|
|
27
|
+
const masked = char.repeat(str.length - start - end);
|
|
28
|
+
return str.slice(0, start) + masked + str.slice(str.length - end);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// src/array/index.ts
|
|
32
|
+
var array_exports = {};
|
|
33
|
+
__export(array_exports, {
|
|
34
|
+
chunk: () => chunk,
|
|
35
|
+
groupBy: () => groupBy,
|
|
36
|
+
range: () => range,
|
|
37
|
+
sortBy: () => sortBy,
|
|
38
|
+
unique: () => unique
|
|
39
|
+
});
|
|
40
|
+
var chunk = (arr, size) => {
|
|
41
|
+
if (size <= 0) return [];
|
|
42
|
+
const result = [];
|
|
43
|
+
for (let i = 0; i < arr.length; i += size) {
|
|
44
|
+
result.push(arr.slice(i, i + size));
|
|
45
|
+
}
|
|
46
|
+
return result;
|
|
47
|
+
};
|
|
48
|
+
var unique = (arr) => Array.from(new Set(arr));
|
|
49
|
+
var groupBy = (arr, keyFn) => {
|
|
50
|
+
const result = {};
|
|
51
|
+
for (const item of arr) {
|
|
52
|
+
const key = keyFn(item);
|
|
53
|
+
(result[key] ??= []).push(item);
|
|
54
|
+
}
|
|
55
|
+
return result;
|
|
56
|
+
};
|
|
57
|
+
var sortBy = (arr, keyFn, order = "asc") => {
|
|
58
|
+
const sign = order === "asc" ? 1 : -1;
|
|
59
|
+
return [...arr].sort((a, b) => {
|
|
60
|
+
const ka = keyFn(a);
|
|
61
|
+
const kb = keyFn(b);
|
|
62
|
+
if (ka < kb) return -1 * sign;
|
|
63
|
+
if (ka > kb) return 1 * sign;
|
|
64
|
+
return 0;
|
|
65
|
+
});
|
|
66
|
+
};
|
|
67
|
+
var range = (start, end, step = 1) => {
|
|
68
|
+
const [from, to] = end === void 0 ? [0, start] : [start, end];
|
|
69
|
+
const result = [];
|
|
70
|
+
for (let i = from; step > 0 ? i < to : i > to; i += step) {
|
|
71
|
+
result.push(i);
|
|
72
|
+
}
|
|
73
|
+
return result;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// src/object/index.ts
|
|
77
|
+
var object_exports = {};
|
|
78
|
+
__export(object_exports, {
|
|
79
|
+
isPlainObject: () => isPlainObject,
|
|
80
|
+
omit: () => omit,
|
|
81
|
+
pick: () => pick
|
|
82
|
+
});
|
|
83
|
+
var pick = (obj, keys) => {
|
|
84
|
+
const result = {};
|
|
85
|
+
for (const key of keys) {
|
|
86
|
+
if (key in obj) result[key] = obj[key];
|
|
87
|
+
}
|
|
88
|
+
return result;
|
|
89
|
+
};
|
|
90
|
+
var omit = (obj, keys) => {
|
|
91
|
+
const result = { ...obj };
|
|
92
|
+
for (const key of keys) {
|
|
93
|
+
delete result[key];
|
|
94
|
+
}
|
|
95
|
+
return result;
|
|
96
|
+
};
|
|
97
|
+
var isPlainObject = (value) => {
|
|
98
|
+
if (typeof value !== "object" || value === null) return false;
|
|
99
|
+
const proto = Object.getPrototypeOf(value);
|
|
100
|
+
return proto === null || proto === Object.prototype;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// src/fn/index.ts
|
|
104
|
+
var fn_exports = {};
|
|
105
|
+
__export(fn_exports, {
|
|
106
|
+
debounce: () => debounce,
|
|
107
|
+
memoize: () => memoize,
|
|
108
|
+
once: () => once,
|
|
109
|
+
retry: () => retry,
|
|
110
|
+
sleep: () => sleep,
|
|
111
|
+
throttle: () => throttle,
|
|
112
|
+
withTimeout: () => withTimeout
|
|
113
|
+
});
|
|
114
|
+
var debounce = (fn, ms) => {
|
|
115
|
+
let timer;
|
|
116
|
+
return (...args) => {
|
|
117
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
118
|
+
timer = setTimeout(() => fn(...args), ms);
|
|
119
|
+
};
|
|
120
|
+
};
|
|
121
|
+
var throttle = (fn, ms) => {
|
|
122
|
+
let last = 0;
|
|
123
|
+
return (...args) => {
|
|
124
|
+
const now = Date.now();
|
|
125
|
+
if (now - last >= ms) {
|
|
126
|
+
last = now;
|
|
127
|
+
fn(...args);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
};
|
|
131
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
132
|
+
var once = (fn) => {
|
|
133
|
+
let called = false;
|
|
134
|
+
let result;
|
|
135
|
+
return (...args) => {
|
|
136
|
+
if (!called) {
|
|
137
|
+
called = true;
|
|
138
|
+
result = fn(...args);
|
|
139
|
+
}
|
|
140
|
+
return result;
|
|
141
|
+
};
|
|
142
|
+
};
|
|
143
|
+
var memoize = (fn, keyFn = (...args) => JSON.stringify(args)) => {
|
|
144
|
+
const cache = /* @__PURE__ */ new Map();
|
|
145
|
+
return (...args) => {
|
|
146
|
+
const key = keyFn(...args);
|
|
147
|
+
const cached = cache.get(key);
|
|
148
|
+
if (cached !== void 0 || cache.has(key)) return cached;
|
|
149
|
+
const result = fn(...args);
|
|
150
|
+
cache.set(key, result);
|
|
151
|
+
return result;
|
|
152
|
+
};
|
|
153
|
+
};
|
|
154
|
+
var retry = async (fn, opts = {}) => {
|
|
155
|
+
const { retries = 3, delay = 100, backoff = 2, onError } = opts;
|
|
156
|
+
let lastError;
|
|
157
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
158
|
+
try {
|
|
159
|
+
return await fn();
|
|
160
|
+
} catch (err) {
|
|
161
|
+
lastError = err;
|
|
162
|
+
onError?.(err, attempt);
|
|
163
|
+
if (attempt < retries) {
|
|
164
|
+
await sleep(delay * Math.pow(backoff, attempt));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
throw lastError;
|
|
169
|
+
};
|
|
170
|
+
var withTimeout = (promise, ms, errorMessage = "Operation timed out") => {
|
|
171
|
+
let timeoutId;
|
|
172
|
+
const timeout = new Promise((_, reject) => {
|
|
173
|
+
timeoutId = setTimeout(() => reject(new Error(errorMessage)), ms);
|
|
174
|
+
});
|
|
175
|
+
return Promise.race([
|
|
176
|
+
promise.finally(() => {
|
|
177
|
+
if (timeoutId !== void 0) clearTimeout(timeoutId);
|
|
178
|
+
}),
|
|
179
|
+
timeout
|
|
180
|
+
]);
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
// src/regex/index.ts
|
|
184
|
+
var regex_exports = {};
|
|
185
|
+
__export(regex_exports, {
|
|
186
|
+
COMMON: () => COMMON,
|
|
187
|
+
KR: () => KR,
|
|
188
|
+
REGEX: () => REGEX,
|
|
189
|
+
createRegexHelper: () => createRegexHelper,
|
|
190
|
+
format: () => format,
|
|
191
|
+
matches: () => matches,
|
|
192
|
+
regexFor: () => regexFor
|
|
193
|
+
});
|
|
194
|
+
var COMMON = {
|
|
195
|
+
/** 단순한 이메일 검증 (공백 없는 local@domain.tld). */
|
|
196
|
+
EMAIL: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
|
|
197
|
+
/** http/https URL. */
|
|
198
|
+
URL: /^https?:\/\/[^\s/$.?#][^\s]*$/i,
|
|
199
|
+
/** UUID v1~v5. */
|
|
200
|
+
UUID: /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,
|
|
201
|
+
/** 영문 + 숫자만 (특수문자/공백 불가). */
|
|
202
|
+
ALPHANUMERIC: /^[a-zA-Z0-9]+$/
|
|
203
|
+
};
|
|
204
|
+
var KR = {
|
|
205
|
+
/** 한글 이름 2~17자. */
|
|
206
|
+
NAME: /^[가-힣]{2,17}$/,
|
|
207
|
+
/** 휴대폰번호 (하이픈 포함). */
|
|
208
|
+
PHONE: /^(01[016-9])-(\d{3,4})-(\d{4})$/,
|
|
209
|
+
/** 휴대폰번호 (하이픈 없음, 숫자만). */
|
|
210
|
+
PHONE_DIGITS: /^(01[016-9])(\d{3,4})(\d{4})$/,
|
|
211
|
+
/** 지역 유선번호 (하이픈 포함). */
|
|
212
|
+
REGION_TEL: /^(0(?:2|3[1-3]|4[1-4]|5[1-5]|6[1-4]))-(\d{3,4})-(\d{4})$/,
|
|
213
|
+
/** 지역 유선번호 (하이픈 없음). */
|
|
214
|
+
REGION_TEL_DIGITS: /^(0(?:2|3[1-3]|4[1-4]|5[1-5]|6[1-4]))(\d{3,4})(\d{4})$/,
|
|
215
|
+
/** 인터넷 전화 070 (하이픈 포함). */
|
|
216
|
+
INTERNET_TEL: /^(070)-(\d{3,4})-(\d{4})$/,
|
|
217
|
+
/** 인터넷 전화 070 (하이픈 없음). */
|
|
218
|
+
INTERNET_TEL_DIGITS: /^(070)(\d{3,4})(\d{4})$/,
|
|
219
|
+
/** 안심번호 050X (하이픈 포함). */
|
|
220
|
+
SAFETY_TEL: /^(050[1-9])-(\d{3,4})-(\d{4})$/,
|
|
221
|
+
/** 안심번호 050X (하이픈 없음). */
|
|
222
|
+
SAFETY_TEL_DIGITS: /^(050[1-9])(\d{3,4})(\d{4})$/
|
|
223
|
+
};
|
|
224
|
+
var REGEX = {
|
|
225
|
+
COMMON,
|
|
226
|
+
KR
|
|
227
|
+
};
|
|
228
|
+
var regexFor = (registry, path) => {
|
|
229
|
+
const segments = path.split(".");
|
|
230
|
+
let current = registry;
|
|
231
|
+
for (const segment of segments) {
|
|
232
|
+
if (current === null || typeof current !== "object" || current instanceof RegExp) {
|
|
233
|
+
throw new Error(
|
|
234
|
+
`regexFor: cannot traverse path "${path}" \u2014 "${segment}" is not a navigable object`
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
current = current[segment];
|
|
238
|
+
if (current === void 0) {
|
|
239
|
+
throw new Error(
|
|
240
|
+
`regexFor: path "${path}" not found \u2014 missing segment "${segment}"`
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (!(current instanceof RegExp)) {
|
|
245
|
+
throw new Error(
|
|
246
|
+
`regexFor: path "${path}" does not resolve to a RegExp (got ${typeof current})`
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
return current;
|
|
250
|
+
};
|
|
251
|
+
var matches = (registry, path, input) => regexFor(registry, path).test(input);
|
|
252
|
+
var format = (registry, path, input, replacement) => {
|
|
253
|
+
const regex = regexFor(registry, path);
|
|
254
|
+
if (!regex.test(input)) return null;
|
|
255
|
+
return typeof replacement === "string" ? input.replace(regex, replacement) : input.replace(regex, replacement);
|
|
256
|
+
};
|
|
257
|
+
var createRegexHelper = (registry) => ({
|
|
258
|
+
registry,
|
|
259
|
+
regexFor: (path) => regexFor(registry, path),
|
|
260
|
+
matches: (path, input) => matches(registry, path, input),
|
|
261
|
+
format: (path, input, replacement) => format(registry, path, input, replacement)
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
export { COMMON, KR, REGEX, array_exports as array, camelToKebab, capitalize, chunk, createRegexHelper, debounce, fn_exports as fn, format, formatNumber, groupBy, isPlainObject, kebabToCamel, mask, matches, memoize, object_exports as object, omit, once, pick, range, regex_exports as regex, regexFor, retry, sleep, sortBy, string_exports as string, throttle, truncate, unique, withTimeout };
|
|
265
|
+
//# sourceMappingURL=index.js.map
|
|
266
|
+
//# sourceMappingURL=index.js.map
|