@hamjimin/xplat-back 0.6.3 → 0.7.0

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/xplat-back CHANGED
@@ -1,38 +1,52 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // Directly invoke initializeProject to ensure CLI runs when called from the bin entry.
4
- const { initializeProject } = require('../dist/cli/init.js');
5
-
6
3
  const args = process.argv.slice(2);
7
- const config = {};
4
+ const subCommand = args[0];
5
+
6
+ if (subCommand === 'db:setup') {
7
+ const { setupDatabase } = require('../dist/cli/db-setup.js');
8
+ setupDatabase().catch((error) => {
9
+ console.error('❌ DB 설정 중 오류가 발생했습니다:', error.message);
10
+ if (process.env.DEBUG) {
11
+ console.error(error);
12
+ }
13
+ process.exit(1);
14
+ });
15
+ } else {
16
+ const { initializeProject } = require('../dist/cli/init.js');
8
17
 
9
- // Parse CLI options
10
- for (let i = 0; i < args.length; i++) {
11
- const arg = args[i];
18
+ const config = {};
12
19
 
13
- switch (arg) {
14
- case '--port':
15
- case '-p':
16
- config.port = args[++i];
17
- break;
18
- case '--cors':
19
- case '-c':
20
- config.corsOrigins = args[++i];
21
- break;
22
- case '--router-dir':
23
- case '-r':
24
- config.routerDirectory = args[++i];
25
- break;
26
- case '--router-path':
27
- config.routerBasePath = args[++i];
28
- break;
29
- case '--help':
30
- case '-h':
31
- console.log(`
20
+ for (let i = 0; i < args.length; i++) {
21
+ const arg = args[i];
22
+
23
+ switch (arg) {
24
+ case '--port':
25
+ case '-p':
26
+ config.port = args[++i];
27
+ break;
28
+ case '--cors':
29
+ case '-c':
30
+ config.corsOrigins = args[++i];
31
+ break;
32
+ case '--router-dir':
33
+ case '-r':
34
+ config.routerDirectory = args[++i];
35
+ break;
36
+ case '--router-path':
37
+ config.routerBasePath = args[++i];
38
+ break;
39
+ case '--help':
40
+ case '-h':
41
+ console.log(`
32
42
  xplat-back CLI
33
43
 
34
44
  Usage:
35
45
  xplat-back [options] [project-name]
46
+ xplat-back db:setup MySQL DDL 설치 (인터랙티브)
47
+
48
+ Commands:
49
+ db:setup 데이터베이스 테이블 생성 (MySQL 접속 정보 입력 후 DDL 실행)
36
50
 
37
51
  Options:
38
52
  -p, --port <port> 서버 포트 (기본값: 4000)
@@ -44,21 +58,23 @@ Options:
44
58
  Examples:
45
59
  xplat-back
46
60
  xplat-back my-project
61
+ xplat-back db:setup
47
62
  xplat-back --port 3000 --cors "http://localhost:3000,http://localhost:5173"
48
- `);
49
- process.exit(0);
50
- break;
51
- default:
52
- if (!arg.startsWith('-')) {
53
- config.projectName = arg;
54
- }
63
+ `);
64
+ process.exit(0);
65
+ break;
66
+ default:
67
+ if (!arg.startsWith('-')) {
68
+ config.projectName = arg;
69
+ }
70
+ }
55
71
  }
56
- }
57
72
 
