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
package/bin/create.js
CHANGED
|
@@ -193,6 +193,15 @@ async function run() {
|
|
|
193
193
|
path.join(targetDir, "entities"),
|
|
194
194
|
);
|
|
195
195
|
|
|
196
|
+
// samples/ (없어도 빈 디렉터리 생성)
|
|
197
|
+
const samplesSrc = path.join(templateDir, "samples");
|
|
198
|
+
const samplesDest = path.join(targetDir, "samples");
|
|
199
|
+
if (fs.existsSync(samplesSrc)) {
|
|
200
|
+
copyDir(samplesSrc, samplesDest);
|
|
201
|
+
} else {
|
|
202
|
+
fs.mkdirSync(samplesDest, { recursive: true });
|
|
203
|
+
}
|
|
204
|
+
|
|
196
205
|
// .env.example 복사 + .env 생성 (없을 때만)
|
|
197
206
|
copyFile(
|
|
198
207
|
path.join(templateDir, ".env.example"),
|
|
@@ -260,7 +269,8 @@ async function run() {
|
|
|
260
269
|
├── .env ← 환경 변수 설정 (여기를 먼저 수정하세요)
|
|
261
270
|
├── configs/ ← CORS, JWT 등 서버 설정
|
|
262
271
|
├── entities/ ← 엔티티 스키마 JSON (샘플 포함)
|
|
263
|
-
|
|
272
|
+
├── scripts/ ← 운영 스크립트
|
|
273
|
+
└── samples/ ← 샘플 파일
|
|
264
274
|
|
|
265
275
|
다음 단계:
|
|
266
276
|
${cdCmd ? ` ${cdCmd}` : ""}
|
package/package.json
CHANGED
package/template/.env.example
CHANGED
|
@@ -9,6 +9,22 @@ ENCRYPTION_KEY=your-32-char-hex-encryption-key-here
|
|
|
9
9
|
# 운영에서는 충분히 긴 랜덤 시크릿을 사용하세요.
|
|
10
10
|
JWT_SECRET=your-jwt-secret-here
|
|
11
11
|
|
|
12
|
-
#
|
|
12
|
+
# 서버 포트 (configs/server.json의 port를 오버라이드)
|
|
13
|
+
SERVER_PORT=47200
|
|
14
|
+
|
|
15
|
+
# Database values (database.json의 ${ENV_VAR}와 매핑)
|
|
16
|
+
DB_NAME_DEVELOPMENT=your-development-db-name
|
|
17
|
+
DB_USER_DEVELOPMENT=your-development-db-user
|
|
13
18
|
DB_PASSWORD_DEVELOPMENT=your-development-db-password
|
|
19
|
+
|
|
20
|
+
DB_NAME_PRODUCTION=your-production-db-name
|
|
21
|
+
DB_USER_PRODUCTION=your-production-db-user
|
|
14
22
|
DB_PASSWORD_PRODUCTION=your-production-db-password
|
|
23
|
+
|
|
24
|
+
# PostgreSQL group (선택)
|
|
25
|
+
# DB_NAME_POSTGRESQL=your-postgresql-db-name
|
|
26
|
+
# DB_USER_POSTGRESQL=your-postgresql-db-user
|
|
27
|
+
# DB_PASSWORD_POSTGRESQL=your-postgresql-db-password
|
|
28
|
+
|
|
29
|
+
# 푸시 알림 (push.json에서 ${FCM_PROJECT_ID} 등으로 참조 가능)
|
|
30
|
+
# FCM_PROJECT_ID=your-firebase-project-id
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "OAuth 2.0 설정 예시. 사용할 프로바이더만 남기고 실제 값으로 교체하세요.",
|
|
3
|
+
|
|
4
|
+
"state_secret": "${OAUTH_STATE_SECRET}",
|
|
5
|
+
"state_ttl_sec": 600,
|
|
6
|
+
|
|
7
|
+
"providers": {
|
|
8
|
+
"google": {
|
|
9
|
+
"client_id": "${GOOGLE_CLIENT_ID}",
|
|
10
|
+
"client_secret": "${GOOGLE_CLIENT_SECRET}",
|
|
11
|
+
"redirect_url": "https://your-domain.com/v1/oauth/google/callback",
|
|
12
|
+
"scopes": ["openid", "email", "profile"]
|
|
13
|
+
},
|
|
14
|
+
"github": {
|
|
15
|
+
"client_id": "${GITHUB_CLIENT_ID}",
|
|
16
|
+
"client_secret": "${GITHUB_CLIENT_SECRET}",
|
|
17
|
+
"redirect_url": "https://your-domain.com/v1/oauth/github/callback"
|
|
18
|
+
},
|
|
19
|
+
"naver": {
|
|
20
|
+
"client_id": "${NAVER_CLIENT_ID}",
|
|
21
|
+
"client_secret": "${NAVER_CLIENT_SECRET}",
|
|
22
|
+
"redirect_url": "https://your-domain.com/v1/oauth/naver/callback"
|
|
23
|
+
},
|
|
24
|
+
"kakao": {
|
|
25
|
+
"_comment": "Kakao — 커스텀 엔드포인트 예시",
|
|
26
|
+
"client_id": "${KAKAO_CLIENT_ID}",
|
|
27
|
+
"client_secret": "${KAKAO_CLIENT_SECRET}",
|
|
28
|
+
"redirect_url": "https://your-domain.com/v1/oauth/kakao/callback",
|
|
29
|
+
"auth_url": "https://kauth.kakao.com/oauth/authorize",
|
|
30
|
+
"token_url": "https://kauth.kakao.com/oauth/token",
|
|
31
|
+
"user_info_url": "https://kapi.kakao.com/v2/user/me",
|
|
32
|
+
"scopes": ["profile_nickname", "account_email"],
|
|
33
|
+
"email_field": "kakao_account.email",
|
|
34
|
+
"name_field": "properties.nickname"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"workers": 2,
|
|
3
|
+
"queue_size": 500,
|
|
4
|
+
"fcm": {
|
|
5
|
+
"enabled": true,
|
|
6
|
+
"type": "service_account",
|
|
7
|
+
"project_id": "your-firebase-project-id",
|
|
8
|
+
"private_key_id": "abc123def456...",
|
|
9
|
+
"private_key_file": "./configs/keys/firebase.pem",
|
|
10
|
+
"client_email": "firebase-adminsdk-xxxxx@your-firebase-project-id.iam.gserviceaccount.com",
|
|
11
|
+
"client_id": "123456789012345678901",
|
|
12
|
+
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
|
13
|
+
"token_uri": "https://oauth2.googleapis.com/token",
|
|
14
|
+
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
|
|
15
|
+
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-xxxxx%40your-firebase-project-id.iam.gserviceaccount.com"
|
|
16
|
+
},
|
|
17
|
+
"apns": {
|
|
18
|
+
"enabled": false,
|
|
19
|
+
"_comment": "APNs 직접 전송 (미구현 - 예약)",
|
|
20
|
+
"key_file": "./configs/keys/apns.p8",
|
|
21
|
+
"key_id": "ABCDE12345",
|
|
22
|
+
"team_id": "FGHIJ67890",
|
|
23
|
+
"bundle_id": "com.example.myapp",
|
|
24
|
+
"production": false
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -50,11 +50,11 @@
|
|
|
50
50
|
|
|
51
51
|
### 3. examples/Auth/account.json
|
|
52
52
|
|
|
53
|
-
사용자 관리 엔티티 (`entities/examples/Auth/` 에 위치 → 배포 시 `dist/entities/Auth/account.json`)
|
|
53
|
+
사용자 관리 엔티티 (`entities/examples/Auth/` 에 위치 → 배포 시 `dist/entities/System/Auth/account.json`)
|
|
54
54
|
|
|
55
55
|
> **⚠️ JWT 인증을 사용하려면 필수입니다.**
|
|
56
56
|
> HMAC 인증만 사용하는 경우에는 필요하지 않습니다.
|
|
57
|
-
> `jwt.json`에 `secret`이 설정된 경우 서버 기동 시 `entities/Auth/account.json`이 존재하지 않으면 오류가 발생합니다.
|
|
57
|
+
> `jwt.json`에 `secret`이 설정된 경우 서버 기동 시 `entities/System/Auth/account.json`이 존재하지 않으면 오류가 발생합니다.
|
|
58
58
|
> `email`, `rbac_role` 필드가 index에 포함되어 있어야 합니다.
|
|
59
59
|
> `./scripts/reset-all.sh --apply` 실행 시 파일이 없으면 자동으로 생성됩니다.
|
|
60
60
|
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
|
|
69
69
|
### 4. examples/Auth/rbac_roles.json
|
|
70
70
|
|
|
71
|
-
RBAC 역할 정의 엔티티 (`entities/examples/Auth/` 에 위치 → 배포 시 `dist/entities/Auth/rbac_roles.json`)
|
|
71
|
+
RBAC 역할 정의 엔티티 (`entities/examples/Auth/` 에 위치 → 배포 시 `dist/entities/System/Auth/rbac_roles.json`)
|
|
72
72
|
|
|
73
73
|
> **⚠️ RBAC 인증을 사용하려면 필수입니다.**
|
|
74
74
|
> `reset-all` 실행 시 `reset_defaults`의 5개 역할이 자동 시딩됩니다.
|
|
@@ -84,7 +84,7 @@ RBAC 역할 정의 엔티티 (`entities/examples/Auth/` 에 위치 → 배포
|
|
|
84
84
|
|
|
85
85
|
### 5. examples/Auth/api_keys.json
|
|
86
86
|
|
|
87
|
-
API 키 관리 엔티티 (`entities/examples/Auth/` 에 위치 → 배포 시 `dist/entities/Auth/api_keys.json`)
|
|
87
|
+
API 키 관리 엔티티 (`entities/examples/Auth/` 에 위치 → 배포 시 `dist/entities/System/Auth/api_keys.json`)
|
|
88
88
|
|
|
89
89
|
> **⚠️ HMAC 인증을 사용하려면 필수입니다.**
|
|
90
90
|
> `reset-all` 실행 시 admin 역할의 API 키 1개가 자동 생성되며 `key_value`와 `hmac_secret`이 출력됩니다.
|
|
@@ -34,20 +34,6 @@
|
|
|
34
34
|
}
|
|
35
35
|
],
|
|
36
36
|
"hooks": {
|
|
37
|
-
"before_insert": [
|
|
38
|
-
{
|
|
39
|
-
"type": "submit",
|
|
40
|
-
"entity": "user",
|
|
41
|
-
"match": {
|
|
42
|
-
"email": "${new.email}"
|
|
43
|
-
},
|
|
44
|
-
"data": {
|
|
45
|
-
"email": "${new.email}",
|
|
46
|
-
"status": "active"
|
|
47
|
-
},
|
|
48
|
-
"assign_seq_to": "user_seq"
|
|
49
|
-
}
|
|
50
|
-
],
|
|
51
37
|
"after_insert": [
|
|
52
38
|
{
|
|
53
39
|
"type": "sql",
|
|
@@ -57,9 +57,25 @@ signature = HMAC-SHA256(hmacSecret, payload) → hex
|
|
|
57
57
|
|
|
58
58
|
| 디렉토리 | 프레임워크 | 인증 방식 |
|
|
59
59
|
| -------------- | ---------------------- | --------- |
|
|
60
|
+
| `entities/` | 엔티티 설정 예제 | — |
|
|
60
61
|
| `php/ci4/` | CodeIgniter 4 | HMAC |
|
|
61
62
|
| `php/laravel/` | Laravel | HMAC |
|
|
62
63
|
| `java/` | Java (표준 라이브러리) | HMAC |
|
|
63
64
|
| `node/` | Node.js (fetch) | HMAC |
|
|
64
65
|
| `python/` | Python (requests) | HMAC |
|
|
65
66
|
| `react/` | React + TypeScript | JWT |
|
|
67
|
+
|
|
68
|
+
### CI4 설정 방식
|
|
69
|
+
|
|
70
|
+
- `php/ci4` 샘플은 `app/Config/EntityServer.php` 중심으로 설정을 관리합니다.
|
|
71
|
+
- 다른 언어 샘플은 각 라이브러리/클라이언트 파일 내부 설정값을 직접 수정해 사용합니다.
|
|
72
|
+
|
|
73
|
+
### Push 헬퍼
|
|
74
|
+
|
|
75
|
+
- 모든 샘플 클라이언트는 푸시 트리거용 헬퍼를 제공합니다. - `push(pushEntity, payload[, transactionId])` : 내부적으로 `submit(pushEntity, payload)` 호출 - `pushLogList(...)` : 내부적으로 `list('push_log', ...)` 호출
|
|
76
|
+
- Python 샘플은 PEP8 네이밍으로 `push_log_list(...)` 메서드를 제공합니다.
|
|
77
|
+
|
|
78
|
+
### 요청 본문(암호 패킷) 읽기 헬퍼
|
|
79
|
+
|
|
80
|
+
- 모든 샘플에 요청 본문 파싱 헬퍼를 제공합니다. - `application/octet-stream`이면 XChaCha20-Poly1305 패킷을 복호화해 JSON 반환 - 그 외 Content-Type이면 평문 JSON 파싱 - 옵션으로 `requireEncrypted=true` 정책을 줄 수 있습니다.
|
|
81
|
+
- 언어별 메서드명 - Node/React/Kotlin/Java/Swift/Laravel: `readRequestBody(...)` - Python: `read_request_body(...)` - Flutter: `readRequestBody(...)`
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "contact",
|
|
3
|
+
"description": "기본 필드 타입 예제 — 인덱스 필드 자동 추론 패턴 모음",
|
|
4
|
+
"index": {
|
|
5
|
+
"name": {
|
|
6
|
+
"comment": "이름 (*_name → VARCHAR(100) 자동 추론)"
|
|
7
|
+
},
|
|
8
|
+
"email": {
|
|
9
|
+
"comment": "이메일 (*email* → VARCHAR(255) 자동 추론)",
|
|
10
|
+
"type": "email"
|
|
11
|
+
},
|
|
12
|
+
"phone": {
|
|
13
|
+
"comment": "전화번호 (*phone* → VARCHAR(50) 자동 추론)"
|
|
14
|
+
},
|
|
15
|
+
"birth_date": {
|
|
16
|
+
"comment": "생년월일 (*_date → DATE 자동 추론)"
|
|
17
|
+
},
|
|
18
|
+
"joined_at": {
|
|
19
|
+
"comment": "가입일시 (*_at → DATETIME 자동 추론)"
|
|
20
|
+
},
|
|
21
|
+
"visit_count": {
|
|
22
|
+
"comment": "방문 횟수 (*_count → INT 자동 추론)"
|
|
23
|
+
},
|
|
24
|
+
"total_amount": {
|
|
25
|
+
"comment": "총 금액 (*_amount → DECIMAL(15,2) 자동 추론)"
|
|
26
|
+
},
|
|
27
|
+
"is_verified": {
|
|
28
|
+
"comment": "인증 여부 (is_* → TINYINT(1) 자동 추론)"
|
|
29
|
+
},
|
|
30
|
+
"user_seq": {
|
|
31
|
+
"comment": "연결된 사용자 seq (*_seq → BIGINT UNSIGNED 자동 추론)"
|
|
32
|
+
},
|
|
33
|
+
"status": {
|
|
34
|
+
"comment": "상태 (enum 배열로 허용값 제한)",
|
|
35
|
+
"type": ["active", "inactive"],
|
|
36
|
+
"default": "active"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "product",
|
|
3
|
+
"description": "명시적 타입 선언 & defaults 예제 — 자동 추론이 안 되는 필드에 types 사용",
|
|
4
|
+
"index": {
|
|
5
|
+
"sku": {
|
|
6
|
+
"comment": "상품 코드",
|
|
7
|
+
"required": true,
|
|
8
|
+
"unique": true
|
|
9
|
+
},
|
|
10
|
+
"category": {
|
|
11
|
+
"comment": "카테고리",
|
|
12
|
+
"type": ["electronics", "furniture", "clothing", "food"],
|
|
13
|
+
"default": "electronics"
|
|
14
|
+
},
|
|
15
|
+
"price": {
|
|
16
|
+
"comment": "정가 (decimal — 자동추론 불가, 명시 선언)",
|
|
17
|
+
"type": "decimal"
|
|
18
|
+
},
|
|
19
|
+
"stock_qty": {
|
|
20
|
+
"comment": "재고 수량 (*_qty → INT 자동 추론)"
|
|
21
|
+
},
|
|
22
|
+
"weight": {
|
|
23
|
+
"comment": "무게(kg) — 자동추론 없음, types에서 decimal 선언",
|
|
24
|
+
"type": "decimal"
|
|
25
|
+
},
|
|
26
|
+
"is_available": {
|
|
27
|
+
"comment": "판매 가능 여부 (is_* → TINYINT(1) 자동 추론)",
|
|
28
|
+
"default": true
|
|
29
|
+
},
|
|
30
|
+
"launched_at": {
|
|
31
|
+
"comment": "출시일시 (*_at → DATETIME 자동 추론)"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"types": {
|
|
35
|
+
"description": "text",
|
|
36
|
+
"thumbnail_url": "varchar(500)",
|
|
37
|
+
"spec_json": "text",
|
|
38
|
+
"barcode": "varchar(50)"
|
|
39
|
+
},
|
|
40
|
+
"comments": {
|
|
41
|
+
"description": "상품 설명 (긴 텍스트)",
|
|
42
|
+
"thumbnail_url": "썸네일 이미지 URL",
|
|
43
|
+
"spec_json": "상세 스펙 JSON",
|
|
44
|
+
"barcode": "바코드"
|
|
45
|
+
},
|
|
46
|
+
"defaults": {
|
|
47
|
+
"is_available": true,
|
|
48
|
+
"stock_qty": 0
|
|
49
|
+
},
|
|
50
|
+
"reset_defaults": [
|
|
51
|
+
{
|
|
52
|
+
"sku": "ELEC-001",
|
|
53
|
+
"category": "electronics",
|
|
54
|
+
"price": 99000,
|
|
55
|
+
"stock_qty": 100,
|
|
56
|
+
"is_available": true,
|
|
57
|
+
"description": "샘플 전자제품"
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"sku": "FURN-001",
|
|
61
|
+
"category": "furniture",
|
|
62
|
+
"price": 299000,
|
|
63
|
+
"stock_qty": 20,
|
|
64
|
+
"is_available": true,
|
|
65
|
+
"description": "샘플 가구"
|
|
66
|
+
}
|
|
67
|
+
]
|
|
68
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "member",
|
|
3
|
+
"description": "hash & unique 예제 — 민감 필드 해시 저장 + 단일/복합 유니크",
|
|
4
|
+
"index": {
|
|
5
|
+
"email": {
|
|
6
|
+
"comment": "이메일 (고유 — 중복 불가)",
|
|
7
|
+
"type": "email",
|
|
8
|
+
"required": true,
|
|
9
|
+
"unique": true
|
|
10
|
+
},
|
|
11
|
+
"phone": {
|
|
12
|
+
"comment": "전화번호 (해시 저장 — 평문 노출 차단)",
|
|
13
|
+
"hash": true
|
|
14
|
+
},
|
|
15
|
+
"resident_id": {
|
|
16
|
+
"comment": "주민등록번호 앞 7자리 (해시 + 유니크 — 중복 가입 방지)",
|
|
17
|
+
"hash": true,
|
|
18
|
+
"unique": true
|
|
19
|
+
},
|
|
20
|
+
"nickname": {
|
|
21
|
+
"comment": "닉네임 (unique — 중복 불가)"
|
|
22
|
+
},
|
|
23
|
+
"org_seq": {
|
|
24
|
+
"comment": "소속 조직 seq"
|
|
25
|
+
},
|
|
26
|
+
"status": {
|
|
27
|
+
"comment": "상태",
|
|
28
|
+
"type": ["active", "inactive", "withdrawn"],
|
|
29
|
+
"default": "active"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"unique": [["org_seq", "nickname"]]
|
|
33
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "team_member",
|
|
3
|
+
"description": "외래키(fk) & 복합 유니크 예제 — 팀-사용자 다대다 중간 테이블",
|
|
4
|
+
"index": {
|
|
5
|
+
"team_seq": {
|
|
6
|
+
"comment": "팀 seq (fk: team.seq)",
|
|
7
|
+
"required": true
|
|
8
|
+
},
|
|
9
|
+
"user_seq": {
|
|
10
|
+
"comment": "사용자 seq (fk: user.seq)",
|
|
11
|
+
"required": true
|
|
12
|
+
},
|
|
13
|
+
"role": {
|
|
14
|
+
"comment": "팀 내 역할",
|
|
15
|
+
"type": ["owner", "admin", "member", "viewer"],
|
|
16
|
+
"default": "member"
|
|
17
|
+
},
|
|
18
|
+
"invited_by": {
|
|
19
|
+
"comment": "초대한 사용자 seq (fk: user.seq, nullable)"
|
|
20
|
+
},
|
|
21
|
+
"joined_at": {
|
|
22
|
+
"comment": "참가일시 (*_at → DATETIME 자동 추론)"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"unique": [["team_seq", "user_seq"]],
|
|
26
|
+
"fk": {
|
|
27
|
+
"team_seq": "team.seq",
|
|
28
|
+
"user_seq": "user.seq",
|
|
29
|
+
"invited_by": "user.seq"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "config_item",
|
|
3
|
+
"description": "캐시 예제 — 자주 읽히고 드물게 변경되는 설정값에 엔티티 레벨 캐시 활성화",
|
|
4
|
+
"index": {
|
|
5
|
+
"key": {
|
|
6
|
+
"comment": "설정 키 (고유)",
|
|
7
|
+
"required": true,
|
|
8
|
+
"unique": true
|
|
9
|
+
},
|
|
10
|
+
"category": {
|
|
11
|
+
"comment": "설정 카테고리",
|
|
12
|
+
"type": ["system", "ui", "feature_flag", "notification"],
|
|
13
|
+
"default": "system"
|
|
14
|
+
},
|
|
15
|
+
"is_public": {
|
|
16
|
+
"comment": "공개 여부 (is_* → TINYINT(1) 자동 추론)"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"types": {
|
|
20
|
+
"value": "text",
|
|
21
|
+
"description": "text"
|
|
22
|
+
},
|
|
23
|
+
"comments": {
|
|
24
|
+
"value": "설정 값 (JSON 또는 문자열)",
|
|
25
|
+
"description": "설정 설명"
|
|
26
|
+
},
|
|
27
|
+
"cache": {
|
|
28
|
+
"enabled": true,
|
|
29
|
+
"ttl_seconds": 600
|
|
30
|
+
},
|
|
31
|
+
"reset_defaults": [
|
|
32
|
+
{
|
|
33
|
+
"key": "site.name",
|
|
34
|
+
"category": "system",
|
|
35
|
+
"value": "My Service",
|
|
36
|
+
"is_public": true,
|
|
37
|
+
"description": "서비스명"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"key": "feature.dark_mode",
|
|
41
|
+
"category": "feature_flag",
|
|
42
|
+
"value": "true",
|
|
43
|
+
"is_public": true,
|
|
44
|
+
"description": "다크모드 기능 활성화"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"key": "notification.email_enabled",
|
|
48
|
+
"category": "notification",
|
|
49
|
+
"value": "true",
|
|
50
|
+
"is_public": false,
|
|
51
|
+
"description": "이메일 알림 활성화"
|
|
52
|
+
}
|
|
53
|
+
]
|
|
54
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "article",
|
|
3
|
+
"description": "이력 보존(history_ttl) & 완전삭제(hard_delete) 예제 — 게시글 수정 이력 3년 보관",
|
|
4
|
+
"index": {
|
|
5
|
+
"author_seq": {
|
|
6
|
+
"comment": "작성자 user seq",
|
|
7
|
+
"required": true
|
|
8
|
+
},
|
|
9
|
+
"title": {
|
|
10
|
+
"comment": "제목",
|
|
11
|
+
"required": true
|
|
12
|
+
},
|
|
13
|
+
"category": {
|
|
14
|
+
"comment": "카테고리",
|
|
15
|
+
"type": ["notice", "blog", "faq", "news"],
|
|
16
|
+
"default": "blog"
|
|
17
|
+
},
|
|
18
|
+
"status": {
|
|
19
|
+
"comment": "게시 상태",
|
|
20
|
+
"type": ["draft", "published", "archived"],
|
|
21
|
+
"default": "draft"
|
|
22
|
+
},
|
|
23
|
+
"published_at": {
|
|
24
|
+
"comment": "게시일시 (*_at → DATETIME 자동 추론)"
|
|
25
|
+
},
|
|
26
|
+
"view_count": {
|
|
27
|
+
"comment": "조회 수 (*_count → INT 자동 추론)"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"types": {
|
|
31
|
+
"content": "text",
|
|
32
|
+
"summary": "text",
|
|
33
|
+
"tags_json": "text"
|
|
34
|
+
},
|
|
35
|
+
"comments": {
|
|
36
|
+
"content": "본문 (마크다운)",
|
|
37
|
+
"summary": "요약 (목록 표시용)",
|
|
38
|
+
"tags_json": "태그 배열 JSON"
|
|
39
|
+
},
|
|
40
|
+
"history_ttl": 1095,
|
|
41
|
+
"hard_delete": false
|
|
42
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "workspace",
|
|
3
|
+
"description": "license_scope 예제 — 멀티테넌트에서 라이선스별 데이터 완전 분리",
|
|
4
|
+
"license_scope": true,
|
|
5
|
+
"index": {
|
|
6
|
+
"name": {
|
|
7
|
+
"comment": "워크스페이스명",
|
|
8
|
+
"required": true
|
|
9
|
+
},
|
|
10
|
+
"plan": {
|
|
11
|
+
"comment": "플랜",
|
|
12
|
+
"type": ["free", "starter", "pro", "enterprise"],
|
|
13
|
+
"default": "free"
|
|
14
|
+
},
|
|
15
|
+
"max_members": {
|
|
16
|
+
"comment": "최대 팀원 수 (자동추론: 직접 uint 선언)"
|
|
17
|
+
},
|
|
18
|
+
"owner_seq": {
|
|
19
|
+
"comment": "소유자 user seq"
|
|
20
|
+
},
|
|
21
|
+
"is_active": {
|
|
22
|
+
"comment": "활성 여부 (is_* → TINYINT(1) 자동 추론)",
|
|
23
|
+
"default": true
|
|
24
|
+
},
|
|
25
|
+
"expires_at": {
|
|
26
|
+
"comment": "만료일시 (*_at → DATETIME 자동 추론)"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"types": {
|
|
30
|
+
"max_members": "uint",
|
|
31
|
+
"settings_json": "text"
|
|
32
|
+
},
|
|
33
|
+
"comments": {
|
|
34
|
+
"settings_json": "워크스페이스 설정 JSON"
|
|
35
|
+
},
|
|
36
|
+
"defaults": {
|
|
37
|
+
"max_members": 5
|
|
38
|
+
},
|
|
39
|
+
"cache": {
|
|
40
|
+
"enabled": true,
|
|
41
|
+
"ttl_seconds": 120
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "user_point",
|
|
3
|
+
"description": "SQL 훅 예제 — INSERT/UPDATE 시 자동 감사 로그 기록 (실행형) + SELECT 결과 주입 (조회형)",
|
|
4
|
+
"index": {
|
|
5
|
+
"user_seq": {
|
|
6
|
+
"comment": "사용자 seq",
|
|
7
|
+
"required": true,
|
|
8
|
+
"unique": true
|
|
9
|
+
},
|
|
10
|
+
"point": {
|
|
11
|
+
"comment": "보유 포인트",
|
|
12
|
+
"type": "uint"
|
|
13
|
+
},
|
|
14
|
+
"grade": {
|
|
15
|
+
"comment": "등급",
|
|
16
|
+
"type": ["bronze", "silver", "gold", "platinum"],
|
|
17
|
+
"default": "bronze"
|
|
18
|
+
},
|
|
19
|
+
"updated_at": {
|
|
20
|
+
"comment": "마지막 포인트 변경일시 (*_at → DATETIME 자동 추론)"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"hooks": {
|
|
24
|
+
"after_insert": [
|
|
25
|
+
{
|
|
26
|
+
"comment": "포인트 생성 이력 기록 (실행형 SQL 훅)",
|
|
27
|
+
"type": "sql",
|
|
28
|
+
"query": "INSERT INTO point_history (user_seq, delta, reason, created_time) VALUES (?, ?, ?, NOW())",
|
|
29
|
+
"params": ["${new.user_seq}", "${new.point}", "initial_grant"],
|
|
30
|
+
"async": false
|
|
31
|
+
}
|
|
32
|
+
],
|
|
33
|
+
"after_update": [
|
|
34
|
+
{
|
|
35
|
+
"comment": "포인트 변경 이력 기록 (실행형 SQL 훅)",
|
|
36
|
+
"type": "sql",
|
|
37
|
+
"query": "INSERT INTO point_history (user_seq, delta, reason, created_time) VALUES (?, ?, ?, NOW())",
|
|
38
|
+
"params": ["${new.user_seq}", "${new.point}", "update"],
|
|
39
|
+
"async": true
|
|
40
|
+
}
|
|
41
|
+
],
|
|
42
|
+
"after_get": [
|
|
43
|
+
{
|
|
44
|
+
"comment": "최근 포인트 변경 이력 5건 주입 (조회형 SQL 훅)",
|
|
45
|
+
"type": "sql",
|
|
46
|
+
"query": "SELECT delta, reason, created_time FROM point_history WHERE user_seq = ? ORDER BY created_time DESC LIMIT 5",
|
|
47
|
+
"params": ["${new.user_seq}"],
|
|
48
|
+
"assign_to": "recent_history"
|
|
49
|
+
}
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "post",
|
|
3
|
+
"description": "entity 훅 예제 — after_get/after_list 시 관련 엔티티 데이터를 자동 주입",
|
|
4
|
+
"index": {
|
|
5
|
+
"author_seq": {
|
|
6
|
+
"comment": "작성자 user seq",
|
|
7
|
+
"required": true
|
|
8
|
+
},
|
|
9
|
+
"title": {
|
|
10
|
+
"comment": "제목",
|
|
11
|
+
"required": true
|
|
12
|
+
},
|
|
13
|
+
"status": {
|
|
14
|
+
"comment": "상태",
|
|
15
|
+
"type": ["draft", "published", "hidden"],
|
|
16
|
+
"default": "draft"
|
|
17
|
+
},
|
|
18
|
+
"comment_count": {
|
|
19
|
+
"comment": "댓글 수 (*_count → INT 자동 추론)"
|
|
20
|
+
},
|
|
21
|
+
"like_count": {
|
|
22
|
+
"comment": "좋아요 수 (*_count → INT 자동 추론)"
|
|
23
|
+
},
|
|
24
|
+
"published_at": {
|
|
25
|
+
"comment": "게시일시 (*_at → DATETIME 자동 추론)"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"types": {
|
|
29
|
+
"content": "text"
|
|
30
|
+
},
|
|
31
|
+
"comments": {
|
|
32
|
+
"content": "본문"
|
|
33
|
+
},
|
|
34
|
+
"hooks": {
|
|
35
|
+
"after_get": [
|
|
36
|
+
{
|
|
37
|
+
"comment": "게시글 조회 시 작성자 프로필 자동 주입",
|
|
38
|
+
"type": "entity",
|
|
39
|
+
"entity": "user",
|
|
40
|
+
"action": "get",
|
|
41
|
+
"conditions": {
|
|
42
|
+
"seq": "${new.author_seq}"
|
|
43
|
+
},
|
|
44
|
+
"assign_to": "author"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"comment": "게시글 조회 시 최근 댓글 10개 자동 주입",
|
|
48
|
+
"type": "entity",
|
|
49
|
+
"entity": "comment",
|
|
50
|
+
"action": "list",
|
|
51
|
+
"conditions": {
|
|
52
|
+
"post_seq": "${new.seq}",
|
|
53
|
+
"status": "visible"
|
|
54
|
+
},
|
|
55
|
+
"assign_to": "comments"
|
|
56
|
+
}
|
|
57
|
+
],
|
|
58
|
+
"after_list": [
|
|
59
|
+
{
|
|
60
|
+
"comment": "목록 조회 시 각 게시글에 작성자 이름 주입",
|
|
61
|
+
"type": "entity",
|
|
62
|
+
"entity": "user",
|
|
63
|
+
"action": "get",
|
|
64
|
+
"conditions": {
|
|
65
|
+
"seq": "${new.author_seq}"
|
|
66
|
+
},
|
|
67
|
+
"assign_to": "author"
|
|
68
|
+
}
|
|
69
|
+
]
|
|
70
|
+
}
|
|
71
|
+
}
|