@simplysm/core-common 13.0.15 → 13.0.18

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.
@@ -9,63 +9,63 @@ import type { Time } from "../types/time";
9
9
 
10
10
  //#region 인터페이스
11
11
 
12
- export interface ReadonlyArrayExt<T> {
12
+ export interface ReadonlyArrayExt<TItem> {
13
13
  /**
14
14
  * 조건에 맞는 단일 요소 반환
15
15
  * @param predicate 필터 조건 (생략 시 배열 전체 대상)
16
16
  * @returns 요소가 없으면 undefined
17
17
  * @throws ArgumentError 조건에 맞는 요소가 2개 이상이면 발생
18
18
  */
19
- single(predicate?: (item: T, index: number) => boolean): T | undefined;
19
+ single(predicate?: (item: TItem, index: number) => boolean): TItem | undefined;
20
20
 
21
21
  /**
22
22
  * 첫 번째 요소 반환
23
23
  * @param predicate 필터 조건 (생략 시 첫 번째 요소 반환)
24
24
  * @returns 요소가 없으면 undefined
25
25
  */
26
- first(predicate?: (item: T, index: number) => boolean): T | undefined;
26
+ first(predicate?: (item: TItem, index: number) => boolean): TItem | undefined;
27
27
 
28
28
  /** 비동기 필터 (순차 실행) */
29
- filterAsync(predicate: (item: T, index: number) => Promise<boolean>): Promise<T[]>;
29
+ filterAsync(predicate: (item: TItem, index: number) => Promise<boolean>): Promise<TItem[]>;
30
30
 
31
31
  /**
32
32
  * 마지막 요소 반환
33
33
  * @param predicate 필터 조건 (생략 시 마지막 요소 반환)
34
34
  * @returns 요소가 없으면 undefined
35
35
  */
36
- last(predicate?: (item: T, index: number) => boolean): T | undefined;
36
+ last(predicate?: (item: TItem, index: number) => boolean): TItem | undefined;
37
37
 
38
38
  /** null/undefined 제거 */
39
- filterExists(): NonNullable<T>[];
39
+ filterExists(): NonNullable<TItem>[];
40
40
 
41
41
  /** 특정 타입의 요소만 필터링 (PrimitiveTypeStr 또는 생성자 타입) */
42
- ofType<K extends PrimitiveTypeStr>(type: K): Extract<T, PrimitiveTypeMap[K]>[];
43
- ofType<N extends T>(type: Type<N>): N[];
42
+ ofType<K extends PrimitiveTypeStr>(type: K): Extract<TItem, PrimitiveTypeMap[K]>[];
43
+ ofType<N extends TItem>(type: Type<N>): N[];
44
44
 
45
45
  /** 비동기 매핑 (순차 실행) */
46
- mapAsync<R>(selector: (item: T, index: number) => Promise<R>): Promise<R[]>;
46
+ mapAsync<R>(selector: (item: TItem, index: number) => Promise<R>): Promise<R[]>;
47
47
 
48
48
  /** 중첩 배열 평탄화 */
49
- mapMany(): T extends readonly (infer U)[] ? U[] : T;
49
+ mapMany(): TItem extends readonly (infer U)[] ? U[] : TItem;
50
50
 
51
51
  /** 매핑 후 평탄화 */
52
- mapMany<R>(selector: (item: T, index: number) => R[]): R[];
52
+ mapMany<R>(selector: (item: TItem, index: number) => R[]): R[];
53
53
 
54
54
  /** 비동기 매핑 후 평탄화 (순차 실행) */
55
- mapManyAsync<R>(selector: (item: T, index: number) => Promise<R[]>): Promise<R[]>;
55
+ mapManyAsync<R>(selector: (item: TItem, index: number) => Promise<R[]>): Promise<R[]>;
56
56
 
57
57
  /**
58
58
  * 비동기 병렬 처리 (Promise.all 사용)
59
59
  * @note 하나라도 reject되면 전체가 fail-fast로 reject됨 (Promise.all 동작)
60
60
  */
61
- parallelAsync<R>(fn: (item: T, index: number) => Promise<R>): Promise<R[]>;
61
+ parallelAsync<R>(fn: (item: TItem, index: number) => Promise<R>): Promise<R[]>;
62
62
 
63
63
  /**
64
64
  * 키 기준 그룹화
65
65
  * @param keySelector 그룹 키 선택 함수
66
66
  * @note O(n²) 복잡도 (객체 키 지원을 위해 깊은 비교 사용). primitive 키만 필요하면 toArrayMap()이 O(n)으로 더 효율적
67
67
  */
68
- groupBy<K>(keySelector: (item: T, index: number) => K): { key: K; values: T[] }[];
68
+ groupBy<K>(keySelector: (item: TItem, index: number) => K): { key: K; values: TItem[] }[];
69
69
 
70
70
  /**
71
71
  * 키 기준 그룹화 (값 변환 포함)
@@ -74,44 +74,47 @@ export interface ReadonlyArrayExt<T> {
74
74
  * @note O(n²) 복잡도 (객체 키 지원을 위해 깊은 비교 사용). primitive 키만 필요하면 toArrayMap()이 O(n)으로 더 효율적
75
75
  */
76
76
  groupBy<K, V>(
77
- keySelector: (item: T, index: number) => K,
78
- valueSelector: (item: T, index: number) => V,
77
+ keySelector: (item: TItem, index: number) => K,
78
+ valueSelector: (item: TItem, index: number) => V,
79
79
  ): {
80
80
  key: K;
81
81
  values: V[];
82
82
  }[];
83
83
 
84
- toMap<K>(keySelector: (item: T, index: number) => K): Map<K, T>;
84
+ toMap<K>(keySelector: (item: TItem, index: number) => K): Map<K, TItem>;
85
85
 
86
- toMap<K, V>(keySelector: (item: T, index: number) => K, valueSelector: (item: T, index: number) => V): Map<K, V>;
86
+ toMap<K, V>(
87
+ keySelector: (item: TItem, index: number) => K,
88
+ valueSelector: (item: TItem, index: number) => V,
89
+ ): Map<K, V>;
87
90
 
88
- toMapAsync<K>(keySelector: (item: T, index: number) => Promise<K>): Promise<Map<K, T>>;
91
+ toMapAsync<K>(keySelector: (item: TItem, index: number) => Promise<K>): Promise<Map<K, TItem>>;
89
92
 
90
93
  toMapAsync<K, V>(
91
- keySelector: (item: T, index: number) => Promise<K> | K,
92
- valueSelector: (item: T, index: number) => Promise<V> | V,
94
+ keySelector: (item: TItem, index: number) => Promise<K> | K,
95
+ valueSelector: (item: TItem, index: number) => Promise<V> | V,
93
96
  ): Promise<Map<K, V>>;
94
97
 
95
- toArrayMap<K>(keySelector: (item: T, index: number) => K): Map<K, T[]>;
98
+ toArrayMap<K>(keySelector: (item: TItem, index: number) => K): Map<K, TItem[]>;
96
99
 
97
100
  toArrayMap<K, V>(
98
- keySelector: (item: T, index: number) => K,
99
- valueSelector: (item: T, index: number) => V,
101
+ keySelector: (item: TItem, index: number) => K,
102
+ valueSelector: (item: TItem, index: number) => V,
100
103
  ): Map<K, V[]>;
101
104
 
102
- toSetMap<K>(keySelector: (item: T, index: number) => K): Map<K, Set<T>>;
105
+ toSetMap<K>(keySelector: (item: TItem, index: number) => K): Map<K, Set<TItem>>;
103
106
  toSetMap<K, V>(
104
- keySelector: (item: T, index: number) => K,
105
- valueSelector: (item: T, index: number) => V,
107
+ keySelector: (item: TItem, index: number) => K,
108
+ valueSelector: (item: TItem, index: number) => V,
106
109
  ): Map<K, Set<V>>;
107
110
 
108
- toMapValues<K, V>(keySelector: (item: T, index: number) => K, valueSelector: (items: T[]) => V): Map<K, V>;
111
+ toMapValues<K, V>(keySelector: (item: TItem, index: number) => K, valueSelector: (items: TItem[]) => V): Map<K, V>;
109
112
 
110
- toObject(keySelector: (item: T, index: number) => string): Record<string, T>;
113
+ toObject(keySelector: (item: TItem, index: number) => string): Record<string, TItem>;
111
114
 
112
115
  toObject<V>(
113
- keySelector: (item: T, index: number) => string,
114
- valueSelector: (item: T, index: number) => V,
116
+ keySelector: (item: TItem, index: number) => string,
117
+ valueSelector: (item: TItem, index: number) => V,
115
118
  ): Record<string, V>;
116
119
 
117
120
  /**
@@ -150,18 +153,18 @@ export interface ReadonlyArrayExt<T> {
150
153
  * // ]}]
151
154
  * ```
152
155
  */
153
- toTree<K extends keyof T, P extends keyof T>(keyProp: K, parentKey: P): TreeArray<T>[];
156
+ toTree<K extends keyof TItem, P extends keyof TItem>(keyProp: K, parentKey: P): TreeArray<TItem>[];
154
157
 
155
158
  /**
156
159
  * 중복 제거
157
160
  * @param options matchAddress: 주소 비교 (true면 Set 사용), keyFn: 커스텀 키 함수 (O(n) 성능)
158
161
  * @note 객체 배열에서 keyFn 없이 사용 시 O(n²) 복잡도. 대량 데이터는 keyFn 사용 권장
159
162
  */
160
- distinct(options?: boolean | { matchAddress?: boolean; keyFn?: (item: T) => string | number }): T[];
163
+ distinct(options?: boolean | { matchAddress?: boolean; keyFn?: (item: TItem) => string | number }): TItem[];
161
164
 
162
- orderBy(selector?: (item: T) => string | number | DateOnly | DateTime | Time | undefined): T[];
165
+ orderBy(selector?: (item: TItem) => string | number | DateOnly | DateTime | Time | undefined): TItem[];
163
166
 
164
- orderByDesc(selector?: (item: T) => string | number | DateOnly | DateTime | Time | undefined): T[];
167
+ orderByDesc(selector?: (item: TItem) => string | number | DateOnly | DateTime | Time | undefined): TItem[];
165
168
 
166
169
  /**
167
170
  * 두 배열 비교 (INSERT/DELETE/UPDATE)
@@ -169,68 +172,74 @@ export interface ReadonlyArrayExt<T> {
169
172
  * @param options keys: 키 비교용, excludes: 비교 제외 속성
170
173
  * @note target에 중복 키가 있으면 첫 번째 매칭만 사용됨
171
174
  */
172
- diffs<P>(target: P[], options?: { keys?: string[]; excludes?: string[] }): ArrayDiffsResult<T, P>[];
173
-
174
- oneWayDiffs<K extends keyof T>(
175
- orgItems: T[] | Map<T[K], T>,
176
- keyPropNameOrFn: K | ((item: T) => K),
175
+ diffs<TOtherItem>(
176
+ target: TOtherItem[],
177
+ options?: { keys?: string[]; excludes?: string[] },
178
+ ): ArrayDiffsResult<TItem, TOtherItem>[];
179
+
180
+ oneWayDiffs<K extends keyof TItem>(
181
+ orgItems: TItem[] | Map<TItem[K], TItem>,
182
+ keyPropNameOrFn: K | ((item: TItem) => K),
177
183
  options?: {
178
184
  includeSame?: boolean;
179
185
  excludes?: string[];
180
186
  includes?: string[];
181
187
  },
182
- ): ArrayDiffs2Result<T>[];
188
+ ): ArrayDiffs2Result<TItem>[];
183
189
 
184
- merge<P>(target: P[], options?: { keys?: string[]; excludes?: string[] }): (T | P | (T & P))[];
190
+ merge<TOtherItem>(
191
+ target: TOtherItem[],
192
+ options?: { keys?: string[]; excludes?: string[] },
193
+ ): (TItem | TOtherItem | (TItem & TOtherItem))[];
185
194
 
186
195
  /**
187
196
  * 요소의 합계 반환
188
197
  * @param selector 값 선택 함수 (생략 시 요소 자체를 number로 사용)
189
198
  * @returns 빈 배열인 경우 0 반환
190
199
  */
191
- sum(selector?: (item: T, index: number) => number): number;
200
+ sum(selector?: (item: TItem, index: number) => number): number;
192
201
 
193
- min(): T extends number | string ? T | undefined : never;
202
+ min(): TItem extends number | string ? TItem | undefined : never;
194
203
 
195
- min<P extends number | string>(selector?: (item: T, index: number) => P): P | undefined;
204
+ min<P extends number | string>(selector?: (item: TItem, index: number) => P): P | undefined;
196
205
 
197
- max(): T extends number | string ? T | undefined : never;
206
+ max(): TItem extends number | string ? TItem | undefined : never;
198
207
 
199
- max<P extends number | string>(selector?: (item: T, index: number) => P): P | undefined;
208
+ max<P extends number | string>(selector?: (item: TItem, index: number) => P): P | undefined;
200
209
 
201
- shuffle(): T[];
210
+ shuffle(): TItem[];
202
211
  }
203
212
 
204
213
  /**
205
214
  * 원본 배열을 변경하는 확장 메서드
206
215
  * @mutates 모든 메서드가 원본 배열을 직접 변경합니다
207
216
  */
208
- export interface MutableArrayExt<T> {
217
+ export interface MutableArrayExt<TItem> {
209
218
  /**
210
219
  * 원본 배열에서 중복 제거
211
220
  * @param options matchAddress: 주소 비교 (true면 Set 사용), keyFn: 커스텀 키 함수 (O(n) 성능)
212
221
  * @note 객체 배열에서 keyFn 없이 사용 시 O(n²) 복잡도. 대량 데이터는 keyFn 사용 권장
213
222
  * @mutates
214
223
  */
215
- distinctThis(options?: boolean | { matchAddress?: boolean; keyFn?: (item: T) => string | number }): T[];
224
+ distinctThis(options?: boolean | { matchAddress?: boolean; keyFn?: (item: TItem) => string | number }): TItem[];
216
225
 
217
226
  /** 원본 배열 오름차순 정렬 @mutates */
218
- orderByThis(selector?: (item: T) => string | number | DateOnly | DateTime | Time | undefined): T[];
227
+ orderByThis(selector?: (item: TItem) => string | number | DateOnly | DateTime | Time | undefined): TItem[];
219
228
 
220
229
  /** 원본 배열 내림차순 정렬 @mutates */
221
- orderByDescThis(selector?: (item: T) => string | number | DateOnly | DateTime | Time | undefined): T[];
230
+ orderByDescThis(selector?: (item: TItem) => string | number | DateOnly | DateTime | Time | undefined): TItem[];
222
231
 
223
232
  /** 원본 배열에 항목 삽입 @mutates */
224
- insert(index: number, ...items: T[]): this;
233
+ insert(index: number, ...items: TItem[]): this;
225
234
 
226
235
  /** 원본 배열에서 항목 제거 @mutates */
227
- remove(item: T): this;
236
+ remove(item: TItem): this;
228
237
 
229
238
  /** 원본 배열에서 조건에 맞는 항목 제거 @mutates */
230
- remove(selector: (item: T, index: number) => boolean): this;
239
+ remove(selector: (item: TItem, index: number) => boolean): this;
231
240
 
232
241
  /** 원본 배열에서 항목 토글 (있으면 제거, 없으면 추가) @mutates */
233
- toggle(item: T): this;
242
+ toggle(item: TItem): this;
234
243
 
235
244
  /** 원본 배열 비우기 @mutates */
236
245
  clear(): this;
@@ -240,17 +249,17 @@ export interface MutableArrayExt<T> {
240
249
 
241
250
  //#region 내보내기 타입
242
251
 
243
- export type ArrayDiffsResult<T, P> =
244
- | { source: undefined; target: P } // INSERT
245
- | { source: T; target: undefined } // DELETE
246
- | { source: T; target: P }; // UPDATE
252
+ export type ArrayDiffsResult<TOriginal, TOther> =
253
+ | { source: undefined; target: TOther } // INSERT
254
+ | { source: TOriginal; target: undefined } // DELETE
255
+ | { source: TOriginal; target: TOther }; // UPDATE
247
256
 
248
- export type ArrayDiffs2Result<T> =
249
- | { type: "create"; item: T; orgItem: undefined }
250
- | { type: "update"; item: T; orgItem: T }
251
- | { type: "same"; item: T; orgItem: T };
257
+ export type ArrayDiffs2Result<TItem> =
258
+ | { type: "create"; item: TItem; orgItem: undefined }
259
+ | { type: "update"; item: TItem; orgItem: TItem }
260
+ | { type: "same"; item: TItem; orgItem: TItem };
252
261
 
253
- export type TreeArray<T> = T & { children: TreeArray<T>[] };
262
+ export type TreeArray<TNode> = TNode & { children: TreeArray<TNode>[] };
254
263
 
255
264
  /** 정렬/비교 가능한 타입 */
256
265
  export type ComparableType = string | number | boolean | DateTime | DateOnly | Time | undefined;
@@ -4,7 +4,7 @@
4
4
  * 브라우저와 Node.js 모두에서 사용 가능한 타입 안전한 이벤트 에미터이다.
5
5
  * 내부적으로 EventTarget을 사용하여 구현되어 있다.
6
6
  *
7
- * @typeParam T 이벤트 타입 맵. 키는 이벤트 이름, 값은 이벤트 데이터 타입
7
+ * @typeParam TEvents 이벤트 타입 맵. 키는 이벤트 이름, 값은 이벤트 데이터 타입
8
8
  *
9
9
  * @example
10
10
  * interface MyEvents {
@@ -20,7 +20,7 @@
20
20
  * emitter.emit("data", "hello");
21
21
  * emitter.emit("done"); // void 타입은 인자 없이 호출
22
22
  */
23
- export class EventEmitter<T extends { [K in keyof T]: unknown } = Record<string, unknown>> {
23
+ export class EventEmitter<TEvents extends { [K in keyof TEvents]: unknown } = Record<string, unknown>> {
24
24
  private readonly _target = new EventTarget();
25
25
  // 이벤트 타입별로 리스너 맵 관리 (같은 리스너를 다른 이벤트에 등록 가능)
26
26
  // 다형적 리스너 관리를 위해 Function 타입 사용
@@ -33,7 +33,7 @@ export class EventEmitter<T extends { [K in keyof T]: unknown } = Record<string,
33
33
  * @param listener 이벤트 핸들러
34
34
  * @note 같은 리스너를 같은 이벤트에 중복 등록하면 무시됨
35
35
  */
36
- on<K extends keyof T & string>(type: K, listener: (data: T[K]) => void): void {
36
+ on<K extends keyof TEvents & string>(type: K, listener: (data: TEvents[K]) => void): void {
37
37
  // 이벤트 타입별 맵 가져오거나 생성
38
38
  let typeMap = this._listenerMap.get(type);
39
39
  if (!typeMap) {
@@ -55,7 +55,7 @@ export class EventEmitter<T extends { [K in keyof T]: unknown } = Record<string,
55
55
  * @param type 이벤트 타입
56
56
  * @param listener 제거할 이벤트 핸들러
57
57
  */
58
- off<K extends keyof T & string>(type: K, listener: (data: T[K]) => void): void {
58
+ off<K extends keyof TEvents & string>(type: K, listener: (data: TEvents[K]) => void): void {
59
59
  const typeMap = this._listenerMap.get(type);
60
60
  if (!typeMap) return;
61
61
 
@@ -77,7 +77,7 @@ export class EventEmitter<T extends { [K in keyof T]: unknown } = Record<string,
77
77
  * @param type 이벤트 타입
78
78
  * @param args 이벤트 데이터 (void 타입이면 생략)
79
79
  */
80
- emit<K extends keyof T & string>(type: K, ...args: T[K] extends void ? [] : [data: T[K]]): void {
80
+ emit<K extends keyof TEvents & string>(type: K, ...args: TEvents[K] extends void ? [] : [data: TEvents[K]]): void {
81
81
  this._target.dispatchEvent(new CustomEvent(type, { detail: args[0] }));
82
82
  }
83
83
 
@@ -87,7 +87,7 @@ export class EventEmitter<T extends { [K in keyof T]: unknown } = Record<string,
87
87
  * @param type 이벤트 타입
88
88
  * @returns 등록된 리스너 수
89
89
  */
90
- listenerCount<K extends keyof T & string>(type: K): number {
90
+ listenerCount<K extends keyof TEvents & string>(type: K): number {
91
91
  return this._listenerMap.get(type)?.size ?? 0;
92
92
  }
93
93
 
package/src/index.ts CHANGED
@@ -7,8 +7,8 @@ import "./extensions/map-ext";
7
7
 
8
8
  export * from "./env";
9
9
 
10
- // arr-extension에서 타입만 re-export
11
- export type { ArrayDiffsResult, ArrayDiffs2Result, TreeArray } from "./extensions/arr-ext";
10
+ // arr-extension re-export
11
+ export * from "./extensions/arr-ext";
12
12
 
13
13
  //#region errors
14
14
  export * from "./errors/sd-error";
@@ -19,23 +19,11 @@ import consola from "consola";
19
19
  * map.dispose();
20
20
  * }
21
21
  */
22
- export class LazyGcMap<K, V> {
22
+ export class LazyGcMap<TKey, TValue> {
23
23
  private static readonly _logger = consola.withTag("LazyGcMap");
24
24
 
25
- /**
26
- * dispose() 미호출 감지용 registry
27
- * @note FinalizationRegistry는 Chrome 84+, Node 14.6+ 지원
28
- * 미지원 환경에서는 경고 없이 동작하지만, dispose() 미호출 시 메모리 누수 가능
29
- */
30
- private static readonly _registry =
31
- typeof FinalizationRegistry !== "undefined"
32
- ? new FinalizationRegistry<string>((id) => {
33
- LazyGcMap._logger.warn(`LazyGcMap(${id})이 dispose() 없이 가비지 수집됨. 메모리 누수 가능성 있음.`);
34
- })
35
- : undefined;
36
-
37
25
  // 실제 데이터와 마지막 접근 시간을 함께 저장
38
- private readonly _map = new Map<K, { value: V; lastAccess: number }>();
26
+ private readonly _map = new Map<TKey, { value: TValue; lastAccess: number }>();
39
27
 
40
28
  // GC 타이머
41
29
  private _gcTimer?: ReturnType<typeof setInterval>;
@@ -43,8 +31,6 @@ export class LazyGcMap<K, V> {
43
31
  private _isGcRunning = false;
44
32
  // destroy 호출 여부
45
33
  private _isDestroyed = false;
46
- // 인스턴스 식별자 (경고 메시지용)
47
- private readonly _instanceId = Math.random().toString(36).slice(2);
48
34
 
49
35
  /**
50
36
  * @param _options 설정 옵션
@@ -56,11 +42,9 @@ export class LazyGcMap<K, V> {
56
42
  private readonly _options: {
57
43
  gcInterval?: number;
58
44
  expireTime: number;
59
- onExpire?: (key: K, value: V) => void | Promise<void>;
45
+ onExpire?: (key: TKey, value: TValue) => void | Promise<void>;
60
46
  },
61
- ) {
62
- LazyGcMap._registry?.register(this, this._instanceId);
63
- }
47
+ ) {}
64
48
 
65
49
  /** 저장된 항목 수 */
66
50
  get size(): number {
@@ -68,13 +52,13 @@ export class LazyGcMap<K, V> {
68
52
  }
69
53
 
70
54
  /** 키 존재 여부 확인 (접근 시간 갱신 안함) */
71
- has(key: K): boolean {
55
+ has(key: TKey): boolean {
72
56
  if (this._isDestroyed) return false;
73
57
  return this._map.has(key);
74
58
  }
75
59
 
76
60
  /** 값 조회 (접근 시간 갱신됨) */
77
- get(key: K): V | undefined {
61
+ get(key: TKey): TValue | undefined {
78
62
  if (this._isDestroyed) return undefined;
79
63
  const item = this._map.get(key);
80
64
  if (item == null) return undefined;
@@ -85,7 +69,7 @@ export class LazyGcMap<K, V> {
85
69
  }
86
70
 
87
71
  /** 값 저장 (접근 시간 설정 및 GC 타이머 시작) */
88
- set(key: K, value: V): void {
72
+ set(key: TKey, value: TValue): void {
89
73
  if (this._isDestroyed) return;
90
74
  this._map.set(key, { value, lastAccess: Date.now() });
91
75
  // 데이터가 들어왔으므로 GC 타이머 가동
@@ -93,7 +77,7 @@ export class LazyGcMap<K, V> {
93
77
  }
94
78
 
95
79
  /** 항목 삭제 (비었으면 GC 타이머 중지) */
96
- delete(key: K): boolean {
80
+ delete(key: TKey): boolean {
97
81
  if (this._isDestroyed) return false;
98
82
  const result = this._map.delete(key);
99
83
  // 비었으면 타이머 중지
@@ -107,8 +91,6 @@ export class LazyGcMap<K, V> {
107
91
  dispose(): void {
108
92
  if (this._isDestroyed) return;
109
93
  this._isDestroyed = true;
110
- LazyGcMap._registry?.unregister(this);
111
-
112
94
  this._map.clear();
113
95
  this._stopGc();
114
96
  }
@@ -133,7 +115,7 @@ export class LazyGcMap<K, V> {
133
115
  * @param factory 키가 없을 때 값을 생성하는 함수
134
116
  * @returns 기존 값 또는 새로 생성된 값
135
117
  */
136
- getOrCreate(key: K, factory: () => V): V {
118
+ getOrCreate(key: TKey, factory: () => TValue): TValue {
137
119
  if (this._isDestroyed) {
138
120
  throw new Error("LazyGcMap이 이미 dispose되었습니다.");
139
121
  }
@@ -149,7 +131,7 @@ export class LazyGcMap<K, V> {
149
131
  }
150
132
 
151
133
  /** 값들만 순회 (Iterator) */
152
- *values(): IterableIterator<V> {
134
+ *values(): IterableIterator<TValue> {
153
135
  if (this._isDestroyed) return;
154
136
  for (const item of this._map.values()) {
155
137
  yield item.value;
@@ -157,13 +139,13 @@ export class LazyGcMap<K, V> {
157
139
  }
158
140
 
159
141
  /** 키들만 순회 (Iterator) */
160
- *keys(): IterableIterator<K> {
142
+ *keys(): IterableIterator<TKey> {
161
143
  if (this._isDestroyed) return;
162
144
  yield* this._map.keys();
163
145
  }
164
146
 
165
147
  /** 엔트리 순회 (Iterator) */
166
- *entries(): IterableIterator<[K, V]> {
148
+ *entries(): IterableIterator<[TKey, TValue]> {
167
149
  if (this._isDestroyed) return;
168
150
  for (const [key, item] of this._map.entries()) {
169
151
  yield [key, item.value];
@@ -191,7 +173,7 @@ export class LazyGcMap<K, V> {
191
173
  const now = Date.now();
192
174
 
193
175
  // 1. 만료된 항목 수집 (삭제 전)
194
- const expiredEntries: { key: K; item: { value: V; lastAccess: number } }[] = [];
176
+ const expiredEntries: { key: TKey; item: { value: TValue; lastAccess: number } }[] = [];
195
177
  for (const [key, item] of this._map) {
196
178
  if (now - item.lastAccess > this._options.expireTime) {
197
179
  expiredEntries.push({ key, item });
package/src/utils/json.ts CHANGED
@@ -172,7 +172,7 @@ export function jsonStringify(
172
172
  * @security 개발 모드(`__DEV__`)에서만 에러 메시지에 JSON 문자열 전체가 포함된다.
173
173
  * 프로덕션 모드에서는 JSON 길이만 포함된다.
174
174
  */
175
- export function jsonParse<T = unknown>(json: string): T {
175
+ export function jsonParse<TResult = unknown>(json: string): TResult {
176
176
  try {
177
177
  return objNullToUndefined(
178
178
  JSON.parse(json, (_key, value: unknown) => {
@@ -218,7 +218,7 @@ export function jsonParse<T = unknown>(json: string): T {
218
218
 
219
219
  return value;
220
220
  }),
221
- ) as T;
221
+ ) as TResult;
222
222
  } catch (err) {
223
223
  if (env.DEV) {
224
224
  throw new SdError(err, "JSON 파싱 에러: \n" + json);