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.
Files changed (46) hide show
  1. package/bin/create.js +11 -1
  2. package/package.json +1 -1
  3. package/template/.env.example +17 -1
  4. package/template/configs/jwt.json +1 -0
  5. package/template/configs/oauth.json +37 -0
  6. package/template/configs/push.json +26 -0
  7. package/template/entities/README.md +4 -4
  8. package/template/entities/{Auth → System/Auth}/account.json +0 -14
  9. package/template/samples/README.md +16 -0
  10. package/template/samples/entities/01_basic_fields.json +39 -0
  11. package/template/samples/entities/02_types_and_defaults.json +68 -0
  12. package/template/samples/entities/03_hash_and_unique.json +33 -0
  13. package/template/samples/entities/04_fk_and_composite_unique.json +31 -0
  14. package/template/samples/entities/05_cache.json +54 -0
  15. package/template/samples/entities/06_history_and_hard_delete.json +42 -0
  16. package/template/samples/entities/07_license_scope.json +43 -0
  17. package/template/samples/entities/08_hook_sql.json +52 -0
  18. package/template/samples/entities/09_hook_entity.json +71 -0
  19. package/template/samples/entities/10_hook_submit_delete.json +75 -0
  20. package/template/samples/entities/11_hook_webhook.json +82 -0
  21. package/template/samples/entities/12_hook_push.json +73 -0
  22. package/template/samples/entities/13_read_only.json +51 -0
  23. package/template/samples/entities/14_optimistic_lock.json +29 -0
  24. package/template/samples/entities/15_reset_defaults.json +95 -0
  25. package/template/samples/entities/README.md +94 -0
  26. package/template/samples/entities/order_notification.json +51 -0
  27. package/template/samples/flutter/lib/entity_server_client.dart +91 -0
  28. package/template/samples/java/EntityServerClient.java +117 -0
  29. package/template/samples/kotlin/EntityServerClient.kt +86 -0
  30. package/template/samples/node/src/EntityServerClient.js +116 -0
  31. package/template/samples/php/ci4/Config/EntityServer.php +15 -0
  32. package/template/samples/php/ci4/Controllers/EntityController.php +202 -0
  33. package/template/samples/php/ci4/Controllers/ProductController.php +16 -76
  34. package/template/samples/php/ci4/Libraries/EntityServer.php +150 -11
  35. package/template/samples/php/laravel/Services/EntityServerService.php +56 -0
  36. package/template/samples/python/entity_server.py +106 -0
  37. package/template/samples/react/src/api/entityServerClient.ts +123 -0
  38. package/template/samples/react/src/hooks/useEntity.ts +68 -0
  39. package/template/samples/swift/EntityServerClient.swift +105 -0
  40. package/template/scripts/normalize-entities.sh +10 -10
  41. package/template/scripts/run.sh +108 -29
  42. package/template/scripts/update-server.ps1 +92 -2
  43. package/template/scripts/update-server.sh +73 -2
  44. /package/template/entities/{Auth → System/Auth}/api_keys.json +0 -0
  45. /package/template/entities/{Auth → System/Auth}/license.json +0 -0
  46. /package/template/entities/{Auth → System/Auth}/rbac_roles.json +0 -0
