entity-server-client 0.1.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/LICENSE +21 -0
- package/README.md +51 -0
- package/build.mjs +26 -0
- package/dist/hooks/useEntityServer.d.ts +12 -0
- package/dist/index.d.ts +272 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +7 -0
- package/dist/react.d.ts +1 -0
- package/dist/react.js +2 -0
- package/dist/react.js.map +7 -0
- package/docs/apis.md +505 -0
- package/docs/react.md +80 -0
- package/package.json +38 -0
- package/src/hooks/useEntityServer.ts +50 -0
- package/src/index.ts +597 -0
- package/src/react.ts +1 -0
- package/tsconfig.json +14 -0
package/docs/apis.md
ADDED
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
# 함수별 사용법
|
|
2
|
+
|
|
3
|
+
## 목차
|
|
4
|
+
|
|
5
|
+
| 구분 | 바로가기 |
|
|
6
|
+
| ------------------ | ---------------------------------------- |
|
|
7
|
+
| import | [import](#import) |
|
|
8
|
+
| 인스턴스 생성/설정 | [인스턴스 생성/설정](#인스턴스-생성설정) |
|
|
9
|
+
| 인증 | [인증](#인증) |
|
|
10
|
+
| 트랜잭션 | [트랜잭션](#트랜잭션) |
|
|
11
|
+
| 엔티티 CRUD / 조회 | [엔티티 CRUD / 조회](#엔티티-crud--조회) |
|
|
12
|
+
| 푸시 관련 | [푸시 관련](#푸시-관련) |
|
|
13
|
+
| 암호화 패킷 처리 | [암호화 패킷 처리](#암호화-패킷-처리) |
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## import
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import {
|
|
21
|
+
EntityServerClient,
|
|
22
|
+
entityServer,
|
|
23
|
+
type EntityListParams,
|
|
24
|
+
type EntityListResult,
|
|
25
|
+
type EntityHistoryRecord,
|
|
26
|
+
type EntityQueryRequest,
|
|
27
|
+
type RegisterPushDeviceOptions,
|
|
28
|
+
type EntityServerClientOptions,
|
|
29
|
+
} from "entity-server-client";
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
React Hook:
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import { useEntityServer } from "entity-server-client/react";
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## 인스턴스 생성/설정
|
|
41
|
+
|
|
42
|
+
### `new EntityServerClient(options?)`
|
|
43
|
+
|
|
44
|
+
| 옵션 | 타입 | 기본값 | 설명 |
|
|
45
|
+
| ---------------- | -------- | ------------------------------------------------------ | ----------------------------- |
|
|
46
|
+
| `baseUrl` | `string` | `VITE_ENTITY_SERVER_URL` 또는 `http://localhost:47200` | 서버 주소 |
|
|
47
|
+
| `token` | `string` | `""` | JWT Access Token |
|
|
48
|
+
| `packetMagicLen` | `number` | `VITE_PACKET_MAGIC_LEN` 또는 `4` | 암호화 패킷 magic 바이트 길이 |
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
// 직접 생성
|
|
52
|
+
const client = new EntityServerClient({
|
|
53
|
+
baseUrl: "https://api.example.com",
|
|
54
|
+
token: "eyJhbGciOi...",
|
|
55
|
+
packetMagicLen: 4,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// 싱글톤 (환경변수 자동 읽기)
|
|
59
|
+
import { entityServer } from "entity-server-client";
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### `configure(options)`
|
|
63
|
+
|
|
64
|
+
런타임에 설정을 갱신합니다.
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
client.configure({
|
|
68
|
+
baseUrl: "https://api.example.com",
|
|
69
|
+
token: "new-access-token",
|
|
70
|
+
packetMagicLen: 6,
|
|
71
|
+
});
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### `setToken(token)` / `setPacketMagicLen(length)` / `getPacketMagicLen()`
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
client.setToken("eyJhbGciOi...");
|
|
78
|
+
client.setPacketMagicLen(4);
|
|
79
|
+
const len = client.getPacketMagicLen(); // 4
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## 인증
|
|
85
|
+
|
|
86
|
+
### `login(email, password)`
|
|
87
|
+
|
|
88
|
+
이메일 + 비밀번호로 로그인합니다. 성공 시 내부 토큰이 자동으로 설정됩니다.
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
const auth = await client.login("admin@example.com", "password");
|
|
92
|
+
// auth.access_token — 이후 요청에 자동 사용됨
|
|
93
|
+
// auth.refresh_token — 만료 후 재발급에 사용
|
|
94
|
+
// auth.expires_in — 만료까지 남은 초
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### `refreshToken(refreshToken)`
|
|
98
|
+
|
|
99
|
+
Refresh Token으로 Access Token을 재발급받습니다. 성공 시 내부 토큰이 자동 교체됩니다.
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
const result = await client.refreshToken(auth.refresh_token);
|
|
103
|
+
// result.access_token
|
|
104
|
+
// result.expires_in
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## 트랜잭션
|
|
110
|
+
|
|
111
|
+
여러 submit/delete를 하나의 DB 트랜잭션으로 묶습니다.
|
|
112
|
+
트랜잭션은 **5분 TTL**을 가집니다. 이 안에 commit 또는 rollback을 해야 합니다.
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
transStart → submit/delete (여러 개) → transCommit
|
|
116
|
+
└→ transRollback (실패 시)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### `transStart()`
|
|
120
|
+
|
|
121
|
+
트랜잭션을 시작하고 내부 활성 트랜잭션 ID를 저장합니다.
|
|
122
|
+
이후 `submit()`/`delete()`는 `X-Transaction-ID` 헤더를 자동 포함합니다.
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
const txId = await client.transStart();
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### `transCommit(transactionId?)`
|
|
129
|
+
|
|
130
|
+
큐에 쌓인 모든 작업을 단일 DB 트랜잭션으로 일괄 실행합니다.
|
|
131
|
+
하나라도 실패하면 전체 ROLLBACK됩니다.
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
await client.transStart();
|
|
135
|
+
await client.submit("order", { product_seq: 1, qty: 2 });
|
|
136
|
+
await client.submit("inventory", { seq: 1, stock: 48 });
|
|
137
|
+
const result = await client.transCommit();
|
|
138
|
+
// result.results → [{ entity: "order", action: "submit", seq: 55 }, ...]
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
> **트랜잭션 중 submit 응답**: commit 전에는 `{ ok: true, queued: true, seq: "$tx.0" }` 형태의
|
|
142
|
+
> placeholder가 반환됩니다. `$tx.0`, `$tx.1` 값은 commit 시 실제 seq로 자동 치환되므로
|
|
143
|
+
> 후속 submit의 외래키 값으로 그대로 사용할 수 있습니다.
|
|
144
|
+
|
|
145
|
+
### `transRollback(transactionId?)`
|
|
146
|
+
|
|
147
|
+
- 아직 commit 전 (큐에 남아있음): 큐를 버립니다. DB에 아무 변경 없음.
|
|
148
|
+
- 이미 commit 후: history 기반으로 전체 되돌립니다.
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
try {
|
|
152
|
+
await client.transStart();
|
|
153
|
+
await client.submit("order", { ... });
|
|
154
|
+
await client.transCommit();
|
|
155
|
+
} catch (e) {
|
|
156
|
+
await client.transRollback(); // 활성 트랜잭션 자동 참조
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## 엔티티 CRUD / 조회
|
|
163
|
+
|
|
164
|
+
### `get(entity, seq, opts?)`
|
|
165
|
+
|
|
166
|
+
시퀀스 ID로 엔티티 단건을 조회합니다.
|
|
167
|
+
|
|
168
|
+
| 옵션 | 타입 | 설명 |
|
|
169
|
+
| ----------- | --------- | ----------------------------------------- |
|
|
170
|
+
| `skipHooks` | `boolean` | `true`이면 `after_get` 훅을 실행하지 않음 |
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
const result = await client.get<Account>("account", 1);
|
|
174
|
+
result.data; // Account 타입 객체
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
### `list(entity, params?)`
|
|
180
|
+
|
|
181
|
+
페이지네이션/정렬/필터 조건으로 엔티티 목록을 조회합니다.
|
|
182
|
+
|
|
183
|
+
| 파라미터 | 타입 | 기본값 | 설명 |
|
|
184
|
+
| ------------ | ------------------------- | ------- | --------------------------------------------------- |
|
|
185
|
+
| `page` | `number` | `1` | 페이지 번호 |
|
|
186
|
+
| `limit` | `number` | `20` | 페이지당 레코드 수 (최대 1000) |
|
|
187
|
+
| `orderBy` | `string` | — | 정렬 기준 필드명. `-` 접두사로 내림차순 지정 가능 |
|
|
188
|
+
| `orderDir` | `"ASC" \| "DESC"` | `"ASC"` | 정렬 방향 (`orderBy: "-field"`와 동일 효과) |
|
|
189
|
+
| `fields` | `string[]` | — | 반환할 필드 목록 (**미지정 시 인덱스 필드만 반환**) |
|
|
190
|
+
| `conditions` | `Record<string, unknown>` | — | 필터 조건 |
|
|
191
|
+
|
|
192
|
+
#### `fields` — 반환 필드 지정
|
|
193
|
+
|
|
194
|
+
`fields`는 `entity.json`의 `index`로 선언된 필드명만 지정할 수 있습니다.
|
|
195
|
+
존재하지 않는 필드를 지정하면 서버 에러가 발생합니다.
|
|
196
|
+
|
|
197
|
+
| 값 | 설명 |
|
|
198
|
+
| ------------------- | -------------------------------------------------------------- |
|
|
199
|
+
| 미지정 (기본값) | 인덱스 선언 필드만 반환. **복호화를 건너뛰어 가장 빠름** |
|
|
200
|
+
| `["*"]` | 전체 필드 반환 (복호화 수행) |
|
|
201
|
+
| `["name", "email"]` | 지정한 인덱스 필드만 반환. 모두 인덱스 필드면 역시 복호화 생략 |
|
|
202
|
+
|
|
203
|
+
> `seq`, `created_time`, `updated_time`, `license_seq`는 `fields` 지정 여부와 무관하게 항상 포함됩니다.
|
|
204
|
+
|
|
205
|
+
#### `conditions` — 필터 조건
|
|
206
|
+
|
|
207
|
+
인덱스 테이블의 필드(`index` / `hash` / `unique`로 선언된 필드)에만 조건을 걸 수 있습니다.
|
|
208
|
+
인덱스에 없는 필드로 필터를 걸면 동작하지 않거나 에러가 발생합니다.
|
|
209
|
+
|
|
210
|
+
```ts
|
|
211
|
+
// 기본값 (인덱스 필드만, 가장 빠름)
|
|
212
|
+
const result = await client.list("account");
|
|
213
|
+
result.data.items; // 레코드 배열
|
|
214
|
+
result.data.total; // 필터 조건 일치 전체 건수
|
|
215
|
+
result.data.page; // 현재 페이지
|
|
216
|
+
result.data.limit; // 페이지당 레코드 수
|
|
217
|
+
|
|
218
|
+
// 정렬 + 필터 + 필드 선택
|
|
219
|
+
const result = await client.list("account", {
|
|
220
|
+
page: 1,
|
|
221
|
+
limit: 20,
|
|
222
|
+
orderBy: "created_time",
|
|
223
|
+
orderDir: "DESC",
|
|
224
|
+
fields: ["seq", "name", "email"], // index 필드만 → 복호화 생략
|
|
225
|
+
conditions: { status: "active" },
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// 전체 필드 반환 (fields: ["*"])
|
|
229
|
+
const full = await client.list("account", {
|
|
230
|
+
fields: ["*"],
|
|
231
|
+
conditions: { status: "active" },
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// orderBy에 - 접두사로 내림차순 (orderDir: "DESC"와 동일)
|
|
235
|
+
const desc = await client.list("account", { orderBy: "-created_time" });
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
### `count(entity, conditions?)`
|
|
241
|
+
|
|
242
|
+
레코드 건수를 조회합니다. `conditions`는 `list()`와 동일한 필터 규칙을 따릅니다.
|
|
243
|
+
|
|
244
|
+
```ts
|
|
245
|
+
const total = await client.count("account");
|
|
246
|
+
total.count; // 전체 건수
|
|
247
|
+
|
|
248
|
+
const active = await client.count("account", { status: "active" });
|
|
249
|
+
active.count; // 조건 일치 건수
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
### `query(entity, req)`
|
|
255
|
+
|
|
256
|
+
커스텀 SQL로 엔티티를 조회합니다.
|
|
257
|
+
|
|
258
|
+
**제약사항**:
|
|
259
|
+
|
|
260
|
+
- SELECT 쿼리만 허용 (INSERT/UPDATE/DELETE 불가)
|
|
261
|
+
- 인덱스 테이블(`entity_idx_*`)만 접근 가능. 암호화된 본문 필드는 조회 불가
|
|
262
|
+
- `SELECT *` 불가. 인덱스 선언 필드만 SELECT 가능
|
|
263
|
+
- 최대 반환 건수: 1000
|
|
264
|
+
|
|
265
|
+
`entity`는 URL 라우트 경로용 기본 엔티티명으로, 실제 조회 대상은 SQL에서 결정됩니다.
|
|
266
|
+
|
|
267
|
+
| 필드 | 타입 | 설명 |
|
|
268
|
+
| -------- | ----------- | ----------------------------------------------- |
|
|
269
|
+
| `sql` | `string` | SELECT SQL문. 사용자 입력은 반드시 `?`로 바인딩 |
|
|
270
|
+
| `params` | `unknown[]` | `?` 플레이스홀더에 바인딩할 값 배열 |
|
|
271
|
+
| `limit` | `number` | 최대 반환 건수 (최대 1000) |
|
|
272
|
+
|
|
273
|
+
응답: `{ ok: true, data: { items: T[], count: number } }`
|
|
274
|
+
|
|
275
|
+
```ts
|
|
276
|
+
// 단일 엔티티
|
|
277
|
+
const result = await client.query("account", {
|
|
278
|
+
sql: "SELECT seq, name, email FROM account WHERE status = ?",
|
|
279
|
+
params: ["active"],
|
|
280
|
+
limit: 50,
|
|
281
|
+
});
|
|
282
|
+
result.data.items; // 레코드 배열
|
|
283
|
+
result.data.count; // 반환된 건수
|
|
284
|
+
|
|
285
|
+
// JOIN으로 여러 엔티티 조합
|
|
286
|
+
const joined = await client.query("order", {
|
|
287
|
+
sql: `
|
|
288
|
+
SELECT o.seq, o.status, u.name AS user_name, u.email
|
|
289
|
+
FROM order o
|
|
290
|
+
JOIN account u ON u.data_seq = o.account_seq
|
|
291
|
+
WHERE o.status = ?
|
|
292
|
+
ORDER BY o.seq DESC
|
|
293
|
+
`,
|
|
294
|
+
params: ["pending"],
|
|
295
|
+
limit: 100,
|
|
296
|
+
});
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
> SQL Injection 방지: 사용자 입력값은 반드시 `params`로 바인딩하세요.
|
|
300
|
+
|
|
301
|
+
---
|
|
302
|
+
|
|
303
|
+
### `submit(entity, data, opts?)`
|
|
304
|
+
|
|
305
|
+
엔티티를 생성 또는 수정합니다.
|
|
306
|
+
|
|
307
|
+
- `data`에 `seq`가 **없으면** INSERT
|
|
308
|
+
- `data`에 `seq`가 **있으면** UPDATE
|
|
309
|
+
- `unique` 선언 필드 기준 중복 감지 시 자동 UPDATE (upsert)
|
|
310
|
+
|
|
311
|
+
응답의 `seq`는 생성/수정된 레코드의 시퀀스 ID입니다.
|
|
312
|
+
|
|
313
|
+
| 옵션 | 타입 | 설명 |
|
|
314
|
+
| --------------- | --------- | ----------------------------------------------------------------- |
|
|
315
|
+
| `transactionId` | `string` | 수동 트랜잭션 ID. 미지정 시 활성 트랜잭션 자동 사용 |
|
|
316
|
+
| `skipHooks` | `boolean` | `true`이면 `before/after_insert`, `before/after_update` 훅 미실행 |
|
|
317
|
+
|
|
318
|
+
```ts
|
|
319
|
+
// INSERT
|
|
320
|
+
const res = await client.submit("account", {
|
|
321
|
+
name: "홍길동",
|
|
322
|
+
email: "hong@example.com",
|
|
323
|
+
});
|
|
324
|
+
res.seq; // 생성된 seq
|
|
325
|
+
|
|
326
|
+
// UPDATE (seq 포함 시)
|
|
327
|
+
await client.submit("account", { seq: 1, name: "홍길순" });
|
|
328
|
+
|
|
329
|
+
// 훅 없이 저장
|
|
330
|
+
await client.submit("account", { name: "테스트" }, { skipHooks: true });
|
|
331
|
+
|
|
332
|
+
// 트랜잭션 내에서 — seq placeholder 활용
|
|
333
|
+
await client.transStart();
|
|
334
|
+
const r1 = await client.submit("order", { product_seq: 1, qty: 2 });
|
|
335
|
+
// r1.seq → "$tx.0" (commit 후 실제 seq로 치환)
|
|
336
|
+
await client.submit("order_item", { order_seq: r1.seq, name: "상품A" });
|
|
337
|
+
await client.transCommit();
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
---
|
|
341
|
+
|
|
342
|
+
### `delete(entity, seq, opts?)`
|
|
343
|
+
|
|
344
|
+
엔티티를 삭제합니다.
|
|
345
|
+
|
|
346
|
+
| 옵션 | 타입 | 기본값 | 설명 |
|
|
347
|
+
| --------------- | --------- | ------- | --------------------------------------------------------- |
|
|
348
|
+
| `hard` | `boolean` | `false` | `true`이면 완전 삭제. `false`이면 소프트 삭제 (복원 가능) |
|
|
349
|
+
| `transactionId` | `string` | — | 수동 트랜잭션 ID. 미지정 시 활성 트랜잭션 자동 사용 |
|
|
350
|
+
| `skipHooks` | `boolean` | `false` | `true`이면 `before/after_delete` 훅 미실행 |
|
|
351
|
+
|
|
352
|
+
> **소프트 삭제 (기본)**: DB에서 제거하지 않고 삭제 표시만 합니다. `rollback()`으로 복원 가능합니다.
|
|
353
|
+
> **하드 삭제**: DB에서 완전히 제거됩니다. 복원 불가능합니다.
|
|
354
|
+
|
|
355
|
+
```ts
|
|
356
|
+
// 소프트 삭제 (기본)
|
|
357
|
+
const res = await client.delete("account", 2);
|
|
358
|
+
res.deleted; // 삭제된 seq
|
|
359
|
+
|
|
360
|
+
// 하드 삭제
|
|
361
|
+
await client.delete("account", 3, { hard: true });
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
---
|
|
365
|
+
|
|
366
|
+
### `history(entity, seq, params?)`
|
|
367
|
+
|
|
368
|
+
엔티티 단건의 변경 이력을 조회합니다. `params`는 `page`, `limit`만 지원합니다.
|
|
369
|
+
|
|
370
|
+
응답 `data.items`의 각 레코드 구조:
|
|
371
|
+
|
|
372
|
+
| 필드 | 타입 | 설명 |
|
|
373
|
+
| ---------------- | -------------- | ------------------------------------------------------------------------------------ |
|
|
374
|
+
| `seq` | `number` | 이력 레코드 seq (`rollback()` 호출 시 사용) |
|
|
375
|
+
| `action` | `string` | `"INSERT"` \| `"UPDATE"` \| `"DELETE_SOFT"` \| `"DELETE_HARD"` \| `"ROLLBACK"` |
|
|
376
|
+
| `data_snapshot` | `T \| null` | **after 통일 모델**: INSERT/UPDATE → 변경 **후** 데이터, DELETE → 삭제 **전** 데이터 |
|
|
377
|
+
| `changed_by` | `number\|null` | 변경한 계정 seq. 시스템 변경이면 `null` |
|
|
378
|
+
| `changed_time` | `string` | 변경 시각 (ISO 8601) |
|
|
379
|
+
| `transaction_id` | `string` | 해당 변경이 속한 트랜잭션 ID. rollback 시 이 ID 기준으로 전체 롤백됨 |
|
|
380
|
+
|
|
381
|
+
```ts
|
|
382
|
+
const result = await client.history("account", 1, { page: 1, limit: 50 });
|
|
383
|
+
result.data.total; // 전체 이력 건수
|
|
384
|
+
result.data.items[0].action; // "UPDATE"
|
|
385
|
+
result.data.items[0].data_snapshot; // 변경 후 데이터
|
|
386
|
+
result.data.items[0].transaction_id; // "TX-20260201-abc123"
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
---
|
|
390
|
+
|
|
391
|
+
### `rollback(entity, historySeq)`
|
|
392
|
+
|
|
393
|
+
특정 이력 레코드의 `transaction_id`를 조회해 **해당 트랜잭션 전체**를 롤백합니다.
|
|
394
|
+
|
|
395
|
+
한 번의 트랜잭션에 여러 엔티티가 변경됐다면 모든 엔티티가 함께 되돌아갑니다.
|
|
396
|
+
|
|
397
|
+
```ts
|
|
398
|
+
// history()로 이력 seq 조회 후 rollback
|
|
399
|
+
const hist = await client.history("account", 1);
|
|
400
|
+
const targetHistorySeq = hist.data.items[0].seq;
|
|
401
|
+
await client.rollback("account", targetHistorySeq);
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
---
|
|
405
|
+
|
|
406
|
+
## 푸시 관련
|
|
407
|
+
|
|
408
|
+
### `push(pushEntity, payload, opts?)`
|
|
409
|
+
|
|
410
|
+
`submit()`의 별칭입니다. 푸시 관련 엔티티에 데이터를 제출합니다.
|
|
411
|
+
|
|
412
|
+
```ts
|
|
413
|
+
await client.push("push_message", {
|
|
414
|
+
title: "새 알림",
|
|
415
|
+
body: "내용",
|
|
416
|
+
account_seq: 1,
|
|
417
|
+
});
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
### `pushLogList(params?)`
|
|
421
|
+
|
|
422
|
+
`push_log` 엔티티의 목록을 조회합니다. `list()`의 별칭입니다.
|
|
423
|
+
|
|
424
|
+
```ts
|
|
425
|
+
const logs = await client.pushLogList({
|
|
426
|
+
page: 1,
|
|
427
|
+
limit: 30,
|
|
428
|
+
orderBy: "-created_time",
|
|
429
|
+
conditions: { account_seq: 1 },
|
|
430
|
+
});
|
|
431
|
+
logs.data.items;
|
|
432
|
+
logs.data.total;
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
### `registerPushDevice(accountSeq, deviceId, pushToken, opts?)`
|
|
436
|
+
|
|
437
|
+
`account_device` 엔티티에 디바이스를 등록합니다.
|
|
438
|
+
|
|
439
|
+
| 옵션 | 타입 | 설명 |
|
|
440
|
+
| ---------------- | --------- | --------------------------------- |
|
|
441
|
+
| `platform` | `string` | 플랫폼 (예: `"web"`, `"android"`) |
|
|
442
|
+
| `deviceType` | `string` | 디바이스 종류 (예: `"mobile"`) |
|
|
443
|
+
| `browser` | `string` | 브라우저명 (예: `"Chrome"`) |
|
|
444
|
+
| `browserVersion` | `string` | 브라우저 버전 |
|
|
445
|
+
| `pushEnabled` | `boolean` | 푸시 수신 여부. 기본값 `true` |
|
|
446
|
+
| `transactionId` | `string` | 트랜잭션 ID |
|
|
447
|
+
|
|
448
|
+
```ts
|
|
449
|
+
const res = await client.registerPushDevice(
|
|
450
|
+
1,
|
|
451
|
+
"device-uuid-001",
|
|
452
|
+
"fcm-token-abc",
|
|
453
|
+
{
|
|
454
|
+
platform: "web",
|
|
455
|
+
browser: "Chrome",
|
|
456
|
+
browserVersion: "120",
|
|
457
|
+
pushEnabled: true,
|
|
458
|
+
},
|
|
459
|
+
);
|
|
460
|
+
res.seq; // 등록된 account_device seq
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
### `updatePushDeviceToken(deviceSeq, pushToken, opts?)`
|
|
464
|
+
|
|
465
|
+
등록된 디바이스의 푸시 토큰을 갱신합니다.
|
|
466
|
+
|
|
467
|
+
```ts
|
|
468
|
+
await client.updatePushDeviceToken(10, "new-fcm-token", { pushEnabled: true });
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
### `disablePushDevice(deviceSeq, opts?)`
|
|
472
|
+
|
|
473
|
+
디바이스의 푸시 수신을 비활성화합니다 (`push_enabled: false`).
|
|
474
|
+
|
|
475
|
+
```ts
|
|
476
|
+
await client.disablePushDevice(10);
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
---
|
|
480
|
+
|
|
481
|
+
## 암호화 패킷 처리
|
|
482
|
+
|
|
483
|
+
### 자동 복호화
|
|
484
|
+
|
|
485
|
+
서버 응답이 `Content-Type: application/octet-stream`이면 `request()` 내부에서 자동으로 복호화됩니다.
|
|
486
|
+
XChaCha20-Poly1305 알고리즘을 사용하며, 키는 현재 JWT 토큰의 SHA-256 해시입니다.
|
|
487
|
+
|
|
488
|
+
별도 처리 없이 일반 응답과 동일하게 사용하면 됩니다.
|
|
489
|
+
|
|
490
|
+
### `readRequestBody(body, contentType?, requireEncrypted?)`
|
|
491
|
+
|
|
492
|
+
원시 암호화 payload를 직접 파싱할 때 사용합니다.
|
|
493
|
+
주로 서버 측 미들웨어나 SSR 환경에서 클라이언트로부터 받은 암호화 body를 처리할 때 활용합니다.
|
|
494
|
+
|
|
495
|
+
```ts
|
|
496
|
+
// 암호화 body 복호화
|
|
497
|
+
const data = client.readRequestBody(
|
|
498
|
+
arrayBuffer, // ArrayBuffer | Uint8Array
|
|
499
|
+
"application/octet-stream",
|
|
500
|
+
true, // requireEncrypted: 암호화 아니면 에러
|
|
501
|
+
);
|
|
502
|
+
|
|
503
|
+
// 일반 JSON body 파싱
|
|
504
|
+
const data2 = client.readRequestBody(jsonString, "application/json");
|
|
505
|
+
```
|
package/docs/react.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# React 전용 가이드
|
|
2
|
+
|
|
3
|
+
## import
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { useEntityServer } from "entity-server-client/react";
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## 기본 사용
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { useEntityServer } from "entity-server-client/react";
|
|
13
|
+
|
|
14
|
+
export function AccountPage() {
|
|
15
|
+
const client = useEntityServer({
|
|
16
|
+
tokenResolver: () => localStorage.getItem("auth_access_token"),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// 예: 버튼 클릭 시 목록 조회
|
|
20
|
+
const onClick = async () => {
|
|
21
|
+
const res = await client.list("account", { page: 1, limit: 20 });
|
|
22
|
+
console.log(res.data);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
return <button onClick={onClick}>불러오기</button>;
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## 옵션
|
|
30
|
+
|
|
31
|
+
### `singleton` (기본: `true`)
|
|
32
|
+
|
|
33
|
+
- `true`: 패키지 전역 인스턴스(`entityServer`)를 사용
|
|
34
|
+
- `false`: 훅 호출 스코프마다 새 인스턴스 생성
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
const client = useEntityServer({
|
|
38
|
+
singleton: false,
|
|
39
|
+
baseUrl: "http://localhost:47200",
|
|
40
|
+
});
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### `tokenResolver`
|
|
44
|
+
|
|
45
|
+
렌더 시점에 토큰을 읽어 자동으로 `setToken`을 적용합니다.
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
const client = useEntityServer({
|
|
49
|
+
tokenResolver: () => sessionStorage.getItem("access_token"),
|
|
50
|
+
});
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### `baseUrl`, `token`, `packetMagicLen`
|
|
54
|
+
|
|
55
|
+
`EntityServerClientOptions`와 동일하게 전달할 수 있습니다.
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
const client = useEntityServer({
|
|
59
|
+
baseUrl: import.meta.env.VITE_ENTITY_SERVER_URL,
|
|
60
|
+
packetMagicLen: Number(import.meta.env.VITE_PACKET_MAGIC_LEN ?? 4),
|
|
61
|
+
});
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## React Query와 함께 사용 예시
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
import { useQuery } from "@tanstack/react-query";
|
|
68
|
+
import { useEntityServer } from "entity-server-client/react";
|
|
69
|
+
|
|
70
|
+
export function useAccountList() {
|
|
71
|
+
const client = useEntityServer({
|
|
72
|
+
tokenResolver: () => localStorage.getItem("auth_access_token"),
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
return useQuery({
|
|
76
|
+
queryKey: ["account", "list"],
|
|
77
|
+
queryFn: () => client.list("account", { page: 1, limit: 20 }),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "entity-server-client",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"default": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./react": {
|
|
15
|
+
"types": "./dist/react.d.ts",
|
|
16
|
+
"default": "./dist/react.js"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"main": "./dist/index.js",
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsc --declaration --emitDeclarationOnly && node build.mjs",
|
|
23
|
+
"prepublishOnly": "npm run build"
|
|
24
|
+
},
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"react": ">=18"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@noble/ciphers": "^1.3.0",
|
|
30
|
+
"@noble/hashes": "^1.7.2"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/react": "^19.2.2",
|
|
34
|
+
"esbuild": "^0.25.0",
|
|
35
|
+
"react": "^19.2.0",
|
|
36
|
+
"typescript": "^5.9.3"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import {
|
|
3
|
+
EntityServerClient,
|
|
4
|
+
entityServer,
|
|
5
|
+
type EntityServerClientOptions,
|
|
6
|
+
} from "../index";
|
|
7
|
+
|
|
8
|
+
export interface UseEntityServerOptions extends EntityServerClientOptions {
|
|
9
|
+
singleton?: boolean;
|
|
10
|
+
tokenResolver?: () => string | undefined | null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* React 환경에서 EntityServerClient 인스턴스를 반환합니다.
|
|
15
|
+
*
|
|
16
|
+
* - `singleton=true`(기본): 패키지 전역 `entityServer` 인스턴스를 반환합니다.
|
|
17
|
+
* - `singleton=false`: 컴포넌트 스코프의 새 인스턴스를 생성합니다.
|
|
18
|
+
*/
|
|
19
|
+
export function useEntityServer(
|
|
20
|
+
options: UseEntityServerOptions = {},
|
|
21
|
+
): EntityServerClient {
|
|
22
|
+
const {
|
|
23
|
+
singleton = true,
|
|
24
|
+
tokenResolver,
|
|
25
|
+
baseUrl,
|
|
26
|
+
packetMagicLen,
|
|
27
|
+
token,
|
|
28
|
+
} = options;
|
|
29
|
+
|
|
30
|
+
return useMemo(() => {
|
|
31
|
+
const client = singleton
|
|
32
|
+
? entityServer
|
|
33
|
+
: new EntityServerClient({
|
|
34
|
+
baseUrl,
|
|
35
|
+
packetMagicLen,
|
|
36
|
+
token,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
if (singleton) {
|
|
40
|
+
client.configure({ baseUrl, packetMagicLen, token });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const resolvedToken = tokenResolver?.();
|
|
44
|
+
if (typeof resolvedToken === "string") {
|
|
45
|
+
client.setToken(resolvedToken);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return client;
|
|
49
|
+
}, [singleton, tokenResolver, baseUrl, packetMagicLen, token]);
|
|
50
|
+
}
|