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
package/docs/apis.md
CHANGED
|
@@ -1,790 +1,25 @@
|
|
|
1
|
-
#
|
|
1
|
+
# entity-server-client API 문서
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
| 구분 | 바로가기 |
|
|
6
|
-
| ------------------ | ---------------------------------------- |
|
|
7
|
-
| import | [import](#import) |
|
|
8
|
-
| 서버 헬스체크 | [서버 헬스체크](#서버-헬스체크) |
|
|
9
|
-
| 인스턴스 생성/설정 | [인스턴스 생성/설정](#인스턴스-생성설정) |
|
|
10
|
-
| 인증 | [인증](#인증) |
|
|
11
|
-
| 트랜잭션 | [트랜잭션](#트랜잭션) |
|
|
12
|
-
| 엔티티 CRUD / 조회 | [엔티티 CRUD / 조회](#엔티티-crud--조회) |
|
|
13
|
-
| 푸시 관련 | [푸시 관련](#푸시-관련) |
|
|
14
|
-
| 암호화 패킷 처리 | [암호화 패킷 처리](#암호화-패킷-처리) |
|
|
15
|
-
| React Hook | [React Hook](#react-hook) |
|
|
16
|
-
|
|
17
|
-
---
|
|
18
|
-
|
|
19
|
-
## import
|
|
20
|
-
|
|
21
|
-
```ts
|
|
22
|
-
import {
|
|
23
|
-
EntityServerClient,
|
|
24
|
-
entityServer,
|
|
25
|
-
type EntityListParams,
|
|
26
|
-
type EntityListResult,
|
|
27
|
-
type EntityHistoryRecord,
|
|
28
|
-
type EntityQueryRequest,
|
|
29
|
-
type RegisterPushDeviceOptions,
|
|
30
|
-
type EntityServerClientOptions,
|
|
31
|
-
} from "entity-server-client";
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
React Hook:
|
|
35
|
-
|
|
36
|
-
```ts
|
|
37
|
-
import { useEntityServer } from "entity-server-client/react";
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
---
|
|
41
|
-
|
|
42
|
-
## 서버 헬스체크
|
|
43
|
-
|
|
44
|
-
### 개요
|
|
45
|
-
|
|
46
|
-
앱 시작 시 서버의 패킷 암호화 설정을 감지하여 클라이언트 설정을 자동으로 맞춥니다.
|
|
47
|
-
|
|
48
|
-
**헬스체크 응답:**
|
|
49
|
-
|
|
50
|
-
```json
|
|
51
|
-
{
|
|
52
|
-
"ok": true,
|
|
53
|
-
"packet_encryption": false
|
|
54
|
-
}
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
### 자동 초기화 (권장)
|
|
58
|
-
|
|
59
|
-
**admin-web 예시:**
|
|
60
|
-
|
|
61
|
-
```ts
|
|
62
|
-
import { checkServerHealth, entityServer } from "entity-server-client";
|
|
63
|
-
|
|
64
|
-
// 앱 초기화 시 한 번 실행 (예: App.tsx mount, main.tsx 초기화)
|
|
65
|
-
const health = await checkServerHealth();
|
|
66
|
-
// → 서버의 packet_encryption: true 이면
|
|
67
|
-
// 클라이언트의 encryptRequests: true 로 자동 설정됨
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
**수동 초기화:**
|
|
71
|
-
|
|
72
|
-
```ts
|
|
73
|
-
const res = await fetch("/v1/health");
|
|
74
|
-
const { packet_encryption } = await res.json();
|
|
75
|
-
|
|
76
|
-
if (packet_encryption) {
|
|
77
|
-
entityServer.configure({ encryptRequests: true });
|
|
78
|
-
}
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
> **중요:** `packetMagicLen` 값은 클라이언트가 미리 알고 있어야 합니다.
|
|
82
|
-
> 빌드 시점에 `VITE_PACKET_MAGIC_LEN` 환경변수로 결정되며,
|
|
83
|
-
> 서버와 일치해야 올바르게 복호화됩니다.
|
|
84
|
-
|
|
85
|
-
---
|
|
86
|
-
|
|
87
|
-
## 인스턴스 생성/설정
|
|
88
|
-
|
|
89
|
-
### `new EntityServerClient(options?)`
|
|
90
|
-
|
|
91
|
-
| 옵션 | 타입 | 기본값 | 설명 |
|
|
92
|
-
| ------------------ | ---------------------------------- | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
93
|
-
| `baseUrl` | `string` | `VITE_ENTITY_SERVER_URL` 또는 `http://localhost:47200` | 서버 주소 |
|
|
94
|
-
| `token` | `string` | `""` | JWT Access Token |
|
|
95
|
-
| `packetMagicLen` | `number` | `VITE_PACKET_MAGIC_LEN` 또는 `4` | 암호화 패킷 magic 바이트 길이 |
|
|
96
|
-
| `encryptRequests` | `boolean` | `false` | `true` 로 설정하면 인증된 POST/PUT/PATCH 요청 바디를 XChaCha20-Poly1305 로 암호화하여 전송합니다. 서버에서 `requirePacketEncryption = true` 로 설정된 경우 반드시 활성화해야 합니다. |
|
|
97
|
-
| `keepSession` | `boolean` | `false` | `true`이면 `login()` 성공 후 Access Token 만료 전에 자동으로 갱신합니다. |
|
|
98
|
-
| `refreshBuffer` | `number` | `60` | 만료 몇 초 전에 갱신을 시도할지 설정합니다. 예: `expires_in=3600`, `refreshBuffer=60` → 3540초 후 갱신 |
|
|
99
|
-
| `onTokenRefreshed` | `(accessToken, expiresIn) => void` | — | 자동 갱신 성공 시 호출됩니다. 새 `access_token`을 받아 앱 레벨 저장소에 업데이트하세요. |
|
|
100
|
-
| `onSessionExpired` | `(error: Error) => void` | — | 세션 유지 갱신 실패 시 호출됩니다 (refresh_token 만료 등). 로그인 페이지로 이동하는 등의 처리를 여기서 합니다. |
|
|
101
|
-
|
|
102
|
-
```ts
|
|
103
|
-
// 직접 생성
|
|
104
|
-
const client = new EntityServerClient({
|
|
105
|
-
baseUrl: "https://api.example.com",
|
|
106
|
-
token: "eyJhbGciOi...",
|
|
107
|
-
packetMagicLen: 4,
|
|
108
|
-
encryptRequests: true, // 요청 바디 암호화 활성화
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
// 싱글톤 (환경변수 자동 읽기)
|
|
112
|
-
import { entityServer } from "entity-server-client";
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
### `configure(options)`
|
|
116
|
-
|
|
117
|
-
런타임에 설정을 갱신합니다.
|
|
118
|
-
|
|
119
|
-
```ts
|
|
120
|
-
client.configure({
|
|
121
|
-
baseUrl: "https://api.example.com",
|
|
122
|
-
token: "new-access-token",
|
|
123
|
-
packetMagicLen: 6,
|
|
124
|
-
encryptRequests: true,
|
|
125
|
-
});
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
### `setToken(token)` / `setPacketMagicLen(length)` / `getPacketMagicLen()`
|
|
129
|
-
|
|
130
|
-
```ts
|
|
131
|
-
client.setToken("eyJhbGciOi...");
|
|
132
|
-
client.setPacketMagicLen(4);
|
|
133
|
-
const len = client.getPacketMagicLen(); // 4
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
---
|
|
137
|
-
|
|
138
|
-
## 인증
|
|
139
|
-
|
|
140
|
-
### `login(email, password)`
|
|
141
|
-
|
|
142
|
-
이메일 + 비밀번호로 로그인합니다. 성공 시 내부 토큰이 자동으로 설정됩니다.
|
|
143
|
-
|
|
144
|
-
```ts
|
|
145
|
-
const auth = await client.login("admin@example.com", "password");
|
|
146
|
-
// auth.access_token — 이후 요청에 자동 사용됨
|
|
147
|
-
// auth.refresh_token — 만료 후 재발급에 사용
|
|
148
|
-
// auth.expires_in — 만료까지 남은 초
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
### `refreshToken(refreshToken)`
|
|
152
|
-
|
|
153
|
-
Refresh Token으로 Access Token을 재발급받습니다.
|
|
154
|
-
앱이 명시적으로 호출해야 하며, 성공하면 내부 `this.token`을 새 Access Token으로 교체합니다.
|
|
155
|
-
|
|
156
|
-
> **패키지는 401을 감지해 자동으로 `refreshToken()`을 호출하지 않습니다.**
|
|
157
|
-
> 401 발생 시 앱이 직접 catch해서 호출해야 합니다.
|
|
158
|
-
|
|
159
|
-
```ts
|
|
160
|
-
const result = await client.refreshToken(auth.refresh_token);
|
|
161
|
-
// 성공 → this.token이 result.access_token으로 교체됨
|
|
162
|
-
// result.access_token
|
|
163
|
-
// result.expires_in
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
### `logout(refreshToken)`
|
|
167
|
-
|
|
168
|
-
서버에 로그아웃을 요청하고 내부 토큰을 초기화합니다.
|
|
169
|
-
Refresh Token을 서버에 전달해 무효화하므로 해당 토큰으로 더 이상 재발급이 불가능합니다.
|
|
170
|
-
Refresh Token이 이미 만료된 경우에도 서버는 성공으로 응답합니다.
|
|
171
|
-
|
|
172
|
-
```ts
|
|
173
|
-
await client.logout(auth.refresh_token);
|
|
174
|
-
// 이후 client 내부 token = "" 으로 초기화됨
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
### 토큰 만료 처리
|
|
178
|
-
|
|
179
|
-
**패키지가 자동으로 처리하는 것:**
|
|
180
|
-
|
|
181
|
-
| 동작 | 내부 토큰 갱신 |
|
|
182
|
-
| --------------------- | -------------- |
|
|
183
|
-
| `login()` 성공 | ✅ 자동 세팅 |
|
|
184
|
-
| `refreshToken()` 성공 | ✅ 자동 교체 |
|
|
185
|
-
| `logout()` 호출 | ✅ `""` 초기화 |
|
|
186
|
-
|
|
187
|
-
**패키지가 처리하지 않는 것:**
|
|
188
|
-
|
|
189
|
-
- **401 자동 재시도 없음**: access_token이 만료되어 서버가 401을 반환하면 패키지는 에러를 throw합니다. 앱이 직접 잡아서 `refreshToken()`을 호출하고 재시도해야 합니다.
|
|
190
|
-
- **토큰 영속성 없음**: 페이지 새로고침 시 싱글톤 인스턴스가 초기화되어 내부 token이 `""` 로 리셋됩니다. 앱이 직접 복원 후 `setToken()` 또는 `configure({ token })` 으로 재세팅해야 합니다.
|
|
191
|
-
|
|
192
|
-
**401 발생 후 앱의 처리 흐름:**
|
|
193
|
-
|
|
194
|
-
```
|
|
195
|
-
API 요청 → 401 응답 → entityServer.refreshToken(refresh_token) 호출
|
|
196
|
-
↓ 성공 ↓ 실패 (refresh_token도 만료)
|
|
197
|
-
setToken(new_access_token) 로그인 페이지로 이동
|
|
198
|
-
원래 요청 재시도
|
|
199
|
-
```
|
|
200
|
-
|
|
201
|
-
**주의:** `refreshToken()`이 성공해 패키지 내부 토큰이 교체되더라도, 앱 내 다른 HTTP 클라이언트(axios 등)에는 별도로 토큰을 전달해야 합니다.
|
|
202
|
-
|
|
203
|
-
### `keepSession` — 세션 유지 (Silent Refresh)
|
|
204
|
-
|
|
205
|
-
`keepSession: true` 옵션 설정 시, `login()` 성공 후 만료 `refreshBuffer`초 전에 패키지 내부 타이머가 자동으로 `refreshToken()`을 호출합니다.
|
|
206
|
-
갱신이 성공하면 타이머를 다시 예약해 로그인 상태를 계속 유지합니다.
|
|
207
|
-
`logout()` 또는 `stopKeepSession()` 호출 시 타이머가 정리됩니다.
|
|
208
|
-
|
|
209
|
-
```ts
|
|
210
|
-
entityServer.configure({
|
|
211
|
-
keepSession: true,
|
|
212
|
-
refreshBuffer: 60, // 만료 60초 전에 갱신 (기본값)
|
|
213
|
-
onTokenRefreshed: (accessToken, expiresIn) => {
|
|
214
|
-
// 갱신 성공 — 앱 레벨 저장소 업데이트
|
|
215
|
-
localStorage.setItem("auth_access_token", accessToken);
|
|
216
|
-
},
|
|
217
|
-
onSessionExpired: (error) => {
|
|
218
|
-
// refresh_token 만료 등으로 갱신 실패
|
|
219
|
-
console.error("세션 만료:", error);
|
|
220
|
-
window.location.href = "/login";
|
|
221
|
-
},
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
// 이후 login() 하면 타이머 자동 시작
|
|
225
|
-
const auth = await entityServer.login(email, password);
|
|
226
|
-
// expires_in=3600이면 3540초 후 자동 갱신, 이후 반복
|
|
227
|
-
```
|
|
228
|
-
|
|
229
|
-
**수동으로 타이머 중지:**
|
|
230
|
-
|
|
231
|
-
```ts
|
|
232
|
-
entityServer.stopKeepSession();
|
|
233
|
-
```
|
|
234
|
-
|
|
235
|
-
> **페이지 새로고침 시**: 타이머는 메모리에만 존재하므로 새로고침 시 초기화됩니다.
|
|
236
|
-
> `useEntityServer`의 `resumeSession` 옵션을 사용하면 페이지 새로고침 후에도 세션을 자동으로 복원할 수 있습니다.
|
|
237
|
-
|
|
238
|
-
---
|
|
239
|
-
|
|
240
|
-
## 트랜잭션
|
|
241
|
-
|
|
242
|
-
여러 submit/delete를 하나의 DB 트랜잭션으로 묶습니다.
|
|
243
|
-
트랜잭션은 **5분 TTL**을 가집니다. 이 안에 commit 또는 rollback을 해야 합니다.
|
|
244
|
-
|
|
245
|
-
```
|
|
246
|
-
transStart → submit/delete (여러 개) → transCommit
|
|
247
|
-
└→ transRollback (실패 시)
|
|
248
|
-
```
|
|
249
|
-
|
|
250
|
-
### `transStart()`
|
|
251
|
-
|
|
252
|
-
트랜잭션을 시작하고 내부 활성 트랜잭션 ID를 저장합니다.
|
|
253
|
-
이후 `submit()`/`delete()`는 `X-Transaction-ID` 헤더를 자동 포함합니다.
|
|
254
|
-
|
|
255
|
-
```ts
|
|
256
|
-
const txId = await client.transStart();
|
|
257
|
-
```
|
|
258
|
-
|
|
259
|
-
### `transCommit(transactionId?)`
|
|
260
|
-
|
|
261
|
-
큐에 쌓인 모든 작업을 단일 DB 트랜잭션으로 일괄 실행합니다.
|
|
262
|
-
하나라도 실패하면 전체 ROLLBACK됩니다.
|
|
263
|
-
|
|
264
|
-
```ts
|
|
265
|
-
await client.transStart();
|
|
266
|
-
await client.submit("order", { product_seq: 1, qty: 2 });
|
|
267
|
-
await client.submit("inventory", { seq: 1, stock: 48 });
|
|
268
|
-
const result = await client.transCommit();
|
|
269
|
-
// result.results → [{ entity: "order", action: "submit", seq: 55 }, ...]
|
|
270
|
-
```
|
|
271
|
-
|
|
272
|
-
> **트랜잭션 중 submit 응답**: commit 전에는 `{ ok: true, queued: true, seq: "$tx.0" }` 형태의
|
|
273
|
-
> placeholder가 반환됩니다. `$tx.0`, `$tx.1` 값은 commit 시 실제 seq로 자동 치환되므로
|
|
274
|
-
> 후속 submit의 외래키 값으로 그대로 사용할 수 있습니다.
|
|
275
|
-
|
|
276
|
-
### `transRollback(transactionId?)`
|
|
277
|
-
|
|
278
|
-
- 아직 commit 전 (큐에 남아있음): 큐를 버립니다. DB에 아무 변경 없음.
|
|
279
|
-
- 이미 commit 후: history 기반으로 전체 되돌립니다.
|
|
280
|
-
|
|
281
|
-
```ts
|
|
282
|
-
try {
|
|
283
|
-
await client.transStart();
|
|
284
|
-
await client.submit("order", { ... });
|
|
285
|
-
await client.transCommit();
|
|
286
|
-
} catch (e) {
|
|
287
|
-
await client.transRollback(); // 활성 트랜잭션 자동 참조
|
|
288
|
-
}
|
|
289
|
-
```
|
|
290
|
-
|
|
291
|
-
---
|
|
292
|
-
|
|
293
|
-
## 엔티티 CRUD / 조회
|
|
294
|
-
|
|
295
|
-
### `get(entity, seq, opts?)`
|
|
296
|
-
|
|
297
|
-
시퀀스 ID로 엔티티 단건을 조회합니다.
|
|
298
|
-
|
|
299
|
-
| 옵션 | 타입 | 설명 |
|
|
300
|
-
| ----------- | --------- | ----------------------------------------- |
|
|
301
|
-
| `skipHooks` | `boolean` | `true`이면 `after_get` 훅을 실행하지 않음 |
|
|
302
|
-
|
|
303
|
-
```ts
|
|
304
|
-
const result = await client.get<Account>("account", 1);
|
|
305
|
-
result.data; // Account 타입 객체
|
|
306
|
-
```
|
|
307
|
-
|
|
308
|
-
---
|
|
309
|
-
|
|
310
|
-
### `find(entity, conditions?, opts?)`
|
|
311
|
-
|
|
312
|
-
조건(conditions)으로 첫 번째 일치 레코드를 조회합니다.
|
|
313
|
-
`data` 컬럼을 **항상 완전히 복호화**하여 반환합니다. 레코드가 없으면 `404` 에러가 됩니다.
|
|
314
|
-
|
|
315
|
-
| 파라미터 | 타입 | 설명 |
|
|
316
|
-
| ------------ | ------------------------- | ----------------------------------- |
|
|
317
|
-
| `conditions` | `Record<string, unknown>` | 검색 조건 (인덱스 필드만 조건 가능) |
|
|
318
|
-
| `skipHooks` | `boolean` | `true`이면 훅 실행 건너뛰기 |
|
|
319
|
-
|
|
320
|
-
```ts
|
|
321
|
-
// 이메일로 계정 단건 조회
|
|
322
|
-
const result = await client.find<Account>("account", {
|
|
323
|
-
email: "hong@example.com",
|
|
324
|
-
});
|
|
325
|
-
result.data; // Account 전체 필드 (passwd 포함)
|
|
326
|
-
|
|
327
|
-
// skipHooks 옵션
|
|
328
|
-
const result = await client.find<Account>(
|
|
329
|
-
"account",
|
|
330
|
-
{ code: "A001" },
|
|
331
|
-
{ skipHooks: true },
|
|
332
|
-
);
|
|
333
|
-
```
|
|
334
|
-
|
|
335
|
-
> **`get` vs `find`**
|
|
336
|
-
>
|
|
337
|
-
> - `get(entity, seq)`: seq(일련번호)를 정확히 알고 있을 때 빠르게 조회
|
|
338
|
-
> - `find(entity, conditions)`: 조건으로 검색, 항상 data 전체 복호화 반환
|
|
339
|
-
|
|
340
|
-
---
|
|
341
|
-
|
|
342
|
-
### `list(entity, params?)`
|
|
343
|
-
|
|
344
|
-
페이지네이션/정렬/필터 조건으로 엔티티 목록을 조회합니다.
|
|
345
|
-
|
|
346
|
-
| 파라미터 | 타입 | 기본값 | 설명 |
|
|
347
|
-
| ------------ | ------------------------- | ------- | --------------------------------------------------- |
|
|
348
|
-
| `page` | `number` | `1` | 페이지 번호 |
|
|
349
|
-
| `limit` | `number` | `20` | 페이지당 레코드 수 (최대 1000) |
|
|
350
|
-
| `orderBy` | `string` | — | 정렬 기준 필드명. `-` 접두사로 내림차순 지정 가능 |
|
|
351
|
-
| `orderDir` | `"ASC" \| "DESC"` | `"ASC"` | 정렬 방향 (`orderBy: "-field"`와 동일 효과) |
|
|
352
|
-
| `fields` | `string[]` | — | 반환할 필드 목록 (**미지정 시 인덱스 필드만 반환**) |
|
|
353
|
-
| `conditions` | `Record<string, unknown>` | — | 필터 조건 |
|
|
354
|
-
|
|
355
|
-
#### `fields` — 반환 필드 지정
|
|
356
|
-
|
|
357
|
-
`fields`는 `entity.json`의 `index`로 선언된 필드명만 지정할 수 있습니다.
|
|
358
|
-
존재하지 않는 필드를 지정하면 서버 에러가 발생합니다.
|
|
359
|
-
|
|
360
|
-
| 값 | 설명 |
|
|
361
|
-
| ------------------- | -------------------------------------------------------------- |
|
|
362
|
-
| 미지정 (기본값) | 인덱스 선언 필드만 반환. **복호화를 건너뛰어 가장 빠름** |
|
|
363
|
-
| `["*"]` | 전체 필드 반환 (복호화 수행) |
|
|
364
|
-
| `["name", "email"]` | 지정한 인덱스 필드만 반환. 모두 인덱스 필드면 역시 복호화 생략 |
|
|
365
|
-
|
|
366
|
-
> `seq`, `created_time`, `updated_time`, `license_seq`는 `fields` 지정 여부와 무관하게 항상 포함됩니다.
|
|
367
|
-
|
|
368
|
-
#### `conditions` — 필터 조건
|
|
369
|
-
|
|
370
|
-
인덱스 테이블의 필드(`index` / `hash` / `unique`로 선언된 필드)에만 조건을 걸 수 있습니다.
|
|
371
|
-
인덱스에 없는 필드로 필터를 걸면 동작하지 않거나 에러가 발생합니다.
|
|
372
|
-
|
|
373
|
-
```ts
|
|
374
|
-
// 기본값 (인덱스 필드만, 가장 빠름)
|
|
375
|
-
const result = await client.list("account");
|
|
376
|
-
result.data.items; // 레코드 배열
|
|
377
|
-
result.data.total; // 필터 조건 일치 전체 건수
|
|
378
|
-
result.data.page; // 현재 페이지
|
|
379
|
-
result.data.limit; // 페이지당 레코드 수
|
|
380
|
-
|
|
381
|
-
// 정렬 + 필터 + 필드 선택
|
|
382
|
-
const result = await client.list("account", {
|
|
383
|
-
page: 1,
|
|
384
|
-
limit: 20,
|
|
385
|
-
orderBy: "created_time",
|
|
386
|
-
orderDir: "DESC",
|
|
387
|
-
fields: ["seq", "name", "email"], // index 필드만 → 복호화 생략
|
|
388
|
-
conditions: { status: "active" },
|
|
389
|
-
});
|
|
3
|
+
각 섹션은 별도 파일로 분리되어 있습니다.
|
|
390
4
|
|
|
391
|
-
|
|
392
|
-
const full = await client.list("account", {
|
|
393
|
-
fields: ["*"],
|
|
394
|
-
conditions: { status: "active" },
|
|
395
|
-
});
|
|
396
|
-
|
|
397
|
-
// orderBy에 - 접두사로 내림차순 (orderDir: "DESC"와 동일)
|
|
398
|
-
const desc = await client.list("account", { orderBy: "-created_time" });
|
|
399
|
-
```
|
|
400
|
-
|
|
401
|
-
---
|
|
402
|
-
|
|
403
|
-
### `count(entity, conditions?)`
|
|
404
|
-
|
|
405
|
-
레코드 건수를 조회합니다. `conditions`는 `list()`와 동일한 필터 규칙을 따릅니다.
|
|
406
|
-
|
|
407
|
-
```ts
|
|
408
|
-
const total = await client.count("account");
|
|
409
|
-
total.count; // 전체 건수
|
|
410
|
-
|
|
411
|
-
const active = await client.count("account", { status: "active" });
|
|
412
|
-
active.count; // 조건 일치 건수
|
|
413
|
-
```
|
|
414
|
-
|
|
415
|
-
---
|
|
416
|
-
|
|
417
|
-
### `query(entity, req)`
|
|
418
|
-
|
|
419
|
-
커스텀 SQL로 엔티티를 조회합니다.
|
|
420
|
-
|
|
421
|
-
**제약사항**:
|
|
422
|
-
|
|
423
|
-
- SELECT 쿼리만 허용 (INSERT/UPDATE/DELETE 불가)
|
|
424
|
-
- 인덱스 테이블(`entity_idx_*`)만 접근 가능. 암호화된 본문 필드는 조회 불가
|
|
425
|
-
- `SELECT *` 불가. 인덱스 선언 필드만 SELECT 가능
|
|
426
|
-
- 최대 반환 건수: 1000
|
|
427
|
-
|
|
428
|
-
`entity`는 URL 라우트 경로용 기본 엔티티명으로, 실제 조회 대상은 SQL에서 결정됩니다.
|
|
429
|
-
|
|
430
|
-
| 필드 | 타입 | 설명 |
|
|
431
|
-
| -------- | ----------- | ----------------------------------------------- |
|
|
432
|
-
| `sql` | `string` | SELECT SQL문. 사용자 입력은 반드시 `?`로 바인딩 |
|
|
433
|
-
| `params` | `unknown[]` | `?` 플레이스홀더에 바인딩할 값 배열 |
|
|
434
|
-
| `limit` | `number` | 최대 반환 건수 (최대 1000) |
|
|
435
|
-
|
|
436
|
-
응답: `{ ok: true, data: { items: T[], count: number } }`
|
|
437
|
-
|
|
438
|
-
```ts
|
|
439
|
-
// 단일 엔티티
|
|
440
|
-
const result = await client.query("account", {
|
|
441
|
-
sql: "SELECT seq, name, email FROM account WHERE status = ?",
|
|
442
|
-
params: ["active"],
|
|
443
|
-
limit: 50,
|
|
444
|
-
});
|
|
445
|
-
result.data.items; // 레코드 배열
|
|
446
|
-
result.data.count; // 반환된 건수
|
|
447
|
-
|
|
448
|
-
// JOIN으로 여러 엔티티 조합
|
|
449
|
-
const joined = await client.query("order", {
|
|
450
|
-
sql: `
|
|
451
|
-
SELECT o.seq, o.status, u.name AS user_name, u.email
|
|
452
|
-
FROM order o
|
|
453
|
-
JOIN account u ON u.data_seq = o.account_seq
|
|
454
|
-
WHERE o.status = ?
|
|
455
|
-
ORDER BY o.seq DESC
|
|
456
|
-
`,
|
|
457
|
-
params: ["pending"],
|
|
458
|
-
limit: 100,
|
|
459
|
-
});
|
|
460
|
-
```
|
|
461
|
-
|
|
462
|
-
> SQL Injection 방지: 사용자 입력값은 반드시 `params`로 바인딩하세요.
|
|
463
|
-
|
|
464
|
-
---
|
|
465
|
-
|
|
466
|
-
### `submit(entity, data, opts?)`
|
|
467
|
-
|
|
468
|
-
엔티티를 생성 또는 수정합니다.
|
|
469
|
-
|
|
470
|
-
- `data`에 `seq`가 **없으면** INSERT
|
|
471
|
-
- `data`에 `seq`가 **있으면** UPDATE
|
|
472
|
-
- `unique` 선언 필드 기준 중복 감지 시 자동 UPDATE (upsert)
|
|
473
|
-
|
|
474
|
-
응답의 `seq`는 생성/수정된 레코드의 시퀀스 ID입니다.
|
|
475
|
-
|
|
476
|
-
| 옵션 | 타입 | 설명 |
|
|
477
|
-
| --------------- | --------- | ----------------------------------------------------------------- |
|
|
478
|
-
| `transactionId` | `string` | 수동 트랜잭션 ID. 미지정 시 활성 트랜잭션 자동 사용 |
|
|
479
|
-
| `skipHooks` | `boolean` | `true`이면 `before/after_insert`, `before/after_update` 훅 미실행 |
|
|
480
|
-
|
|
481
|
-
```ts
|
|
482
|
-
// INSERT
|
|
483
|
-
const res = await client.submit("account", {
|
|
484
|
-
name: "홍길동",
|
|
485
|
-
email: "hong@example.com",
|
|
486
|
-
});
|
|
487
|
-
res.seq; // 생성된 seq
|
|
488
|
-
|
|
489
|
-
// UPDATE (seq 포함 시)
|
|
490
|
-
await client.submit("account", { seq: 1, name: "홍길순" });
|
|
491
|
-
|
|
492
|
-
// 훅 없이 저장
|
|
493
|
-
await client.submit("account", { name: "테스트" }, { skipHooks: true });
|
|
494
|
-
|
|
495
|
-
// 트랜잭션 내에서 — seq placeholder 활용
|
|
496
|
-
await client.transStart();
|
|
497
|
-
const r1 = await client.submit("order", { product_seq: 1, qty: 2 });
|
|
498
|
-
// r1.seq → "$tx.0" (commit 후 실제 seq로 치환)
|
|
499
|
-
await client.submit("order_item", { order_seq: r1.seq, name: "상품A" });
|
|
500
|
-
await client.transCommit();
|
|
501
|
-
```
|
|
502
|
-
|
|
503
|
-
---
|
|
504
|
-
|
|
505
|
-
### `delete(entity, seq, opts?)`
|
|
506
|
-
|
|
507
|
-
엔티티를 삭제합니다.
|
|
508
|
-
|
|
509
|
-
| 옵션 | 타입 | 기본값 | 설명 |
|
|
510
|
-
| --------------- | --------- | ------- | --------------------------------------------------------- |
|
|
511
|
-
| `hard` | `boolean` | `false` | `true`이면 완전 삭제. `false`이면 소프트 삭제 (복원 가능) |
|
|
512
|
-
| `transactionId` | `string` | — | 수동 트랜잭션 ID. 미지정 시 활성 트랜잭션 자동 사용 |
|
|
513
|
-
| `skipHooks` | `boolean` | `false` | `true`이면 `before/after_delete` 훅 미실행 |
|
|
514
|
-
|
|
515
|
-
> **소프트 삭제 (기본)**: DB에서 제거하지 않고 삭제 표시만 합니다. `rollback()`으로 복원 가능합니다.
|
|
516
|
-
> **하드 삭제**: DB에서 완전히 제거됩니다. 복원 불가능합니다.
|
|
517
|
-
|
|
518
|
-
```ts
|
|
519
|
-
// 소프트 삭제 (기본)
|
|
520
|
-
const res = await client.delete("account", 2);
|
|
521
|
-
res.deleted; // 삭제된 seq
|
|
522
|
-
|
|
523
|
-
// 하드 삭제
|
|
524
|
-
await client.delete("account", 3, { hard: true });
|
|
525
|
-
```
|
|
526
|
-
|
|
527
|
-
---
|
|
528
|
-
|
|
529
|
-
### `history(entity, seq, params?)`
|
|
530
|
-
|
|
531
|
-
엔티티 단건의 변경 이력을 조회합니다. `params`는 `page`, `limit`만 지원합니다.
|
|
532
|
-
|
|
533
|
-
응답 `data.items`의 각 레코드 구조:
|
|
534
|
-
|
|
535
|
-
| 필드 | 타입 | 설명 |
|
|
536
|
-
| ---------------- | -------------- | ------------------------------------------------------------------------------------ |
|
|
537
|
-
| `seq` | `number` | 이력 레코드 seq (`rollback()` 호출 시 사용) |
|
|
538
|
-
| `action` | `string` | `"INSERT"` \| `"UPDATE"` \| `"DELETE_SOFT"` \| `"DELETE_HARD"` \| `"ROLLBACK"` |
|
|
539
|
-
| `data_snapshot` | `T \| null` | **after 통일 모델**: INSERT/UPDATE → 변경 **후** 데이터, DELETE → 삭제 **전** 데이터 |
|
|
540
|
-
| `changed_by` | `number\|null` | 변경한 계정 seq. 시스템 변경이면 `null` |
|
|
541
|
-
| `changed_time` | `string` | 변경 시각 (ISO 8601) |
|
|
542
|
-
| `transaction_id` | `string` | 해당 변경이 속한 트랜잭션 ID. rollback 시 이 ID 기준으로 전체 롤백됨 |
|
|
543
|
-
|
|
544
|
-
```ts
|
|
545
|
-
const result = await client.history("account", 1, { page: 1, limit: 50 });
|
|
546
|
-
result.data.total; // 전체 이력 건수
|
|
547
|
-
result.data.items[0].action; // "UPDATE"
|
|
548
|
-
result.data.items[0].data_snapshot; // 변경 후 데이터
|
|
549
|
-
result.data.items[0].transaction_id; // "TX-20260201-abc123"
|
|
550
|
-
```
|
|
551
|
-
|
|
552
|
-
---
|
|
553
|
-
|
|
554
|
-
### `rollback(entity, historySeq)`
|
|
555
|
-
|
|
556
|
-
특정 이력 레코드의 `transaction_id`를 조회해 **해당 트랜잭션 전체**를 롤백합니다.
|
|
557
|
-
|
|
558
|
-
한 번의 트랜잭션에 여러 엔티티가 변경됐다면 모든 엔티티가 함께 되돌아갑니다.
|
|
559
|
-
|
|
560
|
-
```ts
|
|
561
|
-
// history()로 이력 seq 조회 후 rollback
|
|
562
|
-
const hist = await client.history("account", 1);
|
|
563
|
-
const targetHistorySeq = hist.data.items[0].seq;
|
|
564
|
-
await client.rollback("account", targetHistorySeq);
|
|
565
|
-
```
|
|
566
|
-
|
|
567
|
-
---
|
|
568
|
-
|
|
569
|
-
## 푸시 관련
|
|
570
|
-
|
|
571
|
-
### `push(pushEntity, payload, opts?)`
|
|
572
|
-
|
|
573
|
-
`submit()`의 별칭입니다. 푸시 관련 엔티티에 데이터를 제출합니다.
|
|
574
|
-
|
|
575
|
-
```ts
|
|
576
|
-
await client.push("push_message", {
|
|
577
|
-
title: "새 알림",
|
|
578
|
-
body: "내용",
|
|
579
|
-
account_seq: 1,
|
|
580
|
-
});
|
|
581
|
-
```
|
|
582
|
-
|
|
583
|
-
### `pushLogList(params?)`
|
|
584
|
-
|
|
585
|
-
`push_log` 엔티티의 목록을 조회합니다. `list()`의 별칭입니다.
|
|
586
|
-
|
|
587
|
-
```ts
|
|
588
|
-
const logs = await client.pushLogList({
|
|
589
|
-
page: 1,
|
|
590
|
-
limit: 30,
|
|
591
|
-
orderBy: "-created_time",
|
|
592
|
-
conditions: { account_seq: 1 },
|
|
593
|
-
});
|
|
594
|
-
logs.data.items;
|
|
595
|
-
logs.data.total;
|
|
596
|
-
```
|
|
597
|
-
|
|
598
|
-
### `registerPushDevice(accountSeq, deviceId, pushToken, opts?)`
|
|
599
|
-
|
|
600
|
-
`account_device` 엔티티에 디바이스를 등록합니다.
|
|
601
|
-
|
|
602
|
-
| 옵션 | 타입 | 설명 |
|
|
603
|
-
| ---------------- | --------- | --------------------------------- |
|
|
604
|
-
| `platform` | `string` | 플랫폼 (예: `"web"`, `"android"`) |
|
|
605
|
-
| `deviceType` | `string` | 디바이스 종류 (예: `"mobile"`) |
|
|
606
|
-
| `browser` | `string` | 브라우저명 (예: `"Chrome"`) |
|
|
607
|
-
| `browserVersion` | `string` | 브라우저 버전 |
|
|
608
|
-
| `pushEnabled` | `boolean` | 푸시 수신 여부. 기본값 `true` |
|
|
609
|
-
| `transactionId` | `string` | 트랜잭션 ID |
|
|
610
|
-
|
|
611
|
-
```ts
|
|
612
|
-
const res = await client.registerPushDevice(
|
|
613
|
-
1,
|
|
614
|
-
"device-uuid-001",
|
|
615
|
-
"fcm-token-abc",
|
|
616
|
-
{
|
|
617
|
-
platform: "web",
|
|
618
|
-
browser: "Chrome",
|
|
619
|
-
browserVersion: "120",
|
|
620
|
-
pushEnabled: true,
|
|
621
|
-
},
|
|
622
|
-
);
|
|
623
|
-
res.seq; // 등록된 account_device seq
|
|
624
|
-
```
|
|
625
|
-
|
|
626
|
-
### `updatePushDeviceToken(deviceSeq, pushToken, opts?)`
|
|
627
|
-
|
|
628
|
-
등록된 디바이스의 푸시 토큰을 갱신합니다.
|
|
629
|
-
|
|
630
|
-
```ts
|
|
631
|
-
await client.updatePushDeviceToken(10, "new-fcm-token", { pushEnabled: true });
|
|
632
|
-
```
|
|
633
|
-
|
|
634
|
-
### `disablePushDevice(deviceSeq, opts?)`
|
|
635
|
-
|
|
636
|
-
디바이스의 푸시 수신을 비활성화합니다 (`push_enabled: false`).
|
|
637
|
-
|
|
638
|
-
```ts
|
|
639
|
-
await client.disablePushDevice(10);
|
|
640
|
-
```
|
|
641
|
-
|
|
642
|
-
---
|
|
643
|
-
|
|
644
|
-
## React Hook
|
|
645
|
-
|
|
646
|
-
### `useEntityServer(options?)`
|
|
647
|
-
|
|
648
|
-
React 컴포넌트에서 EntityServerClient를 사용할 때 권장되는 훅입니다.
|
|
649
|
-
`submit` / `del` / `query` 의 `isPending`, `error` 상태를 자동으로 관리합니다.
|
|
650
|
-
|
|
651
|
-
```ts
|
|
652
|
-
import { useEntityServer } from "entity-server-client/react";
|
|
653
|
-
```
|
|
654
|
-
|
|
655
|
-
#### 옵션
|
|
656
|
-
|
|
657
|
-
| 옵션 | 타입 | 기본값 | 설명 |
|
|
658
|
-
| ------------------ | ----------------------------------- | ------ | ----------------------------------------------------------------------------------------------- |
|
|
659
|
-
| `singleton` | `boolean` | `true` | `true`이면 전역 `entityServer` 인스턴스 사용 |
|
|
660
|
-
| `baseUrl` | `string` | — | 서버 주소 (singleton일 때 `configure()` 호출) |
|
|
661
|
-
| `packetMagicLen` | `number` | — | 암호화 패킷 magic 바이트 길이 |
|
|
662
|
-
| `token` | `string` | — | JWT Access Token |
|
|
663
|
-
| `tokenResolver` | `() => string \| undefined \| null` | — | 렌더 시점에 토큰을 동적으로 주입하는 함수 |
|
|
664
|
-
| `keepSession` | `boolean` | — | 세션 유지 활성화 (인덱스 참고) |
|
|
665
|
-
| `onTokenRefreshed` | `(accessToken, expiresIn) => void` | — | 세션 유지 성공 콜백 |
|
|
666
|
-
| `onSessionExpired` | `(error: Error) => void` | — | 세션 만료 콜백 (refresh_token 만료 등) |
|
|
667
|
-
| `resumeSession` | `string` | — | 저장된 refresh_token. 마운트 시 `refreshToken()` 호출해 토큰 복원 + `keepSession` 타이머 재시작 |
|
|
668
|
-
|
|
669
|
-
#### 반환값
|
|
670
|
-
|
|
671
|
-
| 필드 | 타입 | 설명 |
|
|
672
|
-
| ----------- | -------------------- | --------------------------------------------------- |
|
|
673
|
-
| `client` | `EntityServerClient` | 인스턴스 직접 접근 (read 메서드 등) |
|
|
674
|
-
| `isPending` | `boolean` | `submit` / `del` / `query` 진행 중 여부 |
|
|
675
|
-
| `error` | `Error \| null` | 마지막 mutation 에러. 성공 또는 `reset()` 시 `null` |
|
|
676
|
-
| `reset` | `() => void` | `isPending`, `error` 초기화 |
|
|
677
|
-
| `submit` | function | `client.submit()` 래퍼 — 상태 자동 관리 |
|
|
678
|
-
| `del` | function | `client.delete()` 래퍼 — 상태 자동 관리 |
|
|
679
|
-
| `query` | function | `client.query()` 래퍼 — 상태 자동 관리 |
|
|
680
|
-
|
|
681
|
-
#### 예시
|
|
682
|
-
|
|
683
|
-
```tsx
|
|
684
|
-
import { useEntityServer } from "entity-server-client/react";
|
|
685
|
-
|
|
686
|
-
function AccountForm() {
|
|
687
|
-
const { submit, del, isPending, error, reset } = useEntityServer();
|
|
688
|
-
|
|
689
|
-
const handleSave = async () => {
|
|
690
|
-
reset();
|
|
691
|
-
await submit("account", { name: "홍길동", email: "hong@example.com" });
|
|
692
|
-
};
|
|
693
|
-
|
|
694
|
-
const handleDelete = async (seq: number) => {
|
|
695
|
-
await del("account", seq);
|
|
696
|
-
};
|
|
697
|
-
|
|
698
|
-
return (
|
|
699
|
-
<div>
|
|
700
|
-
{isPending && <span>저장 중...</span>}
|
|
701
|
-
{error && <span style={{ color: "red" }}>{error.message}</span>}
|
|
702
|
-
<button onClick={handleSave} disabled={isPending}>
|
|
703
|
-
저장
|
|
704
|
-
</button>
|
|
705
|
-
<button onClick={() => handleDelete(1)} disabled={isPending}>
|
|
706
|
-
삭제
|
|
707
|
-
</button>
|
|
708
|
-
</div>
|
|
709
|
-
);
|
|
710
|
-
}
|
|
711
|
-
```
|
|
712
|
-
|
|
713
|
-
토큰을 런타임에 주입하는 경우:
|
|
714
|
-
|
|
715
|
-
```tsx
|
|
716
|
-
const { submit } = useEntityServer({
|
|
717
|
-
tokenResolver: () => localStorage.getItem("access_token"),
|
|
718
|
-
});
|
|
719
|
-
```
|
|
720
|
-
|
|
721
|
-
**페이지 새로고침 후 로그인 상태 + 자동 갱신 복원:**
|
|
722
|
-
|
|
723
|
-
`resumeSession`에 저장된 refresh_token을 넘기면 마운트 시 `refreshToken()`을 호출해
|
|
724
|
-
새 access_token을 발급받고, `keepSession: true`이면 타이머도 자동 재시작됩니다.
|
|
725
|
-
|
|
726
|
-
```tsx
|
|
727
|
-
// App.tsx 또는 로그인 복원을 담당하는 컴포넌트
|
|
728
|
-
export function AppShell() {
|
|
729
|
-
const storedRefreshToken =
|
|
730
|
-
localStorage.getItem("auth_refresh_token") ?? undefined;
|
|
731
|
-
|
|
732
|
-
useEntityServer({
|
|
733
|
-
resumeSession: storedRefreshToken,
|
|
734
|
-
keepSession: true,
|
|
735
|
-
onTokenRefreshed: (accessToken) => {
|
|
736
|
-
localStorage.setItem("auth_access_token", accessToken);
|
|
737
|
-
},
|
|
738
|
-
onSessionExpired: () => {
|
|
739
|
-
window.location.href = "/login";
|
|
740
|
-
},
|
|
741
|
-
});
|
|
742
|
-
|
|
743
|
-
return <RouterOutlet />;
|
|
744
|
-
}
|
|
745
|
-
```
|
|
746
|
-
|
|
747
|
-
> 마운트 시 `resumeSession`이 있으면:
|
|
748
|
-
>
|
|
749
|
-
> 1. `refreshToken(storedRefreshToken)` 호출 → 서버에서 새 access_token 발급
|
|
750
|
-
> 2. 패키지 내부 `this.token` 자동 교체
|
|
751
|
-
> 3. `onTokenRefreshed` 콜백 호출
|
|
752
|
-
> 4. `keepSession: true`이면 다음 만료 전 타이머 자동 예약
|
|
753
|
-
> 5. 실패 시 `onSessionExpired` 콜백 호출
|
|
754
|
-
|
|
755
|
-
컴포넌트별 독립 인스턴스가 필요한 경우:
|
|
756
|
-
|
|
757
|
-
```tsx
|
|
758
|
-
const { client } = useEntityServer({
|
|
759
|
-
singleton: false,
|
|
760
|
-
baseUrl: "https://other-server.example.com",
|
|
761
|
-
});
|
|
762
|
-
```
|
|
763
|
-
|
|
764
|
-
---
|
|
765
|
-
|
|
766
|
-
## 암호화 패킷 처리
|
|
767
|
-
|
|
768
|
-
### 자동 복호화
|
|
769
|
-
|
|
770
|
-
서버 응답이 `Content-Type: application/octet-stream`이면 `request()` 내부에서 자동으로 복호화됩니다.
|
|
771
|
-
XChaCha20-Poly1305 알고리즘을 사용하며, 키는 현재 JWT 토큰의 SHA-256 해시입니다.
|
|
772
|
-
|
|
773
|
-
별도 처리 없이 일반 응답과 동일하게 사용하면 됩니다.
|
|
774
|
-
|
|
775
|
-
### `readRequestBody(body, contentType?, requireEncrypted?)`
|
|
776
|
-
|
|
777
|
-
원시 암호화 payload를 직접 파싱할 때 사용합니다.
|
|
778
|
-
주로 서버 측 미들웨어나 SSR 환경에서 클라이언트로부터 받은 암호화 body를 처리할 때 활용합니다.
|
|
779
|
-
|
|
780
|
-
```ts
|
|
781
|
-
// 암호화 body 복호화
|
|
782
|
-
const data = client.readRequestBody(
|
|
783
|
-
arrayBuffer, // ArrayBuffer | Uint8Array
|
|
784
|
-
"application/octet-stream",
|
|
785
|
-
true, // requireEncrypted: 암호화 아니면 에러
|
|
786
|
-
);
|
|
5
|
+
## 목차
|
|
787
6
|
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
7
|
+
| 구분 | 파일 |
|
|
8
|
+
| ------------------ | -------------------------------------------- |
|
|
9
|
+
| import | [api/import.md](api/import.md) |
|
|
10
|
+
| 서버 헬스체크 | [api/health.md](api/health.md) |
|
|
11
|
+
| 인스턴스 생성/설정 | [api/setup.md](api/setup.md) |
|
|
12
|
+
| 인증 | [api/auth.md](api/auth.md) |
|
|
13
|
+
| 트랜잭션 | [api/transaction.md](api/transaction.md) |
|
|
14
|
+
| 엔티티 CRUD / 조회 | [api/entity.md](api/entity.md) |
|
|
15
|
+
| 푸시 관련 | [api/push.md](api/push.md) |
|
|
16
|
+
| 이메일 인증 / 변경 | [api/email.md](api/email.md) |
|
|
17
|
+
| SMS | [api/sms.md](api/sms.md) |
|
|
18
|
+
| SMTP 메일 | [api/smtp.md](api/smtp.md) |
|
|
19
|
+
| 알림톡 / 친구톡 | [api/alimtalk.md](api/alimtalk.md) |
|
|
20
|
+
| PG 결제 | [api/pg.md](api/pg.md) |
|
|
21
|
+
| 파일 스토리지 | [api/file.md](api/file.md) |
|
|
22
|
+
| 본인인증 | [api/identity.md](api/identity.md) |
|
|
23
|
+
| QR코드 / 바코드 | [api/utils.md](api/utils.md) |
|
|
24
|
+
| React Hook | [api/react.md](api/react.md) |
|
|
25
|
+
| 암호화 패킷 처리 | [api/packet.md](api/packet.md) |
|