entity-server-client 0.2.5 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -1
- package/build.mjs +11 -1
- package/docs/api/alimtalk.md +62 -0
- package/docs/api/auth.md +256 -0
- package/docs/api/email.md +37 -0
- package/docs/api/entity.md +273 -0
- package/docs/api/file.md +80 -0
- package/docs/api/health.md +41 -0
- package/docs/api/identity.md +32 -0
- package/docs/api/import.md +34 -0
- package/docs/api/packet.md +25 -0
- package/docs/api/pg.md +90 -0
- package/docs/api/push.md +107 -0
- package/docs/api/react.md +141 -0
- package/docs/api/setup.md +43 -0
- package/docs/api/sms.md +45 -0
- package/docs/api/smtp.md +33 -0
- package/docs/api/transaction.md +50 -0
- package/docs/api/utils.md +52 -0
- package/docs/apis.md +22 -787
- package/docs/react.md +64 -7
- package/package.json +7 -1
- package/src/EntityServerClient.ts +28 -546
- package/src/client/base.ts +305 -0
- package/src/client/packet.ts +16 -52
- package/src/client/request.ts +46 -9
- package/src/client/utils.ts +17 -2
- package/src/hooks/useEntityServer.ts +3 -4
- package/src/mixins/auth.ts +143 -0
- package/src/mixins/entity.ts +205 -0
- package/src/mixins/file.ts +99 -0
- package/src/mixins/push.ts +109 -0
- package/src/mixins/smtp.ts +20 -0
- package/src/mixins/utils.ts +106 -0
- package/src/packet.ts +84 -0
- package/src/types.ts +93 -1
- package/tests/packet.test.mjs +50 -0
- package/dist/EntityServerClient.d.ts +0 -203
- package/dist/client/hmac.d.ts +0 -8
- package/dist/client/packet.d.ts +0 -22
- package/dist/client/request.d.ts +0 -16
- package/dist/client/utils.d.ts +0 -4
- package/dist/hooks/useEntityServer.d.ts +0 -63
- package/dist/index.d.ts +0 -4
- package/dist/index.js +0 -2
- package/dist/index.js.map +0 -7
- package/dist/react.d.ts +0 -1
- package/dist/react.js +0 -2
- package/dist/react.js.map +0 -7
- package/dist/types.d.ts +0 -165
|
@@ -1,546 +1,28 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
// ─── 초기화 & 설정 ────────────────────────────────────────────────────────
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* EntityServerClient 인스턴스를 생성합니다.
|
|
34
|
-
*
|
|
35
|
-
* 기본값:
|
|
36
|
-
* - `baseUrl`: `VITE_ENTITY_SERVER_URL` 또는 `http://localhost:47200`
|
|
37
|
-
* - `packetMagicLen`: `VITE_ENTITY_SERVER_PACKET_MAGIC_LEN` 또는 `4`
|
|
38
|
-
*/
|
|
39
|
-
constructor(options: EntityServerClientOptions = {}) {
|
|
40
|
-
const envBaseUrl = readEnv("VITE_ENTITY_SERVER_URL");
|
|
41
|
-
const envMagicLen = readEnv("VITE_ENTITY_SERVER_PACKET_MAGIC_LEN");
|
|
42
|
-
|
|
43
|
-
this.baseUrl = (
|
|
44
|
-
options.baseUrl ??
|
|
45
|
-
envBaseUrl ??
|
|
46
|
-
"http://localhost:47200"
|
|
47
|
-
).replace(/\/$/, "");
|
|
48
|
-
this.token = options.token ?? "";
|
|
49
|
-
this.apiKey = options.apiKey ?? "";
|
|
50
|
-
this.hmacSecret = options.hmacSecret ?? "";
|
|
51
|
-
this.packetMagicLen =
|
|
52
|
-
options.packetMagicLen ?? (envMagicLen ? Number(envMagicLen) : 4);
|
|
53
|
-
this.encryptRequests = options.encryptRequests ?? false;
|
|
54
|
-
this.keepSession = options.keepSession ?? false;
|
|
55
|
-
this.refreshBuffer = options.refreshBuffer ?? 60;
|
|
56
|
-
this.onTokenRefreshed = options.onTokenRefreshed;
|
|
57
|
-
this.onSessionExpired = options.onSessionExpired;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/** baseUrl, token, packetMagicLen, encryptRequests 값을 런타임에 갱신합니다. */
|
|
61
|
-
configure(options: Partial<EntityServerClientOptions>): void {
|
|
62
|
-
if (options.baseUrl) this.baseUrl = options.baseUrl.replace(/\/$/, "");
|
|
63
|
-
if (typeof options.token === "string") this.token = options.token;
|
|
64
|
-
if (typeof options.packetMagicLen === "number")
|
|
65
|
-
this.packetMagicLen = options.packetMagicLen;
|
|
66
|
-
if (typeof options.encryptRequests === "boolean")
|
|
67
|
-
this.encryptRequests = options.encryptRequests;
|
|
68
|
-
if (typeof options.apiKey === "string") this.apiKey = options.apiKey;
|
|
69
|
-
if (typeof options.hmacSecret === "string")
|
|
70
|
-
this.hmacSecret = options.hmacSecret;
|
|
71
|
-
if (typeof options.keepSession === "boolean")
|
|
72
|
-
this.keepSession = options.keepSession;
|
|
73
|
-
if (typeof options.refreshBuffer === "number")
|
|
74
|
-
this.refreshBuffer = options.refreshBuffer;
|
|
75
|
-
if (options.onTokenRefreshed)
|
|
76
|
-
this.onTokenRefreshed = options.onTokenRefreshed;
|
|
77
|
-
if (options.onSessionExpired)
|
|
78
|
-
this.onSessionExpired = options.onSessionExpired;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/** 인증 요청에 사용할 JWT Access Token을 설정합니다. */
|
|
82
|
-
setToken(token: string): void {
|
|
83
|
-
this.token = token;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/** HMAC 인증용 API Key를 설정합니다. */
|
|
87
|
-
setApiKey(apiKey: string): void {
|
|
88
|
-
this.apiKey = apiKey;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/** HMAC 인증용 시크릿을 설정합니다. */
|
|
92
|
-
setHmacSecret(secret: string): void {
|
|
93
|
-
this.hmacSecret = secret;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/** 암호화 패킷 magic 길이(`packet_magic_len`)를 설정합니다. */
|
|
97
|
-
setPacketMagicLen(length: number): void {
|
|
98
|
-
this.packetMagicLen = length;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/** 현재 암호화 패킷 magic 길이를 반환합니다. */
|
|
102
|
-
getPacketMagicLen(): number {
|
|
103
|
-
return this.packetMagicLen;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// ─── 세션 유지 ────────────────────────────────────────────────────────────
|
|
107
|
-
|
|
108
|
-
/** @internal 자동 토큰 갱신 타이머를 시작합니다. */
|
|
109
|
-
private _scheduleKeepSession(
|
|
110
|
-
refreshToken: string,
|
|
111
|
-
expiresIn: number,
|
|
112
|
-
): void {
|
|
113
|
-
this._clearRefreshTimer();
|
|
114
|
-
this._sessionRefreshToken = refreshToken;
|
|
115
|
-
const delayMs = Math.max((expiresIn - this.refreshBuffer) * 1000, 0);
|
|
116
|
-
this._refreshTimer = setTimeout(async () => {
|
|
117
|
-
if (!this._sessionRefreshToken) return;
|
|
118
|
-
try {
|
|
119
|
-
const result = await this.refreshToken(
|
|
120
|
-
this._sessionRefreshToken,
|
|
121
|
-
);
|
|
122
|
-
this.onTokenRefreshed?.(result.access_token, result.expires_in);
|
|
123
|
-
this._scheduleKeepSession(
|
|
124
|
-
this._sessionRefreshToken,
|
|
125
|
-
result.expires_in,
|
|
126
|
-
);
|
|
127
|
-
} catch (err) {
|
|
128
|
-
this._clearRefreshTimer();
|
|
129
|
-
this.onSessionExpired?.(
|
|
130
|
-
err instanceof Error ? err : new Error(String(err)),
|
|
131
|
-
);
|
|
132
|
-
}
|
|
133
|
-
}, delayMs);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/** @internal 자동 갱신 타이머를 정리합니다. */
|
|
137
|
-
private _clearRefreshTimer(): void {
|
|
138
|
-
if (this._refreshTimer !== null) {
|
|
139
|
-
clearTimeout(this._refreshTimer);
|
|
140
|
-
this._refreshTimer = null;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* 세션 유지 타이머를 중지합니다.
|
|
146
|
-
* `logout()` 호출 시 자동으로 중지되며, 직접 호출이 필요한 경우는 드뭅니다.
|
|
147
|
-
*/
|
|
148
|
-
stopKeepSession(): void {
|
|
149
|
-
this._clearRefreshTimer();
|
|
150
|
-
this._sessionRefreshToken = null;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// ─── 인증 ─────────────────────────────────────────────────────────────────
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* 서버 헬스 체크를 수행하고 패킷 암호화 활성 여부를 자동으로 감지합니다.
|
|
157
|
-
*
|
|
158
|
-
* 서버가 `packet_encryption: true`를 응답하면 이후 모든 요청에 암호화가 자동 적용됩니다.
|
|
159
|
-
*
|
|
160
|
-
* ```ts
|
|
161
|
-
* await client.checkHealth();
|
|
162
|
-
* await client.login(email, password);
|
|
163
|
-
* ```
|
|
164
|
-
*/
|
|
165
|
-
async checkHealth(): Promise<{ ok: boolean; packet_encryption?: boolean }> {
|
|
166
|
-
const res = await fetch(`${this.baseUrl}/v1/health`, {
|
|
167
|
-
signal: AbortSignal.timeout(3000),
|
|
168
|
-
});
|
|
169
|
-
const data = (await res.json()) as {
|
|
170
|
-
ok: boolean;
|
|
171
|
-
packet_encryption?: boolean;
|
|
172
|
-
};
|
|
173
|
-
if (data.packet_encryption) this.encryptRequests = true;
|
|
174
|
-
return data;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/** 로그인 후 `access_token`을 내부 상태에 저장합니다. */
|
|
178
|
-
async login(
|
|
179
|
-
email: string,
|
|
180
|
-
password: string,
|
|
181
|
-
): Promise<{
|
|
182
|
-
access_token: string;
|
|
183
|
-
refresh_token: string;
|
|
184
|
-
expires_in: number;
|
|
185
|
-
}> {
|
|
186
|
-
const data = await this.request<{
|
|
187
|
-
data: {
|
|
188
|
-
access_token: string;
|
|
189
|
-
refresh_token: string;
|
|
190
|
-
expires_in: number;
|
|
191
|
-
};
|
|
192
|
-
}>("POST", "/v1/auth/login", { email, passwd: password }, false);
|
|
193
|
-
this.token = data.data.access_token;
|
|
194
|
-
if (this.keepSession)
|
|
195
|
-
this._scheduleKeepSession(
|
|
196
|
-
data.data.refresh_token,
|
|
197
|
-
data.data.expires_in,
|
|
198
|
-
);
|
|
199
|
-
return data.data;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/** Refresh Token으로 Access Token을 재발급받아 내부 토큰을 교체합니다. */
|
|
203
|
-
async refreshToken(
|
|
204
|
-
refreshToken: string,
|
|
205
|
-
): Promise<{ access_token: string; expires_in: number }> {
|
|
206
|
-
const data = await this.request<{
|
|
207
|
-
data: { access_token: string; expires_in: number };
|
|
208
|
-
}>("POST", "/v1/auth/refresh", { refresh_token: refreshToken }, false);
|
|
209
|
-
this.token = data.data.access_token;
|
|
210
|
-
if (this.keepSession)
|
|
211
|
-
this._scheduleKeepSession(refreshToken, data.data.expires_in);
|
|
212
|
-
return data.data;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* 서버에 로그아웃을 요청하고 내부 토큰을 초기화합니다.
|
|
217
|
-
* refresh_token을 서버에 전달해 무효화합니다.
|
|
218
|
-
*/
|
|
219
|
-
async logout(refreshToken: string): Promise<{ ok: boolean }> {
|
|
220
|
-
this.stopKeepSession();
|
|
221
|
-
const data = await this.request<{ ok: boolean }>(
|
|
222
|
-
"POST",
|
|
223
|
-
"/v1/auth/logout",
|
|
224
|
-
{ refresh_token: refreshToken },
|
|
225
|
-
false,
|
|
226
|
-
);
|
|
227
|
-
this.token = "";
|
|
228
|
-
return data;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// ─── 트랜잭션 ─────────────────────────────────────────────────────────────
|
|
232
|
-
|
|
233
|
-
/** 트랜잭션을 시작하고 활성 트랜잭션 ID를 저장합니다. */
|
|
234
|
-
async transStart(): Promise<string> {
|
|
235
|
-
const res = await this.request<{ ok: boolean; transaction_id: string }>(
|
|
236
|
-
"POST",
|
|
237
|
-
"/v1/transaction/start",
|
|
238
|
-
undefined,
|
|
239
|
-
false,
|
|
240
|
-
);
|
|
241
|
-
this.activeTxId = res.transaction_id;
|
|
242
|
-
return this.activeTxId;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
/** 활성 트랜잭션(또는 전달된 transactionId)을 롤백합니다. */
|
|
246
|
-
transRollback(transactionId?: string): Promise<{ ok: boolean }> {
|
|
247
|
-
const txId = transactionId ?? this.activeTxId;
|
|
248
|
-
if (!txId)
|
|
249
|
-
return Promise.reject(
|
|
250
|
-
new Error("No active transaction. Call transStart() first."),
|
|
251
|
-
);
|
|
252
|
-
this.activeTxId = null;
|
|
253
|
-
return this.request("POST", `/v1/transaction/rollback/${txId}`);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/**
|
|
257
|
-
* 활성 트랜잭션(또는 전달된 transactionId)을 커밋합니다.
|
|
258
|
-
*
|
|
259
|
-
* @returns `results` 배열: commit된 각 작업의 `entity`, `action`, `seq`
|
|
260
|
-
*/
|
|
261
|
-
transCommit(transactionId?: string): Promise<{
|
|
262
|
-
ok: boolean;
|
|
263
|
-
results: Array<{ entity: string; action: string; seq: number }>;
|
|
264
|
-
}> {
|
|
265
|
-
const txId = transactionId ?? this.activeTxId;
|
|
266
|
-
if (!txId)
|
|
267
|
-
return Promise.reject(
|
|
268
|
-
new Error("No active transaction. Call transStart() first."),
|
|
269
|
-
);
|
|
270
|
-
this.activeTxId = null;
|
|
271
|
-
return this.request("POST", `/v1/transaction/commit/${txId}`);
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
// ─── 엔티티 CRUD ──────────────────────────────────────────────────────────
|
|
275
|
-
|
|
276
|
-
/** 시퀀스 ID로 엔티티 단건을 조회합니다. */
|
|
277
|
-
get<T = unknown>(
|
|
278
|
-
entity: string,
|
|
279
|
-
seq: number,
|
|
280
|
-
opts: { skipHooks?: boolean } = {},
|
|
281
|
-
): Promise<{ ok: boolean; data: T }> {
|
|
282
|
-
const q = opts.skipHooks ? "?skipHooks=true" : "";
|
|
283
|
-
return this.request("GET", `/v1/entity/${entity}/${seq}${q}`);
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
/** 조건으로 엔티티 단건을 조회합니다. data 컬럼을 완전히 복호화하여 반환합니다. */
|
|
287
|
-
find<T = unknown>(
|
|
288
|
-
entity: string,
|
|
289
|
-
conditions?: Record<string, unknown>,
|
|
290
|
-
opts: { skipHooks?: boolean } = {},
|
|
291
|
-
): Promise<{ ok: boolean; data: T }> {
|
|
292
|
-
const q = opts.skipHooks ? "?skipHooks=true" : "";
|
|
293
|
-
return this.request(
|
|
294
|
-
"POST",
|
|
295
|
-
`/v1/entity/${entity}/find${q}`,
|
|
296
|
-
conditions ?? {},
|
|
297
|
-
);
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
/** 페이지네이션/정렬/필터 조건으로 엔티티 목록을 조회합니다. */
|
|
301
|
-
list<T = unknown>(
|
|
302
|
-
entity: string,
|
|
303
|
-
params: EntityListParams = {},
|
|
304
|
-
): Promise<{ ok: boolean; data: EntityListResult<T> }> {
|
|
305
|
-
const { conditions, fields, orderDir, orderBy, ...rest } = params;
|
|
306
|
-
const queryObj: Record<string, unknown> = {
|
|
307
|
-
page: 1,
|
|
308
|
-
limit: 20,
|
|
309
|
-
...rest,
|
|
310
|
-
};
|
|
311
|
-
if (orderBy)
|
|
312
|
-
queryObj.orderBy = orderDir === "DESC" ? `-${orderBy}` : orderBy;
|
|
313
|
-
if (fields?.length) queryObj.fields = fields.join(",");
|
|
314
|
-
return this.request(
|
|
315
|
-
"POST",
|
|
316
|
-
`/v1/entity/${entity}/list?${buildQuery(queryObj)}`,
|
|
317
|
-
conditions ?? {},
|
|
318
|
-
);
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
/**
|
|
322
|
-
* 엔티티 총 건수를 조회합니다.
|
|
323
|
-
*
|
|
324
|
-
* @param conditions 필터 조건 (예: `{ status: "active" }`)
|
|
325
|
-
*/
|
|
326
|
-
count(
|
|
327
|
-
entity: string,
|
|
328
|
-
conditions?: Record<string, unknown>,
|
|
329
|
-
): Promise<{ ok: boolean; count: number }> {
|
|
330
|
-
return this.request(
|
|
331
|
-
"POST",
|
|
332
|
-
`/v1/entity/${entity}/count`,
|
|
333
|
-
conditions ?? {},
|
|
334
|
-
);
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
/**
|
|
338
|
-
* 커스텀 SQL로 엔티티를 조회합니다.
|
|
339
|
-
*
|
|
340
|
-
* SELECT 전용이며 인덱스 테이블만 조회 가능합니다. JOIN 지원.
|
|
341
|
-
*/
|
|
342
|
-
query<T = unknown>(
|
|
343
|
-
entity: string,
|
|
344
|
-
req: EntityQueryRequest,
|
|
345
|
-
): Promise<{ ok: boolean; data: { items: T[]; count: number } }> {
|
|
346
|
-
return this.request("POST", `/v1/entity/${entity}/query`, req);
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
/** 엔티티 데이터를 생성/수정(Submit)합니다. `seq`가 없으면 INSERT, 있으면 UPDATE입니다. */
|
|
350
|
-
submit(
|
|
351
|
-
entity: string,
|
|
352
|
-
data: Record<string, unknown>,
|
|
353
|
-
opts: { transactionId?: string; skipHooks?: boolean } = {},
|
|
354
|
-
): Promise<{ ok: boolean; seq: number }> {
|
|
355
|
-
const txId = opts.transactionId ?? this.activeTxId;
|
|
356
|
-
const extraHeaders = txId ? { "X-Transaction-ID": txId } : undefined;
|
|
357
|
-
const q = opts.skipHooks ? "?skipHooks=true" : "";
|
|
358
|
-
return this.request(
|
|
359
|
-
"POST",
|
|
360
|
-
`/v1/entity/${entity}/submit${q}`,
|
|
361
|
-
data,
|
|
362
|
-
true,
|
|
363
|
-
extraHeaders,
|
|
364
|
-
);
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
/** 시퀀스 ID로 엔티티를 삭제합니다(`hard=true`면 하드 삭제, 기본은 소프트 삭제). */
|
|
368
|
-
delete(
|
|
369
|
-
entity: string,
|
|
370
|
-
seq: number,
|
|
371
|
-
opts: {
|
|
372
|
-
transactionId?: string;
|
|
373
|
-
hard?: boolean;
|
|
374
|
-
skipHooks?: boolean;
|
|
375
|
-
} = {},
|
|
376
|
-
): Promise<{ ok: boolean; deleted: number }> {
|
|
377
|
-
const params = new URLSearchParams();
|
|
378
|
-
if (opts.hard) params.set("hard", "true");
|
|
379
|
-
if (opts.skipHooks) params.set("skipHooks", "true");
|
|
380
|
-
const q = params.size ? `?${params}` : "";
|
|
381
|
-
const txId = opts.transactionId ?? this.activeTxId;
|
|
382
|
-
const extraHeaders = txId ? { "X-Transaction-ID": txId } : undefined;
|
|
383
|
-
return this.request(
|
|
384
|
-
"POST",
|
|
385
|
-
`/v1/entity/${entity}/delete/${seq}${q}`,
|
|
386
|
-
undefined,
|
|
387
|
-
true,
|
|
388
|
-
extraHeaders,
|
|
389
|
-
);
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
/** 엔티티 단건의 변경 이력을 조회합니다. */
|
|
393
|
-
history<T = unknown>(
|
|
394
|
-
entity: string,
|
|
395
|
-
seq: number,
|
|
396
|
-
params: Pick<EntityListParams, "page" | "limit"> = {},
|
|
397
|
-
): Promise<{
|
|
398
|
-
ok: boolean;
|
|
399
|
-
data: EntityListResult<EntityHistoryRecord<T>>;
|
|
400
|
-
}> {
|
|
401
|
-
return this.request(
|
|
402
|
-
"GET",
|
|
403
|
-
`/v1/entity/${entity}/history/${seq}?${buildQuery({ page: 1, limit: 50, ...params })}`,
|
|
404
|
-
);
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
/** 특정 이력 시점으로 엔티티를 롤백합니다. */
|
|
408
|
-
rollback(entity: string, historySeq: number): Promise<{ ok: boolean }> {
|
|
409
|
-
return this.request(
|
|
410
|
-
"POST",
|
|
411
|
-
`/v1/entity/${entity}/rollback/${historySeq}`,
|
|
412
|
-
);
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
// ─── 푸시 ─────────────────────────────────────────────────────────────────
|
|
416
|
-
|
|
417
|
-
/** 푸시 관련 엔티티로 payload를 전송(Submit)합니다. */
|
|
418
|
-
push(
|
|
419
|
-
pushEntity: string,
|
|
420
|
-
payload: Record<string, unknown>,
|
|
421
|
-
opts: { transactionId?: string } = {},
|
|
422
|
-
): Promise<{ ok: boolean; seq: number }> {
|
|
423
|
-
return this.submit(pushEntity, payload, opts);
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
/** 푸시 로그 엔티티 목록을 조회합니다. */
|
|
427
|
-
pushLogList<T = unknown>(
|
|
428
|
-
params: EntityListParams = {},
|
|
429
|
-
): Promise<{ ok: boolean; data: EntityListResult<T> }> {
|
|
430
|
-
return this.list<T>("push_log", params);
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
/** 계정의 푸시 디바이스를 등록합니다. */
|
|
434
|
-
registerPushDevice(
|
|
435
|
-
accountSeq: number,
|
|
436
|
-
deviceId: string,
|
|
437
|
-
pushToken: string,
|
|
438
|
-
opts: RegisterPushDeviceOptions = {},
|
|
439
|
-
): Promise<{ ok: boolean; seq: number }> {
|
|
440
|
-
const {
|
|
441
|
-
platform,
|
|
442
|
-
deviceType,
|
|
443
|
-
browser,
|
|
444
|
-
browserVersion,
|
|
445
|
-
pushEnabled = true,
|
|
446
|
-
transactionId,
|
|
447
|
-
} = opts;
|
|
448
|
-
return this.submit(
|
|
449
|
-
"account_device",
|
|
450
|
-
{
|
|
451
|
-
id: deviceId,
|
|
452
|
-
account_seq: accountSeq,
|
|
453
|
-
push_token: pushToken,
|
|
454
|
-
push_enabled: pushEnabled,
|
|
455
|
-
...(platform ? { platform } : {}),
|
|
456
|
-
...(deviceType ? { device_type: deviceType } : {}),
|
|
457
|
-
...(browser ? { browser } : {}),
|
|
458
|
-
...(browserVersion ? { browser_version: browserVersion } : {}),
|
|
459
|
-
},
|
|
460
|
-
{ transactionId },
|
|
461
|
-
);
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
/** 디바이스 레코드의 푸시 토큰을 갱신합니다. */
|
|
465
|
-
updatePushDeviceToken(
|
|
466
|
-
deviceSeq: number,
|
|
467
|
-
pushToken: string,
|
|
468
|
-
opts: { pushEnabled?: boolean; transactionId?: string } = {},
|
|
469
|
-
): Promise<{ ok: boolean; seq: number }> {
|
|
470
|
-
const { pushEnabled = true, transactionId } = opts;
|
|
471
|
-
return this.submit(
|
|
472
|
-
"account_device",
|
|
473
|
-
{
|
|
474
|
-
seq: deviceSeq,
|
|
475
|
-
push_token: pushToken,
|
|
476
|
-
push_enabled: pushEnabled,
|
|
477
|
-
},
|
|
478
|
-
{ transactionId },
|
|
479
|
-
);
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
/** 디바이스의 푸시 수신을 비활성화합니다. */
|
|
483
|
-
disablePushDevice(
|
|
484
|
-
deviceSeq: number,
|
|
485
|
-
opts: { transactionId?: string } = {},
|
|
486
|
-
): Promise<{ ok: boolean; seq: number }> {
|
|
487
|
-
return this.submit(
|
|
488
|
-
"account_device",
|
|
489
|
-
{ seq: deviceSeq, push_enabled: false },
|
|
490
|
-
{ transactionId: opts.transactionId },
|
|
491
|
-
);
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
// ─── 요청 본문 파싱 ───────────────────────────────────────────────────────
|
|
495
|
-
|
|
496
|
-
/**
|
|
497
|
-
* 요청 바디를 파싱합니다.
|
|
498
|
-
* `application/octet-stream`이면 XChaCha20-Poly1305 복호화, 그 외는 JSON 파싱합니다.
|
|
499
|
-
*
|
|
500
|
-
* @param requireEncrypted `true`이면 암호화된 요청만 허용합니다.
|
|
501
|
-
*/
|
|
502
|
-
readRequestBody<T = Record<string, unknown>>(
|
|
503
|
-
body: ArrayBuffer | Uint8Array | string | T | null | undefined,
|
|
504
|
-
contentType = "application/json",
|
|
505
|
-
requireEncrypted = false,
|
|
506
|
-
): T {
|
|
507
|
-
const key = derivePacketKey(this.hmacSecret, this.token);
|
|
508
|
-
return parseRequestBody<T>(
|
|
509
|
-
body,
|
|
510
|
-
contentType,
|
|
511
|
-
requireEncrypted,
|
|
512
|
-
key,
|
|
513
|
-
this.packetMagicLen,
|
|
514
|
-
);
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
// ─── 내부 헬퍼 ───────────────────────────────────────────────────────────
|
|
518
|
-
|
|
519
|
-
private get _reqOpts(): RequestOptions {
|
|
520
|
-
return {
|
|
521
|
-
baseUrl: this.baseUrl,
|
|
522
|
-
token: this.token,
|
|
523
|
-
apiKey: this.apiKey,
|
|
524
|
-
hmacSecret: this.hmacSecret,
|
|
525
|
-
packetMagicLen: this.packetMagicLen,
|
|
526
|
-
encryptRequests: this.encryptRequests,
|
|
527
|
-
};
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
private request<T>(
|
|
531
|
-
method: string,
|
|
532
|
-
path: string,
|
|
533
|
-
body?: unknown,
|
|
534
|
-
withAuth = true,
|
|
535
|
-
extraHeaders?: Record<string, string>,
|
|
536
|
-
): Promise<T> {
|
|
537
|
-
return entityRequest<T>(
|
|
538
|
-
this._reqOpts,
|
|
539
|
-
method,
|
|
540
|
-
path,
|
|
541
|
-
body,
|
|
542
|
-
withAuth,
|
|
543
|
-
extraHeaders,
|
|
544
|
-
);
|
|
545
|
-
}
|
|
546
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* @file EntityServerClient.ts
|
|
3
|
+
* Mixin 패턴으로 구성된 EntityServerClient.
|
|
4
|
+
*
|
|
5
|
+
* 절(section)별 구현:
|
|
6
|
+
* src/client/base.ts — 상태·생성자·공통 헬퍼
|
|
7
|
+
* src/mixins/auth.ts — 인증 (로그인/로그아웃/me/트랜잭션 등)
|
|
8
|
+
* src/mixins/entity.ts — 트랜잭션 & 엔티티 CRUD
|
|
9
|
+
* src/mixins/push.ts — 푸시 디바이스 관리
|
|
10
|
+
* src/mixins/smtp.ts — SMTP 메일 발송
|
|
11
|
+
* src/mixins/file.ts — 파일 스토리지
|
|
12
|
+
* src/mixins/utils.ts — QR코드/바코드/PDF변환
|
|
13
|
+
*/
|
|
14
|
+
import { EntityServerClientBase } from "./client/base";
|
|
15
|
+
import { AuthMixin } from "./mixins/auth";
|
|
16
|
+
import { EntityMixin } from "./mixins/entity";
|
|
17
|
+
import { PushMixin } from "./mixins/push";
|
|
18
|
+
import { SmtpMixin } from "./mixins/smtp";
|
|
19
|
+
import { FileMixin } from "./mixins/file";
|
|
20
|
+
import { UtilsMixin } from "./mixins/utils";
|
|
21
|
+
|
|
22
|
+
// ─── Composed class ───────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
export class EntityServerClient extends UtilsMixin(
|
|
25
|
+
FileMixin(
|
|
26
|
+
SmtpMixin(PushMixin(EntityMixin(AuthMixin(EntityServerClientBase)))),
|
|
27
|
+
),
|
|
28
|
+
) {}
|