@simplysm/core-common 13.0.0-beta.2 → 13.0.0-beta.21
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/common.types.js +4 -4
- package/dist/errors/argument-error.js +1 -1
- package/dist/errors/not-implemented-error.js +1 -1
- package/dist/errors/timeout-error.js +1 -1
- package/dist/extensions/arr-ext.helpers.js +4 -4
- package/dist/extensions/arr-ext.js +9 -9
- package/dist/features/debounce-queue.js +2 -2
- package/dist/features/serial-queue.js +3 -3
- package/dist/index.js +30 -30
- package/dist/types/date-only.js +2 -2
- package/dist/types/date-time.js +2 -2
- package/dist/types/time.js +2 -2
- package/dist/types/uuid.js +1 -1
- package/dist/utils/bytes.js +1 -1
- package/dist/utils/json.js +8 -8
- package/dist/utils/obj.js +5 -5
- package/dist/utils/primitive.js +5 -5
- package/dist/utils/transferable.js +4 -4
- package/dist/utils/wait.js +1 -1
- package/package.json +7 -4
- package/.cache/typecheck-browser.tsbuildinfo +0 -1
- package/.cache/typecheck-node.tsbuildinfo +0 -1
- package/.cache/typecheck-tests-browser.tsbuildinfo +0 -1
- package/.cache/typecheck-tests-node.tsbuildinfo +0 -1
- package/src/common.types.ts +0 -91
- package/src/env.ts +0 -11
- package/src/errors/argument-error.ts +0 -40
- package/src/errors/not-implemented-error.ts +0 -32
- package/src/errors/sd-error.ts +0 -53
- package/src/errors/timeout-error.ts +0 -36
- package/src/extensions/arr-ext.helpers.ts +0 -53
- package/src/extensions/arr-ext.ts +0 -777
- package/src/extensions/arr-ext.types.ts +0 -258
- package/src/extensions/map-ext.ts +0 -86
- package/src/extensions/set-ext.ts +0 -68
- package/src/features/debounce-queue.ts +0 -116
- package/src/features/event-emitter.ts +0 -112
- package/src/features/serial-queue.ts +0 -94
- package/src/globals.ts +0 -12
- package/src/index.ts +0 -55
- package/src/types/date-only.ts +0 -329
- package/src/types/date-time.ts +0 -294
- package/src/types/lazy-gc-map.ts +0 -244
- package/src/types/time.ts +0 -210
- package/src/types/uuid.ts +0 -113
- package/src/utils/bytes.ts +0 -160
- package/src/utils/date-format.ts +0 -239
- package/src/utils/json.ts +0 -230
- package/src/utils/num.ts +0 -97
- package/src/utils/obj.ts +0 -956
- package/src/utils/path.ts +0 -40
- package/src/utils/primitive.ts +0 -33
- package/src/utils/str.ts +0 -252
- package/src/utils/template-strings.ts +0 -132
- package/src/utils/transferable.ts +0 -269
- package/src/utils/wait.ts +0 -40
- package/src/utils/xml.ts +0 -105
- package/src/zip/sd-zip.ts +0 -218
- package/tests/errors/errors.spec.ts +0 -196
- package/tests/extensions/array-extension.spec.ts +0 -790
- package/tests/extensions/map-extension.spec.ts +0 -147
- package/tests/extensions/set-extension.spec.ts +0 -74
- package/tests/types/date-only.spec.ts +0 -636
- package/tests/types/date-time.spec.ts +0 -391
- package/tests/types/lazy-gc-map.spec.ts +0 -692
- package/tests/types/time.spec.ts +0 -559
- package/tests/types/types.spec.ts +0 -55
- package/tests/types/uuid.spec.ts +0 -91
- package/tests/utils/bytes-utils.spec.ts +0 -230
- package/tests/utils/date-format.spec.ts +0 -371
- package/tests/utils/debounce-queue.spec.ts +0 -272
- package/tests/utils/json.spec.ts +0 -475
- package/tests/utils/number.spec.ts +0 -184
- package/tests/utils/object.spec.ts +0 -827
- package/tests/utils/path.spec.ts +0 -78
- package/tests/utils/primitive.spec.ts +0 -55
- package/tests/utils/sd-event-emitter.spec.ts +0 -216
- package/tests/utils/serial-queue.spec.ts +0 -365
- package/tests/utils/string.spec.ts +0 -294
- package/tests/utils/template-strings.spec.ts +0 -96
- package/tests/utils/transferable.spec.ts +0 -698
- package/tests/utils/wait.spec.ts +0 -145
- package/tests/utils/xml.spec.ts +0 -146
- package/tests/zip/sd-zip.spec.ts +0 -234
package/src/types/time.ts
DELETED
|
@@ -1,210 +0,0 @@
|
|
|
1
|
-
import { ArgumentError } from "../errors/argument-error";
|
|
2
|
-
import { convert12To24, formatDate } from "../utils/date-format";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* 시간 클래스 (날짜제외: HH:mm:ss.fff, 불변)
|
|
6
|
-
*
|
|
7
|
-
* 날짜 정보 없이 시간만 저장하는 불변 클래스이다.
|
|
8
|
-
* 24시간을 초과하거나 음수인 경우 자동으로 정규화된다.
|
|
9
|
-
*
|
|
10
|
-
* @example
|
|
11
|
-
* const now = new Time();
|
|
12
|
-
* const specific = new Time(10, 30, 0);
|
|
13
|
-
* const parsed = Time.parse("10:30:00");
|
|
14
|
-
*/
|
|
15
|
-
export class Time {
|
|
16
|
-
private static readonly MS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
17
|
-
|
|
18
|
-
private readonly _tick: number;
|
|
19
|
-
|
|
20
|
-
/** 현재 시간으로 생성 */
|
|
21
|
-
constructor();
|
|
22
|
-
/** 시분초밀리초로 생성 */
|
|
23
|
-
constructor(hour: number, minute: number, second?: number, millisecond?: number);
|
|
24
|
-
/** tick (밀리초)으로 생성 */
|
|
25
|
-
constructor(tick: number);
|
|
26
|
-
/** Date 객체에서 시간 부분만 추출하여 생성 */
|
|
27
|
-
constructor(date: Date);
|
|
28
|
-
constructor(arg1?: number | Date, arg2?: number, arg3?: number, arg4?: number) {
|
|
29
|
-
if (arg1 === undefined) {
|
|
30
|
-
const now = new Date();
|
|
31
|
-
this._tick =
|
|
32
|
-
(now.getMilliseconds() +
|
|
33
|
-
now.getSeconds() * 1000 +
|
|
34
|
-
now.getMinutes() * 60 * 1000 +
|
|
35
|
-
now.getHours() * 60 * 60 * 1000) %
|
|
36
|
-
Time.MS_PER_DAY;
|
|
37
|
-
} else if (arg2 !== undefined) {
|
|
38
|
-
let tick =
|
|
39
|
-
((arg4 ?? 0) + (arg3 ?? 0) * 1000 + arg2 * 60 * 1000 + (arg1 as number) * 60 * 60 * 1000) % Time.MS_PER_DAY;
|
|
40
|
-
if (tick < 0) tick += Time.MS_PER_DAY;
|
|
41
|
-
this._tick = tick;
|
|
42
|
-
} else if (arg1 instanceof Date) {
|
|
43
|
-
this._tick =
|
|
44
|
-
(arg1.getMilliseconds() +
|
|
45
|
-
arg1.getSeconds() * 1000 +
|
|
46
|
-
arg1.getMinutes() * 60 * 1000 +
|
|
47
|
-
arg1.getHours() * 60 * 60 * 1000) %
|
|
48
|
-
Time.MS_PER_DAY;
|
|
49
|
-
} else {
|
|
50
|
-
let tick = arg1 % Time.MS_PER_DAY;
|
|
51
|
-
if (tick < 0) tick += Time.MS_PER_DAY;
|
|
52
|
-
this._tick = tick;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* 문자열을 파싱하여 Time 인스턴스를 생성
|
|
58
|
-
*
|
|
59
|
-
* @param str 시간 문자열
|
|
60
|
-
* @returns 파싱된 Time 인스턴스
|
|
61
|
-
* @throws ArgumentError 지원하지 않는 형식인 경우
|
|
62
|
-
*
|
|
63
|
-
* @example
|
|
64
|
-
* Time.parse("10:30:00") // HH:mm:ss
|
|
65
|
-
* Time.parse("10:30:00.123") // HH:mm:ss.fff
|
|
66
|
-
* Time.parse("오전 10:30:00") // 오전/오후 HH:mm:ss
|
|
67
|
-
* Time.parse("2025-01-15T10:30:00") // ISO 8601 (시간 부분만 추출)
|
|
68
|
-
*/
|
|
69
|
-
static parse(str: string): Time {
|
|
70
|
-
const match1 = /(오전|오후) ([0-9]{1,2}):([0-9]{1,2}):([0-9]{1,2})(\.([0-9]{1,3}))?$/.exec(str);
|
|
71
|
-
if (match1 != null) {
|
|
72
|
-
const rawHour = Number(match1[2]);
|
|
73
|
-
const isPM = match1[1] === "오후";
|
|
74
|
-
const hour = convert12To24(rawHour, isPM);
|
|
75
|
-
return new Time(hour, Number(match1[3]), Number(match1[4]), Number(match1[6] ? match1[6].padEnd(3, "0") : "0"));
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const match2 = /([0-9]{1,2}):([0-9]{1,2}):([0-9]{1,2})(\.([0-9]{1,3}))?$/.exec(str);
|
|
79
|
-
if (match2 != null) {
|
|
80
|
-
return new Time(
|
|
81
|
-
Number(match2[1]),
|
|
82
|
-
Number(match2[2]),
|
|
83
|
-
Number(match2[3]),
|
|
84
|
-
Number(match2[5] ? match2[5].padEnd(3, "0") : "0"),
|
|
85
|
-
);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// ISO 8601 형식 (예: 2025-01-15T10:30:00.123Z, 2025-01-15T10:30:00+09:00)
|
|
89
|
-
// Date 객체를 사용하여 타임존 변환 처리
|
|
90
|
-
const isoMatch = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.exec(str);
|
|
91
|
-
if (isoMatch != null) {
|
|
92
|
-
const date = new Date(str);
|
|
93
|
-
if (!Number.isNaN(date.getTime())) {
|
|
94
|
-
return new Time(date.getHours(), date.getMinutes(), date.getSeconds(), date.getMilliseconds());
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
throw new ArgumentError(
|
|
99
|
-
`시간 형식을 파싱할 수 없습니다. 지원 형식: 'HH:mm:ss', 'HH:mm:ss.fff', '오전/오후 HH:mm:ss', ISO 8601`,
|
|
100
|
-
{ input: str },
|
|
101
|
-
);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
//#region Getters (읽기 전용)
|
|
105
|
-
|
|
106
|
-
get hour(): number {
|
|
107
|
-
return Math.floor(this._tick / (60 * 60 * 1000));
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
get minute(): number {
|
|
111
|
-
return Math.floor(this._tick / (60 * 1000)) % 60;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
get second(): number {
|
|
115
|
-
return Math.floor(this._tick / 1000) % 60;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
get millisecond(): number {
|
|
119
|
-
return this._tick % 1000;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
get tick(): number {
|
|
123
|
-
return this._tick;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/** 시간 세팅이 제대로 되었는지 여부 */
|
|
127
|
-
get isValid(): boolean {
|
|
128
|
-
return !Number.isNaN(this._tick);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
//#endregion
|
|
132
|
-
|
|
133
|
-
//#region 불변 변환 메서드 (새 인스턴스 반환)
|
|
134
|
-
|
|
135
|
-
/** 지정된 시로 새 인스턴스 반환 */
|
|
136
|
-
setHour(hour: number): Time {
|
|
137
|
-
return new Time(hour, this.minute, this.second, this.millisecond);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/** 지정된 분으로 새 인스턴스 반환 */
|
|
141
|
-
setMinute(minute: number): Time {
|
|
142
|
-
return new Time(this.hour, minute, this.second, this.millisecond);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/** 지정된 초로 새 인스턴스 반환 */
|
|
146
|
-
setSecond(second: number): Time {
|
|
147
|
-
return new Time(this.hour, this.minute, second, this.millisecond);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/** 지정된 밀리초로 새 인스턴스 반환 */
|
|
151
|
-
setMillisecond(millisecond: number): Time {
|
|
152
|
-
return new Time(this.hour, this.minute, this.second, millisecond);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
//#endregion
|
|
156
|
-
|
|
157
|
-
//#region 산술 메서드 (새 인스턴스 반환)
|
|
158
|
-
|
|
159
|
-
/** 지정된 시간을 더한 새 인스턴스 반환 (24시간 순환) */
|
|
160
|
-
addHours(hours: number): Time {
|
|
161
|
-
let newTick = (this._tick + hours * 60 * 60 * 1000) % Time.MS_PER_DAY;
|
|
162
|
-
if (newTick < 0) newTick += Time.MS_PER_DAY;
|
|
163
|
-
return new Time(newTick);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/** 지정된 분을 더한 새 인스턴스 반환 (24시간 순환) */
|
|
167
|
-
addMinutes(minutes: number): Time {
|
|
168
|
-
let newTick = (this._tick + minutes * 60 * 1000) % Time.MS_PER_DAY;
|
|
169
|
-
if (newTick < 0) newTick += Time.MS_PER_DAY;
|
|
170
|
-
return new Time(newTick);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/** 지정된 초를 더한 새 인스턴스 반환 (24시간 순환) */
|
|
174
|
-
addSeconds(seconds: number): Time {
|
|
175
|
-
let newTick = (this._tick + seconds * 1000) % Time.MS_PER_DAY;
|
|
176
|
-
if (newTick < 0) newTick += Time.MS_PER_DAY;
|
|
177
|
-
return new Time(newTick);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/** 지정된 밀리초를 더한 새 인스턴스 반환 (24시간 순환) */
|
|
181
|
-
addMilliseconds(milliseconds: number): Time {
|
|
182
|
-
let newTick = (this._tick + milliseconds) % Time.MS_PER_DAY;
|
|
183
|
-
if (newTick < 0) newTick += Time.MS_PER_DAY;
|
|
184
|
-
return new Time(newTick);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
//#endregion
|
|
188
|
-
|
|
189
|
-
//#region 포맷팅
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* 지정된 포맷으로 문자열 변환
|
|
193
|
-
* @param format 포맷 문자열
|
|
194
|
-
* @see dtFormat 지원 포맷 문자열 참조
|
|
195
|
-
*/
|
|
196
|
-
toFormatString(formatStr: string): string {
|
|
197
|
-
return formatDate(formatStr, {
|
|
198
|
-
hour: this.hour,
|
|
199
|
-
minute: this.minute,
|
|
200
|
-
second: this.second,
|
|
201
|
-
millisecond: this.millisecond,
|
|
202
|
-
});
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
toString(): string {
|
|
206
|
-
return this.toFormatString("HH:mm:ss.fff");
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
//#endregion
|
|
210
|
-
}
|
package/src/types/uuid.ts
DELETED
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
import type { Bytes } from "../common.types";
|
|
2
|
-
import { ArgumentError } from "../errors/argument-error";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* UUID v4 클래스
|
|
6
|
-
*
|
|
7
|
-
* crypto.getRandomValues 기반으로 암호학적으로 안전한 UUID를 생성한다. (Chrome 79+, Node.js 공용)
|
|
8
|
-
*
|
|
9
|
-
* @example
|
|
10
|
-
* const id = Uuid.new();
|
|
11
|
-
* const fromStr = new Uuid("550e8400-e29b-41d4-a716-446655440000");
|
|
12
|
-
*/
|
|
13
|
-
export class Uuid {
|
|
14
|
-
// 0x00 ~ 0xFF에 대한 hex 문자열 미리 계산 (256개)
|
|
15
|
-
private static readonly _hexTable: string[] = Array.from({ length: 256 }, (_, i) => i.toString(16).padStart(2, "0"));
|
|
16
|
-
|
|
17
|
-
private static readonly _uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
18
|
-
|
|
19
|
-
/** 16바이트 배열을 UUID 문자열로 변환 */
|
|
20
|
-
private static _bytesToUuidStr(bytes: Uint8Array): string {
|
|
21
|
-
const h = Uuid._hexTable;
|
|
22
|
-
return (
|
|
23
|
-
h[bytes[0]] +
|
|
24
|
-
h[bytes[1]] +
|
|
25
|
-
h[bytes[2]] +
|
|
26
|
-
h[bytes[3]] +
|
|
27
|
-
"-" +
|
|
28
|
-
h[bytes[4]] +
|
|
29
|
-
h[bytes[5]] +
|
|
30
|
-
"-" +
|
|
31
|
-
h[bytes[6]] +
|
|
32
|
-
h[bytes[7]] +
|
|
33
|
-
"-" +
|
|
34
|
-
h[bytes[8]] +
|
|
35
|
-
h[bytes[9]] +
|
|
36
|
-
"-" +
|
|
37
|
-
h[bytes[10]] +
|
|
38
|
-
h[bytes[11]] +
|
|
39
|
-
h[bytes[12]] +
|
|
40
|
-
h[bytes[13]] +
|
|
41
|
-
h[bytes[14]] +
|
|
42
|
-
h[bytes[15]]
|
|
43
|
-
);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/** 새 UUID v4 인스턴스 생성 */
|
|
47
|
-
static new(): Uuid {
|
|
48
|
-
const bytes = new Uint8Array(16);
|
|
49
|
-
crypto.getRandomValues(bytes);
|
|
50
|
-
|
|
51
|
-
// UUID v4 설정
|
|
52
|
-
bytes[6] = (bytes[6] & 0x0f) | 0x40;
|
|
53
|
-
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
|
54
|
-
|
|
55
|
-
return new Uuid(Uuid._bytesToUuidStr(bytes));
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* 16바이트 Uint8Array에서 UUID 생성
|
|
60
|
-
* @param bytes 16바이트 배열
|
|
61
|
-
* @throws {ArgumentError} 바이트 크기가 16이 아닌 경우
|
|
62
|
-
*/
|
|
63
|
-
static fromBytes(bytes: Bytes): Uuid {
|
|
64
|
-
if (bytes.length !== 16) {
|
|
65
|
-
throw new ArgumentError("UUID 바이트 크기는 16이어야 합니다.", { length: bytes.length });
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return new Uuid(Uuid._bytesToUuidStr(bytes));
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
private readonly _uuid: string;
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* @param uuid UUID 문자열 (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx 형식)
|
|
75
|
-
* @throws {ArgumentError} 형식이 올바르지 않은 경우
|
|
76
|
-
*/
|
|
77
|
-
constructor(uuid: string) {
|
|
78
|
-
if (!Uuid._uuidRegex.test(uuid)) {
|
|
79
|
-
throw new ArgumentError("UUID 형식이 올바르지 않습니다.", { uuid });
|
|
80
|
-
}
|
|
81
|
-
this._uuid = uuid;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/** UUID를 문자열로 변환 */
|
|
85
|
-
toString(): string {
|
|
86
|
-
return this._uuid;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/** UUID를 16바이트 Uint8Array로 변환 */
|
|
90
|
-
toBytes(): Bytes {
|
|
91
|
-
const u = this._uuid;
|
|
92
|
-
// UUID 형식: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx (8-4-4-4-12)
|
|
93
|
-
// 하이픈 위치: 8, 13, 18, 23
|
|
94
|
-
return new Uint8Array([
|
|
95
|
-
Number.parseInt(u.substring(0, 2), 16),
|
|
96
|
-
Number.parseInt(u.substring(2, 4), 16),
|
|
97
|
-
Number.parseInt(u.substring(4, 6), 16),
|
|
98
|
-
Number.parseInt(u.substring(6, 8), 16),
|
|
99
|
-
Number.parseInt(u.substring(9, 11), 16),
|
|
100
|
-
Number.parseInt(u.substring(11, 13), 16),
|
|
101
|
-
Number.parseInt(u.substring(14, 16), 16),
|
|
102
|
-
Number.parseInt(u.substring(16, 18), 16),
|
|
103
|
-
Number.parseInt(u.substring(19, 21), 16),
|
|
104
|
-
Number.parseInt(u.substring(21, 23), 16),
|
|
105
|
-
Number.parseInt(u.substring(24, 26), 16),
|
|
106
|
-
Number.parseInt(u.substring(26, 28), 16),
|
|
107
|
-
Number.parseInt(u.substring(28, 30), 16),
|
|
108
|
-
Number.parseInt(u.substring(30, 32), 16),
|
|
109
|
-
Number.parseInt(u.substring(32, 34), 16),
|
|
110
|
-
Number.parseInt(u.substring(34, 36), 16),
|
|
111
|
-
]);
|
|
112
|
-
}
|
|
113
|
-
}
|
package/src/utils/bytes.ts
DELETED
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
import type { Bytes } from "../common.types";
|
|
2
|
-
import { ArgumentError } from "../errors/argument-error";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Uint8Array 유틸리티 함수 (복잡한 연산만)
|
|
6
|
-
*
|
|
7
|
-
* 기능:
|
|
8
|
-
* - bytesConcat: 여러 Uint8Array 연결
|
|
9
|
-
* - bytesToHex: Uint8Array를 hex 문자열로 변환
|
|
10
|
-
* - bytesFromHex: hex 문자열을 Uint8Array로 변환
|
|
11
|
-
* - bytesToBase64: Uint8Array를 base64 문자열로 변환
|
|
12
|
-
* - bytesFromBase64: base64 문자열을 Uint8Array로 변환
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
/** hex 변환용 룩업 테이블 (성능 최적화) */
|
|
16
|
-
const hexTable: string[] = Array.from({ length: 256 }, (_, i) => i.toString(16).padStart(2, "0"));
|
|
17
|
-
|
|
18
|
-
/** base64 인코딩 테이블 */
|
|
19
|
-
const BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
20
|
-
|
|
21
|
-
/** base64 디코딩 룩업 테이블 (O(1) 조회, 모든 바이트 값 커버) */
|
|
22
|
-
const BASE64_LOOKUP: number[] = Array.from({ length: 256 }, (_, i) => {
|
|
23
|
-
const idx = BASE64_CHARS.indexOf(String.fromCharCode(i));
|
|
24
|
-
return idx === -1 ? 0 : idx;
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* 여러 Uint8Array 연결
|
|
29
|
-
* @param arrays 연결할 Uint8Array 배열
|
|
30
|
-
* @returns 연결된 새 Uint8Array
|
|
31
|
-
* @example
|
|
32
|
-
* const a = new Uint8Array([1, 2]);
|
|
33
|
-
* const b = new Uint8Array([3, 4]);
|
|
34
|
-
* bytesConcat([a, b]);
|
|
35
|
-
* // Uint8Array([1, 2, 3, 4])
|
|
36
|
-
*/
|
|
37
|
-
export function bytesConcat(arrays: Bytes[]): Bytes {
|
|
38
|
-
const total = arrays.reduce((sum, arr) => sum + arr.length, 0);
|
|
39
|
-
const result = new Uint8Array(total);
|
|
40
|
-
let offset = 0;
|
|
41
|
-
for (const arr of arrays) {
|
|
42
|
-
result.set(arr, offset);
|
|
43
|
-
offset += arr.length;
|
|
44
|
-
}
|
|
45
|
-
return result;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* hex 문자열로 변환
|
|
50
|
-
* @param bytes 변환할 Uint8Array
|
|
51
|
-
* @returns 소문자 hex 문자열
|
|
52
|
-
* @example
|
|
53
|
-
* bytesToHex(new Uint8Array([255, 0, 127]));
|
|
54
|
-
* // "ff007f"
|
|
55
|
-
*/
|
|
56
|
-
export function bytesToHex(bytes: Bytes): string {
|
|
57
|
-
const h = hexTable;
|
|
58
|
-
let result = "";
|
|
59
|
-
for (let i = 0; i < bytes.length; i++) {
|
|
60
|
-
result += h[bytes[i]];
|
|
61
|
-
}
|
|
62
|
-
return result;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* hex 문자열에서 Uint8Array로 변환
|
|
67
|
-
* @param hex 변환할 hex 문자열 (소문자/대문자 모두 허용)
|
|
68
|
-
* @returns 변환된 Uint8Array
|
|
69
|
-
* @throws {ArgumentError} 홀수 길이 또는 유효하지 않은 hex 문자가 포함된 경우
|
|
70
|
-
* @example
|
|
71
|
-
* bytesFromHex("ff007f");
|
|
72
|
-
* // Uint8Array([255, 0, 127])
|
|
73
|
-
*/
|
|
74
|
-
export function bytesFromHex(hex: string): Bytes {
|
|
75
|
-
if (hex.length % 2 !== 0) {
|
|
76
|
-
throw new ArgumentError("hex 문자열은 짝수 길이여야 합니다", { hex });
|
|
77
|
-
}
|
|
78
|
-
if (hex.length > 0 && !/^[0-9a-fA-F]+$/.test(hex)) {
|
|
79
|
-
throw new ArgumentError("유효하지 않은 hex 문자가 포함되어 있습니다", { hex });
|
|
80
|
-
}
|
|
81
|
-
const bytes = new Uint8Array(hex.length / 2);
|
|
82
|
-
for (let i = 0; i < bytes.length; i++) {
|
|
83
|
-
bytes[i] = Number.parseInt(hex.substring(i * 2, i * 2 + 2), 16);
|
|
84
|
-
}
|
|
85
|
-
return bytes;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Bytes를 base64 문자열로 변환
|
|
90
|
-
* @param bytes 변환할 Uint8Array
|
|
91
|
-
* @returns base64 인코딩된 문자열
|
|
92
|
-
* @example
|
|
93
|
-
* bytesToBase64(new Uint8Array([72, 101, 108, 108, 111]));
|
|
94
|
-
* // "SGVsbG8="
|
|
95
|
-
*/
|
|
96
|
-
export function bytesToBase64(bytes: Bytes): string {
|
|
97
|
-
if (bytes.length === 0) {
|
|
98
|
-
return "";
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
let result = "";
|
|
102
|
-
const len = bytes.length;
|
|
103
|
-
for (let i = 0; i < len; i += 3) {
|
|
104
|
-
const b1 = bytes[i];
|
|
105
|
-
const b2 = i + 1 < len ? bytes[i + 1] : 0;
|
|
106
|
-
const b3 = i + 2 < len ? bytes[i + 2] : 0;
|
|
107
|
-
result += BASE64_CHARS[b1 >> 2];
|
|
108
|
-
result += BASE64_CHARS[((b1 & 3) << 4) | (b2 >> 4)];
|
|
109
|
-
result += i + 1 < len ? BASE64_CHARS[((b2 & 15) << 2) | (b3 >> 6)] : "=";
|
|
110
|
-
result += i + 2 < len ? BASE64_CHARS[b3 & 63] : "=";
|
|
111
|
-
}
|
|
112
|
-
return result;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* base64 문자열을 Bytes로 변환
|
|
117
|
-
* @param base64 변환할 base64 문자열
|
|
118
|
-
* @returns 디코딩된 Uint8Array
|
|
119
|
-
* @throws {ArgumentError} 유효하지 않은 base64 문자가 포함된 경우
|
|
120
|
-
* @example
|
|
121
|
-
* bytesFromBase64("SGVsbG8=");
|
|
122
|
-
* // Uint8Array([72, 101, 108, 108, 111])
|
|
123
|
-
*/
|
|
124
|
-
export function bytesFromBase64(base64: string): Bytes {
|
|
125
|
-
// 공백 제거 및 패딩 정규화
|
|
126
|
-
const cleanBase64 = base64.replace(/\s/g, "").replace(/=+$/, "");
|
|
127
|
-
|
|
128
|
-
// 빈 문자열 처리
|
|
129
|
-
if (cleanBase64.length === 0) {
|
|
130
|
-
return new Uint8Array(0);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// 유효성 검사: 문자
|
|
134
|
-
if (!/^[A-Za-z0-9+/]+$/.test(cleanBase64)) {
|
|
135
|
-
throw new ArgumentError("유효하지 않은 base64 문자가 포함되어 있습니다", { base64: base64.substring(0, 20) });
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// 유효성 검사: 길이 (패딩 제거 후 나머지가 1이면 유효하지 않음)
|
|
139
|
-
if (cleanBase64.length % 4 === 1) {
|
|
140
|
-
throw new ArgumentError("유효하지 않은 base64 길이입니다", { length: cleanBase64.length });
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
const len = cleanBase64.length;
|
|
144
|
-
const byteLen = Math.floor((len * 3) / 4);
|
|
145
|
-
const bytes = new Uint8Array(byteLen);
|
|
146
|
-
|
|
147
|
-
let byteIdx = 0;
|
|
148
|
-
for (let i = 0; i < len; i += 4) {
|
|
149
|
-
const c1 = BASE64_LOOKUP[cleanBase64.charCodeAt(i)];
|
|
150
|
-
const c2 = i + 1 < len ? BASE64_LOOKUP[cleanBase64.charCodeAt(i + 1)] : 0;
|
|
151
|
-
const c3 = i + 2 < len ? BASE64_LOOKUP[cleanBase64.charCodeAt(i + 2)] : 0;
|
|
152
|
-
const c4 = i + 3 < len ? BASE64_LOOKUP[cleanBase64.charCodeAt(i + 3)] : 0;
|
|
153
|
-
|
|
154
|
-
bytes[byteIdx++] = (c1 << 2) | (c2 >> 4);
|
|
155
|
-
if (byteIdx < byteLen) bytes[byteIdx++] = ((c2 & 15) << 4) | (c3 >> 2);
|
|
156
|
-
if (byteIdx < byteLen) bytes[byteIdx++] = ((c3 & 3) << 6) | c4;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
return bytes;
|
|
160
|
-
}
|