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
|
@@ -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(
|