58
- initializeProject(config).catch((error) => {
59
- console.error('❌ 초기화 중 오류가 발생했습니다:', error.message);
60
- if (process.env.DEBUG) {
61
- console.error(error);
62
- }
63
- process.exit(1);
64
- });
73
+ initializeProject(config).catch((error) => {
74
+ console.error('❌ 초기화 중 오류가 발생했습니다:', error.message);
75
+ if (process.env.DEBUG) {
76
+ console.error(error);
77
+ }
78
+ process.exit(1);
79
+ });
80
+ }
@@ -0,0 +1 @@
1
+ export declare function setupDatabase(): Promise<void>;
@@ -0,0 +1,329 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.setupDatabase = setupDatabase;
40
+ const fs = __importStar(require("fs"));
41
+ const path = __importStar(require("path"));
42
+ const inquirer_1 = __importDefault(require("inquirer"));
43
+ const mysql = __importStar(require("mysql2/promise"));
44
+ const DDL_MODULES = [
45
+ {
46
+ file: '01_user.sql',
47
+ name: '사용자 (User / Employee / Partner)',
48
+ tables: ['user', 'employee', 'partner'],
49
+ deps: [],
50
+ },
51
+ {
52
+ file: '02_product.sql',
53
+ name: '상품 (Category / Product / Variants / Brand / Side_Product)',
54
+ tables: ['category', 'product', 'variants', 'brand', 'side_product'],
55
+ deps: ['01_user.sql'],
56
+ },
57
+ {
58
+ file: '03_package.sql',
59
+ name: '패키지 (Product_Package / Detail / Variant)',
60
+ tables: ['product_package', 'product_package_detail', 'product_package_variant'],
61
+ deps: ['02_product.sql'],
62
+ },
63
+ {
64
+ file: '04_order.sql',
65
+ name: '주문 (Orders / Order_Items / Order_Manage)',
66
+ tables: ['orders', 'order_items', 'order_manage', 'order_manage_comment'],
67
+ deps: ['01_user.sql'],
68
+ },
69
+ {
70
+ file: '05_payment.sql',
71
+ name: '결제 (Payment / Calcurate)',
72
+ tables: ['payment', 'calcurate'],
73
+ deps: ['01_user.sql', '04_order.sql'],
74
+ },
75
+ {
76
+ file: '06_delivery.sql',
77
+ name: '배송 (Delivery / Reservation)',
78
+ tables: ['delivery', 'reservation'],
79
+ deps: ['04_order.sql'],
80
+ },
81
+ {
82
+ file: '07_cart.sql',
83
+ name: '장바구니 (Cart / Cart_Items)',
84
+ tables: ['cart', 'cart_items'],
85
+ deps: ['01_user.sql', '02_product.sql'],
86
+ },
87
+ {
88
+ file: '08_campaign.sql',
89
+ name: '캠페인 (Campaign / Promotion / Event)',
90
+ tables: ['campaign', 'promotion_detail', 'event_detail'],
91
+ deps: ['03_package.sql'],
92
+ },
93
+ {
94
+ file: '09_review.sql',
95
+ name: '리뷰 (Product_Review / Package_Review)',
96
+ tables: ['product_review', 'review_images', 'package_product_review', 'package_review_images'],
97
+ deps: ['01_user.sql', '02_product.sql', '03_package.sql'],
98
+ },
99
+ {
100
+ file: '10_chain.sql',
101
+ name: '연결 테이블 (Chain_*)',
102
+ tables: [
103
+ 'chain_category_detail', 'chain_product_detail', 'chain_variant_detail',
104
+ 'chain_product_brand', 'chain_order_delivery', 'chain_order_cart',
105
+ 'chain_order_reservation', 'chain_campaign_package',
106
+ ],
107
+ deps: [
108
+ '01_user.sql', '02_product.sql', '03_package.sql',
109
+ '04_order.sql', '06_delivery.sql', '07_cart.sql', '08_campaign.sql',
110
+ ],
111
+ },
112
+ ];
113
+ function resolveDependencies(selectedFiles) {
114
+ const resolved = new Set();
115
+ function addWithDeps(file) {
116
+ if (resolved.has(file))
117
+ return;
118
+ const mod = DDL_MODULES.find(m => m.file === file);
119
+ if (!mod)
120
+ return;
121
+ for (const dep of mod.deps) {
122
+ addWithDeps(dep);
123
+ }
124
+ resolved.add(file);
125
+ }
126
+ for (const file of selectedFiles) {
127
+ addWithDeps(file);
128
+ }
129
+ return DDL_MODULES
130
+ .filter(m => resolved.has(m.file))
131
+ .map(m => m.file);
132
+ }
133
+ function getDdlDirectory() {
134
+ const candidates = [
135
+ path.join(__dirname, '..', '..', 'templates', 'ddl'),
136
+ path.join(__dirname, '..', 'templates', 'ddl'),
137
+ path.join(__dirname, '..', '..', '..', 'templates', 'ddl'),
138
+ ];
139
+ for (const dir of candidates) {
140
+ if (fs.existsSync(dir))
141
+ return dir;
142
+ }
143
+ const packageRoot = path.resolve(__dirname);
144
+ let current = packageRoot;
145
+ for (let i = 0; i < 5; i++) {
146
+ const ddlDir = path.join(current, 'templates', 'ddl');
147
+ if (fs.existsSync(ddlDir))
148
+ return ddlDir;
149
+ current = path.dirname(current);
150
+ }
151
+ throw new Error('templates/ddl 디렉토리를 찾을 수 없습니다.');
152
+ }
153
+ async function setupDatabase() {
154
+ console.log('\n🗄️ xplat-back 데이터베이스 설정\n');
155
+ console.log('─'.repeat(50));
156
+ // Step 1: MySQL 접속 정보
157
+ const { dbConfig } = await inquirer_1.default.prompt([
158
+ {
159
+ type: 'input',
160
+ name: 'dbConfig.host',
161
+ message: 'DB Host:',
162
+ default: 'localhost',
163
+ },
164
+ {
165
+ type: 'input',
166
+ name: 'dbConfig.user',
167
+ message: 'DB User:',
168
+ default: 'root',
169
+ },
170
+ {
171
+ type: 'password',
172
+ name: 'dbConfig.password',
173
+ message: 'DB Password:',
174
+ mask: '*',
175
+ },
176
+ {
177
+ type: 'input',
178
+ name: 'dbConfig.database',
179
+ message: 'DB Name:',
180
+ validate: (input) => input.trim() ? true : 'DB 이름을 입력해주세요.',
181
+ },
182
+ {
183
+ type: 'number',
184
+ name: 'dbConfig.port',
185
+ message: 'DB Port:',
186
+ default: 3306,
187
+ },
188
+ ]);
189
+ // Step 2: 연결 테스트
190
+ console.log('\n⏳ MySQL 연결 테스트 중...');
191
+ let connection;
192
+ try {
193
+ connection = await mysql.createConnection({
194
+ host: dbConfig.host,
195
+ user: dbConfig.user,
196
+ password: dbConfig.password,
197
+ port: dbConfig.port,
198
+ multipleStatements: true,
199
+ });
200
+ }
201
+ catch (err) {
202
+ console.error(`\n❌ MySQL 연결 실패: ${err.message}`);
203
+ console.error(' 접속 정보를 확인하고 다시 시도해주세요.\n');
204
+ process.exit(1);
205
+ }
206
+ try {
207
+ await connection.query(`CREATE DATABASE IF NOT EXISTS \`${dbConfig.database}\``);
208
+ await connection.query(`USE \`${dbConfig.database}\``);
209
+ console.log('✅ MySQL 연결 성공!\n');
210
+ }
211
+ catch (err) {
212
+ console.error(`\n❌ 데이터베이스 설정 실패: ${err.message}\n`);
213
+ await connection.end();
214
+ process.exit(1);
215
+ }
216
+ // Step 3: 기존 테이블 확인
217
+ let existingTables = [];
218
+ try {
219
+ const [rows] = await connection.query('SHOW TABLES');
220
+ existingTables = rows.map((row) => Object.values(row)[0]);
221
+ }
222
+ catch {
223
+ // ignore
224
+ }
225
+ // Step 4: DDL 모듈 선택 (체크박스)
226
+ console.log('─'.repeat(50));
227
+ const choices = DDL_MODULES.map(mod => {
228
+ const existCount = mod.tables.filter(t => existingTables.includes(t)).length;
229
+ const statusTag = existCount === mod.tables.length
230
+ ? ' (✅ 이미 설치됨)'
231
+ : existCount > 0
232
+ ? ` (⚠️ 일부 ${existCount}/${mod.tables.length} 존재)`
233
+ : '';
234
+ return {
235
+ name: `${mod.name}${statusTag}\n 테이블: ${mod.tables.join(', ')}`,
236
+ value: mod.file,
237
+ short: mod.name,
238
+ };
239
+ });
240
+ const { selectedModules } = await inquirer_1.default.prompt([
241
+ {
242
+ type: 'checkbox',
243
+ name: 'selectedModules',
244
+ message: '설치할 DDL 모듈을 선택하세요 (스페이스바로 선택):',
245
+ choices,
246
+ pageSize: 20,
247
+ },
248
+ ]);
249
+ if (selectedModules.length === 0) {
250
+ console.log('\n⚠️ 선택된 모듈이 없습니다. 종료합니다.\n');
251
+ await connection.end();
252
+ return;
253
+ }
254
+ // Step 5: 의존성 해결
255
+ const resolvedFiles = resolveDependencies(selectedModules);
256
+ const addedDeps = resolvedFiles.filter(f => !selectedModules.includes(f));
257
+ if (addedDeps.length > 0) {
258
+ const depNames = addedDeps.map(f => {
259
+ const mod = DDL_MODULES.find(m => m.file === f);
260
+ return mod ? ` → ${mod.name} (${f})` : ` → ${f}`;
261
+ });
262
+ console.log('\n📎 의존성으로 자동 추가된 모듈:');
263
+ depNames.forEach(n => console.log(n));
264
+ }
265
+ // Step 6: 실행할 테이블 목록 표시 및 최종 확인
266
+ console.log('\n─'.repeat(50));
267
+ console.log(`\n📋 실행 대상 DDL (${resolvedFiles.length}개 파일):\n`);
268
+ for (const file of resolvedFiles) {
269
+ const mod = DDL_MODULES.find(m => m.file === file);
270
+ const icon = selectedModules.includes(file) ? '📦' : '🔗';
271
+ console.log(` ${icon} ${file} - ${mod.name}`);
272
+ }
273
+ const { confirmExec } = await inquirer_1.default.prompt([
274
+ {
275
+ type: 'confirm',
276
+ name: 'confirmExec',
277
+ message: `\n위 ${resolvedFiles.length}개 DDL 파일을 실행하시겠습니까?`,
278
+ default: false,
279
+ },
280
+ ]);
281
+ if (!confirmExec) {
282
+ console.log('\n❌ 취소되었습니다.\n');
283
+ await connection.end();
284
+ return;
285
+ }
286
+ // Step 7: DDL 순차 실행
287
+ const ddlDir = getDdlDirectory();
288
+ console.log('\n⏳ DDL 실행 중...\n');
289
+ let successCount = 0;
290
+ let skipCount = 0;
291
+ const errors = [];
292
+ for (const file of resolvedFiles) {
293
+ const filePath = path.join(ddlDir, file);
294
+ if (!fs.existsSync(filePath)) {
295
+ console.log(` ⚠️ ${file} 파일을 찾을 수 없습니다. 건너뜁니다.`);
296
+ skipCount++;
297
+ continue;
298
+ }
299
+ const sql = fs.readFileSync(filePath, 'utf-8');
300
+ try {
301
+ await connection.query(sql);
302
+ const mod = DDL_MODULES.find(m => m.file === file);
303
+ console.log(` ✅ ${file} → ${mod.tables.join(', ')}`);
304
+ successCount++;
305
+ }
306
+ catch (err) {
307
+ if (err.code === 'ER_TABLE_EXISTS_ERROR') {
308
+ console.log(` ⚠️ ${file} → 이미 존재하는 테이블이 있어 건너뜁니다.`);
309
+ skipCount++;
310
+ }
311
+ else {
312
+ console.log(` ❌ ${file} → ${err.message}`);
313
+ errors.push({ file, error: err.message });
314
+ }
315
+ }
316
+ }
317
+ await connection.end();
318
+ // Step 8: 결과 요약
319
+ console.log('\n' + '─'.repeat(50));
320
+ console.log('\n📊 실행 결과:\n');
321
+ console.log(` ✅ 성공: ${successCount}개`);
322
+ if (skipCount > 0)
323
+ console.log(` ⚠️ 건너뜀: ${skipCount}개`);
324
+ if (errors.length > 0) {
325
+ console.log(` ❌ 실패: ${errors.length}개`);
326
+ errors.forEach(e => console.log(` - ${e.file}: ${e.error}`));
327
+ }
328
+ console.log('\n🎉 데이터베이스 설정이 완료되었습니다!\n');
329
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hamjimin/xplat-back",
3
- "version": "0.6.3",
3
+ "version": "0.7.0",
4
4
  "description": "Express + TypeScript 백엔드 스캐폴딩 도구 (zium-backend 기반)",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -35,33 +35,36 @@
35
35
  "author": "xplat",
36
36
  "license": "ISC",
37
37
  "dependencies": {
38
+ "@aws-sdk/client-s3": "^3.922.0",
39
+ "@aws-sdk/s3-request-presigner": "^3.922.0",
38
40
  "@nestjs/common": "^10.4.15",
39
41
  "@nestjs/core": "^10.4.15",
40
42
  "@nestjs/platform-express": "^10.4.15",
43
+ "bcrypt": "^5.1.1",
41
44
  "class-transformer": "^0.5.1",
42
45
  "class-validator": "^0.14.1",
43
46
  "cookie-parser": "^1.4.6",
44
47
  "cors": "^2.8.5",
48
+ "crypto-js": "^4.2.0",
45
49
  "express": "^4.18.2",
50
+ "inquirer": "^8.2.7",
46
51
  "jsonwebtoken": "^9.0.2",
47
- "bcrypt": "^5.1.1",
48
- "crypto-js": "^4.2.0",
49
52
  "lodash": "^4.17.21",
53
+ "mysql2": "^3.17.2",
50
54
  "reflect-metadata": "^0.2.2",
51
55
  "rxjs": "^7.8.1",
52
- "uuid": "^9.0.1",
53
- "@aws-sdk/client-s3": "^3.922.0",
54
- "@aws-sdk/s3-request-presigner": "^3.922.0"
56
+ "uuid": "^9.0.1"
55
57
  },
56
58
  "devDependencies": {
57
- "@types/express": "^4.17.21",
58
- "@types/node": "^20.10.0",
59
+ "@types/bcrypt": "^5.0.2",
59
60
  "@types/cookie-parser": "^1.4.6",
60
61
  "@types/cors": "^2.8.17",
61
- "@types/jsonwebtoken": "^9.0.5",
62
- "@types/bcrypt": "^5.0.2",
63
62
  "@types/crypto-js": "^4.2.2",
63
+ "@types/express": "^4.17.21",
64
+ "@types/inquirer": "^8.2.12",
65
+ "@types/jsonwebtoken": "^9.0.5",
64
66
  "@types/lodash": "^4.14.202",
67
+ "@types/node": "^20.10.0",
65
68
  "@types/uuid": "^9.0.8",
66
69
  "typescript": "^5.3.3"
67
70
  }
@@ -0,0 +1,133 @@
1
+ # E-Commerce Platform ERD
2
+
3
+ > 36 tables / MySQL / DDL: `templates/ddl/`
4
+
5
+ ## Table Overview
6
+
7
+ ### User (01_user.sql)
8
+ | Table | PK | Columns |
9
+ |-------|----|----|
10
+ | user | id | name, phone |
11
+ | employee | id | name, phone |
12
+ | partner | id | name, phone |
13
+
14
+ ### Product (02_product.sql)
15
+ | Table | PK | FK | Columns |
16
+ |-------|----|----|---------|
17
+ | category | id | - | name |
18
+ | product | id | category_id | name, description, price, status, type, count, discount_start_date, discount_end_date, discount_price, discount_percent, delivery_price, delivery_type, delivery_remaining_price |
19
+ | variants | id | product_id | name, description, price, type, count, delivery_price, delivery_remaining_price, delivery_type |
20
+ | brand | id | employee_id | name, description |
21
+ | side_product | id | product_id | name, discount_percent, discount_price, price, count, type |
22
+
23
+ ### Package (03_package.sql)
24
+ | Table | PK | FK | Columns |
25
+ |-------|----|----|---------|
26
+ | product_package | id | order_id | status, campaign_type, payment_type, discount_percent, discount_price, point, payment_price, delivery_price, delivery_type, delivery_remaining_price |
27
+ | product_package_detail | id | product_package_id, product_id | name, price, type, count, payment_price, point, discount_price, discount_percent, delivery_price, delivery_type, delivery_remaining_price |
28
+ | product_package_variant | id | product_package_detail_id, variant_id, product_package_id | name, price, type, count, payment_price, point, discount_price, discount_percent, delivery_price, delivery_type, delivery_remaining_price |
29
+
30
+ ### Order (04_order.sql)
31
+ | Table | PK | FK | Columns |
32
+ |-------|----|----|---------|
33
+ | orders | id | user_id | status |
34
+ | order_items | id | order_id | product_id, variant_id, description, price, campaign_yn, campaign_id, type, count |
35
+ | order_manage | id | order_id, user_id, order_item_id | delivery_status, card_info(JSON), price, payment_type, payment_date |
36
+ | order_manage_comment | id | order_manage_id, partner_id, employee_id | context |
37
+
38
+ ### Payment (05_payment.sql)
39
+ | Table | PK | FK | Columns |
40
+ |-------|----|----|---------|
41
+ | payment | id | order_id | payment_type, card_info(JSON), price, refund_info(JSON), payment_date |
42
+ | calcurate | id | partner_id, order_id, payment_id | calcurate_price, calcurate_date |
43
+
44
+ ### Delivery (06_delivery.sql)
45
+ | Table | PK | FK | Columns |
46
+ |-------|----|----|---------|
47
+ | delivery | id | - | status, delivery_number, company, location, address, address_detail, zip_code, price |
48
+ | reservation | id | order_item_id | status, date_time |
49
+
50
+ ### Cart (07_cart.sql)
51
+ | Table | PK | FK | Columns |
52
+ |-------|----|----|---------|
53
+ | cart | id | user_id | status |
54
+ | cart_items | id | cart_id, product_id, variant_id | count |
55
+
56
+ ### Campaign (08_campaign.sql)
57
+ | Table | PK | FK | Columns |
58
+ |-------|----|----|---------|
59
+ | campaign | id | - | title, description, start_date, end_date, type, status, condition |
60
+ | promotion_detail | id | campaign_id, package_id | status, discount_price, discount_percent |
61
+ | event_detail | id | campaign_id, package_id | status, discount_price, discount_percent |
62
+
63
+ ### Review (09_review.sql)
64
+ | Table | PK | FK | Columns |
65
+ |-------|----|----|---------|
66
+ | product_review | id | product_id, user_id | status, user_pwd, context, image_id |
67
+ | review_images | id | review_id | file_name, file_size, file_url, file_ext |
68
+ | package_product_review | id | package_id, user_id | status, user_pwd, context, image_id, score |
69
+ | package_review_images | id | review_id | file_name, file_size, file_url, file_ext |
70
+
71
+ ### Chain (10_chain.sql)
72
+ | Table | FK1 | FK2 |
73
+ |-------|-----|-----|
74
+ | chain_category_detail | category_id | detail_id |
75
+ | chain_product_detail | product_id | detail_id |
76
+ | chain_variant_detail | variant_id | detail_id |
77
+ | chain_product_brand | product_id | brand_id |
78
+ | chain_order_delivery | order_id, order_item_id | delivery_id |
79
+ | chain_order_cart | order_id | cart_id |
80
+ | chain_order_reservation | order_id | reservation_id |
81
+ | chain_campaign_package | campaign_id | package_id |
82
+
83
+ ## Relationship Diagram (Text)
84
+
85
+ ```
86
+ user ──1:N──> orders
87
+ user ──1:N──> cart
88
+ user ──1:N──> product_review
89
+ user ──1:N──> package_product_review
90
+
91
+ category ──1:N──> product
92
+ product ──1:N──> variants
93
+ product ──1:N──> side_product
94
+ product ──1:N──> product_review
95
+
96
+ product_package ──1:N──> product_package_detail ──1:N──> product_package_variant
97
+
98
+ orders ──1:N──> order_items
99
+ orders ──1:N──> payment
100
+ orders ──1:N──> order_manage ──1:N──> order_manage_comment
101
+ orders ──1:N──> calcurate
102
+
103
+ order_items ──1:N──> reservation
104
+
105
+ cart ──1:N──> cart_items
106
+
107
+ campaign ──1:N──> promotion_detail
108
+ campaign ──1:N──> event_detail
109
+
110
+ brand ──1:N──> chain_product_brand <──N:1── product
111
+
112
+ -- Chain tables (M:N resolvers)
113
+ orders <──> delivery via chain_order_delivery
114
+ orders <──> cart via chain_order_cart
115
+ orders <──> reservation via chain_order_reservation
116
+ campaign <──> product_package via chain_campaign_package
117
+ category/product/variants <──> detail via chain_*_detail
118
+ ```
119
+
120
+ ## DDL Execution Order
121
+
122
+ ```
123
+ 01_user.sql -- user, employee, partner (no deps)
124
+ 02_product.sql -- category, product, variants, brand, side_product
125
+ 03_package.sql -- product_package, product_package_detail, product_package_variant
126
+ 04_order.sql -- orders, order_items, order_manage, order_manage_comment
127
+ 05_payment.sql -- payment, calcurate
128
+ 06_delivery.sql -- delivery, reservation
129
+ 07_cart.sql -- cart, cart_items
130
+ 08_campaign.sql -- campaign, promotion_detail, event_detail
131
+ 09_review.sql -- product_review, review_images, package_product_review, package_review_images
132
+ 10_chain.sql -- all chain/junction tables
133
+ ```
@@ -0,0 +1,30 @@
1
+ -- =============================================
2
+ -- User / Employee / Partner
3
+ -- =============================================
4
+
5
+ CREATE TABLE IF NOT EXISTS `user` (
6
+ `id` VARCHAR(255) NOT NULL,
7
+ `name` VARCHAR(100) NOT NULL,
8
+ `phone` VARCHAR(20),
9
+ `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
10
+ `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
11
+ PRIMARY KEY (`id`)
12
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
13
+
14
+ CREATE TABLE IF NOT EXISTS `employee` (
15
+ `id` VARCHAR(255) NOT NULL,
16
+ `name` VARCHAR(100) NOT NULL,
17
+ `phone` VARCHAR(20),
18
+ `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
19
+ `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
20
+ PRIMARY KEY (`id`)
21
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
22
+
23
+ CREATE TABLE IF NOT EXISTS `partner` (
24
+ `id` VARCHAR(255) NOT NULL,
25
+ `name` VARCHAR(100),
26
+ `phone` VARCHAR(20),
27
+ `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
28
+ `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
29
+ PRIMARY KEY (`id`)
30
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;