create-express-esm 1.1.11 β†’ 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,13 +31,29 @@
29
31
 
30
32
  ```bash
31
33
  npx create-express-esm
32
- npm install -g create-express-esm
33
- npm install create-express-esm
34
34
  ```
35
35
 
36
- λ˜λŠ” μ „μ—­μœΌλ‘œ μ„€μΉ˜ν•˜μ—¬ μ‚¬μš©ν•  μˆ˜λ„ μžˆμŠ΅λ‹ˆλ‹€
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
37
49
 
50
+ # 4. μ„œλ²„ μ‹€ν–‰
51
+ npm run dev
38
52
  ```
53
+
54
+ λ˜λŠ” μ „μ—­μœΌλ‘œ μ„€μΉ˜ν•˜μ—¬ μ‚¬μš©ν•  μˆ˜λ„ μžˆμŠ΅λ‹ˆλ‹€.
55
+
56
+ ```bash
39
57
  npm install -g create-express-esm
40
58
  create-express-esm
41
59
  ```
@@ -43,36 +61,40 @@ create-express-esm
43
61
  ## πŸ“‚ Project Structure (폴더 ꡬ쑰)
44
62
 
45
63
  이 λ„κ΅¬λŠ” **Layered Architecture (κ³„μΈ΅ν˜• μ•„ν‚€ν…μ²˜)**λ₯Ό 기반으둜 ν”„λ‘œμ νŠΈλ₯Ό μƒμ„±ν•©λ‹ˆλ‹€.
46
- **관심사 뢄리(Separation of Concerns)** 원칙을 μ μš©ν•˜μ—¬, 둜직이 μ„žμ΄μ§€ μ•Šκ³  μœ μ§€λ³΄μˆ˜κ°€ μ‰¬μš΄ ꡬ쑰λ₯Ό μ œκ³΅ν•©λ‹ˆλ‹€.
64
+ 관심사 뢄리(Separation of Concerns) 원칙을 μ μš©ν•˜μ—¬ μœ μ§€λ³΄μˆ˜κ°€ μ‰¬μš΄ ꡬ쑰λ₯Ό μ œκ³΅ν•©λ‹ˆλ‹€.
47
65
 
48
66
  ```text
49
67
  my-app/
68
+ β”œβ”€β”€ prisma/ # πŸ—„οΈ Prisma Schema & Migrations
50
69
  β”œβ”€β”€ src/
51
70
  β”‚ β”œβ”€β”€ controllers/ # πŸ•ΉοΈ μš”μ²­ 처리 및 응닡 λ°˜ν™˜ (Controller Layer)
52
71
  β”‚ β”œβ”€β”€ services/ # 🧠 λΉ„μ¦ˆλ‹ˆμŠ€ 둜직 처리 (Service Layer)
53
72
  β”‚ β”œβ”€β”€ routes/ # 🚦 API μ—”λ“œν¬μΈνŠΈ μ •μ˜ (Route Layer)
54
- β”‚ β”œβ”€β”€ app.ts (or .js) # πŸ—οΈ Express μ•± μ„€μ • 및 미듀웨어
55
- β”‚ β”œβ”€β”€ server.ts (.js) # πŸš€ μ„œλ²„ μ§„μž…μ  (Entry Point)
73
+ β”‚ β”œβ”€β”€ middlewares/ # πŸ›‘οΈ μ „μ—­ μ—λŸ¬ ν•Έλ“€λŸ¬ 및 μ»€μŠ€ν…€ 미듀웨어
74
+ β”‚ β”œβ”€β”€ utils/ # πŸ› οΈ AppError 클래슀 λ“± 곡톡 μœ ν‹Έλ¦¬ν‹°
75
+ β”‚ β”œβ”€β”€ lib/ # πŸ–‡οΈ Prisma Client μΈμŠ€ν„΄μŠ€ (Singleton)
76
+ β”‚ β”œβ”€β”€ app.ts # πŸ—οΈ Express μ•± μ„€μ • 및 미듀웨어
77
+ β”‚ β”œβ”€β”€ server.ts # πŸš€ μ„œλ²„ μ§„μž…μ  (Entry Point)
56
78
  β”‚ └── app.test.ts # πŸ§ͺ Vitest μƒ˜ν”Œ ν…ŒμŠ€νŠΈ μ½”λ“œ