@@ -0,0 +1,75 @@
1
+ {
2
+ "name": "project",
3
+ "description": "submit & delete 훅 예제 — 프로젝트 생성 시 기본 태스크 자동 생성, 삭제 시 연관 데이터 정리",
4
+ "index": {
5
+ "owner_seq": {
6
+ "comment": "프로젝트 소유자 user seq",
7
+ "required": true
8
+ },
9
+ "name": {
10
+ "comment": "프로젝트명",
11
+ "required": true
12
+ },
13
+ "status": {
14
+ "comment": "진행 상태",
15
+ "type": ["planning", "active", "on_hold", "completed", "cancelled"],
16
+ "default": "planning"
17
+ },
18
+ "due_date": {
19
+ "comment": "마감일 (*_date → DATE 자동 추론)"
20
+ },
21
+ "task_count": {
22
+ "comment": "태스크 수 (*_count → INT 자동 추론)"
23
+ }
24
+ },
25
+ "types": {
26
+ "description": "text"
27
+ },
28
+ "hooks": {
29
+ "after_insert": [
30
+ {
31
+ "comment": "프로젝트 생성 시 기본 태스크 3개 자동 생성 (submit 훅)",
32
+ "type": "submit",
33
+ "entity": "task",
34
+ "data": {
35
+ "project_seq": "${new.seq}",
36
+ "title": "요구사항 분석",
37
+ "status": "todo",
38
+ "priority": "high"
39
+ }
40
+ },
41
+ {
42
+ "comment": "프로젝트 멤버에 소유자 자동 추가 (submit 훅 Upsert)",
43
+ "type": "submit",
44
+ "entity": "project_member",
45
+ "match": {
46
+ "project_seq": "${new.seq}",
47
+ "user_seq": "${new.owner_seq}"
48
+ },
49
+ "data": {
50
+ "project_seq": "${new.seq}",
51
+ "user_seq": "${new.owner_seq}",
52
+ "role": "owner"
53
+ }
54
+ }
55
+ ],
56
+ "after_delete": [
57
+ {
58
+ "comment": "프로젝트 삭제 시 모든 태스크 삭제 (delete 훅)",
59
+ "type": "delete",
60
+ "entity": "task",
61
+ "match": {
62
+ "project_seq": "${old.seq}"
63
+ }
64
+ },
65
+ {
66
+ "comment": "프로젝트 삭제 시 멤버 목록 삭제 (delete 훅)",
67
+ "type": "delete",
68
+ "entity": "project_member",
69
+ "match": {
70
+ "project_seq": "${old.seq}"
71
+ }
72
+ }
73
+ ]
74
+ }
75
+ }
@@ -0,0 +1,82 @@
1
+ {
2
+ "name": "payment",
3
+ "description": "webhook 훅 예제 — 결제 완료/환불 시 외부 정산 서비스 실시간 통보",
4
+ "index": {
5
+ "order_seq": {
6
+ "comment": "주문 seq",
7
+ "required": true
8
+ },
9
+ "user_seq": {
10
+ "comment": "결제자 user seq",
11
+ "required": true
12
+ },
13
+ "amount": {
14
+ "comment": "결제 금액 (*_amount → DECIMAL(15,2) 자동 추론)",
15
+ "required": true
16
+ },
17
+ "method": {
18
+ "comment": "결제 수단",
19
+ "type": ["card", "bank_transfer", "virtual_account", "point"],
20
+ "required": true
21
+ },
22
+ "status": {
23
+ "comment": "결제 상태",
24
+ "type": ["pending", "paid", "cancelled", "refunded"],
25
+ "default": "pending"
26
+ },
27
+ "paid_at": {
28
+ "comment": "결제 완료일시 (*_at → DATETIME 자동 추론)"
29
+ }
30
+ },
31
+ "types": {
32
+ "pg_transaction_id": "varchar(100)",
33
+ "failure_reason": "text"
34
+ },
35
+ "comments": {
36
+ "pg_transaction_id": "PG사 거래 ID",
37
+ "failure_reason": "실패 사유"
38
+ },
39
+ "hooks": {
40
+ "after_insert": [
41
+ {
42
+ "comment": "결제 생성 시 정산 서버로 비동기 통보 (webhook 훅)",
43
+ "type": "webhook",
44
+ "url": "https://settlement.internal/hooks/payment-created",
45
+ "method": "POST",
46
+ "headers": {
47
+ "Authorization": "Bearer ${ctx.webhook_secret}",
48
+ "Content-Type": "application/json"
49
+ },
50
+ "body": {
51
+ "payment_seq": "${new.seq}",
52
+ "order_seq": "${new.order_seq}",
53
+ "user_seq": "${new.user_seq}",
54
+ "amount": "${new.amount}",
55
+ "method": "${new.method}",
56
+ "status": "${new.status}"
57
+ },
58
+ "async": true,
59
+ "timeout": 5000
60
+ }
61
+ ],
62
+ "after_update": [
63
+ {
64
+ "comment": "결제 상태 변경(환불 등) 시 정산 서버에 동기 통보 (webhook 훅)",
65
+ "type": "webhook",
66
+ "url": "https://settlement.internal/hooks/payment-updated",
67
+ "method": "POST",
68
+ "headers": {
69
+ "Authorization": "Bearer ${ctx.webhook_secret}"
70
+ },
71
+ "body": {
72
+ "payment_seq": "${new.seq}",
73
+ "old_status": "${old.status}",
74
+ "new_status": "${new.status}",
75
+ "amount": "${new.amount}"
76
+ },
77
+ "async": false,
78
+ "timeout": 8000
79
+ }
80
+ ]
81
+ }
82
+ }
@@ -0,0 +1,73 @@
1
+ {
2
+ "name": "delivery",
3
+ "description": "push 훅 예제 — 배송 상태 변경 시 고객에게 푸시 알림 전송 (다단계 상태 알림)",
4
+ "index": {
5
+ "order_seq": {
6
+ "comment": "주문 seq",
7
+ "required": true
8
+ },
9
+ "customer_seq": {
10
+ "comment": "고객 user seq",
11
+ "required": true
12
+ },
13
+ "tracking_number": {
14
+ "comment": "운송장 번호",
15
+ "unique": true
16
+ },
17
+ "carrier": {
18
+ "comment": "택배사",
19
+ "type": ["cj", "lotte", "hanjin", "post", "direct"],
20
+ "default": "cj"
21
+ },
22
+ "status": {
23
+ "comment": "배송 상태",
24
+ "type": [
25
+ "ready",
26
+ "picked_up",
27
+ "in_transit",
28
+ "out_for_delivery",
29
+ "delivered",
30
+ "failed"
31
+ ],
32
+ "default": "ready"
33
+ },
34
+ "estimated_date": {
35
+ "comment": "예상 도착일 (*_date → DATE 자동 추론)"
36
+ },
37
+ "delivered_at": {
38
+ "comment": "배송 완료일시 (*_at → DATETIME 자동 추론)"
39
+ }
40
+ },
41
+ "hooks": {
42
+ "after_insert": [
43
+ {
44
+ "comment": "배송 시작 푸시 알림",
45
+ "type": "push",
46
+ "target_user_field": "customer_seq",
47
+ "title": "배송 시작",
48
+ "push_body": "주문 #${new.order_seq}의 배송이 시작되었습니다. 운송장: ${new.tracking_number}",
49
+ "push_data": {
50
+ "action": "delivery_started",
51
+ "delivery_seq": "${new.seq}",
52
+ "tracking_number": "${new.tracking_number}"
53
+ }
54
+ }
55
+ ],
56
+ "after_update": [
57
+ {
58
+ "comment": "배송 상태 변경 시 푸시 알림",
59
+ "type": "push",
60
+ "target_user_field": "customer_seq",
61
+ "title": "배송 상태 업데이트",
62
+ "push_body": "배송 상태가 '${new.status}'(으)로 변경되었습니다. 운송장: ${new.tracking_number}",
63
+ "push_data": {
64
+ "action": "delivery_status_changed",
65
+ "delivery_seq": "${new.seq}",
66
+ "old_status": "${old.status}",
67
+ "new_status": "${new.status}",
68
+ "tracking_number": "${new.tracking_number}"
69
+ }
70
+ }
71
+ ]
72
+ }
73
+ }
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "activity_log",
3
+ "description": "read_only 예제 — API를 통한 수정·삭제가 불가하고 서버 내부(훅/SP) 로만 기록되는 감사 로그",
4
+ "read_only": true,
5
+ "license_scope": false,
6
+ "hard_delete": true,
7
+ "history_ttl": 0,
8
+ "index": {
9
+ "actor_seq": {
10
+ "comment": "행위자 user seq (null = 시스템)"
11
+ },
12
+ "entity_name": {
13
+ "comment": "대상 엔티티명",
14
+ "required": true
15
+ },
16
+ "entity_seq": {
17
+ "comment": "대상 레코드 seq"
18
+ },
19
+ "action": {
20
+ "comment": "수행된 작업",
21
+ "type": [
22
+ "INSERT",
23
+ "UPDATE",
24
+ "DELETE",
25
+ "LOGIN",
26
+ "LOGOUT",
27
+ "EXPORT",
28
+ "IMPORT"
29
+ ],
30
+ "required": true
31
+ },
32
+ "ip_address": {
33
+ "comment": "요청 IP",
34
+ "type": "varchar(45)"
35
+ },
36
+ "result_code": {
37
+ "comment": "결과 코드 (HTTP 상태)",
38
+ "type": "int"
39
+ }
40
+ },
41
+ "types": {
42
+ "before_snapshot": "text",
43
+ "after_snapshot": "text",
44
+ "detail": "text"
45
+ },
46
+ "comments": {
47
+ "before_snapshot": "변경 전 데이터 스냅샷 (JSON)",
48
+ "after_snapshot": "변경 후 데이터 스냅샷 (JSON)",
49
+ "detail": "추가 상세 정보"
50
+ }
51
+ }
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "inventory",
3
+ "description": "optimistic_lock 예제 — 동시 수정 충돌 방지가 필요한 재고 엔티티",
4
+ "optimistic_lock": true,
5
+ "index": {
6
+ "product_seq": {
7
+ "comment": "상품 seq",
8
+ "required": true,
9
+ "unique": true
10
+ },
11
+ "warehouse_code": {
12
+ "comment": "창고 코드",
13
+ "required": true
14
+ },
15
+ "qty_available": {
16
+ "comment": "가용 재고 수량 (*_qty → INT 자동 추론)"
17
+ },
18
+ "qty_reserved": {
19
+ "comment": "예약된 재고 수량 (*_qty → INT 자동 추론)"
20
+ },
21
+ "location": {
22
+ "comment": "창고 내 위치 (예: A-01-03)"
23
+ },
24
+ "last_stocked_at": {
25
+ "comment": "마지막 입고일시 (*_at → DATETIME 자동 추론)"
26
+ }
27
+ },
28
+ "unique": [["product_seq", "warehouse_code"]]
29
+ }
@@ -0,0 +1,95 @@
1
+ {
2
+ "name": "country",
3
+ "description": "reset_defaults 예제 — reset-all 실행 시 주요 국가 코드 자동 시딩",
4
+ "index": {
5
+ "code": {
6
+ "comment": "ISO 3166-1 alpha-2 국가 코드 (예: KR, US)",
7
+ "required": true,
8
+ "unique": true
9
+ },
10
+ "name_ko": {
11
+ "comment": "국가명 (한국어)"
12
+ },
13
+ "name_en": {
14
+ "comment": "국가명 (영어)"
15
+ },
16
+ "region": {
17
+ "comment": "대륙/지역",
18
+ "type": [
19
+ "asia",
20
+ "europe",
21
+ "americas",
22
+ "africa",
23
+ "oceania",
24
+ "middle_east"
25
+ ]
26
+ },
27
+ "phone_code": {
28
+ "comment": "국가 전화 코드 (예: +82)"
29
+ },
30
+ "is_active": {
31
+ "comment": "서비스 지원 여부 (is_* → TINYINT(1) 자동 추론)",
32
+ "default": true
33
+ }
34
+ },
35
+ "license_scope": false,
36
+ "hard_delete": true,
37
+ "reset_defaults": [
38
+ {
39
+ "code": "KR",
40
+ "name_ko": "대한민국",
41
+ "name_en": "South Korea",
42
+ "region": "asia",
43
+ "phone_code": "+82",
44
+ "is_active": true
45
+ },
46
+ {
47
+ "code": "US",
48
+ "name_ko": "미국",
49
+ "name_en": "United States",
50
+ "region": "americas",
51
+ "phone_code": "+1",
52
+ "is_active": true
53
+ },
54
+ {
55
+ "code": "JP",
56
+ "name_ko": "일본",
57
+ "name_en": "Japan",
58
+ "region": "asia",
59
+ "phone_code": "+81",
60
+ "is_active": true
61
+ },
62
+ {
63
+ "code": "CN",
64
+ "name_ko": "중국",
65
+ "name_en": "China",
66
+ "region": "asia",
67
+ "phone_code": "+86",
68
+ "is_active": true
69
+ },
70
+ {
71
+ "code": "DE",
72
+ "name_ko": "독일",
73
+ "name_en": "Germany",
74
+ "region": "europe",
75
+ "phone_code": "+49",
76
+ "is_active": true
77
+ },
78
+ {
79
+ "code": "GB",
80
+ "name_ko": "영국",
81
+ "name_en": "United Kingdom",
82
+ "region": "europe",
83
+ "phone_code": "+44",
84
+ "is_active": true
85
+ },
86
+ {
87
+ "code": "SG",
88
+ "name_ko": "싱가포르",
89
+ "name_en": "Singapore",
90
+ "region": "asia",
91
+ "phone_code": "+65",
92
+ "is_active": true
93
+ }
94
+ ]
95
+ }
@@ -0,0 +1,94 @@
1
+ # 엔티티 설정 예제
2
+
3
+ `entities/` 디렉토리에 배치하는 `.json` 설정 파일 예제 모음입니다.
4
+ 엔티티 서버의 **모든 주요 기능을 하나씩** 알아볼 수 있도록 주제별로 구성하였습니다.
5
+
6
+ > **전체 레퍼런스**: [docs/ops/entity-config-guide.md](../../docs/ops/entity-config-guide.md)
7
+ > **훅 가이드**: [docs/ops/hooks.md](../../docs/ops/hooks.md)
8
+
9
+ ---
10
+
11
+ ## 기능별 샘플 목록
12
+
13
+ ### 기본 필드 & 타입
14
+
15
+ | 파일 | 엔티티 | 설명 |
16
+ | -------------------------------------------------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------- |
17
+ | [01_basic_fields.json](01_basic_fields.json) | `contact` | 필드명 패턴으로 타입이 **자동 추론**되는 모든 케이스 (`_seq`, `_date`, `_at`, `is_*`, `_count`, `_amount`, `email`, `phone`) |
18
+ | [02_types_and_defaults.json](02_types_and_defaults.json) | `product` | 자동 추론이 안 될 때 **`types` 명시 선언** + `defaults` + `reset_defaults` 시딩 |
19
+
20
+ ### 제약 & 참조
21
+
22
+ | 파일 | 엔티티 | 설명 |
23
+ | ------------------------------------------------------------------ | ------------- | ------------------------------------------------------------------------ |
24
+ | [03_hash_and_unique.json](03_hash_and_unique.json) | `member` | **`hash`** (민감 필드 해시 저장) + **단일 `unique`** + **복합 `unique`** |
25
+ | [04_fk_and_composite_unique.json](04_fk_and_composite_unique.json) | `team_member` | **`fk`** 외래키 참조 + **복합 유니크** (`team_seq + user_seq`) |
26
+
27
+ ### 성능 & 운영
28
+
29
+ | 파일 | 엔티티 | 설명 |
30
+ | ------------------------------------------------------------------ | ------------- | ------------------------------------------------------------------------- |
31
+ | [05_cache.json](05_cache.json) | `config_item` | **`cache`** 엔티티 레벨 캐시 (`ttl_seconds: 600`) |
32
+ | [06_history_and_hard_delete.json](06_history_and_hard_delete.json) | `article` | **`history_ttl`** 수정 이력 3년 보관 + **`hard_delete: false`** 논리 삭제 |
33
+ | [14_optimistic_lock.json](14_optimistic_lock.json) | `inventory` | **`optimistic_lock`** 동시 수정 충돌 방지 (재고 등 경쟁 조건 있는 데이터) |
34
+
35
+ ### 멀티테넌트
36
+
37
+ | 파일 | 엔티티 | 설명 |
38
+ | ---------------------------------------------- | ----------- | ------------------------------------------------------------ |
39
+ | [07_license_scope.json](07_license_scope.json) | `workspace` | **`license_scope: true`** 라이선스별 데이터 완전 분리 + 캐시 |
40
+
41
+ ### 특수 모드
42
+
43
+ | 파일 | 엔티티 | 설명 |
44
+ | ------------------------------------------------ | -------------- | -------------------------------------------------------------------------------- |
45
+ | [13_read_only.json](13_read_only.json) | `activity_log` | **`read_only: true`** API로 수정 불가, 서버 내부(훅/SP)에서만 기록되는 감사 로그 |
46
+ | [15_reset_defaults.json](15_reset_defaults.json) | `country` | **`reset_defaults`** 대량 시딩 — `reset-all` 시 국가 코드 자동 입력 |
47
+
48
+ ### 훅 (Hooks)
49
+
50
+ | 파일 | 엔티티 | 훅 타입 | 설명 |
51
+ | -------------------------------------------------------- | ------------ | ------------------- | ------------------------------------------------------------------------- |
52
+ | [08_hook_sql.json](08_hook_sql.json) | `user_point` | `sql` | 실행형 SQL (포인트 이력 INSERT) + 조회형 SQL (`assign_to`로 결과 주입) |
53
+ | [09_hook_entity.json](09_hook_entity.json) | `post` | `entity` | `after_get`/`after_list`에서 관련 엔티티 자동 주입 (작성자 프로필, 댓글) |
54
+ | [10_hook_submit_delete.json](10_hook_submit_delete.json) | `project` | `submit` / `delete` | 생성 시 연관 엔티티 자동 생성(Upsert 포함), 삭제 시 연관 데이터 자동 정리 |
55
+ | [11_hook_webhook.json](11_hook_webhook.json) | `payment` | `webhook` | 외부 HTTP 통보 — 비동기(결제 생성) + 동기(상태 변경) |
56
+ | [12_hook_push.json](12_hook_push.json) | `delivery` | `push` | 배송 상태 변경 시 고객 FCM 푸시 알림 |
57
+
58
+ ### 이전 예제
59
+
60
+ | 파일 | 엔티티 | 설명 |
61
+ | -------------------------------------------------- | -------------------- | --------------------------------------------- |
62
+ | [order_notification.json](order_notification.json) | `order_notification` | 주문 생성/수정 시 push 훅 (push 훅 기본 예제) |
63
+
64
+ ---
65
+
66
+ ## 훅 타입 한눈에 보기
67
+
68
+ | 훅 타입 | 핵심 필드 | 주요 용도 | 샘플 |
69
+ | --------- | ----------------------------------------- | ----------------------- | ---------------------------------------------- |
70
+ | `sql` | `query`, `params` | SQL 실행 또는 결과 주입 | `08_hook_sql.json` |
71
+ | `entity` | `entity`, `action`, `assign_to` | 관련 데이터 자동 로드 | `09_hook_entity.json` |
72
+ | `submit` | `entity`, `data`, `match` | 다른 엔티티 생성/수정 | `10_hook_submit_delete.json` |
73
+ | `delete` | `entity`, `match` | 다른 엔티티 삭제 | `10_hook_submit_delete.json` |
74
+ | `webhook` | `url`, `body`, `async` | 외부 HTTP 호출 | `11_hook_webhook.json` |
75
+ | `push` | `target_user_field`, `title`, `push_body` | FCM 푸시 알림 전송 | `12_hook_push.json`, `order_notification.json` |
76
+
77
+ ## 훅 실행 시점
78
+
79
+ | 시점 | 설명 | 주로 사용 |
80
+ | -------------- | -------------- | -------------------------- |
81
+ | `after_insert` | 데이터 삽입 후 | 감사 로그, 알림, 연관 생성 |
82
+ | `after_update` | 데이터 수정 후 | 변경 이력, 상태 알림 |
83
+ | `after_delete` | 데이터 삭제 후 | 연관 데이터 정리 |
84
+ | `after_get` | 단건 조회 후 | 관련 데이터 병합 |
85
+ | `after_list` | 목록 조회 후 | 각 항목 추가 정보 주입 |
86
+
87
+ ## 훅 템플릿 변수
88
+
89
+ | 변수 | 설명 |
90
+ | -------------------- | ----------------------------- |
91
+ | `${new.field}` | 삽입/수정 후 데이터의 필드 값 |
92
+ | `${old.field}` | 수정/삭제 전 데이터의 필드 값 |
93
+ | `${ctx.license_seq}` | 현재 요청의 라이선스 seq |
94
+ | `${ctx.user_seq}` | 현재 로그인 사용자 seq |
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "order_notification",
3
+ "description": "주문 알림 예제 — 주문 생성 시 고객에게 푸시 전송",
4
+ "index": {
5
+ "customer_seq": {
6
+ "comment": "고객 user seq",
7
+ "required": true
8
+ },
9
+ "order_number": {
10
+ "comment": "주문 번호",
11
+ "required": true
12
+ },
13
+ "status": {
14
+ "comment": "주문 상태",
15
+ "type": ["placed", "confirmed", "shipped", "delivered", "cancelled"],
16
+ "default": "placed"
17
+ },
18
+ "total_amount": {
19
+ "comment": "주문 금액",
20
+ "type": "uint"
21
+ }
22
+ },
23
+ "hooks": {
24
+ "after_insert": [
25
+ {
26
+ "type": "push",
27
+ "target_user_field": "customer_seq",
28
+ "title": "주문 접수",
29
+ "push_body": "주문 #${new.order_number}이 접수되었습니다. 금액: ${new.total_amount}원",
30
+ "push_data": {
31
+ "action": "order_created",
32
+ "order_seq": "${new.seq}",
33
+ "order_number": "${new.order_number}"
34
+ }
35
+ }
36
+ ],
37
+ "after_update": [
38
+ {
39
+ "type": "push",
40
+ "target_user_field": "customer_seq",
41
+ "title": "주문 상태 변경",
42
+ "push_body": "주문 #${new.order_number} 상태: ${new.status}",
43
+ "push_data": {
44
+ "action": "order_updated",
45
+ "order_seq": "${new.seq}",
46
+ "status": "${new.status}"
47
+ }
48
+ }
49
+ ]
50
+ }
51
+ }
@@ -125,6 +125,97 @@ class EntityServerClient {
125
125
  Future<Map<String, dynamic>> rollback(String entity, int historySeq) =>
126
126
  _request('POST', '/v1/entity/$entity/rollback/$historySeq');
127
127
 
128
+ /// 푸시 발송 트리거 엔티티에 submit합니다.
129
+ Future<Map<String, dynamic>> push(
130
+ String pushEntity,
131
+ Map<String, dynamic> payload, {
132
+ String? transactionId,
133
+ }) =>
134
+ submit(pushEntity, payload, transactionId: transactionId);
135
+
136
+ /// push_log 목록 조회 헬퍼
137
+ Future<Map<String, dynamic>> pushLogList({int page = 1, int limit = 20}) =>
138
+ list('push_log', page: page, limit: limit);
139
+
140
+ /// account_device 디바이스 등록/갱신 헬퍼 (push_token 단일 필드)
141
+ Future<Map<String, dynamic>> registerPushDevice(
142
+ int accountSeq,
143
+ String deviceId,
144
+ String pushToken, {
145
+ String? platform,
146
+ String? deviceType,
147
+ String? browser,
148
+ String? browserVersion,
149
+ bool pushEnabled = true,
150
+ String? transactionId,
151
+ }) {
152
+ final payload = <String, dynamic>{
153
+ 'id': deviceId,
154
+ 'account_seq': accountSeq,
155
+ 'push_token': pushToken,
156
+ 'push_enabled': pushEnabled,
157
+ if (platform != null && platform.isNotEmpty) 'platform': platform,
158
+ if (deviceType != null && deviceType.isNotEmpty)
159
+ 'device_type': deviceType,
160
+ if (browser != null && browser.isNotEmpty) 'browser': browser,
161
+ if (browserVersion != null && browserVersion.isNotEmpty)
162
+ 'browser_version': browserVersion,
163
+ };
164
+ return submit('account_device', payload, transactionId: transactionId);
165
+ }
166
+
167
+ /// account_device.seq 기준 push_token 갱신 헬퍼
168
+ Future<Map<String, dynamic>> updatePushDeviceToken(
169
+ int deviceSeq,
170
+ String pushToken, {
171
+ bool pushEnabled = true,
172
+ String? transactionId,
173
+ }) {
174
+ return submit('account_device', {
175
+ 'seq': deviceSeq,
176
+ 'push_token': pushToken,
177
+ 'push_enabled': pushEnabled,
178
+ }, transactionId: transactionId);
179
+ }
180
+
181
+ /// account_device.seq 기준 푸시 수신 비활성화 헬퍼
182
+ Future<Map<String, dynamic>> disablePushDevice(
183
+ int deviceSeq, {
184
+ String? transactionId,
185
+ }) {
186
+ return submit('account_device', {
187
+ 'seq': deviceSeq,
188
+ 'push_enabled': false,
189
+ }, transactionId: transactionId);
190
+ }
191
+
192
+ /// 요청 본문을 읽어 JSON으로 반환합니다.
193
+ /// - application/octet-stream: 암호 패킷 복호화
194
+ /// - 그 외: 평문 JSON 파싱
195
+ Future<Map<String, dynamic>> readRequestBody(
196
+ Uint8List rawBody, {
197
+ String contentType = 'application/json',
198
+ bool requireEncrypted = false,
199
+ }) async {
200
+ final lowered = contentType.toLowerCase();
201
+ final isEncrypted = lowered.contains('application/octet-stream');
202
+
203
+ if (requireEncrypted && !isEncrypted) {
204
+ throw Exception(
205
+ 'Encrypted request required: Content-Type must be application/octet-stream');
206
+ }
207
+
208
+ if (isEncrypted) {
209
+ if (rawBody.isEmpty) {
210
+ throw Exception('Encrypted request body is empty');
211
+ }
212
+ return await _decryptPacket(rawBody);
213
+ }
214
+
215
+ if (rawBody.isEmpty) return {};
216
+ return jsonDecode(utf8.decode(rawBody)) as Map<String, dynamic>;
217
+ }
218
+
128
219
  // ─── 내부 ─────────────────────────────────────────────────────────
129
220
 
130
221
  Future<Map<String, dynamic>> _request(