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.
Files changed (50) hide show
  1. package/README.md +32 -1
  2. package/build.mjs +11 -1
  3. package/docs/api/alimtalk.md +62 -0
  4. package/docs/api/auth.md +256 -0
  5. package/docs/api/email.md +37 -0
  6. package/docs/api/entity.md +273 -0
  7. package/docs/api/file.md +80 -0
  8. package/docs/api/health.md +41 -0
  9. package/docs/api/identity.md +32 -0
  10. package/docs/api/import.md +34 -0
  11. package/docs/api/packet.md +25 -0
  12. package/docs/api/pg.md +90 -0
  13. package/docs/api/push.md +107 -0
  14. package/docs/api/react.md +141 -0
  15. package/docs/api/setup.md +43 -0
  16. package/docs/api/sms.md +45 -0
  17. package/docs/api/smtp.md +33 -0
  18. package/docs/api/transaction.md +50 -0
  19. package/docs/api/utils.md +52 -0
  20. package/docs/apis.md +22 -787
  21. package/docs/react.md +64 -7
  22. package/package.json +7 -1
  23. package/src/EntityServerClient.ts +28 -546
  24. package/src/client/base.ts +305 -0
  25. package/src/client/packet.ts +16 -52
  26. package/src/client/request.ts +46 -9
  27. package/src/client/utils.ts +17 -2
  28. package/src/hooks/useEntityServer.ts +3 -4
  29. package/src/mixins/auth.ts +143 -0
  30. package/src/mixins/entity.ts +205 -0
  31. package/src/mixins/file.ts +99 -0
  32. package/src/mixins/push.ts +109 -0
  33. package/src/mixins/smtp.ts +20 -0
  34. package/src/mixins/utils.ts +106 -0
  35. package/src/packet.ts +84 -0
  36. package/src/types.ts +93 -1
  37. package/tests/packet.test.mjs +50 -0
  38. package/dist/EntityServerClient.d.ts +0 -203
  39. package/dist/client/hmac.d.ts +0 -8
  40. package/dist/client/packet.d.ts +0 -22
  41. package/dist/client/request.d.ts +0 -16
  42. package/dist/client/utils.d.ts +0 -4
  43. package/dist/hooks/useEntityServer.d.ts +0 -63
  44. package/dist/index.d.ts +0 -4
  45. package/dist/index.js +0 -2
  46. package/dist/index.js.map +0 -7
  47. package/dist/react.d.ts +0 -1
  48. package/dist/react.js +0 -2
  49. package/dist/react.js.map +0 -7
  50. package/dist/types.d.ts +0 -165