57
- β”œβ”€β”€ .env # πŸ” ν™˜κ²½ λ³€μˆ˜ (μžλ™ 생성)
79
+ β”œβ”€β”€ .env # πŸ” ν™˜κ²½ λ³€μˆ˜ (DATABASE_URL μžλ™ 생성)
80
+ β”œβ”€β”€ docker-compose.yml # 🐳 PostgreSQL μ»¨ν…Œμ΄λ„ˆ μ„€μ •
58
81
  β”œβ”€β”€ vitest.config.ts # πŸ§ͺ Vitest μ„€μ • 파일
59
82
  β”œβ”€β”€ tsconfig.json # βš™οΈ TS 컴파일러 μ„€μ • (TS 선택 μ‹œ)
60
83
  └── package.json # πŸ“¦ μ˜μ‘΄μ„± 및 슀크립트
61
84
  ```
62
85
 
63
- μ£ΌμΈλ‹˜, μš”μ²­ν•˜μ‹  λŒ€λ‘œ πŸ›  Tech Stack (기술 μŠ€νƒ) μ„Ήμ…˜λΆ€ν„° λ§ˆμ§€λ§‰κΉŒμ§€μ˜ λ‚΄μš©μ„ λ§ˆν¬λ‹€μš΄ μ½”λ“œλ‘œ 정리해 λ“œλ¦½λ‹ˆλ‹€. 이 뢀뢄은 μ£ΌμΈλ‹˜μ΄ μ΄λ²ˆμ— ν•΄κ²°ν•˜μ‹  기술적 도전 κ³Όμ œλ“€μ΄ κ³ μŠ€λž€νžˆ 담겨 μžˆμ–΄ ν¬νŠΈν΄λ¦¬μ˜€λ‘œμ„œμ˜ κ°€μΉ˜κ°€ 맀우 λ†’μŠ΅λ‹ˆλ‹€.
64
-
65
- Markdown
66
-
67
86
  ## πŸ›  Tech Stack (기술 μŠ€νƒ)
68
87
 
69
88
  - **Runtime**: Node.js (v20+)
70
89
  - **Framework**: Express.js
71
90
  - **Language**: JavaScript (ESM) / TypeScript 5.x
91
+ - **ORM**: Prisma (PostgreSQL)
92
+ - **Infrastructure**: Docker Compose
72
93
  - **Testing**: Vitest, Supertest
73
94
  - **Dev Tools**:
74
95
  - `tsx` (TypeScript Execution Engine)
75
96
  - `nodemon` (Hot Reload)
97
+ - `@clack/prompts` (Interactive CLI UI)
76
98
  - `dotenv` (Environment Variables)
77
99
  - `cors` (Cross-Origin Resource Sharing)
78
100
  - `chalk` (CLI Styling)
@@ -80,15 +102,18 @@ Markdown
80
102
  ## πŸ“ Retrospective
81
103
 
82
104
  - **ν‘œμ€€ν™”λœ ν™˜κ²½μ˜ μ€‘μš”μ„±**: CJSμ—μ„œ ESM으둜 λ„˜μ–΄κ°€λŠ” 과도기적 문제λ₯Ό ν•΄κ²°ν•˜λ©° λͺ¨λ˜ μžλ°”μŠ€ν¬λ¦½νŠΈ λͺ¨λ“ˆ μ‹œμŠ€ν…œμ— λŒ€ν•œ κΉŠμ€ 이해λ₯Ό μ–»μ—ˆμŠ΅λ‹ˆλ‹€.
105
+ - **μ—λŸ¬ ν•Έλ“€λ§μ˜ 쀑앙화**: κ°œλ³„ μ»¨νŠΈλ‘€λŸ¬μ—μ„œ 반볡되던 μ—λŸ¬ 처리 λ‘œμ§μ„ μ „μ—­ λ―Έλ“€μ›¨μ–΄λ‘œ μœ„μž„ν•˜μ—¬ μ½”λ“œ 가독성과 μœ μ§€λ³΄μˆ˜μ„±μ„ κ·ΉλŒ€ν™”ν–ˆμŠ΅λ‹ˆλ‹€.
106
+ - **인프라 ν™˜κ²½ 이슈 ν•΄κ²°**: 둜컬 PostgreSQL과의 포트 좩돌(5432 vs 5433) 및 도컀 λ³Όλ₯¨ 인증 문제λ₯Ό ν•΄κ²°ν•˜λ©°, μ‚¬μš©μžμ—κ²Œ κ°€μž₯ μ•ˆμ •μ μΈ DB μ—°κ²° κ°€μ΄λ“œλ₯Ό μ œκ³΅ν•˜λŠ” 데 μ„±κ³΅ν–ˆμŠ΅λ‹ˆλ‹€.
83
107
  - **UX 기반 섀계**: μ‚¬μš©μžκ°€ ν”„λ‘œμ νŠΈλ₯Ό μƒμ„±ν•˜μžλ§ˆμž `npm run dev`와 `npm test`λ₯Ό μ¦‰μ‹œ μ‹€ν–‰ν•  수 μžˆλŠ” "Zero-Config" ν™˜κ²½μ„ μ œκ³΅ν•˜λŠ” 데 μ§‘μ€‘ν–ˆμŠ΅λ‹ˆλ‹€.
84
- - **배포 ν”„λ‘œμ„ΈμŠ€μ˜ μ„±μˆ™**: μˆ˜λ™ 배포의 μœ„ν—˜μ„±μ„ CI/CD와 OIDC λ„μž…μ„ 톡해 μžλ™ν™”ν•˜λ©° μ†Œν”„νŠΈμ›¨μ–΄ 릴리슀 κ³Όμ •μ˜ μ•ˆμ •μ„±μ„ ν™•λ³΄ν–ˆμŠ΅λ‹ˆλ‹€.
85
108
 
86
109
  ## πŸ—ΊοΈ Roadmap (Future Plans)
87
110
 
88
111
  - [x] **TypeScript Support**: `.ts` ν…œν”Œλ¦Ώ 및 `tsx` ν™˜κ²½ μ΅œμ ν™”
89
112
  - [x] **Test Environment**: Vitest 및 Supertest μ„€μ • μžλ™ν™”
90
- - [ ] **Interactive UI Upgrade**: `Clack` 라이브러리λ₯Ό ν†΅ν•œ μ‹œκ°μ  CLI UI κ°œμ„ 
91
- - [ ] **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 λ“± μ£Όμš” ν”Œλž«νΌ 배포 κ°€μ΄λ“œλΌμΈ μΆ”κ°€
92
117
 
93
118
  ## πŸ“ License
94
119
 
package/bin/cli.js CHANGED
@@ -1,52 +1,75 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { input, select, confirm } from '@inquirer/prompts';
3
+ import * as p from '@clack/prompts';
4
4
  import fs from 'fs-extra';
5
5
  import path from 'path';
6
6
  import chalk from 'chalk';
7
7
  import { execSync } from 'child_process';
8
8
  import { fileURLToPath } from 'url';
9
9
 
10
+ // ESM ν™˜κ²½μ—μ„œ __dirname κ΅¬ν˜„
10
11
  const __filename = fileURLToPath(import.meta.url);
11
12
  const __dirname = path.dirname(__filename);
12
13
 
13
14
  async function run() {
14
- console.log(chalk.blue.bold('\nπŸš€ Create Express ESM μ‹œμž‘!\n'));
15
+ console.clear();
16
+
17
+ // 1. μ‹œμž‘ 인사
18
+ p.intro(`${chalk.bgBlue.white(' create-express-esm ')} ${chalk.dim('v1.2.0-beta')}`);
15
19
 
16
20
  try {
17
- // 1. μ‚¬μš©μž 질문
18
- const projectName = await input({
19
- message: '생성할 ν”„λ‘œμ νŠΈ 이름을 μž…λ ₯ν•˜μ„Έμš”:',
20
- default: 'my-app',
21
- });
22
-
23
- const language = await select({
24
- message: 'μ‚¬μš©ν•  μ–Έμ–΄λ₯Ό μ„ νƒν•˜μ„Έμš”:',
25
- choices: [
26
- { name: 'JavaScript (ESM)', value: 'js' },
27
- { name: 'TypeScript', value: 'ts' },
28
- ],
29
- });
30
-
31
- const useTest = await confirm({
32
- message: 'Vitest ν…ŒμŠ€νŠΈ ν™˜κ²½μ„ μΆ”κ°€ν•˜μ‹œκ² μŠ΅λ‹ˆκΉŒ?',
33
- default: true,
34
- });
21
+ // 2. μ‚¬μš©μž 질문 κ·Έλ£Ή
22
+ const project = await p.group(
23
+ {
24
+ projectName: () =>
25
+ p.text({
26
+ message: 'ν”„λ‘œμ νŠΈ 이름을 μž…λ ₯ν•˜μ„Έμš”:',
27
+ placeholder: 'my-app',
28
+ validate: (value) => {
29
+ if (value.length === 0) return 'ν”„λ‘œμ νŠΈ 이름은 ν•„μˆ˜μž…λ‹ˆλ‹€!';
30
+ if (fs.existsSync(path.join(process.cwd(), value))) return 'ν•΄λ‹Ή 폴더가 이미 μ‘΄μž¬ν•©λ‹ˆλ‹€.';
31
+ },
32
+ }),
33
+ language: () =>
34
+ p.select({
35
+ message: 'μ‚¬μš©ν•  μ–Έμ–΄λ₯Ό μ„ νƒν•˜μ„Έμš”:',
36
+ options: [
37
+ { value: 'js', label: 'JavaScript (ESM)' },
38
+ { value: 'ts', label: 'TypeScript' },
39
+ ],
40
+ }),
41
+ useTest: () =>
42
+ p.confirm({
43
+ message: 'Vitest ν…ŒμŠ€νŠΈ ν™˜κ²½μ„ μΆ”κ°€ν•˜μ‹œκ² μŠ΅λ‹ˆκΉŒ?',
44
+ initialValue: true,
45
+ }),
46
+ useDb: () =>
47
+ p.confirm({
48
+ message: 'Prisma ORM (PostgreSQL) 및 μ „μ—­ μ—λŸ¬ 핸듀링을 μΆ”κ°€ν•˜μ‹œκ² μŠ΅λ‹ˆκΉŒ?',
49
+ initialValue: false,
50
+ }),
51
+ },
52
+ {
53
+ onCancel: () => {
54
+ p.cancel('ν”„λ‘œμ νŠΈ 생성이 μ·¨μ†Œλ˜μ—ˆμŠ΅λ‹ˆλ‹€.');
55
+ process.exit(0);
56
+ },
57
+ }
58
+ );
35
59
 
60
+ const { projectName, language, useTest, useDb } = project;
36
61
  const targetPath = path.join(process.cwd(), projectName);
37
62
  const templatePath = path.join(__dirname, '../template', language);
63
+ const commonPath = path.join(__dirname, '../template/common');
38
64
 
39
- // 2. 폴더 쑴재 μ—¬λΆ€ 확인
40
- if (fs.existsSync(targetPath)) {
41
- console.error(chalk.red(`\n❌ 였λ₯˜: '${projectName}' 폴더가 이미 μ‘΄μž¬ν•©λ‹ˆλ‹€.`));
42
- process.exit(1);
43
- }
65
+ // 3. 파일 ꡬ성 μ‹œμž‘
66
+ const s = p.spinner();
67
+ s.start('ν”„λ‘œμ νŠΈ ν…œν”Œλ¦Ώμ„ λ³΅μ‚¬ν•˜λŠ” 쀑...');
44
68
 
45
- // 3. κΈ°λ³Έ ν…œν”Œλ¦Ώ 볡사
46
- console.log(chalk.cyan(`\nπŸ“‚ [${language.toUpperCase()}] ν…œν”Œλ¦Ώ ꡬ성을 μ‹œμž‘ν•©λ‹ˆλ‹€...`));
69
+ // κΈ°λ³Έ μ–Έμ–΄ ν…œν”Œλ¦Ώ 볡사
47
70
  await fs.copy(templatePath, targetPath);
48
71
 
49
- // 4. λ„νŠΈ 파일 λ³€ν™˜ (_env -> .env λ“±)
72
+ // λ„νŠΈ 파일 λ³€ν™˜ (_env -> .env λ“±)
50
73
  const renameMap = {
51
74
  'gitignore': '.gitignore',
52
75
  '_gitignore': '.gitignore',
@@ -55,109 +78,137 @@ async function run() {
55
78
 
56
79
  for (const [oldName, newName] of Object.entries(renameMap)) {
57
80
  const oldFilePath = path.join(targetPath, oldName);
58
- const newFilePath = path.join(targetPath, newName);
59
81
  if (await fs.pathExists(oldFilePath)) {
60
- await fs.move(oldFilePath, newFilePath, { overwrite: true });
82
+ await fs.move(oldFilePath, path.join(targetPath, newName), { overwrite: true });
61
83
  if (newName === '.env') {
62
- await fs.copy(newFilePath, path.join(targetPath, '.env.example'));
84
+ await fs.copy(path.join(targetPath, '.env'), path.join(targetPath, '.env.example'));
63
85
  }
64
86
  }
65
87
  }
66
-
67
- // 5. package.json 동적 μˆ˜μ •
88
+
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 동적 μ΅œμ ν™”
68
150
  const pkgPath = path.join(targetPath, 'package.json');
69
151
  const pkg = await fs.readJson(pkgPath);
70
152
  pkg.name = projectName;
71
153
 
72
- // [μΆ”κ°€λœ λΆ€λΆ„] TypeScript ν™˜κ²½μ—μ„œ ESM μ—λŸ¬λ₯Ό λ°©μ§€ν•˜κΈ° μœ„ν•œ tsx μ„€μ •
73
154
  if (language === 'ts') {
74
- console.log(chalk.yellow(`βš™οΈ TypeScript ESM μ‹€ν–‰ ν™˜κ²½(tsx)을 μ΅œμ ν™”ν•˜λŠ” 쀑...`));
75
-
76
- // ts-node λŒ€μ‹  tsxλ₯Ό μ‚¬μš©ν•˜μ—¬ .js ν™•μž₯자 μž„ν¬νŠΈ 문제 ν•΄κ²°
77
155
  pkg.scripts.dev = "nodemon --exec tsx src/server.ts";
78
-
79
- // μ˜μ‘΄μ„± ꡐ체
80
- pkg.devDependencies = {
81
- ...pkg.devDependencies,
82
- "tsx": "^4.7.0"
83
- };
84
-
85
- // 기쑴에 ts-nodeκ°€ μžˆλ‹€λ©΄ 제거 (쀑볡 λ°©μ§€)
86
- delete pkg.devDependencies['ts-node'];
156
+ pkg.devDependencies["tsx"] = "^4.7.0";
87
157
  }
88
158
 
89
- // Vitest μ„€μ • (이슈 #3 κ΅¬ν˜„ λΆ€λΆ„)
90
- if (useTest) {
91
- console.log(chalk.yellow(`πŸ§ͺ Vitest μ„€μ • 및 μƒ˜ν”Œ ν…ŒμŠ€νŠΈλ₯Ό μƒμ„±ν•˜λŠ” 쀑...`));
92
-
93
- pkg.scripts = {
94
- ...pkg.scripts,
95
- "test": "vitest",
96
- "test:ui": "vitest --ui",
97
- "test:run": "vitest run"
98
- };
99
-
100
- const testDeps = {
101
- "vitest": "^1.0.0",
102
- "supertest": "^6.3.3"
103
- };
104
-
105
- if (language === 'ts') {
106
- testDeps["@types/supertest"] = "^2.0.12";
107
- }
108
-
109
- pkg.devDependencies = {
110
- ...pkg.devDependencies,
111
- ...testDeps
112
- };
113
-
114
- // Vitest μ„€μ • 파일 생성
159
+ // ν…ŒμŠ€νŠΈ ν™˜κ²½ μ„€μ • (λΉ„μ‚¬μš© μ‹œ κ΄€λ ¨ 파일 및 νŒ¨ν‚€μ§€ 제거)
160
+ if (!useTest) {
115
161
  const configExt = language === 'ts' ? 'ts' : 'js';
116
- const configContent = `import { defineConfig } from 'vitest/config';
117
-
118
- export default defineConfig({
119
- test: {
120
- globals: true,
121
- environment: 'node',
122
- },
123
- });`;
124
- await fs.writeFile(path.join(targetPath, `vitest.config.${configExt}`), configContent);
125
-
126
- // μƒ˜ν”Œ ν…ŒμŠ€νŠΈ 파일 생성
127
162
  const testFileExt = language === 'ts' ? 'ts' : 'js';
128
- const testContent = `import { describe, it, expect } from 'vitest';
129
- import request from 'supertest';
130
- import app from './app.js';
131
-
132
- describe('API Health Check Test', () => {
133
- it('GET / μš”μ²­μ΄ 성곡해야 ν•œλ‹€', async () => {
134
- const res = await request(app).get('/');
135
- expect(res.status).toBe(200);
136
- expect(res.text).toContain('Server is Running');
137
- });
138
- });`;
139
- await fs.writeFile(path.join(targetPath, `src/app.test.${testFileExt}`), testContent);
163
+ await fs.remove(path.join(targetPath, `vitest.config.${configExt}`));
164
+ await fs.remove(path.join(targetPath, `src/app.test.${testFileExt}`));
165
+ delete pkg.scripts.test;
166
+ delete pkg.scripts["test:ui"];
167
+ delete pkg.scripts["test:run"];
168
+ delete pkg.devDependencies.vitest;
169
+ delete pkg.devDependencies.supertest;
170
+ if (pkg.devDependencies["@types/supertest"]) delete pkg.devDependencies["@types/supertest"];
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";
140
182
  }
141
183
 
142
184
  await fs.writeJson(pkgPath, pkg, { spaces: 2 });
143
- console.log(chalk.green(`βœ… λͺ¨λ“  ꡬ성 μ™„λ£Œ!`));
185
+ s.stop('파일 ꡬ성 μ™„λ£Œ!');
144
186
 
145
- // 6. νŒ¨ν‚€μ§€ μžλ™ μ„€μΉ˜
146
- console.log(chalk.yellow(`\nπŸ“¦ μ˜μ‘΄μ„± νŒ¨ν‚€μ§€λ₯Ό μ„€μΉ˜ν•©λ‹ˆλ‹€... (npm install)`));
147
- execSync('npm install', { cwd: targetPath, stdio: 'inherit' });
187
+ // 6. μ˜μ‘΄μ„± μ„€μΉ˜
188
+ const installSpinner = p.spinner();
189
+ installSpinner.start('μ˜μ‘΄μ„± νŒ¨ν‚€μ§€λ₯Ό μ„€μΉ˜ν•˜λŠ” 쀑... (npm install)');
190
+
191
+ try {
192
+ execSync('npm install', { cwd: targetPath, stdio: 'ignore' });
193
+ installSpinner.stop('μ„€μΉ˜ μ™„λ£Œ!');
194
+ } catch (e) {
195
+ installSpinner.stop(chalk.red('μ„€μΉ˜ μ‹€νŒ¨ (μˆ˜λ™ μ„€μΉ˜κ°€ ν•„μš”ν•  수 μžˆμŠ΅λ‹ˆλ‹€)'));
196
+ }
148
197
 
149
- console.log(chalk.green(`\n✨ ν”„λ‘œμ νŠΈ 생성 성곡!`));
150
- console.log(chalk.white(`\nλ‹€μŒ λͺ…λ Ήμ–΄λ₯Ό μž…λ ₯ν•΄ λ³΄μ„Έμš”:\n`));
151
- console.log(chalk.cyan(` cd ${projectName}`));
152
- if (useTest) console.log(chalk.cyan(` npm test`));
153
- console.log(chalk.cyan(` npm run dev\n`));
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`;
205
+
206
+ p.note(chalk.cyan(nextSteps), 'μ‹œμž‘ν•˜λ €λ©΄ λ‹€μŒ λͺ…λ Ήμ–΄λ₯Ό μž…λ ₯ν•˜μ„Έμš”');
207
+ p.outro(chalk.green('✨ λͺ¨λ“  μ€€λΉ„κ°€ λλ‚¬μŠ΅λ‹ˆλ‹€. 즐거운 개발 λ˜μ„Έμš”!'));
154
208
 
