entity-server-client 0.3.2 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/EntityServerClient.d.ts +432 -0
- package/dist/client/base.d.ts +84 -0
- package/dist/client/hmac.d.ts +8 -0
- package/dist/client/packet.d.ts +24 -0
- package/dist/client/request.d.ts +16 -0
- package/dist/client/utils.d.ts +8 -0
- package/dist/hooks/useEntityServer.d.ts +63 -0
- package/{src/index.ts → dist/index.d.ts} +1 -3
- package/dist/index.js +2 -0
- package/dist/index.js.map +7 -0
- package/dist/mixins/alimtalk.d.ts +56 -0
- package/dist/mixins/auth.d.ts +100 -0
- package/dist/mixins/email.d.ts +51 -0
- package/dist/mixins/entity.d.ts +126 -0
- package/dist/mixins/file.d.ts +85 -0
- package/dist/mixins/identity.d.ts +52 -0
- package/dist/mixins/llm.d.ts +94 -0
- package/dist/mixins/pg.d.ts +63 -0
- package/dist/mixins/push.d.ts +101 -0
- package/dist/mixins/sms.d.ts +55 -0
- package/dist/mixins/smtp.d.ts +51 -0
- package/dist/mixins/utils.d.ts +88 -0
- package/dist/packet.d.ts +11 -0
- package/dist/packet.js +2 -0
- package/dist/packet.js.map +7 -0
- package/dist/react.js +2 -0
- package/dist/react.js.map +7 -0
- package/{src/types.ts → dist/types.d.ts} +2 -42
- package/package.json +9 -36
- package/LICENSE +0 -21
- package/README.md +0 -128
- package/build.mjs +0 -36
- package/docs/api/alimtalk.md +0 -62
- package/docs/api/auth.md +0 -256
- package/docs/api/email.md +0 -37
- package/docs/api/entity.md +0 -273
- package/docs/api/file.md +0 -80
- package/docs/api/health.md +0 -47
- package/docs/api/identity.md +0 -32
- package/docs/api/import.md +0 -45
- package/docs/api/packet.md +0 -90
- package/docs/api/pg.md +0 -90
- package/docs/api/push.md +0 -107
- package/docs/api/react.md +0 -141
- package/docs/api/request.md +0 -118
- package/docs/api/setup.md +0 -43
- package/docs/api/sms.md +0 -45
- package/docs/api/smtp.md +0 -33
- package/docs/api/transaction.md +0 -50
- package/docs/api/utils.md +0 -52
- package/docs/apis.md +0 -26
- package/docs/react.md +0 -137
- package/src/EntityServerClient.ts +0 -28
- package/src/client/base.ts +0 -348
- package/src/client/hmac.ts +0 -41
- package/src/client/packet.ts +0 -77
- package/src/client/request.ts +0 -139
- package/src/client/utils.ts +0 -33
- package/src/hooks/useEntityServer.ts +0 -154
- package/src/mixins/auth.ts +0 -143
- package/src/mixins/entity.ts +0 -205
- package/src/mixins/file.ts +0 -99
- package/src/mixins/push.ts +0 -109
- package/src/mixins/smtp.ts +0 -20
- package/src/mixins/utils.ts +0 -106
- package/src/packet.ts +0 -84
- package/tests/packet.test.mjs +0 -50
- package/tsconfig.json +0 -14
- /package/{src/react.ts → dist/react.d.ts} +0 -0
package/docs/api/alimtalk.md
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
# 알림톡 / 친구톡
|
|
2
|
-
|
|
3
|
-
카카오 비즈메시지(알림톡/친구톡)를 발송합니다.
|
|
4
|
-
|
|
5
|
-
## `alimtalkSend(req)`
|
|
6
|
-
|
|
7
|
-
카카오 알림톡을 발송합니다. 사전 승인된 템플릿을 사용합니다.
|
|
8
|
-
|
|
9
|
-
| 필드 | 타입 | 설명 |
|
|
10
|
-
| -------------- | -------- | ------------------------- |
|
|
11
|
-
| `to` | `string` | 수신 전화번호 |
|
|
12
|
-
| `templateCode` | `string` | 승인된 알림톡 템플릿 코드 |
|
|
13
|
-
| `variables` | `object` | 템플릿 변수 (키-값 맵) |
|
|
14
|
-
|
|
15
|
-
```ts
|
|
16
|
-
const res = await client.alimtalkSend({
|
|
17
|
-
to: "01012345678",
|
|
18
|
-
templateCode: "ORDER_CONFIRM",
|
|
19
|
-
variables: {
|
|
20
|
-
orderId: "20260201-001",
|
|
21
|
-
totalAmount: "50,000",
|
|
22
|
-
},
|
|
23
|
-
});
|
|
24
|
-
res.seq; // 발송 로그 seq
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
## `alimtalkStatus(seq)`
|
|
28
|
-
|
|
29
|
-
알림톡 발송 처리 상태를 조회합니다.
|
|
30
|
-
|
|
31
|
-
```ts
|
|
32
|
-
const res = await client.alimtalkStatus(7);
|
|
33
|
-
res.status; // "sent" | "failed" | "pending"
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
## `alimtalkTemplates()`
|
|
37
|
-
|
|
38
|
-
등록된 알림톡 템플릿 목록을 조회합니다.
|
|
39
|
-
|
|
40
|
-
```ts
|
|
41
|
-
const res = await client.alimtalkTemplates();
|
|
42
|
-
res.items; // 템플릿 배열 [{ code, name, content, ... }]
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
## `friendtalkSend(req)`
|
|
46
|
-
|
|
47
|
-
카카오 친구톡을 발송합니다. 승인된 채널 친구에게만 발송 가능합니다.
|
|
48
|
-
|
|
49
|
-
| 필드 | 타입 | 설명 |
|
|
50
|
-
| --------- | -------- | ------------------------------------ |
|
|
51
|
-
| `to` | `string` | 수신 전화번호 |
|
|
52
|
-
| `type` | `string` | 메시지 타입 (`"text"`, `"image"` 등) |
|
|
53
|
-
| `content` | `string` | 메시지 본문 |
|
|
54
|
-
|
|
55
|
-
```ts
|
|
56
|
-
const res = await client.friendtalkSend({
|
|
57
|
-
to: "01012345678",
|
|
58
|
-
type: "text",
|
|
59
|
-
content: "안녕하세요! 신상품이 입고되었습니다.",
|
|
60
|
-
});
|
|
61
|
-
res.seq; // 발송 로그 seq
|
|
62
|
-
```
|
package/docs/api/auth.md
DELETED
|
@@ -1,256 +0,0 @@
|
|
|
1
|
-
# 인증
|
|
2
|
-
|
|
3
|
-
## `login(email, password)`
|
|
4
|
-
|
|
5
|
-
이메일 + 비밀번호로 로그인합니다. 성공 시 내부 토큰이 자동으로 설정됩니다.
|
|
6
|
-
|
|
7
|
-
```ts
|
|
8
|
-
const auth = await client.login("admin@example.com", "password");
|
|
9
|
-
// auth.access_token — 이후 요청에 자동 사용됨
|
|
10
|
-
// auth.refresh_token — 만료 후 재발급에 사용
|
|
11
|
-
// auth.expires_in — 만료까지 남은 초
|
|
12
|
-
```
|
|
13
|
-
|
|
14
|
-
## `refreshToken(refreshToken)`
|
|
15
|
-
|
|
16
|
-
Refresh Token으로 Access Token을 재발급받습니다.
|
|
17
|
-
앱이 명시적으로 호출해야 하며, 성공하면 내부 `this.token`을 새 Access Token으로 교체합니다.
|
|
18
|
-
|
|
19
|
-
> **패키지는 401을 감지해 자동으로 `refreshToken()`을 호출하지 않습니다.**
|
|
20
|
-
> 401 발생 시 앱이 직접 catch해서 호출해야 합니다.
|
|
21
|
-
|
|
22
|
-
```ts
|
|
23
|
-
const result = await client.refreshToken(auth.refresh_token);
|
|
24
|
-
// 성공 → this.token이 result.access_token으로 교체됨
|
|
25
|
-
// result.access_token
|
|
26
|
-
// result.expires_in
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
## `logout(refreshToken)`
|
|
30
|
-
|
|
31
|
-
서버에 로그아웃을 요청하고 내부 토큰을 초기화합니다.
|
|
32
|
-
Refresh Token을 서버에 전달해 무효화하므로 해당 토큰으로 더 이상 재발급이 불가능합니다.
|
|
33
|
-
Refresh Token이 이미 만료된 경우에도 서버는 성공으로 응답합니다.
|
|
34
|
-
|
|
35
|
-
```ts
|
|
36
|
-
await client.logout(auth.refresh_token);
|
|
37
|
-
// 이후 client 내부 token = "" 으로 초기화됨
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
## 토큰 만료 처리
|
|
41
|
-
|
|
42
|
-
**패키지가 자동으로 처리하는 것:**
|
|
43
|
-
|
|
44
|
-
| 동작 | 내부 토큰 갱신 |
|
|
45
|
-
| --------------------- | -------------- |
|
|
46
|
-
| `login()` 성공 | ✅ 자동 세팅 |
|
|
47
|
-
| `refreshToken()` 성공 | ✅ 자동 교체 |
|
|
48
|
-
| `logout()` 호출 | ✅ `""` 초기화 |
|
|
49
|
-
|
|
50
|
-
**패키지가 처리하지 않는 것:**
|
|
51
|
-
|
|
52
|
-
- **401 자동 재시도 없음**: access_token이 만료되어 서버가 401을 반환하면 패키지는 에러를 throw합니다. 앱이 직접 잡아서 `refreshToken()`을 호출하고 재시도해야 합니다.
|
|
53
|
-
- **토큰 영속성 없음**: 페이지 새로고침 시 싱글톤 인스턴스가 초기화되어 내부 token이 `""` 로 리셋됩니다. 앱이 직접 복원 후 `setToken()` 또는 `configure({ token })` 으로 재세팅해야 합니다.
|
|
54
|
-
|
|
55
|
-
**401 발생 후 앱의 처리 흐름:**
|
|
56
|
-
|
|
57
|
-
```
|
|
58
|
-
API 요청 → 401 응답 → entityServer.refreshToken(refresh_token) 호출
|
|
59
|
-
↓ 성공 ↓ 실패 (refresh_token도 만료)
|
|
60
|
-
setToken(new_access_token) 로그인 페이지로 이동
|
|
61
|
-
원래 요청 재시도
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
**주의:** `refreshToken()`이 성공해 패키지 내부 토큰이 교체되더라도, 앱 내 다른 HTTP 클라이언트(axios 등)에는 별도로 토큰을 전달해야 합니다.
|
|
65
|
-
|
|
66
|
-
## `keepSession` — 세션 유지 (Silent Refresh)
|
|
67
|
-
|
|
68
|
-
`keepSession: true` 옵션 설정 시, `login()` 성공 후 만료 `refreshBuffer`초 전에 패키지 내부 타이머가 자동으로 `refreshToken()`을 호출합니다.
|
|
69
|
-
갱신이 성공하면 타이머를 다시 예약해 로그인 상태를 계속 유지합니다.
|
|
70
|
-
`logout()` 또는 `stopKeepSession()` 호출 시 타이머가 정리됩니다.
|
|
71
|
-
|
|
72
|
-
```ts
|
|
73
|
-
entityServer.configure({
|
|
74
|
-
keepSession: true,
|
|
75
|
-
refreshBuffer: 60, // 만료 60초 전에 갱신 (기본값)
|
|
76
|
-
onTokenRefreshed: (accessToken, expiresIn) => {
|
|
77
|
-
// 갱신 성공 — 앱 레벨 저장소 업데이트
|
|
78
|
-
localStorage.setItem("auth_access_token", accessToken);
|
|
79
|
-
},
|
|
80
|
-
onSessionExpired: (error) => {
|
|
81
|
-
// refresh_token 만료 등으로 갱신 실패
|
|
82
|
-
console.error("세션 만료:", error);
|
|
83
|
-
window.location.href = "/login";
|
|
84
|
-
},
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
// 이후 login() 하면 타이머 자동 시작
|
|
88
|
-
const auth = await entityServer.login(email, password);
|
|
89
|
-
// expires_in=3600이면 3540초 후 자동 갱신, 이후 반복
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
**수동으로 타이머 중지:**
|
|
93
|
-
|
|
94
|
-
```ts
|
|
95
|
-
entityServer.stopKeepSession();
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
> **페이지 새로고침 시**: 타이머는 메모리에만 존재하므로 새로고침 시 초기화됩니다.
|
|
99
|
-
> `useEntityServer`의 `resumeSession` 옵션을 사용하면 페이지 새로고침 후에도 세션을 자동으로 복원할 수 있습니다.
|
|
100
|
-
|
|
101
|
-
## OAuth 연동
|
|
102
|
-
|
|
103
|
-
### `oauthLink(provider, code, state?)`
|
|
104
|
-
|
|
105
|
-
OAuth 프로바이더를 현재 계정에 연동합니다.
|
|
106
|
-
|
|
107
|
-
```ts
|
|
108
|
-
await client.oauthLink("google", "auth-code-from-callback");
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
### `oauthUnlink(provider)`
|
|
112
|
-
|
|
113
|
-
OAuth 프로바이더 연동을 해제합니다.
|
|
114
|
-
|
|
115
|
-
```ts
|
|
116
|
-
await client.oauthUnlink("google");
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
### `oauthProviders()`
|
|
120
|
-
|
|
121
|
-
현재 계정에 연동된 OAuth 프로바이더 목록을 반환합니다.
|
|
122
|
-
|
|
123
|
-
```ts
|
|
124
|
-
const res = await client.oauthProviders();
|
|
125
|
-
res.data; // [{ provider: "google", email: "...", linked_at: "..." }]
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
### `oauthTokenRefresh(provider)`
|
|
129
|
-
|
|
130
|
-
특정 OAuth 프로바이더의 액세스 토큰을 갱신합니다.
|
|
131
|
-
|
|
132
|
-
```ts
|
|
133
|
-
const res = await client.oauthTokenRefresh("google");
|
|
134
|
-
res.access_token;
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
## 2단계 인증 (2FA)
|
|
138
|
-
|
|
139
|
-
### `twoFactorSetup()`
|
|
140
|
-
|
|
141
|
-
2FA 설정을 시작하고 QR 코드 / 시크릿을 반환합니다.
|
|
142
|
-
|
|
143
|
-
```ts
|
|
144
|
-
const res = await client.twoFactorSetup();
|
|
145
|
-
res.qr_url; // QR 코드 URL
|
|
146
|
-
res.secret; // TOTP 시크릿
|
|
147
|
-
res.setup_token;
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
### `twoFactorSetupVerify(code, setupToken)`
|
|
151
|
-
|
|
152
|
-
TOTP 코드로 2FA 설정을 완료합니다.
|
|
153
|
-
|
|
154
|
-
```ts
|
|
155
|
-
const res = await client.twoFactorSetupVerify("123456", setupToken);
|
|
156
|
-
res.recovery_codes; // 복구 코드 목록
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
### `twoFactorDisable(code)`
|
|
160
|
-
|
|
161
|
-
2FA를 비활성화합니다.
|
|
162
|
-
|
|
163
|
-
```ts
|
|
164
|
-
await client.twoFactorDisable("123456");
|
|
165
|
-
```
|
|
166
|
-
|
|
167
|
-
### `twoFactorStatus()`
|
|
168
|
-
|
|
169
|
-
2FA 활성화 여부를 조회합니다.
|
|
170
|
-
|
|
171
|
-
```ts
|
|
172
|
-
const res = await client.twoFactorStatus();
|
|
173
|
-
res.enabled; // true | false
|
|
174
|
-
```
|
|
175
|
-
|
|
176
|
-
### `twoFactorVerify(twoFactorToken, code)`
|
|
177
|
-
|
|
178
|
-
로그인 후 발급된 임시 토큰으로 TOTP 코드를 검증하여 최종 JWT를 발급받습니다.
|
|
179
|
-
|
|
180
|
-
```ts
|
|
181
|
-
const res = await client.twoFactorVerify(twoFactorToken, "123456");
|
|
182
|
-
res.access_token;
|
|
183
|
-
res.refresh_token;
|
|
184
|
-
```
|
|
185
|
-
|
|
186
|
-
### `twoFactorRecovery(twoFactorToken, recoveryCode)`
|
|
187
|
-
|
|
188
|
-
복구 코드로 2FA를 우회하여 최종 JWT를 발급받습니다.
|
|
189
|
-
|
|
190
|
-
```ts
|
|
191
|
-
const res = await client.twoFactorRecovery(twoFactorToken, "recovery-code");
|
|
192
|
-
res.access_token;
|
|
193
|
-
```
|
|
194
|
-
|
|
195
|
-
### `twoFactorRegenerateRecovery(code)`
|
|
196
|
-
|
|
197
|
-
복구 코드를 재생성합니다.
|
|
198
|
-
|
|
199
|
-
```ts
|
|
200
|
-
const res = await client.twoFactorRegenerateRecovery("123456");
|
|
201
|
-
res.recovery_codes;
|
|
202
|
-
```
|
|
203
|
-
|
|
204
|
-
## 계정 관리
|
|
205
|
-
|
|
206
|
-
### `me()`
|
|
207
|
-
|
|
208
|
-
현재 로그인된 사용자 정보를 반환합니다.
|
|
209
|
-
|
|
210
|
-
```ts
|
|
211
|
-
const res = await client.me();
|
|
212
|
-
res.data; // 계정 정보
|
|
213
|
-
```
|
|
214
|
-
|
|
215
|
-
### `changePassword(currentPasswd, newPasswd)`
|
|
216
|
-
|
|
217
|
-
비밀번호를 변경합니다.
|
|
218
|
-
|
|
219
|
-
```ts
|
|
220
|
-
await client.changePassword("current-pw", "new-pw");
|
|
221
|
-
```
|
|
222
|
-
|
|
223
|
-
### `withdraw(passwd?)`
|
|
224
|
-
|
|
225
|
-
회원 탈퇴를 요청합니다.
|
|
226
|
-
|
|
227
|
-
```ts
|
|
228
|
-
await client.withdraw("current-pw");
|
|
229
|
-
```
|
|
230
|
-
|
|
231
|
-
### `reactivate(params)`
|
|
232
|
-
|
|
233
|
-
휴면 계정을 재활성화합니다.
|
|
234
|
-
|
|
235
|
-
```ts
|
|
236
|
-
const auth = await client.reactivate({
|
|
237
|
-
email: "user@example.com",
|
|
238
|
-
passwd: "password",
|
|
239
|
-
});
|
|
240
|
-
```
|
|
241
|
-
|
|
242
|
-
### `passwordResetRequest(email)`
|
|
243
|
-
|
|
244
|
-
비밀번호 재설정 메일을 요청합니다. 인증 불필요 (공개 API).
|
|
245
|
-
|
|
246
|
-
```ts
|
|
247
|
-
await client.passwordResetRequest("user@example.com");
|
|
248
|
-
```
|
|
249
|
-
|
|
250
|
-
### `passwordResetConfirm(token, newPasswd)`
|
|
251
|
-
|
|
252
|
-
이메일로 전달된 토큰으로 비밀번호를 재설정합니다. 인증 불필요 (공개 API).
|
|
253
|
-
|
|
254
|
-
```ts
|
|
255
|
-
await client.passwordResetConfirm("reset-token", "new-password");
|
|
256
|
-
```
|
package/docs/api/email.md
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
# 이메일 인증 / 변경
|
|
2
|
-
|
|
3
|
-
## `emailVerificationSend(email)`
|
|
4
|
-
|
|
5
|
-
이메일 인증 코드 또는 링크를 발송합니다. 인증 불필요 (공개 API).
|
|
6
|
-
|
|
7
|
-
```ts
|
|
8
|
-
await client.emailVerificationSend("user@example.com");
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
## `emailVerificationConfirm(token)`
|
|
12
|
-
|
|
13
|
-
이메일로 받은 인증 토큰을 검증합니다. 인증 불필요 (공개 API).
|
|
14
|
-
|
|
15
|
-
```ts
|
|
16
|
-
await client.emailVerificationConfirm("verify-token-from-email");
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
## `emailVerificationStatus()`
|
|
20
|
-
|
|
21
|
-
현재 로그인된 계정의 이메일 인증 상태를 조회합니다. JWT 필요.
|
|
22
|
-
|
|
23
|
-
```ts
|
|
24
|
-
const res = await client.emailVerificationStatus();
|
|
25
|
-
res.verified; // true | false
|
|
26
|
-
res.email; // 현재 이메일
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
## `emailChange(newEmail, code?)`
|
|
30
|
-
|
|
31
|
-
이메일 주소를 변경합니다.
|
|
32
|
-
|
|
33
|
-
```ts
|
|
34
|
-
await client.emailChange("new@example.com");
|
|
35
|
-
// 서버 설정에 따라 code가 필요할 수 있음
|
|
36
|
-
await client.emailChange("new@example.com", "123456");
|
|
37
|
-
```
|
package/docs/api/entity.md
DELETED
|
@@ -1,273 +0,0 @@
|
|
|
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
|
-
```
|