create-express-esm 1.1.12 β†’ 1.2.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/README.md CHANGED
@@ -1,10 +1,10 @@
1
1
  # πŸš€ Create Express ESM (CLI)
2
2
 
3
- > **Modern Express Generator**
3
+ > **Modern Express Generator with Database & Error Handling**
4
4
  >
5
- > "1초 λ§Œμ— μ™„μ„±ν•˜λŠ” Modern Express(ESM) ν™˜κ²½"
5
+ > "1초 λ§Œμ— μ™„μ„±ν•˜λŠ” Modern Express(ESM) + Prisma + Docker ν™˜κ²½"
6
6
  >
7
- > CommonJS(require)κ°€ μ•„λ‹Œ, μ΅œμ‹  ES Modules(import/export) 문법을 기반으둜 ν•˜λŠ” Express ν”„λ‘œμ νŠΈ ꡬ쑰λ₯Ό μžλ™μœΌλ‘œ μƒμ„±ν•΄μ£ΌλŠ” CLI λ„κ΅¬μž…λ‹ˆλ‹€.
7
+ > CommonJS(require)κ°€ μ•„λ‹Œ, μ΅œμ‹  ES Modules(import/export) 문법을 기반으둜 ν•˜λ©°, λ°μ΄ν„°λ² μ΄μŠ€ 연동 및 전문적인 μ—λŸ¬ ν•Έλ“€λ§κΉŒμ§€ ν¬ν•¨λœ Express ν”„λ‘œμ νŠΈ ꡬ쑰λ₯Ό μžλ™μœΌλ‘œ μƒμ„±ν•΄μ£ΌλŠ” CLI λ„κ΅¬μž…λ‹ˆλ‹€.
8
8
 