155
209
  } catch (error) {
156
- if (error.name === 'ExitPromptError') { // μ˜€νƒ€ μˆ˜μ •: ExitPnromptError -> ExitPromptError
157
- console.log(chalk.yellow('\n\nπŸ‘‹ μ„€μΉ˜λ₯Ό μ€‘λ‹¨ν–ˆμŠ΅λ‹ˆλ‹€.'));
158
- } else {
159
- console.error(chalk.red('\n❌ 였λ₯˜ λ°œμƒ:'), error);
160
- }
210
+ p.cancel(`였λ₯˜ λ°œμƒ: ${error.message}`);
211
+ process.exit(1);
161
212
  }
162
213
  }
163
214
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-express-esm",
3
- "version": "1.1.11",
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",
@@ -25,7 +25,7 @@
25
25
  "author": "munjuin",
26
26
  "license": "MIT",
27
27
  "dependencies": {
28
- "@inquirer/prompts": "^8.1.0",
28
+ "@clack/prompts": "^0.11.0",
29
29
  "chalk": "^5.6.2",
30
30
  "commander": "^14.0.2",
31
31
  "fs-extra": "^11.3.2",
@@ -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=8080PORT=3000
2
- NODE_ENV=development
1
+ # PostgreSQL Connection String
2
+ # ν˜•μ‹: postgresql://μ‚¬μš©μž:λΉ„λ°€λ²ˆν˜Έ@호슀트:포트/λ°μ΄ν„°λ² μ΄μŠ€λͺ…?schema=public
3
+ DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/myapp?schema=public"