@simplysm/core-common 13.0.28 → 13.0.29
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/errors/sd-error.d.ts.map +1 -1
- package/dist/errors/sd-error.js +4 -1
- package/dist/errors/sd-error.js.map +1 -1
- package/dist/errors/timeout-error.d.ts.map +1 -1
- package/dist/errors/timeout-error.js.map +1 -1
- package/dist/extensions/arr-ext.d.ts +1 -1
- package/dist/extensions/arr-ext.d.ts.map +1 -1
- package/dist/extensions/arr-ext.helpers.d.ts.map +1 -1
- package/dist/extensions/arr-ext.helpers.js +4 -1
- package/dist/extensions/arr-ext.helpers.js.map +1 -1
- package/dist/extensions/arr-ext.js +15 -5
- package/dist/extensions/arr-ext.js.map +1 -1
- package/dist/extensions/arr-ext.types.d.ts.map +1 -1
- package/dist/features/event-emitter.d.ts.map +1 -1
- package/dist/features/event-emitter.js.map +1 -1
- package/dist/types/date-only.d.ts.map +1 -1
- package/dist/types/date-only.js +15 -5
- package/dist/types/date-only.js.map +1 -1
- package/dist/types/date-time.d.ts.map +1 -1
- package/dist/types/date-time.js +69 -9
- package/dist/types/date-time.js.map +1 -1
- package/dist/types/time.d.ts.map +1 -1
- package/dist/types/time.js +12 -2
- package/dist/types/time.js.map +1 -1
- package/dist/types/uuid.d.ts.map +1 -1
- package/dist/types/uuid.js +4 -1
- package/dist/types/uuid.js.map +1 -1
- package/dist/utils/bytes.d.ts.map +1 -1
- package/dist/utils/bytes.js +3 -1
- package/dist/utils/bytes.js.map +1 -1
- package/dist/utils/date-format.d.ts.map +1 -1
- package/dist/utils/date-format.js.map +1 -1
- package/dist/utils/json.d.ts.map +1 -1
- package/dist/utils/json.js +3 -1
- package/dist/utils/json.js.map +1 -1
- package/dist/utils/num.d.ts.map +1 -1
- package/dist/utils/num.js.map +1 -1
- package/dist/utils/obj.d.ts.map +1 -1
- package/dist/utils/obj.js +19 -5
- package/dist/utils/obj.js.map +1 -1
- package/dist/utils/str.d.ts.map +1 -1
- package/dist/utils/str.js.map +1 -1
- package/dist/utils/transferable.d.ts.map +1 -1
- package/dist/utils/transferable.js +18 -4
- package/dist/utils/transferable.js.map +1 -1
- package/dist/zip/sd-zip.d.ts.map +1 -1
- package/dist/zip/sd-zip.js +7 -1
- package/dist/zip/sd-zip.js.map +1 -1
- package/package.json +2 -2
- package/src/errors/sd-error.ts +11 -2
- package/src/errors/timeout-error.ts +3 -1
- package/src/extensions/arr-ext.helpers.ts +4 -1
- package/src/extensions/arr-ext.ts +48 -15
- package/src/extensions/arr-ext.types.ts +26 -8
- package/src/features/event-emitter.ts +7 -2
- package/src/types/date-only.ts +19 -6
- package/src/types/date-time.ts +70 -9
- package/src/types/time.ts +14 -3
- package/src/types/uuid.ts +5 -2
- package/src/utils/bytes.ts +3 -1
- package/src/utils/date-format.ts +4 -2
- package/src/utils/json.ts +7 -2
- package/src/utils/num.ts +8 -2
- package/src/utils/obj.ts +44 -11
- package/src/utils/str.ts +13 -4
- package/src/utils/transferable.ts +20 -5
- package/src/zip/sd-zip.ts +13 -3
|
@@ -246,7 +246,10 @@ const arrayReadonlyExtensions: ReadonlyArrayExt<any> & ThisType<any[]> = {
|
|
|
246
246
|
return result;
|
|
247
247
|
},
|
|
248
248
|
|
|
249
|
-
toMapValues<T, K, V>(
|
|
249
|
+
toMapValues<T, K, V>(
|
|
250
|
+
keySelector: (item: T, index: number) => K,
|
|
251
|
+
valueSelector: (items: T[]) => V,
|
|
252
|
+
): Map<K, V | T> {
|
|
250
253
|
const itemsMap = new Map<K, T[]>();
|
|
251
254
|
|
|
252
255
|
for (let i = 0; i < this.length; i++) {
|
|
@@ -304,7 +307,9 @@ const arrayReadonlyExtensions: ReadonlyArrayExt<any> & ThisType<any[]> = {
|
|
|
304
307
|
return fn(rootItems);
|
|
305
308
|
},
|
|
306
309
|
|
|
307
|
-
distinct<T>(
|
|
310
|
+
distinct<T>(
|
|
311
|
+
options?: boolean | { matchAddress?: boolean; keyFn?: (item: T) => string | number },
|
|
312
|
+
): T[] {
|
|
308
313
|
// 옵션 정규화
|
|
309
314
|
const opts = typeof options === "boolean" ? { matchAddress: options } : (options ?? {});
|
|
310
315
|
|
|
@@ -367,7 +372,9 @@ const arrayReadonlyExtensions: ReadonlyArrayExt<any> & ThisType<any[]> = {
|
|
|
367
372
|
return result;
|
|
368
373
|
},
|
|
369
374
|
|
|
370
|
-
orderBy<T>(
|
|
375
|
+
orderBy<T>(
|
|
376
|
+
selector?: (item: T) => string | number | DateTime | DateOnly | Time | undefined,
|
|
377
|
+
): T[] {
|
|
371
378
|
return [...this].sort((p, n) => {
|
|
372
379
|
const pp = selector == null ? p : selector(p);
|
|
373
380
|
const pn = selector == null ? n : selector(n);
|
|
@@ -375,7 +382,9 @@ const arrayReadonlyExtensions: ReadonlyArrayExt<any> & ThisType<any[]> = {
|
|
|
375
382
|
});
|
|
376
383
|
},
|
|
377
384
|
|
|
378
|
-
orderByDesc<T>(
|
|
385
|
+
orderByDesc<T>(
|
|
386
|
+
selector?: (item: T) => string | number | DateTime | DateOnly | Time | undefined,
|
|
387
|
+
): T[] {
|
|
379
388
|
return [...this].sort((p, n) => {
|
|
380
389
|
const pp = selector == null ? p : selector(p);
|
|
381
390
|
const pn = selector == null ? n : selector(n);
|
|
@@ -403,7 +412,9 @@ const arrayReadonlyExtensions: ReadonlyArrayExt<any> & ThisType<any[]> = {
|
|
|
403
412
|
|
|
404
413
|
if (keyIndexedTarget) {
|
|
405
414
|
for (const targetItem of uncheckedTarget) {
|
|
406
|
-
const keyStr = JSON.stringify(
|
|
415
|
+
const keyStr = JSON.stringify(
|
|
416
|
+
options!.keys!.map((k) => (targetItem as Record<string, unknown>)[k]),
|
|
417
|
+
);
|
|
407
418
|
const arr = keyIndexedTarget.get(keyStr);
|
|
408
419
|
if (arr) {
|
|
409
420
|
arr.push(targetItem);
|
|
@@ -429,7 +440,9 @@ const arrayReadonlyExtensions: ReadonlyArrayExt<any> & ThisType<any[]> = {
|
|
|
429
440
|
|
|
430
441
|
// 전체 일치가 없고 keys 옵션이 있으면 Map에서 O(1) 조회
|
|
431
442
|
if (sameTarget === undefined && keyIndexedTarget) {
|
|
432
|
-
const sourceKeyStr = JSON.stringify(
|
|
443
|
+
const sourceKeyStr = JSON.stringify(
|
|
444
|
+
options!.keys!.map((k) => (sourceItem as Record<string, unknown>)[k]),
|
|
445
|
+
);
|
|
433
446
|
const candidates = keyIndexedTarget.get(sourceKeyStr);
|
|
434
447
|
if (candidates && candidates.length > 0) {
|
|
435
448
|
// uncheckedTargetSet에서 O(1) 조회로 아직 남아있는 첫 번째 항목 선택
|
|
@@ -467,13 +480,16 @@ const arrayReadonlyExtensions: ReadonlyArrayExt<any> & ThisType<any[]> = {
|
|
|
467
480
|
orgItems instanceof Map
|
|
468
481
|
? orgItems
|
|
469
482
|
: orgItems.toMap((orgItem) =>
|
|
470
|
-
typeof keyPropNameOrFn === "function"
|
|
483
|
+
typeof keyPropNameOrFn === "function"
|
|
484
|
+
? keyPropNameOrFn(orgItem)
|
|
485
|
+
: orgItem[keyPropNameOrFn],
|
|
471
486
|
);
|
|
472
487
|
const includeSame = options?.includeSame ?? false;
|
|
473
488
|
|
|
474
489
|
const diffs: ArrayDiffs2Result<T>[] = [];
|
|
475
490
|
for (const item of this) {
|
|
476
|
-
const keyValue =
|
|
491
|
+
const keyValue =
|
|
492
|
+
typeof keyPropNameOrFn === "function" ? keyPropNameOrFn(item) : item[keyPropNameOrFn];
|
|
477
493
|
if (keyValue == null) {
|
|
478
494
|
diffs.push({ type: "create", item, orgItem: undefined });
|
|
479
495
|
continue;
|
|
@@ -542,7 +558,9 @@ const arrayReadonlyExtensions: ReadonlyArrayExt<any> & ThisType<any[]> = {
|
|
|
542
558
|
for (let i = 0; i < this.length; i++) {
|
|
543
559
|
const item = selector !== undefined ? selector(this[i], i) : this[i];
|
|
544
560
|
if (typeof item !== "number") {
|
|
545
|
-
throw new ArgumentError("sum 은 number 에 대해서만 사용할 수 있습니다.", {
|
|
561
|
+
throw new ArgumentError("sum 은 number 에 대해서만 사용할 수 있습니다.", {
|
|
562
|
+
type: typeof item,
|
|
563
|
+
});
|
|
546
564
|
}
|
|
547
565
|
result += item;
|
|
548
566
|
}
|
|
@@ -555,7 +573,9 @@ const arrayReadonlyExtensions: ReadonlyArrayExt<any> & ThisType<any[]> = {
|
|
|
555
573
|
for (let i = 0; i < this.length; i++) {
|
|
556
574
|
const item = selector !== undefined ? selector(this[i], i) : this[i];
|
|
557
575
|
if (typeof item !== "number" && typeof item !== "string") {
|
|
558
|
-
throw new ArgumentError("min 은 number/string 에 대해서만 사용할 수 있습니다.", {
|
|
576
|
+
throw new ArgumentError("min 은 number/string 에 대해서만 사용할 수 있습니다.", {
|
|
577
|
+
type: typeof item,
|
|
578
|
+
});
|
|
559
579
|
}
|
|
560
580
|
if (result === undefined || result > item) {
|
|
561
581
|
result = item;
|
|
@@ -570,7 +590,9 @@ const arrayReadonlyExtensions: ReadonlyArrayExt<any> & ThisType<any[]> = {
|
|
|
570
590
|
for (let i = 0; i < this.length; i++) {
|
|
571
591
|
const item = selector !== undefined ? selector(this[i], i) : this[i];
|
|
572
592
|
if (typeof item !== "number" && typeof item !== "string") {
|
|
573
|
-
throw new ArgumentError("max 은 number/string 에 대해서만 사용할 수 있습니다.", {
|
|
593
|
+
throw new ArgumentError("max 은 number/string 에 대해서만 사용할 수 있습니다.", {
|
|
594
|
+
type: typeof item,
|
|
595
|
+
});
|
|
574
596
|
}
|
|
575
597
|
if (result === undefined || result < item) {
|
|
576
598
|
result = item;
|
|
@@ -595,7 +617,9 @@ const arrayReadonlyExtensions: ReadonlyArrayExt<any> & ThisType<any[]> = {
|
|
|
595
617
|
};
|
|
596
618
|
|
|
597
619
|
const arrayMutableExtensions: MutableArrayExt<any> & ThisType<any[]> = {
|
|
598
|
-
distinctThis<T>(
|
|
620
|
+
distinctThis<T>(
|
|
621
|
+
options?: boolean | { matchAddress?: boolean; keyFn?: (item: T) => string | number },
|
|
622
|
+
): T[] {
|
|
599
623
|
// 옵션 정규화
|
|
600
624
|
const opts = typeof options === "boolean" ? { matchAddress: options } : (options ?? {});
|
|
601
625
|
|
|
@@ -700,7 +724,9 @@ const arrayMutableExtensions: MutableArrayExt<any> & ThisType<any[]> = {
|
|
|
700
724
|
return this;
|
|
701
725
|
},
|
|
702
726
|
|
|
703
|
-
orderByThis<T>(
|
|
727
|
+
orderByThis<T>(
|
|
728
|
+
selector?: (item: T) => string | number | DateTime | DateOnly | Time | undefined,
|
|
729
|
+
): T[] {
|
|
704
730
|
return this.sort((p, n) => {
|
|
705
731
|
const pp = selector?.(p) ?? p;
|
|
706
732
|
const pn = selector?.(n) ?? n;
|
|
@@ -708,7 +734,9 @@ const arrayMutableExtensions: MutableArrayExt<any> & ThisType<any[]> = {
|
|
|
708
734
|
});
|
|
709
735
|
},
|
|
710
736
|
|
|
711
|
-
orderByDescThis<T>(
|
|
737
|
+
orderByDescThis<T>(
|
|
738
|
+
selector?: (item: T) => string | number | DateTime | DateOnly | Time | undefined,
|
|
739
|
+
): T[] {
|
|
712
740
|
return this.sort((p, n) => {
|
|
713
741
|
const pp = selector?.(p) ?? p;
|
|
714
742
|
const pn = selector?.(n) ?? n;
|
|
@@ -774,4 +802,9 @@ declare global {
|
|
|
774
802
|
|
|
775
803
|
//#endregion
|
|
776
804
|
|
|
777
|
-
export type {
|
|
805
|
+
export type {
|
|
806
|
+
ArrayDiffsResult,
|
|
807
|
+
ArrayDiffs2Result,
|
|
808
|
+
TreeArray,
|
|
809
|
+
ComparableType,
|
|
810
|
+
} from "./arr-ext.types";
|
|
@@ -108,7 +108,10 @@ export interface ReadonlyArrayExt<TItem> {
|
|
|
108
108
|
valueSelector: (item: TItem, index: number) => V,
|
|
109
109
|
): Map<K, Set<V>>;
|
|
110
110
|
|
|
111
|
-
toMapValues<K, V>(
|
|
111
|
+
toMapValues<K, V>(
|
|
112
|
+
keySelector: (item: TItem, index: number) => K,
|
|
113
|
+
valueSelector: (items: TItem[]) => V,
|
|
114
|
+
): Map<K, V>;
|
|
112
115
|
|
|
113
116
|
toObject(keySelector: (item: TItem, index: number) => string): Record<string, TItem>;
|
|
114
117
|
|
|
@@ -153,18 +156,27 @@ export interface ReadonlyArrayExt<TItem> {
|
|
|
153
156
|
* // ]}]
|
|
154
157
|
* ```
|
|
155
158
|
*/
|
|
156
|
-
toTree<K extends keyof TItem, P extends keyof TItem>(
|
|
159
|
+
toTree<K extends keyof TItem, P extends keyof TItem>(
|
|
160
|
+
keyProp: K,
|
|
161
|
+
parentKey: P,
|
|
162
|
+
): TreeArray<TItem>[];
|
|
157
163
|
|
|
158
164
|
/**
|
|
159
165
|
* 중복 제거
|
|
160
166
|
* @param options matchAddress: 주소 비교 (true면 Set 사용), keyFn: 커스텀 키 함수 (O(n) 성능)
|
|
161
167
|
* @note 객체 배열에서 keyFn 없이 사용 시 O(n²) 복잡도. 대량 데이터는 keyFn 사용 권장
|
|
162
168
|
*/
|
|
163
|
-
distinct(
|
|
169
|
+
distinct(
|
|
170
|
+
options?: boolean | { matchAddress?: boolean; keyFn?: (item: TItem) => string | number },
|
|
171
|
+
): TItem[];
|
|
164
172
|
|
|
165
|
-
orderBy(
|
|
173
|
+
orderBy(
|
|
174
|
+
selector?: (item: TItem) => string | number | DateOnly | DateTime | Time | undefined,
|
|
175
|
+
): TItem[];
|
|
166
176
|
|
|
167
|
-
orderByDesc(
|
|
177
|
+
orderByDesc(
|
|
178
|
+
selector?: (item: TItem) => string | number | DateOnly | DateTime | Time | undefined,
|
|
179
|
+
): TItem[];
|
|
168
180
|
|
|
169
181
|
/**
|
|
170
182
|
* 두 배열 비교 (INSERT/DELETE/UPDATE)
|
|
@@ -221,13 +233,19 @@ export interface MutableArrayExt<TItem> {
|
|
|
221
233
|
* @note 객체 배열에서 keyFn 없이 사용 시 O(n²) 복잡도. 대량 데이터는 keyFn 사용 권장
|
|
222
234
|
* @mutates
|
|
223
235
|
*/
|
|
224
|
-
distinctThis(
|
|
236
|
+
distinctThis(
|
|
237
|
+
options?: boolean | { matchAddress?: boolean; keyFn?: (item: TItem) => string | number },
|
|
238
|
+
): TItem[];
|
|
225
239
|
|
|
226
240
|
/** 원본 배열 오름차순 정렬 @mutates */
|
|
227
|
-
orderByThis(
|
|
241
|
+
orderByThis(
|
|
242
|
+
selector?: (item: TItem) => string | number | DateOnly | DateTime | Time | undefined,
|
|
243
|
+
): TItem[];
|
|
228
244
|
|
|
229
245
|
/** 원본 배열 내림차순 정렬 @mutates */
|
|
230
|
-
orderByDescThis(
|
|
246
|
+
orderByDescThis(
|
|
247
|
+
selector?: (item: TItem) => string | number | DateOnly | DateTime | Time | undefined,
|
|
248
|
+
): TItem[];
|
|
231
249
|
|
|
232
250
|
/** 원본 배열에 항목 삽입 @mutates */
|
|
233
251
|
insert(index: number, ...items: TItem[]): this;
|
|
@@ -20,7 +20,9 @@
|
|
|
20
20
|
* emitter.emit("data", "hello");
|
|
21
21
|
* emitter.emit("done"); // void 타입은 인자 없이 호출
|
|
22
22
|
*/
|
|
23
|
-
export class EventEmitter<
|
|
23
|
+
export class EventEmitter<
|
|
24
|
+
TEvents extends { [K in keyof TEvents]: unknown } = Record<string, unknown>,
|
|
25
|
+
> {
|
|
24
26
|
private readonly _target = new EventTarget();
|
|
25
27
|
// 이벤트 타입별로 리스너 맵 관리 (같은 리스너를 다른 이벤트에 등록 가능)
|
|
26
28
|
// 다형적 리스너 관리를 위해 Function 타입 사용
|
|
@@ -77,7 +79,10 @@ export class EventEmitter<TEvents extends { [K in keyof TEvents]: unknown } = Re
|
|
|
77
79
|
* @param type 이벤트 타입
|
|
78
80
|
* @param args 이벤트 데이터 (void 타입이면 생략)
|
|
79
81
|
*/
|
|
80
|
-
emit<K extends keyof TEvents & string>(
|
|
82
|
+
emit<K extends keyof TEvents & string>(
|
|
83
|
+
type: K,
|
|
84
|
+
...args: TEvents[K] extends void ? [] : [data: TEvents[K]]
|
|
85
|
+
): void {
|
|
81
86
|
this._target.dispatchEvent(new CustomEvent(type, { detail: args[0] }));
|
|
82
87
|
}
|
|
83
88
|
|
package/src/types/date-only.ts
CHANGED
|
@@ -64,7 +64,11 @@ export class DateOnly {
|
|
|
64
64
|
// yyyyMMdd 형식 (타임존 영향 없음)
|
|
65
65
|
const matchCompact = /^(\d{4})(\d{2})(\d{2})$/.exec(str);
|
|
66
66
|
if (matchCompact != null) {
|
|
67
|
-
return new DateOnly(
|
|
67
|
+
return new DateOnly(
|
|
68
|
+
Number(matchCompact[1]),
|
|
69
|
+
Number(matchCompact[2]),
|
|
70
|
+
Number(matchCompact[3]),
|
|
71
|
+
);
|
|
68
72
|
}
|
|
69
73
|
|
|
70
74
|
// ISO 8601 등 기타 형식 (Date.parse 사용, 타임존 변환 적용)
|
|
@@ -80,9 +84,12 @@ export class DateOnly {
|
|
|
80
84
|
return new DateOnly(localTick);
|
|
81
85
|
}
|
|
82
86
|
|
|
83
|
-
throw new ArgumentError(
|
|
84
|
-
|
|
85
|
-
|
|
87
|
+
throw new ArgumentError(
|
|
88
|
+
`날짜 형식을 파싱할 수 없습니다. 지원 형식: 'yyyy-MM-dd', 'yyyyMMdd', ISO 8601 날짜`,
|
|
89
|
+
{
|
|
90
|
+
input: str,
|
|
91
|
+
},
|
|
92
|
+
);
|
|
86
93
|
}
|
|
87
94
|
|
|
88
95
|
//#region 주차 계산
|
|
@@ -156,10 +163,16 @@ export class DateOnly {
|
|
|
156
163
|
* // 미국식 (일요일 시작, 첫 주 1일 이상)
|
|
157
164
|
* new DateOnly(2025, 1, 1).getWeekSeqOfYear(0, 1); // { year: 2025, weekSeq: 1 }
|
|
158
165
|
*/
|
|
159
|
-
getWeekSeqOfYear(
|
|
166
|
+
getWeekSeqOfYear(
|
|
167
|
+
weekStartDay: number = 1,
|
|
168
|
+
minDaysInFirstWeek: number = 4,
|
|
169
|
+
): { year: number; weekSeq: number } {
|
|
160
170
|
const base = this.getBaseYearMonthSeqForWeekSeq(weekStartDay, minDaysInFirstWeek);
|
|
161
171
|
|
|
162
|
-
const firstWeekStart = new DateOnly(base.year, 1, 1).getWeekSeqStartDate(
|
|
172
|
+
const firstWeekStart = new DateOnly(base.year, 1, 1).getWeekSeqStartDate(
|
|
173
|
+
weekStartDay,
|
|
174
|
+
minDaysInFirstWeek,
|
|
175
|
+
);
|
|
163
176
|
|
|
164
177
|
const diffDays = (this.tick - firstWeekStart.tick) / DateOnly.MS_PER_DAY;
|
|
165
178
|
return {
|
package/src/types/date-time.ts
CHANGED
|
@@ -43,7 +43,15 @@ export class DateTime {
|
|
|
43
43
|
if (arg1 === undefined) {
|
|
44
44
|
this.date = new Date();
|
|
45
45
|
} else if (arg2 !== undefined && arg3 !== undefined) {
|
|
46
|
-
this.date = new Date(
|
|
46
|
+
this.date = new Date(
|
|
47
|
+
arg1 as number,
|
|
48
|
+
arg2 - 1,
|
|
49
|
+
arg3,
|
|
50
|
+
arg4 ?? 0,
|
|
51
|
+
arg5 ?? 0,
|
|
52
|
+
arg6 ?? 0,
|
|
53
|
+
arg7 ?? 0,
|
|
54
|
+
);
|
|
47
55
|
} else if (arg1 instanceof Date) {
|
|
48
56
|
this.date = new Date(arg1.getTime());
|
|
49
57
|
} else {
|
|
@@ -72,7 +80,9 @@ export class DateTime {
|
|
|
72
80
|
}
|
|
73
81
|
|
|
74
82
|
const match1 =
|
|
75
|
-
/^([0-9]{4})-([0-9]{2})-([0-9]{2}) (오전|오후) ([0-9]{1,2}):([0-9]{2}):([0-9]{2})(\.([0-9]{1,3}))?$/.exec(
|
|
83
|
+
/^([0-9]{4})-([0-9]{2})-([0-9]{2}) (오전|오후) ([0-9]{1,2}):([0-9]{2}):([0-9]{2})(\.([0-9]{1,3}))?$/.exec(
|
|
84
|
+
str,
|
|
85
|
+
);
|
|
76
86
|
if (match1 != null) {
|
|
77
87
|
const rawHour = Number(match1[5]);
|
|
78
88
|
const isPM = match1[4] === "오후";
|
|
@@ -100,7 +110,10 @@ export class DateTime {
|
|
|
100
110
|
);
|
|
101
111
|
}
|
|
102
112
|
|
|
103
|
-
const match3 =
|
|
113
|
+
const match3 =
|
|
114
|
+
/^([0-9]{4})-([0-9]{2})-([0-9]{2}) ([0-9]{2}):([0-9]{2}):([0-9]{2})(\.([0-9]{1,3}))?$/.exec(
|
|
115
|
+
str,
|
|
116
|
+
);
|
|
104
117
|
if (match3 != null) {
|
|
105
118
|
return new DateTime(
|
|
106
119
|
Number(match3[1]),
|
|
@@ -173,7 +186,15 @@ export class DateTime {
|
|
|
173
186
|
|
|
174
187
|
/** 지정된 연도로 새 인스턴스 반환 */
|
|
175
188
|
setYear(year: number): DateTime {
|
|
176
|
-
return new DateTime(
|
|
189
|
+
return new DateTime(
|
|
190
|
+
year,
|
|
191
|
+
this.month,
|
|
192
|
+
this.day,
|
|
193
|
+
this.hour,
|
|
194
|
+
this.minute,
|
|
195
|
+
this.second,
|
|
196
|
+
this.millisecond,
|
|
197
|
+
);
|
|
177
198
|
}
|
|
178
199
|
|
|
179
200
|
/**
|
|
@@ -202,27 +223,67 @@ export class DateTime {
|
|
|
202
223
|
* 자동으로 다음/이전 달로 조정됨 (예: 1월에 day=32 → 2월 1일)
|
|
203
224
|
*/
|
|
204
225
|
setDay(day: number): DateTime {
|
|
205
|
-
return new DateTime(
|
|
226
|
+
return new DateTime(
|
|
227
|
+
this.year,
|
|
228
|
+
this.month,
|
|
229
|
+
day,
|
|
230
|
+
this.hour,
|
|
231
|
+
this.minute,
|
|
232
|
+
this.second,
|
|
233
|
+
this.millisecond,
|
|
234
|
+
);
|
|
206
235
|
}
|
|
207
236
|
|
|
208
237
|
/** 지정된 시로 새 인스턴스 반환 */
|
|
209
238
|
setHour(hour: number): DateTime {
|
|
210
|
-
return new DateTime(
|
|
239
|
+
return new DateTime(
|
|
240
|
+
this.year,
|
|
241
|
+
this.month,
|
|
242
|
+
this.day,
|
|
243
|
+
hour,
|
|
244
|
+
this.minute,
|
|
245
|
+
this.second,
|
|
246
|
+
this.millisecond,
|
|
247
|
+
);
|
|
211
248
|
}
|
|
212
249
|
|
|
213
250
|
/** 지정된 분으로 새 인스턴스 반환 */
|
|
214
251
|
setMinute(minute: number): DateTime {
|
|
215
|
-
return new DateTime(
|
|
252
|
+
return new DateTime(
|
|
253
|
+
this.year,
|
|
254
|
+
this.month,
|
|
255
|
+
this.day,
|
|
256
|
+
this.hour,
|
|
257
|
+
minute,
|
|
258
|
+
this.second,
|
|
259
|
+
this.millisecond,
|
|
260
|
+
);
|
|
216
261
|
}
|
|
217
262
|
|
|
218
263
|
/** 지정된 초로 새 인스턴스 반환 */
|
|
219
264
|
setSecond(second: number): DateTime {
|
|
220
|
-
return new DateTime(
|
|
265
|
+
return new DateTime(
|
|
266
|
+
this.year,
|
|
267
|
+
this.month,
|
|
268
|
+
this.day,
|
|
269
|
+
this.hour,
|
|
270
|
+
this.minute,
|
|
271
|
+
second,
|
|
272
|
+
this.millisecond,
|
|
273
|
+
);
|
|
221
274
|
}
|
|
222
275
|
|
|
223
276
|
/** 지정된 밀리초로 새 인스턴스 반환 */
|
|
224
277
|
setMillisecond(millisecond: number): DateTime {
|
|
225
|
-
return new DateTime(
|
|
278
|
+
return new DateTime(
|
|
279
|
+
this.year,
|
|
280
|
+
this.month,
|
|
281
|
+
this.day,
|
|
282
|
+
this.hour,
|
|
283
|
+
this.minute,
|
|
284
|
+
this.second,
|
|
285
|
+
millisecond,
|
|
286
|
+
);
|
|
226
287
|
}
|
|
227
288
|
|
|
228
289
|
//#endregion
|
package/src/types/time.ts
CHANGED
|
@@ -36,7 +36,8 @@ export class Time {
|
|
|
36
36
|
Time.MS_PER_DAY;
|
|
37
37
|
} else if (arg2 !== undefined) {
|
|
38
38
|
let tick =
|
|
39
|
-
((arg4 ?? 0) + (arg3 ?? 0) * 1000 + arg2 * 60 * 1000 + (arg1 as number) * 60 * 60 * 1000) %
|
|
39
|
+
((arg4 ?? 0) + (arg3 ?? 0) * 1000 + arg2 * 60 * 1000 + (arg1 as number) * 60 * 60 * 1000) %
|
|
40
|
+
Time.MS_PER_DAY;
|
|
40
41
|
if (tick < 0) tick += Time.MS_PER_DAY;
|
|
41
42
|
this._tick = tick;
|
|
42
43
|
} else if (arg1 instanceof Date) {
|
|
@@ -72,7 +73,12 @@ export class Time {
|
|
|
72
73
|
const rawHour = Number(match1[2]);
|
|
73
74
|
const isPM = match1[1] === "오후";
|
|
74
75
|
const hour = convert12To24(rawHour, isPM);
|
|
75
|
-
return new Time(
|
|
76
|
+
return new Time(
|
|
77
|
+
hour,
|
|
78
|
+
Number(match1[3]),
|
|
79
|
+
Number(match1[4]),
|
|
80
|
+
Number(match1[6] ? match1[6].padEnd(3, "0") : "0"),
|
|
81
|
+
);
|
|
76
82
|
}
|
|
77
83
|
|
|
78
84
|
const match2 = /([0-9]{1,2}):([0-9]{1,2}):([0-9]{1,2})(\.([0-9]{1,3}))?$/.exec(str);
|
|
@@ -91,7 +97,12 @@ export class Time {
|
|
|
91
97
|
if (isoMatch != null) {
|
|
92
98
|
const date = new Date(str);
|
|
93
99
|
if (!Number.isNaN(date.getTime())) {
|
|
94
|
-
return new Time(
|
|
100
|
+
return new Time(
|
|
101
|
+
date.getHours(),
|
|
102
|
+
date.getMinutes(),
|
|
103
|
+
date.getSeconds(),
|
|
104
|
+
date.getMilliseconds(),
|
|
105
|
+
);
|
|
95
106
|
}
|
|
96
107
|
}
|
|
97
108
|
|
package/src/types/uuid.ts
CHANGED
|
@@ -12,9 +12,12 @@ import { ArgumentError } from "../errors/argument-error";
|
|
|
12
12
|
*/
|
|
13
13
|
export class Uuid {
|
|
14
14
|
// 0x00 ~ 0xFF에 대한 hex 문자열 미리 계산 (256개)
|
|
15
|
-
private static readonly _hexTable: string[] = Array.from({ length: 256 }, (_, i) =>
|
|
15
|
+
private static readonly _hexTable: string[] = Array.from({ length: 256 }, (_, i) =>
|
|
16
|
+
i.toString(16).padStart(2, "0"),
|
|
17
|
+
);
|
|
16
18
|
|
|
17
|
-
private static readonly _uuidRegex =
|
|
19
|
+
private static readonly _uuidRegex =
|
|
20
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
18
21
|
|
|
19
22
|
/** 16바이트 배열을 UUID 문자열로 변환 */
|
|
20
23
|
private static _bytesToUuidStr(bytes: Uint8Array): string {
|
package/src/utils/bytes.ts
CHANGED
|
@@ -132,7 +132,9 @@ export function bytesFromBase64(base64: string): Bytes {
|
|
|
132
132
|
|
|
133
133
|
// 유효성 검사: 문자
|
|
134
134
|
if (!/^[A-Za-z0-9+/]+$/.test(cleanBase64)) {
|
|
135
|
-
throw new ArgumentError("유효하지 않은 base64 문자가 포함되어 있습니다", {
|
|
135
|
+
throw new ArgumentError("유효하지 않은 base64 문자가 포함되어 있습니다", {
|
|
136
|
+
base64: base64.substring(0, 20),
|
|
137
|
+
});
|
|
136
138
|
}
|
|
137
139
|
|
|
138
140
|
// 유효성 검사: 길이 (패딩 제거 후 나머지가 1이면 유효하지 않음)
|
package/src/utils/date-format.ts
CHANGED
|
@@ -151,10 +151,12 @@ export function formatDate(
|
|
|
151
151
|
): string {
|
|
152
152
|
const { year, month, day, hour, minute, second, millisecond, timezoneOffsetMinutes } = args;
|
|
153
153
|
|
|
154
|
-
const absOffsetMinutes =
|
|
154
|
+
const absOffsetMinutes =
|
|
155
|
+
timezoneOffsetMinutes !== undefined ? Math.abs(timezoneOffsetMinutes) : undefined;
|
|
155
156
|
const offsetHour = absOffsetMinutes !== undefined ? Math.floor(absOffsetMinutes / 60) : undefined;
|
|
156
157
|
const offsetMinute = absOffsetMinutes !== undefined ? absOffsetMinutes % 60 : undefined;
|
|
157
|
-
const offsetSign =
|
|
158
|
+
const offsetSign =
|
|
159
|
+
timezoneOffsetMinutes !== undefined ? (timezoneOffsetMinutes >= 0 ? "+" : "-") : undefined;
|
|
158
160
|
|
|
159
161
|
const week =
|
|
160
162
|
year !== undefined && month !== undefined && day !== undefined
|
package/src/utils/json.ts
CHANGED
|
@@ -129,7 +129,10 @@ export function jsonStringify(
|
|
|
129
129
|
seen.add(currValue);
|
|
130
130
|
|
|
131
131
|
// toJSON 메서드가 있으면 호출 (Date, DateTime 등 커스텀 타입은 이미 위에서 처리됨)
|
|
132
|
-
if (
|
|
132
|
+
if (
|
|
133
|
+
"toJSON" in currValue &&
|
|
134
|
+
typeof (currValue as { toJSON: unknown }).toJSON === "function"
|
|
135
|
+
) {
|
|
133
136
|
const toJsonResult = (currValue as { toJSON: (key?: string) => unknown }).toJSON(key);
|
|
134
137
|
seen.delete(currValue);
|
|
135
138
|
return convertSpecialTypes(key, toJsonResult);
|
|
@@ -209,7 +212,9 @@ export function jsonParse<TResult = unknown>(json: string): TResult {
|
|
|
209
212
|
}
|
|
210
213
|
if (typed.__type__ === "Uint8Array" && typeof typed.data === "string") {
|
|
211
214
|
if (typed.data === "__hidden__") {
|
|
212
|
-
throw new SdError(
|
|
215
|
+
throw new SdError(
|
|
216
|
+
"redactBytes 옵션으로 직렬화된 Uint8Array는 parse로 복원할 수 없습니다",
|
|
217
|
+
);
|
|
213
218
|
}
|
|
214
219
|
return bytesFromHex(typed.data);
|
|
215
220
|
}
|
package/src/utils/num.ts
CHANGED
|
@@ -86,8 +86,14 @@ export function numIsNullOrEmpty(val: number | undefined): val is 0 | undefined
|
|
|
86
86
|
* numFormat(1234, { min: 2 }) // "1,234.00"
|
|
87
87
|
*/
|
|
88
88
|
export function numFormat(val: number, digit?: { max?: number; min?: number }): string;
|
|
89
|
-
export function numFormat(
|
|
90
|
-
|
|
89
|
+
export function numFormat(
|
|
90
|
+
val: number | undefined,
|
|
91
|
+
digit?: { max?: number; min?: number },
|
|
92
|
+
): string | undefined;
|
|
93
|
+
export function numFormat(
|
|
94
|
+
val: number | undefined,
|
|
95
|
+
digit?: { max?: number; min?: number },
|
|
96
|
+
): string | undefined {
|
|
91
97
|
return val?.toLocaleString(undefined, {
|
|
92
98
|
maximumFractionDigits: digit?.max,
|
|
93
99
|
minimumFractionDigits: digit?.min,
|
package/src/utils/obj.ts
CHANGED
|
@@ -204,7 +204,11 @@ export function objEqual(source: unknown, target: unknown, options?: EqualOption
|
|
|
204
204
|
}
|
|
205
205
|
|
|
206
206
|
if (typeof source === "object" && typeof target === "object") {
|
|
207
|
-
return objEqualObject(
|
|
207
|
+
return objEqualObject(
|
|
208
|
+
source as Record<string, unknown>,
|
|
209
|
+
target as Record<string, unknown>,
|
|
210
|
+
options,
|
|
211
|
+
);
|
|
208
212
|
}
|
|
209
213
|
|
|
210
214
|
return false;
|
|
@@ -229,9 +233,14 @@ function objEqualArray(source: unknown[], target: unknown[], options?: EqualOpti
|
|
|
229
233
|
});
|
|
230
234
|
} else {
|
|
231
235
|
// 재귀 호출 시 topLevelIncludes/topLevelExcludes 옵션은 최상위 레벨에만 적용되므로 제외
|
|
232
|
-
const recursiveOptions = {
|
|
236
|
+
const recursiveOptions = {
|
|
237
|
+
ignoreArrayIndex: options.ignoreArrayIndex,
|
|
238
|
+
onlyOneDepth: options.onlyOneDepth,
|
|
239
|
+
};
|
|
233
240
|
return source.every((sourceItem) => {
|
|
234
|
-
const idx = target.findIndex(
|
|
241
|
+
const idx = target.findIndex(
|
|
242
|
+
(t, i) => !matchedIndices.has(i) && objEqual(t, sourceItem, recursiveOptions),
|
|
243
|
+
);
|
|
235
244
|
if (idx !== -1) {
|
|
236
245
|
matchedIndices.add(idx);
|
|
237
246
|
return true;
|
|
@@ -269,7 +278,11 @@ function objEqualArray(source: unknown[], target: unknown[], options?: EqualOpti
|
|
|
269
278
|
* @note 비문자열 키(객체, 배열 등) 처리 시 O(n²) 복잡도 발생
|
|
270
279
|
* @note 대량 데이터의 경우 onlyOneDepth: true 옵션 사용 권장 (참조 비교로 O(n)으로 개선)
|
|
271
280
|
*/
|
|
272
|
-
function objEqualMap(
|
|
281
|
+
function objEqualMap(
|
|
282
|
+
source: Map<unknown, unknown>,
|
|
283
|
+
target: Map<unknown, unknown>,
|
|
284
|
+
options?: EqualOptions,
|
|
285
|
+
): boolean {
|
|
273
286
|
// Map 비교 시 topLevelIncludes/topLevelExcludes 옵션은 무시됨 (object 속성 키에만 적용)
|
|
274
287
|
const sourceKeys = Array.from(source.keys()).filter((key) => source.get(key) != null);
|
|
275
288
|
const targetKeys = Array.from(target.keys()).filter((key) => target.get(key) != null);
|
|
@@ -392,7 +405,9 @@ function objEqualSet(source: Set<unknown>, target: Set<unknown>, options?: Equal
|
|
|
392
405
|
const targetArr = [...target];
|
|
393
406
|
const matchedIndices = new Set<number>();
|
|
394
407
|
for (const sourceItem of source) {
|
|
395
|
-
const idx = targetArr.findIndex(
|
|
408
|
+
const idx = targetArr.findIndex(
|
|
409
|
+
(t, i) => !matchedIndices.has(i) && objEqual(sourceItem, t, options),
|
|
410
|
+
);
|
|
396
411
|
if (idx === -1) {
|
|
397
412
|
return false;
|
|
398
413
|
}
|
|
@@ -447,7 +462,9 @@ export function objMerge<TSource, TMergeTarget>(
|
|
|
447
462
|
}
|
|
448
463
|
|
|
449
464
|
if (target === null) {
|
|
450
|
-
return opt?.useDelTargetNull
|
|
465
|
+
return opt?.useDelTargetNull
|
|
466
|
+
? (undefined as TSource & TMergeTarget)
|
|
467
|
+
: (objClone(source) as TSource & TMergeTarget);
|
|
451
468
|
}
|
|
452
469
|
|
|
453
470
|
if (typeof target !== "object") {
|
|
@@ -588,7 +605,10 @@ export function objMerge3<
|
|
|
588
605
|
* objOmit(user, ["email"]);
|
|
589
606
|
* // { name: "Alice", age: 30 }
|
|
590
607
|
*/
|
|
591
|
-
export function objOmit<T extends Record<string, unknown>, K extends keyof T>(
|
|
608
|
+
export function objOmit<T extends Record<string, unknown>, K extends keyof T>(
|
|
609
|
+
item: T,
|
|
610
|
+
omitKeys: K[],
|
|
611
|
+
): Omit<T, K> {
|
|
592
612
|
const result: Record<string, unknown> = {};
|
|
593
613
|
for (const key of Object.keys(item)) {
|
|
594
614
|
if (!omitKeys.includes(key as K)) {
|
|
@@ -609,7 +629,10 @@ export function objOmit<T extends Record<string, unknown>, K extends keyof T>(it
|
|
|
609
629
|
* objOmitByFilter(data, (key) => key.startsWith("_"));
|
|
610
630
|
* // { name: "Alice", age: 30 }
|
|
611
631
|
*/
|
|
612
|
-
export function objOmitByFilter<T extends Record<string, unknown>>(
|
|
632
|
+
export function objOmitByFilter<T extends Record<string, unknown>>(
|
|
633
|
+
item: T,
|
|
634
|
+
omitKeyFn: (key: keyof T) => boolean,
|
|
635
|
+
): T {
|
|
613
636
|
const result: Record<string, unknown> = {};
|
|
614
637
|
for (const key of Object.keys(item)) {
|
|
615
638
|
if (!omitKeyFn(key)) {
|
|
@@ -629,7 +652,10 @@ export function objOmitByFilter<T extends Record<string, unknown>>(item: T, omit
|
|
|
629
652
|
* objPick(user, ["name", "age"]);
|
|
630
653
|
* // { name: "Alice", age: 30 }
|
|
631
654
|
*/
|
|
632
|
-
export function objPick<T extends Record<string, unknown>, K extends keyof T>(
|
|
655
|
+
export function objPick<T extends Record<string, unknown>, K extends keyof T>(
|
|
656
|
+
item: T,
|
|
657
|
+
keys: K[],
|
|
658
|
+
): Pick<T, K> {
|
|
633
659
|
const result: Record<string, unknown> = {};
|
|
634
660
|
for (const key of keys) {
|
|
635
661
|
result[key as string] = item[key];
|
|
@@ -669,7 +695,11 @@ function getChainSplits(chain: string): (string | number)[] {
|
|
|
669
695
|
*/
|
|
670
696
|
export function objGetChainValue(obj: unknown, chain: string, optional: true): unknown | undefined;
|
|
671
697
|
export function objGetChainValue(obj: unknown, chain: string): unknown;
|
|
672
|
-
export function objGetChainValue(
|
|
698
|
+
export function objGetChainValue(
|
|
699
|
+
obj: unknown,
|
|
700
|
+
chain: string,
|
|
701
|
+
optional?: true,
|
|
702
|
+
): unknown | undefined {
|
|
673
703
|
const splits = getChainSplits(chain);
|
|
674
704
|
let result: unknown = obj;
|
|
675
705
|
for (const splitItem of splits) {
|
|
@@ -957,7 +987,10 @@ function objMapImpl<TSource extends object, TNewKey extends string, TNewValue>(
|
|
|
957
987
|
): Record<string, TNewValue> {
|
|
958
988
|
const result: Record<string, TNewValue> = {};
|
|
959
989
|
for (const key of Object.keys(obj)) {
|
|
960
|
-
const [newKey, newValue] = fn(
|
|
990
|
+
const [newKey, newValue] = fn(
|
|
991
|
+
key as keyof TSource,
|
|
992
|
+
(obj as Record<string, TSource[keyof TSource]>)[key],
|
|
993
|
+
);
|
|
961
994
|
result[newKey ?? key] = newValue;
|
|
962
995
|
}
|
|
963
996
|
return result;
|