9
9
  [![npm version](https://img.shields.io/npm/v/create-express-esm.svg)](https://www.npmjs.com/package/create-express-esm)
10
10
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
@@ -15,12 +15,14 @@
15
15
 
16
16
  ## ✨ Key Features (핡심 κΈ°λŠ₯)
17
17
 
18
- κΈ°μ‘΄ `express-generator`의 ν•œκ³„λ₯Ό λΆ„μ„ν•˜κ³  κ°œμ„ ν•˜μ—¬ κ°œλ°œν–ˆμŠ΅λ‹ˆλ‹€.
18
+ κΈ°μ‘΄ `express-generator`의 ν•œκ³„λ₯Ό λΆ„μ„ν•˜κ³  κ°œμ„ ν•˜μ—¬ κ°œλ°œν–ˆμŠ΅λ‹ˆλ‹€. v1.2.0 μ—…λ°μ΄νŠΈλ₯Ό 톡해 μ‹€λ¬΄ν˜• ν’€μŠ€νƒ 베이슀λ₯Ό μ œκ³΅ν•©λ‹ˆλ‹€.
19
19
 
20
20
  - **🌐 Multi-Language Support**: JavaScript(ESM)와 **TypeScript** 쀑 μ›ν•˜λŠ” 개발 ν™˜κ²½μ„ 선택할 수 μžˆμŠ΅λ‹ˆλ‹€.
21
+ - **πŸ—„οΈ Database Integration**: **Prisma ORM**κ³Ό **PostgreSQL** ν™˜κ²½μ„ μ¦‰μ‹œ κ΅¬μΆ•ν•©λ‹ˆλ‹€. (Docker Compose μžλ™ 생성)
22
+ - **🚨 Professional Error Handling**: `AppError` ν΄λž˜μŠ€μ™€ **Global Error Middleware**λ₯Ό ν†΅ν•œ 쀑앙 집쀑식 μ—λŸ¬ 처리 μ‹œμŠ€ν…œμ„ μ œκ³΅ν•©λ‹ˆλ‹€.
21
23
  - **πŸ§ͺ Integrated Testing**: μ΅œμ‹  ν…ŒμŠ€νŠΈ ν”„λ ˆμž„μ›Œν¬μΈ **Vitest**와 API ν…ŒμŠ€νŠΈμš© **Supertest** ν™˜κ²½μ„ μžλ™ μ„€μ •ν•©λ‹ˆλ‹€.
22
- - **πŸ— Layered Architecture**: 싀무 ν‘œμ€€μΈ `Controller` - `Service` - `Route` 계측 ꡬ쑰λ₯Ό μ œκ³΅ν•©λ‹ˆλ‹€.
23
- - **⚑️ Modern TS Execution**: `ts-node`의 ESM ν˜Έν™˜μ„± 문제λ₯Ό ν•΄κ²°ν•œ **`tsx`**λ₯Ό μ±„νƒν•˜μ—¬ μΎŒμ ν•œ 개발 ν™˜κ²½μ„ μ œκ³΅ν•©λ‹ˆλ‹€.
24
+ - **πŸ— Layered Architecture**: 싀무 ν‘œμ€€μΈ `Controller` - `Service` - `Route` - `Middleware` 계측 ꡬ쑰λ₯Ό μ œκ³΅ν•©λ‹ˆλ‹€.
25
+ - **⚑️ Modern Execution**: `ts-node`의 ESM ν˜Έν™˜μ„± 문제λ₯Ό ν•΄κ²°ν•œ **`tsx`**λ₯Ό μ±„νƒν•˜μ—¬ μΎŒμ ν•œ 개발 ν™˜κ²½μ„ μ œκ³΅ν•©λ‹ˆλ‹€.
24
26
  - **πŸ“¦ Smart Auto-Installation**: ν”„λ‘œμ νŠΈ 생성 μ¦‰μ‹œ μ˜μ‘΄μ„± μ„€μΉ˜ 및 ν™˜κ²½ λ³€μˆ˜(`.env`) μ„ΈνŒ…μ„ μ™„λ£Œν•©λ‹ˆλ‹€.
25
27
 
26
28
  ## πŸš€ Quick Start (μ‚¬μš©λ²•)
@@ -29,48 +31,70 @@
29
31
 
30
32
  ```bash
31
33
  npx create-express-esm
32
- create-express-esm
33
34
  ```
34
35
 
35
- λ˜λŠ” μ „μ—­μœΌλ‘œ μ„€μΉ˜ν•˜μ—¬ μ‚¬μš©ν•  μˆ˜λ„ μžˆμŠ΅λ‹ˆλ‹€
36
+ ### ν”„λ‘œμ νŠΈ 생성 ν›„ DB μ‹œμž‘ν•˜κΈ°
37
+
38
+ DB μ˜΅μ…˜μ„ μ„ νƒν–ˆλ‹€λ©΄, 단 λͺ‡ μ€„μ˜ λͺ…λ Ήμ–΄λ‘œ 개발 μ€€λΉ„κ°€ λλ‚©λ‹ˆλ‹€.
39
+
40
+ ```bash
41
+ # 1. ν”„λ‘œμ νŠΈ 폴더 이동
42
+ cd my-app
43
+
44
+ # 2. Dockerλ₯Ό ν†΅ν•œ PostgreSQL μ‹€ν–‰
45
+ npm run db:up
46
+
47
+ # 3. Prisma μŠ€ν‚€λ§ˆλ₯Ό DB에 반영 (ν…Œμ΄λΈ” 생성)
48
+ npm run db:push
36
49
 
50
+ # 4. μ„œλ²„ μ‹€ν–‰
51
+ npm run dev
37
52
  ```
53
+
54
+ λ˜λŠ” μ „μ—­μœΌλ‘œ μ„€μΉ˜ν•˜μ—¬ μ‚¬μš©ν•  μˆ˜λ„ μžˆμŠ΅λ‹ˆλ‹€.
55
+
56
+ ```bash
38
57
  npm install -g create-express-esm
58
+ create-express-esm
39
59
  ```
40
60
 
41
61
  ## πŸ“‚ Project Structure (폴더 ꡬ쑰)
42
62
 
43
63
  이 λ„κ΅¬λŠ” **Layered Architecture (κ³„μΈ΅ν˜• μ•„ν‚€ν…μ²˜)**λ₯Ό 기반으둜 ν”„λ‘œμ νŠΈλ₯Ό μƒμ„±ν•©λ‹ˆλ‹€.
44
- **관심사 뢄리(Separation of Concerns)** 원칙을 μ μš©ν•˜μ—¬, 둜직이 μ„žμ΄μ§€ μ•Šκ³  μœ μ§€λ³΄μˆ˜κ°€ μ‰¬μš΄ ꡬ쑰λ₯Ό μ œκ³΅ν•©λ‹ˆλ‹€.
64
+ 관심사 뢄리(Separation of Concerns) 원칙을 μ μš©ν•˜μ—¬ μœ μ§€λ³΄μˆ˜κ°€ μ‰¬μš΄ ꡬ쑰λ₯Ό μ œκ³΅ν•©λ‹ˆλ‹€.
45
65
 
46
66
  ```text
47
67
  my-app/
68
+ β”œβ”€β”€ prisma/ # πŸ—„οΈ Prisma Schema & Migrations
48
69
  β”œβ”€β”€ src/
49
70
  β”‚ β”œβ”€β”€ controllers/ # πŸ•ΉοΈ μš”μ²­ 처리 및 응닡 λ°˜ν™˜ (Controller Layer)
50
71
  β”‚ β”œβ”€β”€ services/ # 🧠 λΉ„μ¦ˆλ‹ˆμŠ€ 둜직 처리 (Service Layer)
51
72
  β”‚ β”œβ”€β”€ routes/ # 🚦 API μ—”λ“œν¬μΈνŠΈ μ •μ˜ (Route Layer)
52
- β”‚ β”œβ”€β”€ app.ts (or .js) # πŸ—οΈ Express μ•± μ„€μ • 및 미듀웨어
53
- β”‚ β”œβ”€β”€ server.ts (.js) # πŸš€ μ„œλ²„ μ§„μž…μ  (Entry Point)
73
+ β”‚ β”œβ”€β”€ middlewares/ # πŸ›‘οΈ μ „μ—­ μ—λŸ¬ ν•Έλ“€λŸ¬ 및 μ»€μŠ€ν…€ 미듀웨어
74
+ β”‚ β”œβ”€β”€ utils/ # πŸ› οΈ AppError 클래슀 λ“± 곡톡 μœ ν‹Έλ¦¬ν‹°
75
+ β”‚ β”œβ”€β”€ lib/ # πŸ–‡οΈ Prisma Client μΈμŠ€ν„΄μŠ€ (Singleton)
76
+ β”‚ β”œβ”€β”€ app.ts # πŸ—οΈ Express μ•± μ„€μ • 및 미듀웨어
77
+ β”‚ β”œβ”€β”€ server.ts # πŸš€ μ„œλ²„ μ§„μž…μ  (Entry Point)
54
78
  β”‚ └── app.test.ts # πŸ§ͺ Vitest μƒ˜ν”Œ ν…ŒμŠ€νŠΈ μ½”λ“œ
55
- β”œβ”€β”€ .env # πŸ” ν™˜κ²½ λ³€μˆ˜ (μžλ™ 생성)
79
+ β”œβ”€β”€ .env # πŸ” ν™˜κ²½ λ³€μˆ˜ (DATABASE_URL μžλ™ 생성)
80
+ β”œβ”€β”€ docker-compose.yml # 🐳 PostgreSQL μ»¨ν…Œμ΄λ„ˆ μ„€μ •
56
81
  β”œβ”€β”€ vitest.config.ts # πŸ§ͺ Vitest μ„€μ • 파일
57
82
  β”œβ”€β”€ tsconfig.json # βš™οΈ TS 컴파일러 μ„€μ • (TS 선택 μ‹œ)
58
83
  └── package.json # πŸ“¦ μ˜μ‘΄μ„± 및 슀크립트
59
84
  ```
60
85
 
61
- μ£ΌμΈλ‹˜, μš”μ²­ν•˜μ‹  λŒ€λ‘œ πŸ›  Tech Stack (기술 μŠ€νƒ) μ„Ήμ…˜λΆ€ν„° λ§ˆμ§€λ§‰κΉŒμ§€μ˜ λ‚΄μš©μ„ λ§ˆν¬λ‹€μš΄ μ½”λ“œλ‘œ 정리해 λ“œλ¦½λ‹ˆλ‹€. 이 뢀뢄은 μ£ΌμΈλ‹˜μ΄ μ΄λ²ˆμ— ν•΄κ²°ν•˜μ‹  기술적 도전 κ³Όμ œλ“€μ΄ κ³ μŠ€λž€νžˆ 담겨 μžˆμ–΄ ν¬νŠΈν΄λ¦¬μ˜€λ‘œμ„œμ˜ κ°€μΉ˜κ°€ 맀우 λ†’μŠ΅λ‹ˆλ‹€.
62
-
63
- Markdown
64
-
65
86
  ## πŸ›  Tech Stack (기술 μŠ€νƒ)
66
87
 
67
88
  - **Runtime**: Node.js (v20+)
68
89
  - **Framework**: Express.js
69
90
  - **Language**: JavaScript (ESM) / TypeScript 5.x
91
+ - **ORM**: Prisma (PostgreSQL)
92
+ - **Infrastructure**: Docker Compose
70
93
  - **Testing**: Vitest, Supertest
71
94
  - **Dev Tools**:
72
95
  - `tsx` (TypeScript Execution Engine)
73
96
  - `nodemon` (Hot Reload)
97
+ - `@clack/prompts` (Interactive CLI UI)
74
98
  - `dotenv` (Environment Variables)
75
99
  - `cors` (Cross-Origin Resource Sharing)
76
100
  - `chalk` (CLI Styling)
@@ -78,15 +102,18 @@ Markdown
78
102
  ## πŸ“ Retrospective
79
103
 
80
104
  - **ν‘œμ€€ν™”λœ ν™˜κ²½μ˜ μ€‘μš”μ„±**: CJSμ—μ„œ ESM으둜 λ„˜μ–΄κ°€λŠ” 과도기적 문제λ₯Ό ν•΄κ²°ν•˜λ©° λͺ¨λ˜ μžλ°”μŠ€ν¬λ¦½νŠΈ λͺ¨λ“ˆ μ‹œμŠ€ν…œμ— λŒ€ν•œ κΉŠμ€ 이해λ₯Ό μ–»μ—ˆμŠ΅λ‹ˆλ‹€.
105
+ - **μ—λŸ¬ ν•Έλ“€λ§μ˜ 쀑앙화**: κ°œλ³„ μ»¨νŠΈλ‘€λŸ¬μ—μ„œ 반볡되던 μ—λŸ¬ 처리 λ‘œμ§μ„ μ „μ—­ λ―Έλ“€μ›¨μ–΄λ‘œ μœ„μž„ν•˜μ—¬ μ½”λ“œ 가독성과 μœ μ§€λ³΄μˆ˜μ„±μ„ κ·ΉλŒ€ν™”ν–ˆμŠ΅λ‹ˆλ‹€.
106
+ - **인프라 ν™˜κ²½ 이슈 ν•΄κ²°**: 둜컬 PostgreSQL과의 포트 좩돌(5432 vs 5433) 및 도컀 λ³Όλ₯¨ 인증 문제λ₯Ό ν•΄κ²°ν•˜λ©°, μ‚¬μš©μžμ—κ²Œ κ°€μž₯ μ•ˆμ •μ μΈ DB μ—°κ²° κ°€μ΄λ“œλ₯Ό μ œκ³΅ν•˜λŠ” 데 μ„±κ³΅ν–ˆμŠ΅λ‹ˆλ‹€.
81
107
  - **UX 기반 섀계**: μ‚¬μš©μžκ°€ ν”„λ‘œμ νŠΈλ₯Ό μƒμ„±ν•˜μžλ§ˆμž `npm run dev`와 `npm test`λ₯Ό μ¦‰μ‹œ μ‹€ν–‰ν•  수 μžˆλŠ” "Zero-Config" ν™˜κ²½μ„ μ œκ³΅ν•˜λŠ” 데 μ§‘μ€‘ν–ˆμŠ΅λ‹ˆλ‹€.
82
- - **배포 ν”„λ‘œμ„ΈμŠ€μ˜ μ„±μˆ™**: μˆ˜λ™ 배포의 μœ„ν—˜μ„±μ„ CI/CD와 OIDC λ„μž…μ„ 톡해 μžλ™ν™”ν•˜λ©° μ†Œν”„νŠΈμ›¨μ–΄ 릴리슀 κ³Όμ •μ˜ μ•ˆμ •μ„±μ„ ν™•λ³΄ν–ˆμŠ΅λ‹ˆλ‹€.
83
108
 
84
109
  ## πŸ—ΊοΈ Roadmap (Future Plans)
85
110
 
86
111
  - [x] **TypeScript Support**: `.ts` ν…œν”Œλ¦Ώ 및 `tsx` ν™˜κ²½ μ΅œμ ν™”
87
112
  - [x] **Test Environment**: Vitest 및 Supertest μ„€μ • μžλ™ν™”
88
- - [ ] **Interactive UI Upgrade**: `Clack` 라이브러리λ₯Ό ν†΅ν•œ μ‹œκ°μ  CLI UI κ°œμ„ 
89
- - [ ] **Database Integration**: Prisma/Sequelize λ“± ORM 선택 μ˜΅μ…˜ μΆ”κ°€
113
+ - [x] **Interactive UI Upgrade**: `Clack` 라이브러리λ₯Ό ν†΅ν•œ μ‹œκ°μ  CLI UI κ°œμ„ 
114
+ - [x] **Database Integration**: Prisma/PostgreSQL 및 Docker 선택 μ˜΅μ…˜ μΆ”κ°€
115
+ - [ ] **Authentication Template**: JWT/Passportλ₯Ό μ΄μš©ν•œ κΈ°λ³Έ 인증 둜직 μΆ”κ°€
116
+ - [ ] **Deployment Guide**: AWS/Render λ“± μ£Όμš” ν”Œλž«νΌ 배포 κ°€μ΄λ“œλΌμΈ μΆ”κ°€
90
117
 
91
118
  ## πŸ“ License
92
119
 
package/bin/cli.js CHANGED
@@ -14,11 +14,11 @@ const __dirname = path.dirname(__filename);
14
14
  async function run() {
15
15
  console.clear();
16
16
 
17
- // 1. μ‹œμž‘ 인사 (Intro)
18
- p.intro(`${chalk.bgBlue.white(' create-express-esm ')} ${chalk.dim('v1.1.9')}`);
17
+ // 1. μ‹œμž‘ 인사
18
+ p.intro(`${chalk.bgBlue.white(' create-express-esm ')} ${chalk.dim('v1.2.0-beta')}`);
19
19
 
20
20
  try {
21
- // 2. μ‚¬μš©μž 질문 κ·Έλ£Ή (Group)
21
+ // 2. μ‚¬μš©μž 질문 κ·Έλ£Ή
22
22
  const project = await p.group(
23
23
  {
24
24
  projectName: () =>
@@ -43,6 +43,11 @@ async function run() {
43
43
  message: 'Vitest ν…ŒμŠ€νŠΈ ν™˜κ²½μ„ μΆ”κ°€ν•˜μ‹œκ² μŠ΅λ‹ˆκΉŒ?',
44
44
  initialValue: true,
45
45
  }),
46
+ useDb: () =>
47
+ p.confirm({
48
+ message: 'Prisma ORM (PostgreSQL) 및 μ „μ—­ μ—λŸ¬ 핸듀링을 μΆ”κ°€ν•˜μ‹œκ² μŠ΅λ‹ˆκΉŒ?',
49
+ initialValue: false,
50
+ }),
46
51
  },
47
52
  {
48
53
  onCancel: () => {
@@ -52,18 +57,19 @@ async function run() {
52
57
  }
53
58
  );
54
59
 
55
- const { projectName, language, useTest } = project;
60
+ const { projectName, language, useTest, useDb } = project;
56
61
  const targetPath = path.join(process.cwd(), projectName);
57
62
  const templatePath = path.join(__dirname, '../template', language);
63
+ const commonPath = path.join(__dirname, '../template/common');
58
64
 
59
- // 3. 파일 ꡬ성 μ‹œμž‘ (Spinner)
65
+ // 3. 파일 ꡬ성 μ‹œμž‘
60
66
  const s = p.spinner();
61
67
  s.start('ν”„λ‘œμ νŠΈ ν…œν”Œλ¦Ώμ„ λ³΅μ‚¬ν•˜λŠ” 쀑...');
62
68
 
63
- // ν…œν”Œλ¦Ώ 전체 볡사 (Vitest 파일 포함)
69
+ // κΈ°λ³Έ μ–Έμ–΄ ν…œν”Œλ¦Ώ 볡사
64
70
  await fs.copy(templatePath, targetPath);
65
71
 
66
- // λ„νŠΈ 파일 λ³€ν™˜ (예: _env -> .env)
72
+ // λ„νŠΈ 파일 λ³€ν™˜ (_env -> .env λ“±)
67
73
  const renameMap = {
68
74
  'gitignore': '.gitignore',
69
75
  '_gitignore': '.gitignore',
@@ -75,50 +81,110 @@ async function run() {
75
81
  if (await fs.pathExists(oldFilePath)) {
76
82
  await fs.move(oldFilePath, path.join(targetPath, newName), { overwrite: true });
77
83
  if (newName === '.env') {
78
- await fs.copy(path.join(targetPath, '.env'), path.join(targetPath, '.env.example'));
84
+ await fs.copy(path.join(targetPath, '.env'), path.join(targetPath, '.env.example'));
79
85
  }
80
86
  }
81
87
  }
82
88
 
83
- // 4. package.json 동적 μ΅œμ ν™”
89
+ // 4. DB 및 μ—λŸ¬ 핸듀링 선택 μ‹œ μΆ”κ°€ 파일 볡사 및 μ½”λ“œ μ£Όμž…
90
+ if (useDb) {
91
+ // (1) Prisma μ„€μ • 및 Docker Compose 볡사
92
+ await fs.copy(path.join(commonPath, 'prisma'), path.join(targetPath, 'prisma'));
93
+ await fs.copy(path.join(commonPath, 'docker-compose.yml'), path.join(targetPath, 'docker-compose.yml'));
94
+
95
+ // (2) μ†ŒμŠ€ μ½”λ“œ 볡사 (lib, services, controllers, routes, utils, middlewares)
96
+ const sourceFolders = ['lib', 'services', 'controllers', 'routes', 'utils', 'middlewares'];
97
+ for (const folder of sourceFolders) {
98
+ const srcFolderPath = path.join(commonPath, 'src', folder);
99
+ const destFolderPath = path.join(targetPath, 'src', folder);
100
+
101
+ if (await fs.pathExists(srcFolderPath)) {
102
+ await fs.ensureDir(destFolderPath);
103
+ const files = await fs.readdir(srcFolderPath);
104
+ for (const file of files) {
105
+ // μ‚¬μš©μžκ°€ μ„ νƒν•œ μ–Έμ–΄(ts/js)와 μΌμΉ˜ν•˜λŠ” 파일만 볡사
106
+ if (file.endsWith(`.${language}`)) {
107
+ await fs.copy(path.join(srcFolderPath, file), path.join(destFolderPath, file));
108
+ }
109
+ }
110
+ }
111
+ }
112
+
113
+ // (3) app.ts / app.js 에 μ½”λ“œ μ£Όμž… (μ€‘μš”!)
114
+ const mainFilePath = path.join(targetPath, `src/app.${language}`);
115
+ if (await fs.pathExists(mainFilePath)) {
116
+ let content = await fs.readFile(mainFilePath, 'utf-8');
117
+
118
+ // 상단 μž„ν¬νŠΈ μ£Όμž…
119
+ const imports = [
120
+ `import userRoutes from './routes/userRoutes.js';`,
121
+ `import { errorHandler } from './middlewares/errorMiddleware.js';`
122
+ ].join('\n');
123
+ content = imports + '\n' + content;
124
+
125
+ // λΌμš°ν„° 등둝 μ£Όμž… (express.json() 뒀에)
126
+ const routeCode = `\napp.use('/users', userRoutes);`;
127
+ content = content.replace('app.use(express.json());', `app.use(express.json());${routeCode}`);
128
+
129
+ // μ „μ—­ μ—λŸ¬ ν•Έλ“€λŸ¬ μ£Όμž… (μ„œλ²„ μ‹€ν–‰ 직전에)
130
+ const errorMiddlewareCode = `\n// μ „μ—­ μ—λŸ¬ ν•Έλ“€λŸ¬ (λͺ¨λ“  λΌμš°ν„° λ‹€μŒμ— μœ„μΉ˜ν•΄μ•Ό 함)\napp.use(errorHandler);\n`;
131
+ if (content.includes('export default app;')) {
132
+ content = content.replace('export default app;', `${errorMiddlewareCode}\nexport default app;`);
133
+ } else {
134
+ content += `\n${errorMiddlewareCode}`;
135
+ }
136
+
137
+ await fs.writeFile(mainFilePath, content);
138
+ }
139
+
140
+ // (4) .env νŒŒμΌμ— DATABASE_URL μΆ”κ°€
141
+ const envPath = path.join(targetPath, '.env');
142
+ const dbUrlContent = `
143
+ # PostgreSQL Connection (Docker Compose default)
144
+ DATABASE_URL="postgresql://myuser:mypassword@localhost:5432/mydb?schema=public"
145
+ `;
146
+ await fs.appendFile(envPath, dbUrlContent);
147
+ }
148
+
149
+ // 5. package.json 동적 μ΅œμ ν™”
84
150
  const pkgPath = path.join(targetPath, 'package.json');
85
151
  const pkg = await fs.readJson(pkgPath);
86
152
  pkg.name = projectName;
87
153
 
88
- // TypeScript 선택 μ‹œ μ‹€ν–‰ ν™˜κ²½(tsx) κ°•μ œ μ„€μ •
89
154
  if (language === 'ts') {
90
155
  pkg.scripts.dev = "nodemon --exec tsx src/server.ts";
91
156
  pkg.devDependencies["tsx"] = "^4.7.0";
92
- // κ΅¬ν˜• ts-node 제거
93
- if (pkg.devDependencies["ts-node"]) delete pkg.devDependencies["ts-node"];
94
157
  }
95
158
 
96
- // ν…ŒμŠ€νŠΈ ν™˜κ²½ μ‚¬μš© 여뢀에 λ”°λ₯Έ 처리
97
- const configExt = language === 'ts' ? 'ts' : 'js';
98
- const testFileExt = language === 'ts' ? 'ts' : 'js';
99
-
159
+ // ν…ŒμŠ€νŠΈ ν™˜κ²½ μ„€μ • (λΉ„μ‚¬μš© μ‹œ κ΄€λ ¨ 파일 및 νŒ¨ν‚€μ§€ 제거)
100
160
  if (!useTest) {
101
- // μ‚¬μš©μžκ°€ μ›μΉ˜ μ•ŠμœΌλ©΄ λ³΅μ‚¬λœ ν…ŒμŠ€νŠΈ 파일 μ‚­μ œ
161
+ const configExt = language === 'ts' ? 'ts' : 'js';
162
+ const testFileExt = language === 'ts' ? 'ts' : 'js';
102
163
  await fs.remove(path.join(targetPath, `vitest.config.${configExt}`));
103
164
  await fs.remove(path.join(targetPath, `src/app.test.${testFileExt}`));
104
-
105
- // package.jsonμ—μ„œ κ΄€λ ¨ μ„€μ • 제거
106
165
  delete pkg.scripts.test;
107
166
  delete pkg.scripts["test:ui"];
108
167
  delete pkg.scripts["test:run"];
109
168
  delete pkg.devDependencies.vitest;
110
169
  delete pkg.devDependencies.supertest;
111
170
  if (pkg.devDependencies["@types/supertest"]) delete pkg.devDependencies["@types/supertest"];
112
- } else {
113
- // μ‚¬μš©μžκ°€ μ›ν•˜λ©΄ μŠ€ν¬λ¦½νŠΈκ°€ ν™•μ‹€νžˆ μžˆλŠ”μ§€ 보μž₯
114
- pkg.scripts.test = "vitest";
115
- pkg.scripts["test:ui"] = "vitest --ui";
171
+ }
172
+
173
+ // DB μ˜μ‘΄μ„± 및 슀크립트 μΆ”κ°€
174
+ if (useDb) {
175
+ pkg.scripts["db:up"] = "docker-compose up -d";
176
+ pkg.scripts["db:push"] = "prisma db push";
177
+ pkg.scripts["prisma:generate"] = "prisma generate";
178
+ pkg.scripts["prisma:studio"] = "prisma studio";
179
+
180
+ pkg.dependencies["@prisma/client"] = "^5.0.0";
181
+ pkg.devDependencies["prisma"] = "^5.0.0";
116
182
  }
117
183
 
118
184
  await fs.writeJson(pkgPath, pkg, { spaces: 2 });
119
185
  s.stop('파일 ꡬ성 μ™„λ£Œ!');
120
186
 
121
- // 5. μ˜μ‘΄μ„± μ„€μΉ˜ (Spinner)
187
+ // 6. μ˜μ‘΄μ„± μ„€μΉ˜
122
188
  const installSpinner = p.spinner();
123
189
  installSpinner.start('μ˜μ‘΄μ„± νŒ¨ν‚€μ§€λ₯Ό μ„€μΉ˜ν•˜λŠ” 쀑... (npm install)');
124
190
 
@@ -129,12 +195,15 @@ async function run() {
129
195
  installSpinner.stop(chalk.red('μ„€μΉ˜ μ‹€νŒ¨ (μˆ˜λ™ μ„€μΉ˜κ°€ ν•„μš”ν•  수 μžˆμŠ΅λ‹ˆλ‹€)'));
130
196
  }
131
197
 
132
- // 6. 마무리 (Note & Outro)
133
- p.note(
134
- chalk.cyan(`cd ${projectName}\n${useTest ? 'npm test\n' : ''}npm run dev`),
135
- 'μ‹œμž‘ν•˜λ €λ©΄ λ‹€μŒ λͺ…λ Ήμ–΄λ₯Ό μž…λ ₯ν•˜μ„Έμš”'
136
- );
198
+ // 7. 마무리
199
+ let nextSteps = `cd ${projectName}\n`;
200
+ if (useDb) {
201
+ nextSteps += `${chalk.bold('npm run db:up')} (DB μ‹€ν–‰)\n`;
202
+ nextSteps += `${chalk.bold('npm run db:push')} (ν…Œμ΄λΈ” 생성)\n`;
203
+ }
204
+ nextSteps += `npm run dev`;
137
205
 
206
+ p.note(chalk.cyan(nextSteps), 'μ‹œμž‘ν•˜λ €λ©΄ λ‹€μŒ λͺ…λ Ήμ–΄λ₯Ό μž…λ ₯ν•˜μ„Έμš”');
138
207
  p.outro(chalk.green('✨ λͺ¨λ“  μ€€λΉ„κ°€ λλ‚¬μŠ΅λ‹ˆλ‹€. 즐거운 개발 λ˜μ„Έμš”!'));
139
208
 
140
209
  } catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-express-esm",
3
- "version": "1.1.12",
3
+ "version": "1.2.0",
4
4
  "description": "A modern CLI tool to bootstrap Express.js applications with ES Modules and Layered Architecture.",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -0,0 +1,16 @@
1
+ version: '3.8'
2
+ services:
3
+ db:
4
+ image: postgres:15-alpine
5
+ restart: always
6
+ environment:
7
+ - POSTGRES_USER=myuser
8
+ - POSTGRES_PASSWORD=mypassword
9
+ - POSTGRES_DB=mydb
10
+ ports:
11
+ - '5432:5432'
12
+ volumes:
13
+ - db-data:/var/lib/postgresql/data
14
+
15
+ volumes:
16
+ db-data:
@@ -0,0 +1,16 @@
1
+ generator client {
2
+ provider = "prisma-client-js"
3
+ }
4
+
5
+ datasource db {
6
+ provider = "postgresql"
7
+ url = env("DATABASE_URL")
8
+ }
9
+
10
+ model User {
11
+ id Int @id @default(autoincrement())
12
+ email String @unique
13
+ name String?
14
+ createdAt DateTime @default(now())
15
+ updatedAt DateTime @updatedAt
16
+ }
@@ -0,0 +1,42 @@
1
+ import { Request, Response, NextFunction } from 'express';
2
+ import { userService } from '../services/userService.js';
3
+
4
+ export const userController = {
5
+ // 1. λͺ¨λ“  μ‚¬μš©μž 쑰회
6
+ async getUsers(req: Request, res: Response, next: NextFunction) {
7
+ try {
8
+ const users = await userService.getAllUsers();
9
+ res.json(users);
10
+ } catch (error) {
11
+ next(error); // μ—λŸ¬λ₯Ό μ „μ—­ ν•Έλ“€λŸ¬λ‘œ 던짐
12
+ }
13
+ },
14
+
15
+ // 2. μƒˆ μ‚¬μš©μž 생성
16
+ async createUser(req: Request, res: Response, next: NextFunction) {
17
+ try {
18
+ const { email, name } = req.body;
19
+ const newUser = await userService.createUser({ email, name });
20
+ res.status(201).json(newUser);
21
+ } catch (error) {
22
+ next(error); // μ—¬κΈ°μ„œ μ—λŸ¬κ°€ λ˜μ Έμ§€λ©΄ errorHandlerκ°€ P2002(쀑볡) 등을 μ²˜λ¦¬ν•¨
23
+ }
24
+ },
25
+
26
+ // 3. νŠΉμ • μ‚¬μš©μž 쑰회
27
+ async getUserById(req: Request, res: Response, next: NextFunction) {
28
+ try {
29
+ const id = parseInt(req.params.id);
30
+ const user = await userService.getUserById(id);
31
+
32
+ if (!user) {
33
+ // AppErrorλ₯Ό μ‚¬μš©ν•΄ 404 μ—λŸ¬λ₯Ό 직접 던질 μˆ˜λ„ μžˆμŠ΅λ‹ˆλ‹€.
34
+ throw new Error('μ‚¬μš©μžλ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€.');
35
+ }
36
+
37
+ res.json(user);
38
+ } catch (error) {
39
+ next(error);
40
+ }
41
+ }
42
+ };
File without changes
File without changes
@@ -0,0 +1,19 @@
1
+ import { Request, Response, NextFunction } from 'express';
2
+ import { AppError } from '../utils/appError.js';
3
+
4
+ export const errorHandler = (err: any, req: Request, res: Response, next: NextFunction) => {
5
+ err.statusCode = err.statusCode || 500;
6
+
7
+ // Prisma 고유 μ—λŸ¬ 처리 (예: 이메일 쀑볡)
8
+ if (err.code === 'P2002') {
9
+ err.message = '이미 μ‚¬μš© 쀑인 데이터(이메일 λ“±)κ°€ μžˆμŠ΅λ‹ˆλ‹€.';
10
+ err.statusCode = 400;
11
+ }
12
+
13
+ res.status(err.statusCode).json({
14
+ status: 'error',
15
+ message: err.message,
16
+ // 개발 ν™˜κ²½μ—μ„œλ§Œ μ—λŸ¬ μŠ€νƒ ν‘œμ‹œ
17
+ stack: process.env.NODE_ENV === 'development' ? err.stack : undefined,
18
+ });
19
+ };
File without changes
File without changes
File without changes
File without changes
@@ -0,0 +1,12 @@
1
+ export class AppError extends Error {
2
+ public statusCode: number;
3
+ public isOperational: boolean;
4
+
5
+ constructor(message: string, statusCode: number) {
6
+ super(message);
7
+ this.statusCode = statusCode;
8
+ this.isOperational = true; // 예츑 κ°€λŠ₯ν•œ μ—λŸ¬μž„μ„ ν‘œμ‹œ
9
+
10
+ Error.captureStackTrace(this, this.constructor);
11
+ }
12
+ }
package/template/js/_env CHANGED
@@ -1,2 +1,3 @@
1
- PORT=3000
2
- NODE_ENV=development
1
+ # PostgreSQL Connection String
2
+ # ν˜•μ‹: postgresql://μ‚¬μš©μž:λΉ„λ°€λ²ˆν˜Έ@호슀트:포트/λ°μ΄ν„°λ² μ΄μŠ€λͺ…?schema=public
3
+ DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/myapp?schema=public"
package/template/ts/_env CHANGED
@@ -0,0 +1,3 @@
1
+ # PostgreSQL Connection String
2
+ # ν˜•μ‹: postgresql://μ‚¬μš©μž:λΉ„λ°€λ²ˆν˜Έ@호슀트:포트/λ°μ΄ν„°λ² μ΄μŠ€λͺ…?schema=public
3
+ DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/myapp?schema=public"