create-entity-server 0.0.9 → 0.0.15
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/bin/create.js +11 -1
- package/package.json +1 -1
- package/template/.env.example +17 -1
- package/template/configs/jwt.json +1 -0
- package/template/configs/oauth.json +37 -0
- package/template/configs/push.json +26 -0
- package/template/entities/README.md +4 -4
- package/template/entities/{Auth → System/Auth}/account.json +0 -14
- package/template/samples/README.md +16 -0
- package/template/samples/entities/01_basic_fields.json +39 -0
- package/template/samples/entities/02_types_and_defaults.json +68 -0
- package/template/samples/entities/03_hash_and_unique.json +33 -0
- package/template/samples/entities/04_fk_and_composite_unique.json +31 -0
- package/template/samples/entities/05_cache.json +54 -0
- package/template/samples/entities/06_history_and_hard_delete.json +42 -0
- package/template/samples/entities/07_license_scope.json +43 -0
- package/template/samples/entities/08_hook_sql.json +52 -0
- package/template/samples/entities/09_hook_entity.json +71 -0
- package/template/samples/entities/10_hook_submit_delete.json +75 -0
- package/template/samples/entities/11_hook_webhook.json +82 -0
- package/template/samples/entities/12_hook_push.json +73 -0
- package/template/samples/entities/13_read_only.json +51 -0
- package/template/samples/entities/14_optimistic_lock.json +29 -0
- package/template/samples/entities/15_reset_defaults.json +95 -0
- package/template/samples/entities/README.md +94 -0
- package/template/samples/entities/order_notification.json +51 -0
- package/template/samples/flutter/lib/entity_server_client.dart +91 -0
- package/template/samples/java/EntityServerClient.java +117 -0
- package/template/samples/kotlin/EntityServerClient.kt +86 -0
- package/template/samples/node/src/EntityServerClient.js +116 -0
- package/template/samples/php/ci4/Config/EntityServer.php +15 -0
- package/template/samples/php/ci4/Controllers/EntityController.php +202 -0
- package/template/samples/php/ci4/Controllers/ProductController.php +16 -76
- package/template/samples/php/ci4/Libraries/EntityServer.php +150 -11
- package/template/samples/php/laravel/Services/EntityServerService.php +56 -0
- package/template/samples/python/entity_server.py +106 -0
- package/template/samples/react/src/api/entityServerClient.ts +123 -0
- package/template/samples/react/src/hooks/useEntity.ts +68 -0
- package/template/samples/swift/EntityServerClient.swift +105 -0
- package/template/scripts/normalize-entities.sh +10 -10
- package/template/scripts/run.sh +108 -29
- package/template/scripts/update-server.ps1 +92 -2
- package/template/scripts/update-server.sh +73 -2
- /package/template/entities/{Auth → System/Auth}/api_keys.json +0 -0
- /package/template/entities/{Auth → System/Auth}/license.json +0 -0
- /package/template/entities/{Auth → System/Auth}/rbac_roles.json +0 -0
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
namespace App\Libraries;
|
|
4
4
|
|
|
5
|
+
use Config\EntityServer as EntityServerConfig;
|
|
6
|
+
|
|
5
7
|
/**
|
|
6
8
|
* Entity Server 클라이언트 라이브러리 (CodeIgniter 4)
|
|
7
9
|
*
|
|
@@ -9,7 +11,7 @@ namespace App\Libraries;
|
|
|
9
11
|
*
|
|
10
12
|
* 설치: app/Libraries/EntityServer.php 에 배치
|
|
11
13
|
*
|
|
12
|
-
* 설정: app/Config/EntityServer.php
|
|
14
|
+
* 설정: app/Config/EntityServer.php 우선 (필요시 .env fallback)
|
|
13
15
|
* ENTITY_SERVER_URL=http://localhost:47200
|
|
14
16
|
* ENTITY_SERVER_API_KEY=your-api-key
|
|
15
17
|
* ENTITY_SERVER_HMAC_SECRET=your-hmac-secret
|
|
@@ -28,20 +30,32 @@ class EntityServer
|
|
|
28
30
|
private string $hmacSecret;
|
|
29
31
|
private int $timeout;
|
|
30
32
|
private int $magicLen;
|
|
33
|
+
private bool $requireEncryptedRequest;
|
|
31
34
|
private ?string $activeTxId = null;
|
|
32
35
|
|
|
33
36
|
public function __construct(
|
|
34
|
-
string $baseUrl =
|
|
35
|
-
string $apiKey =
|
|
36
|
-
string $hmacSecret =
|
|
37
|
-
int $timeout =
|
|
38
|
-
int $magicLen =
|
|
37
|
+
?string $baseUrl = null,
|
|
38
|
+
?string $apiKey = null,
|
|
39
|
+
?string $hmacSecret = null,
|
|
40
|
+
?int $timeout = null,
|
|
41
|
+
?int $magicLen = null,
|
|
42
|
+
?bool $requireEncryptedRequest = null
|
|
39
43
|
) {
|
|
40
|
-
$
|
|
41
|
-
|
|
42
|
-
$
|
|
43
|
-
$
|
|
44
|
-
$
|
|
44
|
+
$config = class_exists(EntityServerConfig::class) ? new EntityServerConfig() : null;
|
|
45
|
+
|
|
46
|
+
$configBaseUrl = $config?->baseUrl ?? env('ENTITY_SERVER_URL', 'http://localhost:47200');
|
|
47
|
+
$configApiKey = $config?->apiKey ?? env('ENTITY_SERVER_API_KEY', '');
|
|
48
|
+
$configHmacSecret = $config?->hmacSecret ?? env('ENTITY_SERVER_HMAC_SECRET', '');
|
|
49
|
+
$configTimeout = $config?->timeout ?? (int) env('ENTITY_SERVER_TIMEOUT', 10);
|
|
50
|
+
$configMagicLen = $config?->magicLen ?? (int) env('ENTITY_PACKET_MAGIC_LEN', 4);
|
|
51
|
+
$configRequireEncrypted = $config?->requireEncryptedRequest ?? true;
|
|
52
|
+
|
|
53
|
+
$this->baseUrl = rtrim($baseUrl ?? (string) $configBaseUrl, '/');
|
|
54
|
+
$this->apiKey = (string) ($apiKey ?? $configApiKey);
|
|
55
|
+
$this->hmacSecret = (string) ($hmacSecret ?? $configHmacSecret);
|
|
56
|
+
$this->timeout = (int) ($timeout ?? $configTimeout);
|
|
57
|
+
$this->magicLen = (int) ($magicLen ?? $configMagicLen);
|
|
58
|
+
$this->requireEncryptedRequest = (bool) ($requireEncryptedRequest ?? $configRequireEncrypted);
|
|
45
59
|
}
|
|
46
60
|
|
|
47
61
|
// ─── CRUD ────────────────────────────────────────────────────────────────
|
|
@@ -155,6 +169,131 @@ class EntityServer
|
|
|
155
169
|
return $this->request('POST', "/v1/entity/{$entity}/rollback/{$historySeq}");
|
|
156
170
|
}
|
|
157
171
|
|
|
172
|
+
/**
|
|
173
|
+
* 푸시 발송 트리거 엔티티에 submit합니다.
|
|
174
|
+
*/
|
|
175
|
+
public function push(string $pushEntity, array $payload, ?string $transactionId = null): array
|
|
176
|
+
{
|
|
177
|
+
return $this->submit($pushEntity, $payload, $transactionId);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* push_log 목록 조회 헬퍼
|
|
182
|
+
*/
|
|
183
|
+
public function pushLogList(array $params = []): array
|
|
184
|
+
{
|
|
185
|
+
return $this->list('push_log', $params);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* 디바이스 등록/갱신 헬퍼 (push_token 단일 필드)
|
|
190
|
+
*
|
|
191
|
+
* - 기본 대상 엔티티: account_device
|
|
192
|
+
* - 신규 등록: seq 미전달
|
|
193
|
+
* - 기존 레코드 갱신: options['seq'] 전달
|
|
194
|
+
*
|
|
195
|
+
* @param int $accountSeq 계정 seq
|
|
196
|
+
* @param string $deviceId 디바이스 고유 ID (account_device.id)
|
|
197
|
+
* @param string $pushToken 푸시 디바이스 토큰
|
|
198
|
+
* @param array $options 추가 필드 (예: platform, device_type, browser, push_enabled, seq)
|
|
199
|
+
* @param string|null $transactionId transStart()로 얻은 트랜잭션 ID
|
|
200
|
+
*/
|
|
201
|
+
public function registerPushDevice(
|
|
202
|
+
int $accountSeq,
|
|
203
|
+
string $deviceId,
|
|
204
|
+
string $pushToken,
|
|
205
|
+
array $options = [],
|
|
206
|
+
?string $transactionId = null
|
|
207
|
+
): array {
|
|
208
|
+
$payload = array_merge([
|
|
209
|
+
'id' => $deviceId,
|
|
210
|
+
'account_seq' => $accountSeq,
|
|
211
|
+
'push_token' => $pushToken,
|
|
212
|
+
'push_enabled' => true,
|
|
213
|
+
], $options);
|
|
214
|
+
|
|
215
|
+
return $this->submit('account_device', $payload, $transactionId);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* account_device.seq 기준으로 push_token 갱신
|
|
220
|
+
*/
|
|
221
|
+
public function updatePushDeviceToken(
|
|
222
|
+
int $deviceSeq,
|
|
223
|
+
string $pushToken,
|
|
224
|
+
bool $pushEnabled = true,
|
|
225
|
+
?string $transactionId = null
|
|
226
|
+
): array {
|
|
227
|
+
return $this->submit('account_device', [
|
|
228
|
+
'seq' => $deviceSeq,
|
|
229
|
+
'push_token' => $pushToken,
|
|
230
|
+
'push_enabled' => $pushEnabled,
|
|
231
|
+
], $transactionId);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* account_device.seq 기준으로 푸시 수신 비활성화
|
|
236
|
+
*/
|
|
237
|
+
public function disablePushDevice(
|
|
238
|
+
int $deviceSeq,
|
|
239
|
+
?string $transactionId = null
|
|
240
|
+
): array {
|
|
241
|
+
return $this->submit('account_device', [
|
|
242
|
+
'seq' => $deviceSeq,
|
|
243
|
+
'push_enabled' => false,
|
|
244
|
+
], $transactionId);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* CI4 IncomingRequest에서 암호화 패킷을 읽어 JSON 배열로 복호화합니다.
|
|
249
|
+
*
|
|
250
|
+
* @param object $request CodeIgniter\HTTP\IncomingRequest
|
|
251
|
+
* @param bool $requireEncrypted true면 평문 JSON 요청을 거부합니다.
|
|
252
|
+
*/
|
|
253
|
+
public function readRequestBody(object $request, ?bool $requireEncrypted = null): array
|
|
254
|
+
{
|
|
255
|
+
$requireEncrypted = $requireEncrypted ?? $this->requireEncryptedRequest;
|
|
256
|
+
|
|
257
|
+
$contentType = '';
|
|
258
|
+
if (method_exists($request, 'getHeaderLine')) {
|
|
259
|
+
$contentType = strtolower((string) $request->getHeaderLine('Content-Type'));
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
$rawBody = '';
|
|
263
|
+
if (method_exists($request, 'getBody')) {
|
|
264
|
+
$rawBody = (string) $request->getBody();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
$isEncryptedPacket = str_contains($contentType, 'application/octet-stream');
|
|
268
|
+
|
|
269
|
+
if ($requireEncrypted && !$isEncryptedPacket) {
|
|
270
|
+
throw new \RuntimeException('Encrypted request required: Content-Type must be application/octet-stream');
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if ($isEncryptedPacket) {
|
|
274
|
+
if ($rawBody === '') {
|
|
275
|
+
throw new \RuntimeException('Encrypted request body is empty');
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
$jsonStr = $this->decryptPacket($rawBody);
|
|
279
|
+
$decoded = json_decode($jsonStr, true);
|
|
280
|
+
if (!is_array($decoded)) {
|
|
281
|
+
throw new \RuntimeException('Invalid encrypted JSON payload');
|
|
282
|
+
}
|
|
283
|
+
return $decoded;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// 선택적으로 평문 허용할 때만 fallback
|
|
287
|
+
if ($rawBody === '') {
|
|
288
|
+
return [];
|
|
289
|
+
}
|
|
290
|
+
$decoded = json_decode($rawBody, true);
|
|
291
|
+
if (!is_array($decoded)) {
|
|
292
|
+
throw new \RuntimeException('Invalid JSON payload');
|
|
293
|
+
}
|
|
294
|
+
return $decoded;
|
|
295
|
+
}
|
|
296
|
+
|
|
158
297
|
// ─── 내부 ─────────────────────────────────────────────────────────────────
|
|
159
298
|
|
|
160
299
|
private function request(string $method, string $path, array $body = [], array $extraHeaders = []): array
|
|
@@ -4,6 +4,7 @@ namespace App\Services;
|
|
|
4
4
|
|
|
5
5
|
use Illuminate\Support\Facades\Http;
|
|
6
6
|
use Illuminate\Support\Str;
|
|
7
|
+
use Illuminate\Http\Request;
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Entity Server 클라이언트 서비스 (Laravel)
|
|
@@ -40,6 +41,7 @@ class EntityServerService
|
|
|
40
41
|
private string $apiKey;
|
|
41
42
|
private string $hmacSecret;
|
|
42
43
|
private int $magicLen;
|
|
44
|
+
private bool $requireEncryptedRequest;
|
|
43
45
|
private ?string $activeTxId = null;
|
|
44
46
|
|
|
45
47
|
public function __construct()
|
|
@@ -48,6 +50,7 @@ class EntityServerService
|
|
|
48
50
|
$this->apiKey = config('services.entity_server.api_key', env('ENTITY_SERVER_API_KEY', ''));
|
|
49
51
|
$this->hmacSecret = config('services.entity_server.hmac_secret', env('ENTITY_SERVER_HMAC_SECRET', ''));
|
|
50
52
|
$this->magicLen = (int) config('services.entity_server.packet_magic_len', env('ENTITY_PACKET_MAGIC_LEN', 4));
|
|
53
|
+
$this->requireEncryptedRequest = (bool) config('services.entity_server.require_encrypted_request', env('ENTITY_REQUIRE_ENCRYPTED_REQUEST', true));
|
|
51
54
|
}
|
|
52
55
|
|
|
53
56
|
// ─── CRUD ────────────────────────────────────────────────────────────────
|
|
@@ -138,6 +141,59 @@ class EntityServerService
|
|
|
138
141
|
return $this->request('POST', "/v1/entity/{$entity}/rollback/{$historySeq}");
|
|
139
142
|
}
|
|
140
143
|
|
|
144
|
+
/**
|
|
145
|
+
* 푸시 발송 트리거 엔티티에 submit합니다.
|
|
146
|
+
*/
|
|
147
|
+
public function push(string $pushEntity, array $payload, ?string $transactionId = null): array
|
|
148
|
+
{
|
|
149
|
+
return $this->submit($pushEntity, $payload, $transactionId);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* push_log 목록 조회 헬퍼
|
|
154
|
+
*/
|
|
155
|
+
public function pushLogList(array $params = []): array
|
|
156
|
+
{
|
|
157
|
+
return $this->list('push_log', $params);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Laravel Request에서 암호화 패킷 또는 평문 JSON 본문을 읽어 배열로 반환합니다.
|
|
162
|
+
*/
|
|
163
|
+
public function readRequestBody(Request $request, ?bool $requireEncrypted = null): array
|
|
164
|
+
{
|
|
165
|
+
$requireEncrypted = $requireEncrypted ?? $this->requireEncryptedRequest;
|
|
166
|
+
|
|
167
|
+
$contentType = strtolower((string) $request->header('Content-Type', ''));
|
|
168
|
+
$rawBody = (string) $request->getContent();
|
|
169
|
+
$isEncrypted = str_contains($contentType, 'application/octet-stream');
|
|
170
|
+
|
|
171
|
+
if ($requireEncrypted && !$isEncrypted) {
|
|
172
|
+
throw new \RuntimeException('Encrypted request required: Content-Type must be application/octet-stream');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if ($isEncrypted) {
|
|
176
|
+
if ($rawBody === '') {
|
|
177
|
+
throw new \RuntimeException('Encrypted request body is empty');
|
|
178
|
+
}
|
|
179
|
+
$jsonStr = $this->decryptPacket($rawBody);
|
|
180
|
+
$decoded = json_decode($jsonStr, true);
|
|
181
|
+
if (!is_array($decoded)) {
|
|
182
|
+
throw new \RuntimeException('Invalid encrypted JSON payload');
|
|
183
|
+
}
|
|
184
|
+
return $decoded;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if ($rawBody === '') {
|
|
188
|
+
return [];
|
|
189
|
+
}
|
|
190
|
+
$decoded = json_decode($rawBody, true);
|
|
191
|
+
if (!is_array($decoded)) {
|
|
192
|
+
throw new \RuntimeException('Invalid JSON payload');
|
|
193
|
+
}
|
|
194
|
+
return $decoded;
|
|
195
|
+
}
|
|
196
|
+
|
|
141
197
|
// ─── 내부 ─────────────────────────────────────────────────────────────────
|
|
142
198
|
|
|
143
199
|
private function request(string $method, string $path, array $body = [], array $extraHeaders = []): array
|
|
@@ -150,6 +150,112 @@ class EntityServerClient:
|
|
|
150
150
|
"""history seq 단위 롤백 (단건)"""
|
|
151
151
|
return self._request("POST", f"/v1/entity/{entity}/rollback/{history_seq}")
|
|
152
152
|
|
|
153
|
+
def push(self, push_entity: str, payload: dict, *, transaction_id: str | None = None) -> dict:
|
|
154
|
+
"""푸시 발송 트리거 엔티티에 submit합니다."""
|
|
155
|
+
return self.submit(push_entity, payload, transaction_id=transaction_id)
|
|
156
|
+
|
|
157
|
+
def push_log_list(self, page: int = 1, limit: int = 20, order_by: str | None = None) -> dict:
|
|
158
|
+
"""push_log 목록 조회 헬퍼"""
|
|
159
|
+
return self.list("push_log", page=page, limit=limit, order_by=order_by)
|
|
160
|
+
|
|
161
|
+
def register_push_device(
|
|
162
|
+
self,
|
|
163
|
+
account_seq: int,
|
|
164
|
+
device_id: str,
|
|
165
|
+
push_token: str,
|
|
166
|
+
*,
|
|
167
|
+
platform: str | None = None,
|
|
168
|
+
device_type: str | None = None,
|
|
169
|
+
browser: str | None = None,
|
|
170
|
+
browser_version: str | None = None,
|
|
171
|
+
push_enabled: bool = True,
|
|
172
|
+
transaction_id: str | None = None,
|
|
173
|
+
) -> dict:
|
|
174
|
+
"""account_device 디바이스 등록/갱신 헬퍼 (push_token 단일 필드)"""
|
|
175
|
+
payload: dict[str, Any] = {
|
|
176
|
+
"id": device_id,
|
|
177
|
+
"account_seq": account_seq,
|
|
178
|
+
"push_token": push_token,
|
|
179
|
+
"push_enabled": push_enabled,
|
|
180
|
+
}
|
|
181
|
+
if platform:
|
|
182
|
+
payload["platform"] = platform
|
|
183
|
+
if device_type:
|
|
184
|
+
payload["device_type"] = device_type
|
|
185
|
+
if browser:
|
|
186
|
+
payload["browser"] = browser
|
|
187
|
+
if browser_version:
|
|
188
|
+
payload["browser_version"] = browser_version
|
|
189
|
+
return self.submit("account_device", payload, transaction_id=transaction_id)
|
|
190
|
+
|
|
191
|
+
def update_push_device_token(
|
|
192
|
+
self,
|
|
193
|
+
device_seq: int,
|
|
194
|
+
push_token: str,
|
|
195
|
+
*,
|
|
196
|
+
push_enabled: bool = True,
|
|
197
|
+
transaction_id: str | None = None,
|
|
198
|
+
) -> dict:
|
|
199
|
+
"""account_device.seq 기준 push_token 갱신 헬퍼"""
|
|
200
|
+
return self.submit(
|
|
201
|
+
"account_device",
|
|
202
|
+
{
|
|
203
|
+
"seq": device_seq,
|
|
204
|
+
"push_token": push_token,
|
|
205
|
+
"push_enabled": push_enabled,
|
|
206
|
+
},
|
|
207
|
+
transaction_id=transaction_id,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
def disable_push_device(
|
|
211
|
+
self,
|
|
212
|
+
device_seq: int,
|
|
213
|
+
*,
|
|
214
|
+
transaction_id: str | None = None,
|
|
215
|
+
) -> dict:
|
|
216
|
+
"""account_device.seq 기준 푸시 수신 비활성화 헬퍼"""
|
|
217
|
+
return self.submit(
|
|
218
|
+
"account_device",
|
|
219
|
+
{
|
|
220
|
+
"seq": device_seq,
|
|
221
|
+
"push_enabled": False,
|
|
222
|
+
},
|
|
223
|
+
transaction_id=transaction_id,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
def read_request_body(
|
|
227
|
+
self,
|
|
228
|
+
raw_body: bytes | str | None,
|
|
229
|
+
content_type: str = "application/json",
|
|
230
|
+
*,
|
|
231
|
+
require_encrypted: bool = False,
|
|
232
|
+
) -> dict:
|
|
233
|
+
"""요청 본문을 읽어 JSON으로 반환합니다.
|
|
234
|
+
- application/octet-stream: 암호 패킷 복호화
|
|
235
|
+
- 그 외: 평문 JSON 파싱
|
|
236
|
+
"""
|
|
237
|
+
lowered = (content_type or "").lower()
|
|
238
|
+
is_encrypted = "application/octet-stream" in lowered
|
|
239
|
+
|
|
240
|
+
if require_encrypted and not is_encrypted:
|
|
241
|
+
raise RuntimeError(
|
|
242
|
+
"Encrypted request required: Content-Type must be application/octet-stream"
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
if is_encrypted:
|
|
246
|
+
if raw_body in (None, b"", ""):
|
|
247
|
+
raise RuntimeError("Encrypted request body is empty")
|
|
248
|
+
|
|
249
|
+
packet = raw_body if isinstance(raw_body, bytes) else raw_body.encode("utf-8")
|
|
250
|
+
return json.loads(self._decrypt_packet(packet))
|
|
251
|
+
|
|
252
|
+
if raw_body in (None, b"", ""):
|
|
253
|
+
return {}
|
|
254
|
+
|
|
255
|
+
if isinstance(raw_body, bytes):
|
|
256
|
+
return json.loads(raw_body.decode("utf-8"))
|
|
257
|
+
return json.loads(raw_body)
|
|
258
|
+
|
|
153
259
|
# ─── 내부 ─────────────────────────────────────────────────────────────────
|
|
154
260
|
|
|
155
261
|
def _request(
|
|
@@ -39,6 +39,15 @@ export interface EntityQueryFilter {
|
|
|
39
39
|
value: unknown;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
export interface RegisterPushDeviceOptions {
|
|
43
|
+
platform?: string;
|
|
44
|
+
deviceType?: string;
|
|
45
|
+
browser?: string;
|
|
46
|
+
browserVersion?: string;
|
|
47
|
+
pushEnabled?: boolean;
|
|
48
|
+
transactionId?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
42
51
|
export class EntityServerClient {
|
|
43
52
|
private baseUrl: string;
|
|
44
53
|
private token: string;
|
|
@@ -220,6 +229,120 @@ export class EntityServerClient {
|
|
|
220
229
|
);
|
|
221
230
|
}
|
|
222
231
|
|
|
232
|
+
push<T = unknown>(
|
|
233
|
+
pushEntity: string,
|
|
234
|
+
payload: Record<string, unknown>,
|
|
235
|
+
opts: { transactionId?: string } = {},
|
|
236
|
+
): Promise<{ ok: boolean; seq?: number; data?: T }> {
|
|
237
|
+
return this.submit<T>(pushEntity, payload, opts);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
pushLogList<T = unknown>(
|
|
241
|
+
params: EntityListParams = {},
|
|
242
|
+
): Promise<{ ok: boolean; data: T[]; total: number }> {
|
|
243
|
+
return this.list<T>("push_log", params);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
registerPushDevice<T = unknown>(
|
|
247
|
+
accountSeq: number,
|
|
248
|
+
deviceId: string,
|
|
249
|
+
pushToken: string,
|
|
250
|
+
opts: RegisterPushDeviceOptions = {},
|
|
251
|
+
): Promise<{ ok: boolean; seq?: number; data?: T }> {
|
|
252
|
+
const {
|
|
253
|
+
platform,
|
|
254
|
+
deviceType,
|
|
255
|
+
browser,
|
|
256
|
+
browserVersion,
|
|
257
|
+
pushEnabled = true,
|
|
258
|
+
transactionId,
|
|
259
|
+
} = opts;
|
|
260
|
+
|
|
261
|
+
return this.submit<T>(
|
|
262
|
+
"account_device",
|
|
263
|
+
{
|
|
264
|
+
id: deviceId,
|
|
265
|
+
account_seq: accountSeq,
|
|
266
|
+
push_token: pushToken,
|
|
267
|
+
push_enabled: pushEnabled,
|
|
268
|
+
...(platform ? { platform } : {}),
|
|
269
|
+
...(deviceType ? { device_type: deviceType } : {}),
|
|
270
|
+
...(browser ? { browser } : {}),
|
|
271
|
+
...(browserVersion ? { browser_version: browserVersion } : {}),
|
|
272
|
+
},
|
|
273
|
+
{ transactionId },
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
updatePushDeviceToken<T = unknown>(
|
|
278
|
+
deviceSeq: number,
|
|
279
|
+
pushToken: string,
|
|
280
|
+
opts: { pushEnabled?: boolean; transactionId?: string } = {},
|
|
281
|
+
): Promise<{ ok: boolean; seq?: number; data?: T }> {
|
|
282
|
+
const { pushEnabled = true, transactionId } = opts;
|
|
283
|
+
return this.submit<T>(
|
|
284
|
+
"account_device",
|
|
285
|
+
{
|
|
286
|
+
seq: deviceSeq,
|
|
287
|
+
push_token: pushToken,
|
|
288
|
+
push_enabled: pushEnabled,
|
|
289
|
+
},
|
|
290
|
+
{ transactionId },
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
disablePushDevice<T = unknown>(
|
|
295
|
+
deviceSeq: number,
|
|
296
|
+
opts: { transactionId?: string } = {},
|
|
297
|
+
): Promise<{ ok: boolean; seq?: number; data?: T }> {
|
|
298
|
+
return this.submit<T>(
|
|
299
|
+
"account_device",
|
|
300
|
+
{
|
|
301
|
+
seq: deviceSeq,
|
|
302
|
+
push_enabled: false,
|
|
303
|
+
},
|
|
304
|
+
{ transactionId: opts.transactionId },
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
readRequestBody<T = Record<string, unknown>>(
|
|
309
|
+
body: ArrayBuffer | Uint8Array | string | T | null | undefined,
|
|
310
|
+
contentType = "application/json",
|
|
311
|
+
requireEncrypted = false,
|
|
312
|
+
): T {
|
|
313
|
+
const lowered = contentType.toLowerCase();
|
|
314
|
+
const isEncrypted = lowered.includes("application/octet-stream");
|
|
315
|
+
|
|
316
|
+
if (requireEncrypted && !isEncrypted) {
|
|
317
|
+
throw new Error(
|
|
318
|
+
"Encrypted request required: Content-Type must be application/octet-stream",
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (isEncrypted) {
|
|
323
|
+
if (body == null) {
|
|
324
|
+
throw new Error("Encrypted request body is empty");
|
|
325
|
+
}
|
|
326
|
+
if (body instanceof ArrayBuffer) {
|
|
327
|
+
return this.decryptPacket<T>(body);
|
|
328
|
+
}
|
|
329
|
+
if (body instanceof Uint8Array) {
|
|
330
|
+
const sliced = body.buffer.slice(
|
|
331
|
+
body.byteOffset,
|
|
332
|
+
body.byteOffset + body.byteLength,
|
|
333
|
+
);
|
|
334
|
+
return this.decryptPacket<T>(sliced);
|
|
335
|
+
}
|
|
336
|
+
throw new Error(
|
|
337
|
+
"Encrypted request body must be ArrayBuffer or Uint8Array",
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (body == null || body === "") return {} as T;
|
|
342
|
+
if (typeof body === "string") return JSON.parse(body) as T;
|
|
343
|
+
return body as T;
|
|
344
|
+
}
|
|
345
|
+
|
|
223
346
|
private async request<T>(
|
|
224
347
|
method: string,
|
|
225
348
|
path: string,
|
|
@@ -103,3 +103,71 @@ export function useEntityRollback(entity: string) {
|
|
|
103
103
|
},
|
|
104
104
|
});
|
|
105
105
|
}
|
|
106
|
+
|
|
107
|
+
/** 푸시 디바이스 등록/갱신 */
|
|
108
|
+
export function useRegisterPushDevice() {
|
|
109
|
+
const qc = useQueryClient();
|
|
110
|
+
return useMutation({
|
|
111
|
+
mutationFn: (args: {
|
|
112
|
+
accountSeq: number;
|
|
113
|
+
deviceId: string;
|
|
114
|
+
pushToken: string;
|
|
115
|
+
platform?: string;
|
|
116
|
+
deviceType?: string;
|
|
117
|
+
browser?: string;
|
|
118
|
+
browserVersion?: string;
|
|
119
|
+
pushEnabled?: boolean;
|
|
120
|
+
transactionId?: string;
|
|
121
|
+
}) =>
|
|
122
|
+
entityServer.registerPushDevice(
|
|
123
|
+
args.accountSeq,
|
|
124
|
+
args.deviceId,
|
|
125
|
+
args.pushToken,
|
|
126
|
+
{
|
|
127
|
+
platform: args.platform,
|
|
128
|
+
deviceType: args.deviceType,
|
|
129
|
+
browser: args.browser,
|
|
130
|
+
browserVersion: args.browserVersion,
|
|
131
|
+
pushEnabled: args.pushEnabled,
|
|
132
|
+
transactionId: args.transactionId,
|
|
133
|
+
},
|
|
134
|
+
),
|
|
135
|
+
onSuccess: () => {
|
|
136
|
+
qc.invalidateQueries({ queryKey: ["entity", "account_device"] });
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** 푸시 디바이스 토큰 갱신 */
|
|
142
|
+
export function useUpdatePushDeviceToken() {
|
|
143
|
+
const qc = useQueryClient();
|
|
144
|
+
return useMutation({
|
|
145
|
+
mutationFn: (args: {
|
|
146
|
+
deviceSeq: number;
|
|
147
|
+
pushToken: string;
|
|
148
|
+
pushEnabled?: boolean;
|
|
149
|
+
transactionId?: string;
|
|
150
|
+
}) =>
|
|
151
|
+
entityServer.updatePushDeviceToken(args.deviceSeq, args.pushToken, {
|
|
152
|
+
pushEnabled: args.pushEnabled,
|
|
153
|
+
transactionId: args.transactionId,
|
|
154
|
+
}),
|
|
155
|
+
onSuccess: () => {
|
|
156
|
+
qc.invalidateQueries({ queryKey: ["entity", "account_device"] });
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** 푸시 디바이스 비활성화 */
|
|
162
|
+
export function useDisablePushDevice() {
|
|
163
|
+
const qc = useQueryClient();
|
|
164
|
+
return useMutation({
|
|
165
|
+
mutationFn: (args: { deviceSeq: number; transactionId?: string }) =>
|
|
166
|
+
entityServer.disablePushDevice(args.deviceSeq, {
|
|
167
|
+
transactionId: args.transactionId,
|
|
168
|
+
}),
|
|
169
|
+
onSuccess: () => {
|
|
170
|
+
qc.invalidateQueries({ queryKey: ["entity", "account_device"] });
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
}
|