@@ -0,0 +1,273 @@
1
+ # 엔티티 CRUD / 조회
2
+
3
+ ## `get(entity, seq, opts?)`
4
+
5
+ 시퀀스 ID로 엔티티 단건을 조회합니다.
6
+
7
+ | 옵션 | 타입 | 설명 |
8
+ | ----------- | --------- | ----------------------------------------- |
9
+ | `skipHooks` | `boolean` | `true`이면 `after_get` 훅을 실행하지 않음 |
10
+
11
+ ```ts
12
+ const result = await client.get<Account>("account", 1);
13
+ result.data; // Account 타입 객체
14
+ ```
15
+
16
+ ---
17
+
18
+ ## `find(entity, conditions?, opts?)`
19
+
20
+ 조건(conditions)으로 첫 번째 일치 레코드를 조회합니다.
21
+ `data` 컬럼을 **항상 완전히 복호화**하여 반환합니다. 레코드가 없으면 `404` 에러가 됩니다.
22
+
23
+ | 파라미터 | 타입 | 설명 |
24
+ | ------------ | ------------------------- | ----------------------------------- |
25
+ | `conditions` | `Record<string, unknown>` | 검색 조건 (인덱스 필드만 조건 가능) |
26
+ | `skipHooks` | `boolean` | `true`이면 훅 실행 건너뛰기 |
27
+
28
+ ```ts
29
+ // 이메일로 계정 단건 조회
30
+ const result = await client.find<Account>("account", {
31
+ email: "hong@example.com",
32
+ });
33
+ result.data; // Account 전체 필드 (passwd 포함)
34
+
35
+ // skipHooks 옵션
36
+ const result = await client.find<Account>(
37
+ "account",
38
+ { code: "A001" },
39
+ { skipHooks: true },
40
+ );
41
+ ```
42
+
43
+ > **`get` vs `find`**
44
+ >
45
+ > - `get(entity, seq)`: seq(일련번호)를 정확히 알고 있을 때 빠르게 조회
46
+ > - `find(entity, conditions)`: 조건으로 검색, 항상 data 전체 복호화 반환
47
+
48
+ ---
49
+
50
+ ## `list(entity, params?)`
51
+
52
+ 페이지네이션/정렬/필터 조건으로 엔티티 목록을 조회합니다.
53
+
54
+ | 파라미터 | 타입 | 기본값 | 설명 |
55
+ | ------------ | ------------------------- | ------- | --------------------------------------------------- |
56
+ | `page` | `number` | `1` | 페이지 번호 |
57
+ | `limit` | `number` | `20` | 페이지당 레코드 수 (최대 1000) |
58
+ | `orderBy` | `string` | — | 정렬 기준 필드명. `-` 접두사로 내림차순 지정 가능 |
59
+ | `orderDir` | `"ASC" \| "DESC"` | `"ASC"` | 정렬 방향 (`orderBy: "-field"`와 동일 효과) |
60
+ | `fields` | `string[]` | — | 반환할 필드 목록 (**미지정 시 인덱스 필드만 반환**) |
61
+ | `conditions` | `Record<string, unknown>` | — | 필터 조건 |
62
+
63
+ ### `fields` — 반환 필드 지정
64
+
65
+ `fields`는 `entity.json`의 `index`로 선언된 필드명만 지정할 수 있습니다.
66
+ 존재하지 않는 필드를 지정하면 서버 에러가 발생합니다.
67
+
68
+ | 값 | 설명 |
69
+ | ------------------- | -------------------------------------------------------------- |
70
+ | 미지정 (기본값) | 인덱스 선언 필드만 반환. **복호화를 건너뛰어 가장 빠름** |
71
+ | `["*"]` | 전체 필드 반환 (복호화 수행) |
72
+ | `["name", "email"]` | 지정한 인덱스 필드만 반환. 모두 인덱스 필드면 역시 복호화 생략 |
73
+
74
+ > `seq`, `created_time`, `updated_time`, `license_seq`는 `fields` 지정 여부와 무관하게 항상 포함됩니다.
75
+
76
+ ### `conditions` — 필터 조건
77
+
78
+ 인덱스 테이블의 필드(`index` / `hash` / `unique`로 선언된 필드)에만 조건을 걸 수 있습니다.
79
+ 인덱스에 없는 필드로 필터를 걸면 동작하지 않거나 에러가 발생합니다.
80
+
81
+ ```ts
82
+ // 기본값 (인덱스 필드만, 가장 빠름)
83
+ const result = await client.list("account");
84
+ result.data.items; // 레코드 배열
85
+ result.data.total; // 필터 조건 일치 전체 건수
86
+ result.data.page; // 현재 페이지
87
+ result.data.limit; // 페이지당 레코드 수
88
+
89
+ // 정렬 + 필터 + 필드 선택
90
+ const result = await client.list("account", {
91
+ page: 1,
92
+ limit: 20,
93
+ orderBy: "created_time",
94
+ orderDir: "DESC",
95
+ fields: ["seq", "name", "email"], // index 필드만 → 복호화 생략
96
+ conditions: { status: "active" },
97
+ });
98
+
99
+ // 전체 필드 반환 (fields: ["*"])
100
+ const full = await client.list("account", {
101
+ fields: ["*"],
102
+ conditions: { status: "active" },
103
+ });
104
+
105
+ // orderBy에 - 접두사로 내림차순 (orderDir: "DESC"와 동일)
106
+ const desc = await client.list("account", { orderBy: "-created_time" });
107
+ ```
108
+
109
+ ---
110
+
111
+ ## `count(entity, conditions?)`
112
+
113
+ 레코드 건수를 조회합니다. `conditions`는 `list()`와 동일한 필터 규칙을 따릅니다.
114
+
115
+ ```ts
116
+ const total = await client.count("account");
117
+ total.count; // 전체 건수
118
+
119
+ const active = await client.count("account", { status: "active" });
120
+ active.count; // 조건 일치 건수
121
+ ```
122
+
123
+ ---
124
+
125
+ ## `query(entity, req)`
126
+
127
+ 커스텀 SQL로 엔티티를 조회합니다.
128
+
129
+ **제약사항**:
130
+
131
+ - SELECT 쿼리만 허용 (INSERT/UPDATE/DELETE 불가)
132
+ - 인덱스 테이블(`entity_idx_*`)만 접근 가능. 암호화된 본문 필드는 조회 불가
133
+ - `SELECT *` 불가. 인덱스 선언 필드만 SELECT 가능
134
+ - 최대 반환 건수: 1000
135
+
136
+ `entity`는 URL 라우트 경로용 기본 엔티티명으로, 실제 조회 대상은 SQL에서 결정됩니다.
137
+
138
+ | 필드 | 타입 | 설명 |
139
+ | -------- | ----------- | ----------------------------------------------- |
140
+ | `sql` | `string` | SELECT SQL문. 사용자 입력은 반드시 `?`로 바인딩 |
141
+ | `params` | `unknown[]` | `?` 플레이스홀더에 바인딩할 값 배열 |
142
+ | `limit` | `number` | 최대 반환 건수 (최대 1000) |
143
+
144
+ 응답: `{ ok: true, data: { items: T[], count: number } }`
145
+
146
+ ```ts
147
+ // 단일 엔티티
148
+ const result = await client.query("account", {
149
+ sql: "SELECT seq, name, email FROM account WHERE status = ?",
150
+ params: ["active"],
151
+ limit: 50,
152
+ });
153
+ result.data.items; // 레코드 배열
154
+ result.data.count; // 반환된 건수
155
+
156
+ // JOIN으로 여러 엔티티 조합
157
+ const joined = await client.query("order", {
158
+ sql: `
159
+ SELECT o.seq, o.status, u.name AS user_name, u.email
160
+ FROM order o
161
+ JOIN account u ON u.data_seq = o.account_seq
162
+ WHERE o.status = ?
163
+ ORDER BY o.seq DESC
164
+ `,
165
+ params: ["pending"],
166
+ limit: 100,
167
+ });
168
+ ```
169
+
170
+ > SQL Injection 방지: 사용자 입력값은 반드시 `params`로 바인딩하세요.
171
+
172
+ ---
173
+
174
+ ## `submit(entity, data, opts?)`
175
+
176
+ 엔티티를 생성 또는 수정합니다.
177
+
178
+ - `data`에 `seq`가 **없으면** INSERT
179
+ - `data`에 `seq`가 **있으면** UPDATE
180
+ - `unique` 선언 필드 기준 중복 감지 시 자동 UPDATE (upsert)
181
+
182
+ 응답의 `seq`는 생성/수정된 레코드의 시퀀스 ID입니다.
183
+
184
+ | 옵션 | 타입 | 설명 |
185
+ | --------------- | --------- | ----------------------------------------------------------------- |
186
+ | `transactionId` | `string` | 수동 트랜잭션 ID. 미지정 시 활성 트랜잭션 자동 사용 |
187
+ | `skipHooks` | `boolean` | `true`이면 `before/after_insert`, `before/after_update` 훅 미실행 |
188
+
189
+ ```ts
190
+ // INSERT
191
+ const res = await client.submit("account", {
192
+ name: "홍길동",
193
+ email: "hong@example.com",
194
+ });
195
+ res.seq; // 생성된 seq
196
+
197
+ // UPDATE (seq 포함 시)
198
+ await client.submit("account", { seq: 1, name: "홍길순" });
199
+
200
+ // 훅 없이 저장
201
+ await client.submit("account", { name: "테스트" }, { skipHooks: true });
202
+
203
+ // 트랜잭션 내에서 — seq placeholder 활용
204
+ await client.transStart();
205
+ const r1 = await client.submit("order", { product_seq: 1, qty: 2 });
206
+ // r1.seq → "$tx.0" (commit 후 실제 seq로 치환)
207
+ await client.submit("order_item", { order_seq: r1.seq, name: "상품A" });
208
+ await client.transCommit();
209
+ ```
210
+
211
+ ---
212
+
213
+ ## `delete(entity, seq, opts?)`
214
+
215
+ 엔티티를 삭제합니다.
216
+
217
+ | 옵션 | 타입 | 기본값 | 설명 |
218
+ | --------------- | --------- | ------- | --------------------------------------------------------- |
219
+ | `hard` | `boolean` | `false` | `true`이면 완전 삭제. `false`이면 소프트 삭제 (복원 가능) |
220
+ | `transactionId` | `string` | — | 수동 트랜잭션 ID. 미지정 시 활성 트랜잭션 자동 사용 |
221
+ | `skipHooks` | `boolean` | `false` | `true`이면 `before/after_delete` 훅 미실행 |
222
+
223
+ > **소프트 삭제 (기본)**: DB에서 제거하지 않고 삭제 표시만 합니다. `rollback()`으로 복원 가능합니다.
224
+ > **하드 삭제**: DB에서 완전히 제거됩니다. 복원 불가능합니다.
225
+
226
+ ```ts
227
+ // 소프트 삭제 (기본)
228
+ const res = await client.delete("account", 2);
229
+ res.deleted; // 삭제된 seq
230
+
231
+ // 하드 삭제
232
+ await client.delete("account", 3, { hard: true });
233
+ ```
234
+
235
+ ---
236
+
237
+ ## `history(entity, seq, params?)`
238
+
239
+ 엔티티 단건의 변경 이력을 조회합니다. `params`는 `page`, `limit`만 지원합니다.
240
+
241
+ 응답 `data.items`의 각 레코드 구조:
242
+
243
+ | 필드 | 타입 | 설명 |
244
+ | ---------------- | -------------- | ------------------------------------------------------------------------------------ |
245
+ | `seq` | `number` | 이력 레코드 seq (`rollback()` 호출 시 사용) |
246
+ | `action` | `string` | `"INSERT"` \| `"UPDATE"` \| `"DELETE_SOFT"` \| `"DELETE_HARD"` \| `"ROLLBACK"` |
247
+ | `data_snapshot` | `T \| null` | **after 통일 모델**: INSERT/UPDATE → 변경 **후** 데이터, DELETE → 삭제 **전** 데이터 |
248
+ | `changed_by` | `number\|null` | 변경한 계정 seq. 시스템 변경이면 `null` |
249
+ | `changed_time` | `string` | 변경 시각 (ISO 8601) |
250
+ | `transaction_id` | `string` | 해당 변경이 속한 트랜잭션 ID. rollback 시 이 ID 기준으로 전체 롤백됨 |
251
+
252
+ ```ts
253
+ const result = await client.history("account", 1, { page: 1, limit: 50 });
254
+ result.data.total; // 전체 이력 건수
255
+ result.data.items[0].action; // "UPDATE"
256
+ result.data.items[0].data_snapshot; // 변경 후 데이터
257
+ result.data.items[0].transaction_id; // "TX-20260201-abc123"
258
+ ```
259
+
260
+ ---
261
+
262
+ ## `rollback(entity, historySeq)`
263
+
264
+ 특정 이력 레코드의 `transaction_id`를 조회해 **해당 트랜잭션 전체**를 롤백합니다.
265
+
266
+ 한 번의 트랜잭션에 여러 엔티티가 변경됐다면 모든 엔티티가 함께 되돌아갑니다.
267
+
268
+ ```ts
269
+ // history()로 이력 seq 조회 후 rollback
270
+ const hist = await client.history("account", 1);
271
+ const targetHistorySeq = hist.data.items[0].seq;
272
+ await client.rollback("account", targetHistorySeq);
273
+ ```
@@ -0,0 +1,80 @@
1
+ # 파일 스토리지
2
+
3
+ ## `fileUpload(entity, file, opts?)`
4
+
5
+ 파일을 업로드합니다 (multipart/form-data). `entity`는 파일이 귀속될 엔티티명입니다.
6
+
7
+ | 옵션 | 타입 | 설명 |
8
+ | ---------- | --------- | ----------------------------------------- |
9
+ | `refSeq` | `number` | 연결할 엔티티 레코드 seq |
10
+ | `isPublic` | `boolean` | `true`이면 인증 없이 URL로 직접 접근 가능 |
11
+
12
+ ```ts
13
+ const input = document.querySelector('input[type="file"]') as HTMLInputElement;
14
+ const file = input.files![0];
15
+
16
+ const res = await client.fileUpload("product", file, {
17
+ refSeq: 10,
18
+ isPublic: false,
19
+ });
20
+ res.uuid; // 파일 고유 UUID
21
+ res.data; // FileMeta 객체
22
+ res.data.name; // 원본 파일명
23
+ res.data.size; // 파일 크기 (bytes)
24
+ res.data.mime; // MIME 타입
25
+ ```
26
+
27
+ ## `fileDownload(entity, uuid)`
28
+
29
+ 파일을 다운로드합니다. `ArrayBuffer`를 반환합니다.
30
+
31
+ ```ts
32
+ const buf = await client.fileDownload("product", "uuid-here");
33
+ const blob = new Blob([buf], { type: "image/png" });
34
+ const url = URL.createObjectURL(blob);
35
+ ```
36
+
37
+ ## `fileDelete(entity, uuid)`
38
+
39
+ 파일을 삭제합니다.
40
+
41
+ ```ts
42
+ await client.fileDelete("product", "uuid-here");
43
+ ```
44
+
45
+ ## `fileList(entity, opts?)`
46
+
47
+ 엔티티에 연결된 파일 목록을 조회합니다.
48
+
49
+ ```ts
50
+ const res = await client.fileList("product", { refSeq: 10 });
51
+ res.data.items; // FileMeta[]
52
+ res.data.total; // 전체 건수
53
+ ```
54
+
55
+ ## `fileMeta(entity, uuid)`
56
+
57
+ 파일 메타 정보를 조회합니다.
58
+
59
+ ```ts
60
+ const res = await client.fileMeta("product", "uuid-here");
61
+ res.data; // FileMeta
62
+ ```
63
+
64
+ ## `fileToken(uuid)`
65
+
66
+ 임시 파일 접근 토큰을 발급합니다. 비공개 파일을 일시적으로 공유할 때 사용합니다.
67
+
68
+ ```ts
69
+ const res = await client.fileToken("uuid-here");
70
+ const tempUrl = `${baseUrl}/v1/files/${res.token}`;
71
+ ```
72
+
73
+ ## `fileUrl(uuid)`
74
+
75
+ 파일 직접 접근 URL을 반환합니다. (fetch 없음, URL 조합만)
76
+
77
+ ```ts
78
+ const url = client.fileUrl("uuid-here");
79
+ // → "http://localhost:47200/v1/files/uuid-here"
80
+ ```
@@ -0,0 +1,41 @@
1
+ # 서버 헬스체크
2
+
3
+ ## 개요
4
+
5
+ 앱 시작 시 서버의 패킷 암호화 설정을 감지하여 클라이언트 설정을 자동으로 맞춥니다.
6
+
7
+ **헬스체크 응답:**
8
+
9
+ ```json
10
+ {
11
+ "ok": true,
12
+ "packet_encryption": false
13
+ }
14
+ ```
15
+
16
+ ## 자동 초기화 (권장)
17
+
18
+ **admin-web 예시:**
19
+
20
+ ```ts
21
+ import { entityServer } from "entity-server-client";
22
+
23
+ // 앱 초기화 시 한 번 실행 (예: App.tsx mount, main.tsx 초기화)
24
+ const health = await entityServer.checkHealth();
25
+ // → 서버의 packet_encryption: true 이면
26
+ // 클라이언트의 encryptRequests: true 로 자동 설정됨
27
+ ```
28
+
29
+ **수동 초기화:**
30
+
31
+ ```ts
32
+ const res = await fetch("/v1/health");
33
+ const { packet_encryption } = await res.json();
34
+
35
+ if (packet_encryption) {
36
+ entityServer.configure({ encryptRequests: true });
37
+ }
38
+ ```
39
+
40
+ > **주의:** 패킷 magic 길이(K)는 `K = 2 + key[31] % 14` 공식으로 패킷 키에서 자동 파생됩니다.
41
+ > 클라이언트와 서버 모두 동일한 패킷 키를 보유하므로 별도 설정 필요 없습니다.
@@ -0,0 +1,32 @@
1
+ # 본인인증
2
+
3
+ ## `identityRequest(opts)`
4
+
5
+ 본인인증 세션을 생성합니다.
6
+
7
+ ```ts
8
+ const res = await client.identityRequest({
9
+ redirect_url: "https://example.com/identity/callback",
10
+ method: "pass",
11
+ });
12
+ res.data; // { request_id: "...", auth_url: "..." } 등
13
+ ```
14
+
15
+ ## `identityResult(requestId)`
16
+
17
+ 본인인증 결과를 조회합니다. 콜백 처리 후 호출합니다.
18
+
19
+ ```ts
20
+ const res = await client.identityResult("request-id-here");
21
+ res.data; // { name, phone, birth, gender, di, ci, ... }
22
+ ```
23
+
24
+ ## `identityVerifyCI(ciHash)`
25
+
26
+ CI(연계정보) 해시로 기존 계정 중복 여부를 확인합니다.
27
+
28
+ ```ts
29
+ const res = await client.identityVerifyCI("ci-hash-string");
30
+ res.data.exists; // true: 이미 가입된 계정 존재
31
+ res.data.account_seq; // 존재할 경우 해당 계정 seq
32
+ ```
@@ -0,0 +1,34 @@
1
+ # import
2
+
3
+ ```ts
4
+ import {
5
+ EntityServerClient,
6
+ entityServer,
7
+ type EntityListParams,
8
+ type EntityListResult,
9
+ type EntityHistoryRecord,
10
+ type EntityQueryRequest,
11
+ type EntityServerClientOptions,
12
+ type RegisterPushDeviceOptions,
13
+ type PushSendRequest,
14
+ type PushSendAllRequest,
15
+ type SmsSendRequest,
16
+ type SmtpSendRequest,
17
+ type AlimtalkSendRequest,
18
+ type FriendtalkSendRequest,
19
+ type PgCreateOrderRequest,
20
+ type PgConfirmPaymentRequest,
21
+ type PgCancelPaymentRequest,
22
+ type FileMeta,
23
+ type FileUploadOptions,
24
+ type IdentityRequestOptions,
25
+ type QRCodeOptions,
26
+ type BarcodeOptions,
27
+ } from "entity-server-client";
28
+ ```
29
+
30
+ React Hook:
31
+
32
+ ```ts
33
+ import { useEntityServer } from "entity-server-client/react";
34
+ ```
@@ -0,0 +1,25 @@
1
+ # 암호화 패킷 처리
2
+
3
+ ## 자동 복호화
4
+
5
+ 서버 응답이 `Content-Type: application/octet-stream`이면 `request()` 내부에서 자동으로 복호화됩니다.
6
+ XChaCha20-Poly1305 알고리즘을 사용하며, 키는 현재 JWT 토큰의 SHA-256 해시입니다.
7
+
8
+ 별도 처리 없이 일반 응답과 동일하게 사용하면 됩니다.
9
+
10
+ ## `readRequestBody(body, contentType?, requireEncrypted?)`
11
+
12
+ 원시 암호화 payload를 직접 파싱할 때 사용합니다.
13
+ 주로 서버 측 미들웨어나 SSR 환경에서 클라이언트로부터 받은 암호화 body를 처리할 때 활용합니다.
14
+
15
+ ```ts
16
+ // 암호화 body 복호화
17
+ const data = client.readRequestBody(
18
+ arrayBuffer, // ArrayBuffer | Uint8Array
19
+ "application/octet-stream",
20
+ true, // requireEncrypted: 암호화 아니면 에러
21
+ );
22
+
23
+ // 일반 JSON body 파싱
24
+ const data2 = client.readRequestBody(jsonString, "application/json");
25
+ ```
package/docs/api/pg.md ADDED
@@ -0,0 +1,90 @@
1
+ # PG 결제
2
+
3
+ 결제 라이프사이클: `pgCreateOrder` → (사용자 결제 진행) → `pgConfirmPayment` → (`pgSyncPayment` | `pgCancelPayment`)
4
+
5
+ ## `pgCreateOrder(req)`
6
+
7
+ 결제 주문을 생성합니다. 결제 전 서버에 주문 정보를 등록합니다.
8
+
9
+ | 필드 | 타입 | 설명 |
10
+ | --------------- | -------- | ----------------------- |
11
+ | `orderId` | `string` | 가맹점 주문 ID (고유값) |
12
+ | `amount` | `number` | 결제 금액 (원 단위) |
13
+ | `orderName` | `string` | 주문명 |
14
+ | `customerName` | `string` | 구매자 이름 |
15
+ | `customerEmail` | `string` | 구매자 이메일 |
16
+
17
+ ```ts
18
+ const order = await client.pgCreateOrder({
19
+ orderId: "order-2026-001",
20
+ amount: 15000,
21
+ orderName: "상품 A",
22
+ customerName: "홍길동",
23
+ customerEmail: "hong@example.com",
24
+ });
25
+ order.paymentKey; // 결제창 호출에 필요한 키
26
+ ```
27
+
28
+ ## `pgGetOrder(orderId)`
29
+
30
+ 주문 정보를 조회합니다.
31
+
32
+ ```ts
33
+ const order = await client.pgGetOrder("order-2026-001");
34
+ order.status; // "pending" | "paid" | "cancelled" ...
35
+ order.amount;
36
+ order.paymentKey;
37
+ ```
38
+
39
+ ## `pgConfirmPayment(req)`
40
+
41
+ 결제 완료 후 서버 측 승인 처리를 합니다. PG사에서 발급된 `paymentKey`와 금액으로 검증합니다.
42
+
43
+ | 필드 | 타입 | 설명 |
44
+ | ------------ | -------- | -------------------------------------- |
45
+ | `paymentKey` | `string` | PG사 결제 키 (클라이언트에서 수신) |
46
+ | `orderId` | `string` | 가맹점 주문 ID |
47
+ | `amount` | `number` | 결제 금액 (주문 금액과 불일치 시 에러) |
48
+
49
+ ```ts
50
+ const result = await client.pgConfirmPayment({
51
+ paymentKey: "toss_paymentKey_abc",
52
+ orderId: "order-2026-001",
53
+ amount: 15000,
54
+ });
55
+ result.status; // "paid"
56
+ ```
57
+
58
+ ## `pgCancelPayment(paymentKey, req?)`
59
+
60
+ 결제를 취소(환불)합니다.
61
+
62
+ ```ts
63
+ // 전액 취소
64
+ await client.pgCancelPayment("toss_paymentKey_abc");
65
+
66
+ // 부분 취소
67
+ await client.pgCancelPayment("toss_paymentKey_abc", {
68
+ cancelReason: "고객 요청",
69
+ cancelAmount: 5000,
70
+ });
71
+ ```
72
+
73
+ ## `pgSyncPayment(paymentKey)`
74
+
75
+ PG사의 실시간 결제 상태를 서버 DB에 동기화합니다.
76
+
77
+ ```ts
78
+ const result = await client.pgSyncPayment("toss_paymentKey_abc");
79
+ result.status; // 동기화 후 상태
80
+ ```
81
+
82
+ ## `pgConfig()`
83
+
84
+ 설정된 PG 정보를 조회합니다. 클라이언트 측 결제창 초기화에 필요한 `clientKey` 등을 포함합니다.
85
+
86
+ ```ts
87
+ const config = await client.pgConfig();
88
+ config.provider; // "toss" | ...
89
+ config.clientKey; // 결제창 초기화용 키
90
+ ```
@@ -0,0 +1,107 @@
1
+ # 푸시 관련
2
+
3
+ ## `push(pushEntity, payload, opts?)`
4
+
5
+ `submit()`의 별칭입니다. 푸시 관련 엔티티에 데이터를 제출합니다.
6
+
7
+ ```ts
8
+ await client.push("push_message", {
9
+ title: "새 알림",
10
+ body: "내용",
11
+ account_seq: 1,
12
+ });
13
+ ```
14
+
15
+ ## `pushLogList(params?)`
16
+
17
+ `push_log` 엔티티의 목록을 조회합니다. `list()`의 별칭입니다.
18
+
19
+ ```ts
20
+ const logs = await client.pushLogList({
21
+ page: 1,
22
+ limit: 30,
23
+ orderBy: "-created_time",
24
+ conditions: { account_seq: 1 },
25
+ });
26
+ logs.data.items;
27
+ logs.data.total;
28
+ ```
29
+
30
+ ## `registerPushDevice(accountSeq, deviceId, pushToken, opts?)`
31
+
32
+ `account_device` 엔티티에 디바이스를 등록합니다.
33
+
34
+ | 옵션 | 타입 | 설명 |
35
+ | ---------------- | --------- | --------------------------------- |
36
+ | `platform` | `string` | 플랫폼 (예: `"web"`, `"android"`) |
37
+ | `deviceType` | `string` | 디바이스 종류 (예: `"mobile"`) |
38
+ | `browser` | `string` | 브라우저명 (예: `"Chrome"`) |
39
+ | `browserVersion` | `string` | 브라우저 버전 |
40
+ | `pushEnabled` | `boolean` | 푸시 수신 여부. 기본값 `true` |
41
+ | `transactionId` | `string` | 트랜잭션 ID |
42
+
43
+ ```ts
44
+ const res = await client.registerPushDevice(
45
+ 1,
46
+ "device-uuid-001",
47
+ "fcm-token-abc",
48
+ {
49
+ platform: "web",
50
+ browser: "Chrome",
51
+ browserVersion: "120",
52
+ pushEnabled: true,
53
+ },
54
+ );
55
+ res.seq; // 등록된 account_device seq
56
+ ```
57
+
58
+ ## `updatePushDeviceToken(deviceSeq, pushToken, opts?)`
59
+
60
+ 등록된 디바이스의 푸시 토큰을 갱신합니다.
61
+
62
+ ```ts
63
+ await client.updatePushDeviceToken(10, "new-fcm-token", { pushEnabled: true });
64
+ ```
65
+
66
+ ## `disablePushDevice(deviceSeq, opts?)`
67
+
68
+ 디바이스의 푸시 수신을 비활성화합니다 (`push_enabled: false`).
69
+
70
+ ```ts
71
+ await client.disablePushDevice(10);
72
+ ```
73
+
74
+ ## `pushSend(req)`
75
+
76
+ 특정 계정에 푸시 알림을 직접 발송합니다.
77
+
78
+ ```ts
79
+ const res = await client.pushSend({
80
+ account_seq: 1,
81
+ title: "알림 제목",
82
+ body: "알림 내용",
83
+ });
84
+ res.seq; // 발송 로그 seq
85
+ ```
86
+
87
+ ## `pushSendAll(req)`
88
+
89
+ 전체 또는 조건에 맞는 모든 사용자에게 푸시 알림을 발송합니다.
90
+
91
+ ```ts
92
+ const res = await client.pushSendAll({
93
+ title: "공지사항",
94
+ body: "전체 공지 내용입니다.",
95
+ });
96
+ res.sent; // 발송 성공 수
97
+ res.failed; // 발송 실패 수
98
+ ```
99
+
100
+ ## `pushStatus(seq)`
101
+
102
+ 푸시 발송 로그의 처리 상태를 조회합니다.
103
+
104
+ ```ts
105
+ const res = await client.pushStatus(42);
106
+ res.status; // "sent" | "failed" | "pending" 등
107
+ ```