@hamjimin/xplat-back 0.1.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 +289 -0
- package/bin/xplat-back +2 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +78 -0
- package/dist/cli/init.d.ts +16 -0
- package/dist/cli/init.js +264 -0
- package/dist/cli/templates/app.ts.example +26 -0
- package/dist/cli/templates/package.json.example +23 -0
- package/dist/cli/templates/restapi/health.ts.example +17 -0
- package/dist/cli/templates/server.ts.example +40 -0
- package/dist/cli/templates/tsconfig.json.example +21 -0
- package/dist/createApp.d.ts +32 -0
- package/dist/createApp.js +145 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +18 -0
- package/dist/types/index.d.ts +75 -0
- package/dist/types/index.js +2 -0
- package/dist/util/route.d.ts +24 -0
- package/dist/util/route.js +86 -0
- package/package.json +46 -0
package/README.md
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
# xplat-back
|
|
2
|
+
|
|
3
|
+
Express + TypeScript 백엔드 스캐폴딩 도구 (zium-backend 기반)
|
|
4
|
+
|
|
5
|
+
파일명과 API endpoint가 자동으로 일치하는 라우팅 시스템을 제공합니다. 디렉토리 구조를 기반으로 자동으로 Express 라우터를 생성하여, 파일명만으로 API endpoint를 매핑할 수 있습니다.
|
|
6
|
+
|
|
7
|
+
## 특징
|
|
8
|
+
|
|
9
|
+
- 🚀 **파일 기반 라우팅**: 파일명과 디렉토리 구조가 API endpoint로 자동 매핑
|
|
10
|
+
- 📁 **자동 라우터 생성**: 디렉토리 구조를 재귀적으로 탐색하여 라우터 자동 생성
|
|
11
|
+
- ⚡ **빠른 프로젝트 설정**: CLI를 통한 프로젝트 초기화
|
|
12
|
+
- 🔧 **TypeScript 지원**: 완전한 TypeScript 타입 정의 포함
|
|
13
|
+
- 🎯 **zium-backend 기반**: 검증된 프로젝트 구조 기반
|
|
14
|
+
|
|
15
|
+
## 설치
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install xplat-back
|
|
19
|
+
# 또는
|
|
20
|
+
yarn add xplat-back
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## 빠른 시작
|
|
24
|
+
|
|
25
|
+
### CLI를 통한 프로젝트 초기화
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npx xplat-back [project-name]
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
옵션:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npx xplat-back my-project --port 3000 --cors "http://localhost:3000,http://localhost:5173"
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
사용 가능한 옵션:
|
|
38
|
+
- `-p, --port <port>`: 서버 포트 (기본값: 4000)
|
|
39
|
+
- `-c, --cors <origins>`: CORS 허용 origins (쉼표로 구분)
|
|
40
|
+
- `-r, --router-dir <directory>`: 라우터 디렉토리 (기본값: src/restapi)
|
|
41
|
+
- `--router-path <path>`: 라우터 base path (기본값: /restapi)
|
|
42
|
+
|
|
43
|
+
### 수동 설정
|
|
44
|
+
|
|
45
|
+
#### 1. 패키지 설치
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npm install express xplat-back
|
|
49
|
+
npm install -D typescript @types/express @types/node ts-node tsconfig-paths
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
#### 2. Express 앱 생성
|
|
53
|
+
|
|
54
|
+
`src/app.ts`:
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
import express, { Application } from 'express';
|
|
58
|
+
import * as path from 'path';
|
|
59
|
+
import { createApp } from 'xplat-back';
|
|
60
|
+
|
|
61
|
+
const app: Application = createApp({
|
|
62
|
+
cors: {
|
|
63
|
+
origin: ['http://localhost:3000'],
|
|
64
|
+
credentials: true,
|
|
65
|
+
},
|
|
66
|
+
router: {
|
|
67
|
+
directory: path.join(__dirname, 'restapi'),
|
|
68
|
+
basePath: '/restapi',
|
|
69
|
+
},
|
|
70
|
+
trustProxy: true,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const port = parseInt(process.env.PORT || '4000', 10);
|
|
74
|
+
|
|
75
|
+
app.listen(port, () => {
|
|
76
|
+
console.log(`🚀 Server is running on port ${port}`);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
export default app;
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
#### 3. 라우트 파일 생성
|
|
83
|
+
|
|
84
|
+
`src/restapi/health.ts`:
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
import express, { Request, Response } from 'express';
|
|
88
|
+
|
|
89
|
+
const router = express.Router();
|
|
90
|
+
|
|
91
|
+
router.get('/', (req: Request, res: Response) => {
|
|
92
|
+
res.json({
|
|
93
|
+
success: true,
|
|
94
|
+
message: 'Server is healthy',
|
|
95
|
+
timestamp: new Date().toISOString(),
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
export = router;
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
이제 `GET /restapi/health` 엔드포인트가 자동으로 생성됩니다!
|
|
103
|
+
|
|
104
|
+
#### 4. 서버 실행
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
npm run dev
|
|
108
|
+
# 또는
|
|
109
|
+
ts-node -r tsconfig-paths/register src/app.ts
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## API 문서
|
|
113
|
+
|
|
114
|
+
### `createApp(config?)`
|
|
115
|
+
|
|
116
|
+
Express 애플리케이션을 생성합니다.
|
|
117
|
+
|
|
118
|
+
#### Parameters
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
interface AppConfig {
|
|
122
|
+
cors?: {
|
|
123
|
+
origin?: string[] | ((origin: string | undefined, callback: (err: Error | null, allow?: boolean) => void) => void);
|
|
124
|
+
credentials?: boolean;
|
|
125
|
+
};
|
|
126
|
+
json?: {
|
|
127
|
+
limit?: string;
|
|
128
|
+
};
|
|
129
|
+
urlencoded?: {
|
|
130
|
+
extended?: boolean;
|
|
131
|
+
};
|
|
132
|
+
router?: {
|
|
133
|
+
directory?: string;
|
|
134
|
+
basePath?: string;
|
|
135
|
+
};
|
|
136
|
+
trustProxy?: boolean;
|
|
137
|
+
middleware?: Array<(req: Request, res: Response, next: NextFunction) => void>;
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
#### 예제
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
import { createApp } from 'xplat-back';
|
|
145
|
+
|
|
146
|
+
const app = createApp({
|
|
147
|
+
cors: {
|
|
148
|
+
origin: process.env.CORS_ALLOW_ORIGINS?.split(',') ?? ['*'],
|
|
149
|
+
credentials: true,
|
|
150
|
+
},
|
|
151
|
+
router: {
|
|
152
|
+
directory: path.join(__dirname, 'restapi'),
|
|
153
|
+
basePath: '/restapi',
|
|
154
|
+
},
|
|
155
|
+
middleware: [
|
|
156
|
+
// 커스텀 미들웨어
|
|
157
|
+
(req, res, next) => {
|
|
158
|
+
console.log(`${req.method} ${req.path}`);
|
|
159
|
+
next();
|
|
160
|
+
},
|
|
161
|
+
],
|
|
162
|
+
});
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### `createRouter(directory)`
|
|
166
|
+
|
|
167
|
+
디렉토리 구조를 기반으로 Express 라우터를 생성합니다.
|
|
168
|
+
|
|
169
|
+
#### Parameters
|
|
170
|
+
|
|
171
|
+
- `directory` (string): 라우트 파일들이 위치한 디렉토리 경로
|
|
172
|
+
|
|
173
|
+
#### 예제
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
import { createRouter } from 'xplat-back';
|
|
177
|
+
import * as path from 'path';
|
|
178
|
+
|
|
179
|
+
const router = createRouter(path.join(__dirname, 'restapi'));
|
|
180
|
+
app.use('/restapi', router);
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
#### 라우팅 규칙
|
|
184
|
+
|
|
185
|
+
디렉토리 구조가 다음과 같을 때:
|
|
186
|
+
|
|
187
|
+
```
|
|
188
|
+
restapi/
|
|
189
|
+
├── health.ts → GET /restapi/health
|
|
190
|
+
├── user/
|
|
191
|
+
│ ├── profile.ts → GET /restapi/user/profile
|
|
192
|
+
│ └── settings.ts → GET /restapi/user/settings
|
|
193
|
+
└── admin/
|
|
194
|
+
└── users.ts → GET /restapi/admin/users
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
- 파일명이 endpoint로 변환됩니다 (`.ts`, `.js` 확장자 제거)
|
|
198
|
+
- 디렉토리는 경로의 일부가 됩니다
|
|
199
|
+
- 각 파일은 Express Router를 export해야 합니다
|
|
200
|
+
|
|
201
|
+
## 라우트 파일 작성법
|
|
202
|
+
|
|
203
|
+
각 라우트 파일은 Express Router를 export해야 합니다:
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
import express, { Request, Response } from 'express';
|
|
207
|
+
|
|
208
|
+
const router = express.Router();
|
|
209
|
+
|
|
210
|
+
// GET /restapi/example
|
|
211
|
+
router.get('/', (req: Request, res: Response) => {
|
|
212
|
+
res.json({ message: 'Hello World' });
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// POST /restapi/example
|
|
216
|
+
router.post('/', (req: Request, res: Response) => {
|
|
217
|
+
res.json({ message: 'Created' });
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
export = router;
|
|
221
|
+
// 또는
|
|
222
|
+
export default router;
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## 프로젝트 구조 예시
|
|
226
|
+
|
|
227
|
+
```
|
|
228
|
+
my-backend-project/
|
|
229
|
+
├── src/
|
|
230
|
+
│ ├── app.ts # Express 앱 설정
|
|
231
|
+
│ ├── server.ts # 서버 시작 (선택적)
|
|
232
|
+
│ └── restapi/ # API 라우트 파일들
|
|
233
|
+
│ ├── health.ts # GET /restapi/health
|
|
234
|
+
│ ├── user/
|
|
235
|
+
│ │ └── profile.ts # GET /restapi/user/profile
|
|
236
|
+
│ └── admin/
|
|
237
|
+
│ └── users.ts # GET /restapi/admin/users
|
|
238
|
+
├── dist/ # TypeScript 컴파일 결과
|
|
239
|
+
├── tsconfig.json
|
|
240
|
+
├── package.json
|
|
241
|
+
└── .env
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
## 환경변수
|
|
245
|
+
|
|
246
|
+
`.env` 파일에서 설정할 수 있는 환경변수:
|
|
247
|
+
|
|
248
|
+
```env
|
|
249
|
+
NODE_ENV=development
|
|
250
|
+
PORT=4000
|
|
251
|
+
CORS_ALLOW_ORIGINS=http://localhost:3000,http://localhost:5173
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
## TypeScript 설정
|
|
255
|
+
|
|
256
|
+
권장 `tsconfig.json`:
|
|
257
|
+
|
|
258
|
+
```json
|
|
259
|
+
{
|
|
260
|
+
"compilerOptions": {
|
|
261
|
+
"target": "ES2021",
|
|
262
|
+
"outDir": "./dist",
|
|
263
|
+
"rootDir": "./src",
|
|
264
|
+
"strict": true,
|
|
265
|
+
"esModuleInterop": true,
|
|
266
|
+
"module": "commonjs",
|
|
267
|
+
"moduleResolution": "node",
|
|
268
|
+
"sourceMap": true,
|
|
269
|
+
"baseUrl": "./src",
|
|
270
|
+
"paths": {
|
|
271
|
+
"@/*": ["*"]
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
"include": ["src/**/*"],
|
|
275
|
+
"exclude": ["node_modules", "dist"]
|
|
276
|
+
}
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
## 라이센스
|
|
280
|
+
|
|
281
|
+
ISC
|
|
282
|
+
|
|
283
|
+
## 기여
|
|
284
|
+
|
|
285
|
+
버그 리포트 및 기능 제안은 이슈로 등록해주세요.
|
|
286
|
+
|
|
287
|
+
## 관련 프로젝트
|
|
288
|
+
|
|
289
|
+
이 패키지는 [zium-backend](https://github.com/your-org/zium-backend) 프로젝트의 구조를 기반으로 만들어졌습니다.
|
package/bin/xplat-back
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
const init_1 = require("./init");
|
|
5
|
+
/**
|
|
6
|
+
* CLI 진입점
|
|
7
|
+
*/
|
|
8
|
+
async function main() {
|
|
9
|
+
try {
|
|
10
|
+
const args = process.argv.slice(2);
|
|
11
|
+
// 간단한 옵션 파싱
|
|
12
|
+
const config = {};
|
|
13
|
+
for (let i = 0; i < args.length; i++) {
|
|
14
|
+
const arg = args[i];
|
|
15
|
+
switch (arg) {
|
|
16
|
+
case '--port':
|
|
17
|
+
case '-p':
|
|
18
|
+
config.port = args[++i];
|
|
19
|
+
break;
|
|
20
|
+
case '--cors':
|
|
21
|
+
case '-c':
|
|
22
|
+
config.corsOrigins = args[++i];
|
|
23
|
+
break;
|
|
24
|
+
case '--router-dir':
|
|
25
|
+
case '-r':
|
|
26
|
+
config.routerDirectory = args[++i];
|
|
27
|
+
break;
|
|
28
|
+
case '--router-path':
|
|
29
|
+
config.routerBasePath = args[++i];
|
|
30
|
+
break;
|
|
31
|
+
case '--help':
|
|
32
|
+
case '-h':
|
|
33
|
+
showHelp();
|
|
34
|
+
process.exit(0);
|
|
35
|
+
break;
|
|
36
|
+
default:
|
|
37
|
+
if (!arg.startsWith('-')) {
|
|
38
|
+
config.projectName = arg;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
await (0, init_1.initializeProject)(config);
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
console.error('❌ 초기화 중 오류가 발생했습니다:', error.message);
|
|
46
|
+
if (process.env.DEBUG) {
|
|
47
|
+
console.error(error);
|
|
48
|
+
}
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* 도움말 출력
|
|
54
|
+
*/
|
|
55
|
+
function showHelp() {
|
|
56
|
+
console.log(`
|
|
57
|
+
xplat-back CLI
|
|
58
|
+
|
|
59
|
+
Usage:
|
|
60
|
+
xplat-back [options] [project-name]
|
|
61
|
+
|
|
62
|
+
Options:
|
|
63
|
+
-p, --port <port> 서버 포트 (기본값: 4000)
|
|
64
|
+
-c, --cors <origins> CORS 허용 origins (쉼표로 구분, 기본값: http://localhost:3000)
|
|
65
|
+
-r, --router-dir <directory> 라우터 디렉토리 (기본값: src/restapi)
|
|
66
|
+
--router-path <path> 라우터 base path (기본값: /restapi)
|
|
67
|
+
-h, --help 도움말 출력
|
|
68
|
+
|
|
69
|
+
Examples:
|
|
70
|
+
xplat-back
|
|
71
|
+
xplat-back my-project
|
|
72
|
+
xplat-back --port 3000 --cors "http://localhost:3000,http://localhost:5173"
|
|
73
|
+
`);
|
|
74
|
+
}
|
|
75
|
+
// CLI 실행
|
|
76
|
+
if (require.main === module) {
|
|
77
|
+
main();
|
|
78
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 초기화 설정 인터페이스
|
|
3
|
+
*/
|
|
4
|
+
export interface InitConfig {
|
|
5
|
+
projectName?: string;
|
|
6
|
+
port?: string;
|
|
7
|
+
corsOrigins?: string;
|
|
8
|
+
routerDirectory?: string;
|
|
9
|
+
routerBasePath?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* 프로젝트 초기화 로직
|
|
13
|
+
*
|
|
14
|
+
* @param config 초기화 설정
|
|
15
|
+
*/
|
|
16
|
+
export declare function initializeProject(config?: InitConfig): Promise<void>;
|
package/dist/cli/init.js
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
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
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.initializeProject = initializeProject;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
/**
|
|
40
|
+
* 프로젝트 초기화 로직
|
|
41
|
+
*
|
|
42
|
+
* @param config 초기화 설정
|
|
43
|
+
*/
|
|
44
|
+
async function initializeProject(config = {}) {
|
|
45
|
+
const projectRoot = process.cwd();
|
|
46
|
+
const projectName = config.projectName || path.basename(projectRoot);
|
|
47
|
+
const port = config.port || '4000';
|
|
48
|
+
const corsOrigins = config.corsOrigins || 'http://localhost:3000';
|
|
49
|
+
const routerDirectory = config.routerDirectory || 'src/restapi';
|
|
50
|
+
const routerBasePath = config.routerBasePath || '/restapi';
|
|
51
|
+
console.log('\n🚀 xplat-back 프로젝트 초기화를 시작합니다...\n');
|
|
52
|
+
// src 디렉토리 생성
|
|
53
|
+
const srcDir = path.join(projectRoot, 'src');
|
|
54
|
+
if (!fs.existsSync(srcDir)) {
|
|
55
|
+
fs.mkdirSync(srcDir, { recursive: true });
|
|
56
|
+
console.log(`✅ src 디렉토리가 생성되었습니다.`);
|
|
57
|
+
}
|
|
58
|
+
// restapi 디렉토리 생성
|
|
59
|
+
const restapiDir = path.join(projectRoot, routerDirectory);
|
|
60
|
+
if (!fs.existsSync(restapiDir)) {
|
|
61
|
+
fs.mkdirSync(restapiDir, { recursive: true });
|
|
62
|
+
console.log(`✅ ${routerDirectory} 디렉토리가 생성되었습니다.`);
|
|
63
|
+
}
|
|
64
|
+
// app.ts 파일 생성
|
|
65
|
+
await createAppFile(projectRoot, port, corsOrigins, routerDirectory, routerBasePath);
|
|
66
|
+
// server.ts 파일 생성 (선택적)
|
|
67
|
+
await createServerFile(projectRoot, port, corsOrigins, routerDirectory, routerBasePath);
|
|
68
|
+
// tsconfig.json 파일 생성
|
|
69
|
+
await createTsConfigFile(projectRoot);
|
|
70
|
+
// package.json 파일 생성 (없는 경우만)
|
|
71
|
+
await createPackageJsonFile(projectRoot, projectName);
|
|
72
|
+
// .env 파일 생성
|
|
73
|
+
await createEnvFile(projectRoot, port, corsOrigins);
|
|
74
|
+
// 예시 라우트 파일 생성
|
|
75
|
+
await createExampleRouteFile(restapiDir);
|
|
76
|
+
console.log('\n✅ 프로젝트 초기화가 완료되었습니다!');
|
|
77
|
+
console.log(`\n생성된 파일:`);
|
|
78
|
+
console.log(` - src/app.ts`);
|
|
79
|
+
console.log(` - src/server.ts (선택적)`);
|
|
80
|
+
console.log(` - tsconfig.json`);
|
|
81
|
+
console.log(` - package.json (없던 경우만)`);
|
|
82
|
+
console.log(` - .env`);
|
|
83
|
+
console.log(` - ${routerDirectory}/health.ts`);
|
|
84
|
+
console.log(`\n다음 단계:`);
|
|
85
|
+
console.log(` 1. npm install (또는 yarn install)`);
|
|
86
|
+
console.log(` 2. npm run dev (또는 yarn dev)로 서버 실행`);
|
|
87
|
+
console.log(` 3. ${routerDirectory} 디렉토리에 API 라우트 파일을 추가하세요\n`);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* app.ts 파일 생성
|
|
91
|
+
*/
|
|
92
|
+
async function createAppFile(projectRoot, port, corsOrigins, routerDirectory, routerBasePath) {
|
|
93
|
+
const appFilePath = path.join(projectRoot, 'src', 'app.ts');
|
|
94
|
+
if (fs.existsSync(appFilePath)) {
|
|
95
|
+
console.log(`⚠️ src/app.ts 파일이 이미 존재합니다. 건너뜁니다.`);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const appContent = `import express, { Application } from 'express';
|
|
99
|
+
import * as path from 'path';
|
|
100
|
+
import { createApp } from 'xplat-back';
|
|
101
|
+
|
|
102
|
+
const app: Application = createApp({
|
|
103
|
+
cors: {
|
|
104
|
+
origin: process.env.CORS_ALLOW_ORIGINS
|
|
105
|
+
? process.env.CORS_ALLOW_ORIGINS.split(',').map(s => s.trim())
|
|
106
|
+
: ['${corsOrigins}'],
|
|
107
|
+
credentials: true,
|
|
108
|
+
},
|
|
109
|
+
router: {
|
|
110
|
+
directory: path.join(__dirname, '${routerDirectory.replace('src/', '')}'),
|
|
111
|
+
basePath: '${routerBasePath}',
|
|
112
|
+
},
|
|
113
|
+
trustProxy: true,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const port = parseInt(process.env.PORT || '${port}', 10);
|
|
117
|
+
|
|
118
|
+
app.listen(port, () => {
|
|
119
|
+
console.log(\`🚀 Server is running on port \${port}\`);
|
|
120
|
+
console.log(\`📡 API endpoint: http://localhost:\${port}${routerBasePath}\`);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
export default app;
|
|
124
|
+
`;
|
|
125
|
+
fs.writeFileSync(appFilePath, appContent, 'utf-8');
|
|
126
|
+
console.log(`✅ src/app.ts 파일이 생성되었습니다.`);
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* server.ts 파일 생성 (선택적)
|
|
130
|
+
*/
|
|
131
|
+
async function createServerFile(projectRoot, port, corsOrigins, routerDirectory, routerBasePath) {
|
|
132
|
+
const serverFilePath = path.join(projectRoot, 'src', 'server.ts');
|
|
133
|
+
if (fs.existsSync(serverFilePath)) {
|
|
134
|
+
console.log(`⚠️ src/server.ts 파일이 이미 존재합니다. 건너뜁니다.`);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const serverContent = `import * as path from 'path';
|
|
138
|
+
import { createApp } from 'xplat-back';
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* 서버 시작 함수
|
|
142
|
+
*/
|
|
143
|
+
async function startServer() {
|
|
144
|
+
const app = createApp({
|
|
145
|
+
cors: {
|
|
146
|
+
origin: process.env.CORS_ALLOW_ORIGINS
|
|
147
|
+
? process.env.CORS_ALLOW_ORIGINS.split(',').map(s => s.trim())
|
|
148
|
+
: ['${corsOrigins}'],
|
|
149
|
+
credentials: true,
|
|
150
|
+
},
|
|
151
|
+
router: {
|
|
152
|
+
directory: path.join(__dirname, '${routerDirectory.replace('src/', '')}'),
|
|
153
|
+
basePath: '${routerBasePath}',
|
|
154
|
+
},
|
|
155
|
+
trustProxy: true,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const port = parseInt(process.env.PORT || '${port}', 10);
|
|
159
|
+
|
|
160
|
+
app.listen(port, () => {
|
|
161
|
+
console.log(\`🚀 Server is running on port \${port}\`);
|
|
162
|
+
console.log(\`📡 API endpoint: http://localhost:\${port}${routerBasePath}\`);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
return app;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// 서버 시작
|
|
169
|
+
if (require.main === module) {
|
|
170
|
+
startServer().catch((error) => {
|
|
171
|
+
console.error('Failed to start server:', error);
|
|
172
|
+
process.exit(1);
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export default startServer;
|
|
177
|
+
`;
|
|
178
|
+
fs.writeFileSync(serverFilePath, serverContent, 'utf-8');
|
|
179
|
+
console.log(`✅ src/server.ts 파일이 생성되었습니다.`);
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* tsconfig.json 파일 생성
|
|
183
|
+
*/
|
|
184
|
+
async function createTsConfigFile(projectRoot) {
|
|
185
|
+
const tsconfigPath = path.join(projectRoot, 'tsconfig.json');
|
|
186
|
+
if (fs.existsSync(tsconfigPath)) {
|
|
187
|
+
console.log(`⚠️ tsconfig.json 파일이 이미 존재합니다. 건너뜁니다.`);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const templatePath = path.join(__dirname, 'templates', 'tsconfig.json.example');
|
|
191
|
+
const templateContent = fs.readFileSync(templatePath, 'utf-8');
|
|
192
|
+
fs.writeFileSync(tsconfigPath, templateContent, 'utf-8');
|
|
193
|
+
console.log(`✅ tsconfig.json 파일이 생성되었습니다.`);
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* package.json 파일 생성 (없는 경우만)
|
|
197
|
+
*/
|
|
198
|
+
async function createPackageJsonFile(projectRoot, projectName) {
|
|
199
|
+
const packageJsonPath = path.join(projectRoot, 'package.json');
|
|
200
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
201
|
+
console.log(`⚠️ package.json 파일이 이미 존재합니다. 건너뜁니다.`);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const packageJsonContent = {
|
|
205
|
+
name: projectName.toLowerCase().replace(/\s+/g, '-'),
|
|
206
|
+
version: '0.1.0',
|
|
207
|
+
description: 'Backend project generated with xplat-back',
|
|
208
|
+
main: 'dist/app.js',
|
|
209
|
+
scripts: {
|
|
210
|
+
build: 'tsc',
|
|
211
|
+
dev: 'ts-node -r tsconfig-paths/register src/app.ts',
|
|
212
|
+
start: 'node dist/app.js',
|
|
213
|
+
clean: 'rm -rf dist',
|
|
214
|
+
},
|
|
215
|
+
dependencies: {
|
|
216
|
+
express: '^4.18.2',
|
|
217
|
+
'xplat-back': '^0.1.0',
|
|
218
|
+
},
|
|
219
|
+
devDependencies: {
|
|
220
|
+
'@types/express': '^4.17.21',
|
|
221
|
+
'@types/node': '^20.10.0',
|
|
222
|
+
'ts-node': '^10.9.2',
|
|
223
|
+
'tsconfig-paths': '^4.2.0',
|
|
224
|
+
typescript: '^5.3.3',
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJsonContent, null, 2) + '\n', 'utf-8');
|
|
228
|
+
console.log(`✅ package.json 파일이 생성되었습니다.`);
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* .env 파일 생성
|
|
232
|
+
*/
|
|
233
|
+
async function createEnvFile(projectRoot, port, corsOrigins) {
|
|
234
|
+
const envPath = path.join(projectRoot, '.env');
|
|
235
|
+
if (fs.existsSync(envPath)) {
|
|
236
|
+
console.log(`⚠️ .env 파일이 이미 존재합니다. 건너뜁니다.`);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const envContent = `# Application Configuration
|
|
240
|
+
NODE_ENV=development
|
|
241
|
+
PORT=${port}
|
|
242
|
+
|
|
243
|
+
# CORS Configuration
|
|
244
|
+
CORS_ALLOW_ORIGINS=${corsOrigins}
|
|
245
|
+
|
|
246
|
+
# Add your environment variables below
|
|
247
|
+
`;
|
|
248
|
+
fs.writeFileSync(envPath, envContent, 'utf-8');
|
|
249
|
+
console.log(`✅ .env 파일이 생성되었습니다.`);
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* 예시 라우트 파일 생성
|
|
253
|
+
*/
|
|
254
|
+
async function createExampleRouteFile(restapiDir) {
|
|
255
|
+
const healthPath = path.join(restapiDir, 'health.ts');
|
|
256
|
+
if (fs.existsSync(healthPath)) {
|
|
257
|
+
console.log(`⚠️ health.ts 파일이 이미 존재합니다. 건너뜁니다.`);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
const templatePath = path.join(__dirname, 'templates', 'restapi', 'health.ts.example');
|
|
261
|
+
const templateContent = fs.readFileSync(templatePath, 'utf-8');
|
|
262
|
+
fs.writeFileSync(healthPath, templateContent, 'utf-8');
|
|
263
|
+
console.log(`✅ health.ts 파일이 생성되었습니다.`);
|
|
264
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import express, { Application } from 'express';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { createApp } from 'xplat-back';
|
|
4
|
+
|
|
5
|
+
const app: Application = createApp({
|
|
6
|
+
cors: {
|
|
7
|
+
origin: process.env.CORS_ALLOW_ORIGINS
|
|
8
|
+
? process.env.CORS_ALLOW_ORIGINS.split(',').map(s => s.trim())
|
|
9
|
+
: ['http://localhost:3000'],
|
|
10
|
+
credentials: true,
|
|
11
|
+
},
|
|
12
|
+
router: {
|
|
13
|
+
directory: path.join(__dirname, 'restapi'),
|
|
14
|
+
basePath: '/restapi',
|
|
15
|
+
},
|
|
16
|
+
trustProxy: true,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const port = parseInt(process.env.PORT || '4000', 10);
|
|
20
|
+
|
|
21
|
+
app.listen(port, () => {
|
|
22
|
+
console.log(`🚀 Server is running on port ${port}`);
|
|
23
|
+
console.log(`📡 API endpoint: http://localhost:${port}/restapi`);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export default app;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "my-backend-project",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Backend project generated with xplat-back",
|
|
5
|
+
"main": "dist/app.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"build": "tsc",
|
|
8
|
+
"dev": "ts-node -r tsconfig-paths/register src/app.ts",
|
|
9
|
+
"start": "node dist/app.js",
|
|
10
|
+
"clean": "rm -rf dist"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"express": "^4.18.2",
|
|
14
|
+
"xplat-back": "^0.1.0"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@types/express": "^4.17.21",
|
|
18
|
+
"@types/node": "^20.10.0",
|
|
19
|
+
"ts-node": "^10.9.2",
|
|
20
|
+
"tsconfig-paths": "^4.2.0",
|
|
21
|
+
"typescript": "^5.3.3"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import express, { Request, Response } from 'express';
|
|
2
|
+
|
|
3
|
+
const router = express.Router();
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 헬스 체크 엔드포인트
|
|
7
|
+
* GET /restapi/health
|
|
8
|
+
*/
|
|
9
|
+
router.get('/', (req: Request, res: Response) => {
|
|
10
|
+
res.json({
|
|
11
|
+
success: true,
|
|
12
|
+
message: 'Server is healthy',
|
|
13
|
+
timestamp: new Date().toISOString(),
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export = router;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import * as path from 'path';
|
|
2
|
+
import { createApp } from 'xplat-back';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 서버 시작 함수
|
|
6
|
+
*/
|
|
7
|
+
async function startServer() {
|
|
8
|
+
const app = createApp({
|
|
9
|
+
cors: {
|
|
10
|
+
origin: process.env.CORS_ALLOW_ORIGINS
|
|
11
|
+
? process.env.CORS_ALLOW_ORIGINS.split(',').map(s => s.trim())
|
|
12
|
+
: ['http://localhost:3000'],
|
|
13
|
+
credentials: true,
|
|
14
|
+
},
|
|
15
|
+
router: {
|
|
16
|
+
directory: path.join(__dirname, 'restapi'),
|
|
17
|
+
basePath: '/restapi',
|
|
18
|
+
},
|
|
19
|
+
trustProxy: true,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const port = parseInt(process.env.PORT || '4000', 10);
|
|
23
|
+
|
|
24
|
+
app.listen(port, () => {
|
|
25
|
+
console.log(`🚀 Server is running on port ${port}`);
|
|
26
|
+
console.log(`📡 API endpoint: http://localhost:${port}/restapi`);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
return app;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// 서버 시작
|
|
33
|
+
if (require.main === module) {
|
|
34
|
+
startServer().catch((error) => {
|
|
35
|
+
console.error('Failed to start server:', error);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export default startServer;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2021",
|
|
4
|
+
"outDir": "./dist",
|
|
5
|
+
"rootDir": "./src",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"module": "commonjs",
|
|
9
|
+
"moduleResolution": "node",
|
|
10
|
+
"sourceMap": true,
|
|
11
|
+
"resolveJsonModule": true,
|
|
12
|
+
"skipLibCheck": true,
|
|
13
|
+
"forceConsistentCasingInFileNames": true,
|
|
14
|
+
"baseUrl": "./src",
|
|
15
|
+
"paths": {
|
|
16
|
+
"@/*": ["*"]
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"include": ["src/**/*"],
|
|
20
|
+
"exclude": ["node_modules", "dist"]
|
|
21
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Application } from 'express';
|
|
2
|
+
import { AppConfig } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Express 앱 생성 함수
|
|
5
|
+
*
|
|
6
|
+
* zium-backend의 app.ts 구조를 기반으로 한 Express 애플리케이션을 생성합니다.
|
|
7
|
+
* 파일명과 API endpoint가 자동으로 일치하도록 라우팅 시스템을 포함합니다.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* import { createApp } from 'xplat-back';
|
|
12
|
+
*
|
|
13
|
+
* const app = createApp({
|
|
14
|
+
* cors: {
|
|
15
|
+
* origin: ['http://localhost:3000'],
|
|
16
|
+
* credentials: true
|
|
17
|
+
* },
|
|
18
|
+
* router: {
|
|
19
|
+
* directory: path.join(__dirname, 'restapi'),
|
|
20
|
+
* basePath: '/restapi'
|
|
21
|
+
* }
|
|
22
|
+
* });
|
|
23
|
+
*
|
|
24
|
+
* app.listen(4000, () => {
|
|
25
|
+
* console.log('Server is running on port 4000');
|
|
26
|
+
* });
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* @param config - 앱 설정 옵션
|
|
30
|
+
* @returns Express Application 인스턴스
|
|
31
|
+
*/
|
|
32
|
+
export declare function createApp(config?: AppConfig): Application;
|
|
@@ -0,0 +1,145 @@
|
|
|
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.createApp = createApp;
|
|
40
|
+
const express_1 = __importDefault(require("express"));
|
|
41
|
+
const cookie_parser_1 = __importDefault(require("cookie-parser"));
|
|
42
|
+
const cors_1 = __importDefault(require("cors"));
|
|
43
|
+
const path = __importStar(require("path"));
|
|
44
|
+
const route_1 = require("./util/route");
|
|
45
|
+
/**
|
|
46
|
+
* Express 앱 생성 함수
|
|
47
|
+
*
|
|
48
|
+
* zium-backend의 app.ts 구조를 기반으로 한 Express 애플리케이션을 생성합니다.
|
|
49
|
+
* 파일명과 API endpoint가 자동으로 일치하도록 라우팅 시스템을 포함합니다.
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```typescript
|
|
53
|
+
* import { createApp } from 'xplat-back';
|
|
54
|
+
*
|
|
55
|
+
* const app = createApp({
|
|
56
|
+
* cors: {
|
|
57
|
+
* origin: ['http://localhost:3000'],
|
|
58
|
+
* credentials: true
|
|
59
|
+
* },
|
|
60
|
+
* router: {
|
|
61
|
+
* directory: path.join(__dirname, 'restapi'),
|
|
62
|
+
* basePath: '/restapi'
|
|
63
|
+
* }
|
|
64
|
+
* });
|
|
65
|
+
*
|
|
66
|
+
* app.listen(4000, () => {
|
|
67
|
+
* console.log('Server is running on port 4000');
|
|
68
|
+
* });
|
|
69
|
+
* ```
|
|
70
|
+
*
|
|
71
|
+
* @param config - 앱 설정 옵션
|
|
72
|
+
* @returns Express Application 인스턴스
|
|
73
|
+
*/
|
|
74
|
+
function createApp(config = {}) {
|
|
75
|
+
const app = (0, express_1.default)();
|
|
76
|
+
// Trust proxy 설정 (실제 IP를 얻기 위해)
|
|
77
|
+
if (config.trustProxy !== false) {
|
|
78
|
+
app.set('trust proxy', true);
|
|
79
|
+
}
|
|
80
|
+
// CORS 설정
|
|
81
|
+
app.use((req, res, next) => {
|
|
82
|
+
res.vary('Origin');
|
|
83
|
+
next();
|
|
84
|
+
});
|
|
85
|
+
const corsOrigin = config.cors?.origin ||
|
|
86
|
+
(process.env.CORS_ALLOW_ORIGINS
|
|
87
|
+
? process.env.CORS_ALLOW_ORIGINS.split(',').map(s => s.trim())
|
|
88
|
+
: ['*']);
|
|
89
|
+
app.use((0, cors_1.default)({
|
|
90
|
+
origin: typeof corsOrigin === 'function'
|
|
91
|
+
? corsOrigin
|
|
92
|
+
: corsOrigin,
|
|
93
|
+
credentials: config.cors?.credentials !== false,
|
|
94
|
+
}));
|
|
95
|
+
// Body parser 설정
|
|
96
|
+
app.use(express_1.default.json({ limit: config.json?.limit || '1mb' }));
|
|
97
|
+
app.use(express_1.default.urlencoded({
|
|
98
|
+
extended: config.urlencoded?.extended !== false
|
|
99
|
+
}));
|
|
100
|
+
// Cookie parser 설정
|
|
101
|
+
app.use((0, cookie_parser_1.default)());
|
|
102
|
+
// 커스텀 미들웨어 적용
|
|
103
|
+
if (config.middleware && config.middleware.length > 0) {
|
|
104
|
+
config.middleware.forEach(middleware => {
|
|
105
|
+
app.use(middleware);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
// 라우터 등록
|
|
109
|
+
if (config.router?.directory) {
|
|
110
|
+
const routerDirectory = config.router.directory;
|
|
111
|
+
const basePath = config.router.basePath || '/restapi';
|
|
112
|
+
// 절대 경로가 아니면 현재 작업 디렉토리를 기준으로 해석
|
|
113
|
+
const absoluteRouterDirectory = path.isAbsolute(routerDirectory)
|
|
114
|
+
? routerDirectory
|
|
115
|
+
: path.join(process.cwd(), routerDirectory);
|
|
116
|
+
try {
|
|
117
|
+
const router = (0, route_1.createRouter)(absoluteRouterDirectory);
|
|
118
|
+
app.use(basePath, router);
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
console.warn(`Warning: Failed to load router from directory "${absoluteRouterDirectory}". ` +
|
|
122
|
+
`Make sure the directory exists and contains route files.`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// 404 핸들러
|
|
126
|
+
app.use((req, res) => {
|
|
127
|
+
res.status(404).json({
|
|
128
|
+
success: false,
|
|
129
|
+
message: 'Not Found',
|
|
130
|
+
path: req.path,
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
// 에러 핸들러
|
|
134
|
+
app.use((err, req, res, next) => {
|
|
135
|
+
console.error('Error:', err);
|
|
136
|
+
res.status(500).json({
|
|
137
|
+
success: false,
|
|
138
|
+
message: process.env.NODE_ENV === 'production'
|
|
139
|
+
? 'Internal Server Error'
|
|
140
|
+
: err.message,
|
|
141
|
+
...(process.env.NODE_ENV !== 'production' && { stack: err.stack }),
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
return app;
|
|
145
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* xplat-back
|
|
3
|
+
*
|
|
4
|
+
* Express + TypeScript 백엔드 스캐폴딩 도구 (zium-backend 기반)
|
|
5
|
+
*
|
|
6
|
+
* 파일명과 API endpoint가 자동으로 일치하는 라우팅 시스템을 제공합니다.
|
|
7
|
+
*
|
|
8
|
+
* @packageDocumentation
|
|
9
|
+
*/
|
|
10
|
+
export { createRouter } from './util/route';
|
|
11
|
+
export { createApp } from './createApp';
|
|
12
|
+
export type { AppConfig, ExtendedRequest, ExtendedResponse, } from './types';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* xplat-back
|
|
4
|
+
*
|
|
5
|
+
* Express + TypeScript 백엔드 스캐폴딩 도구 (zium-backend 기반)
|
|
6
|
+
*
|
|
7
|
+
* 파일명과 API endpoint가 자동으로 일치하는 라우팅 시스템을 제공합니다.
|
|
8
|
+
*
|
|
9
|
+
* @packageDocumentation
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.createApp = exports.createRouter = void 0;
|
|
13
|
+
// 라우팅 유틸리티
|
|
14
|
+
var route_1 = require("./util/route");
|
|
15
|
+
Object.defineProperty(exports, "createRouter", { enumerable: true, get: function () { return route_1.createRouter; } });
|
|
16
|
+
// Express 앱 생성 함수
|
|
17
|
+
var createApp_1 = require("./createApp");
|
|
18
|
+
Object.defineProperty(exports, "createApp", { enumerable: true, get: function () { return createApp_1.createApp; } });
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Request, Response, NextFunction } from 'express';
|
|
2
|
+
/**
|
|
3
|
+
* Express 앱 설정 옵션
|
|
4
|
+
*/
|
|
5
|
+
export interface AppConfig {
|
|
6
|
+
/**
|
|
7
|
+
* CORS 설정
|
|
8
|
+
*/
|
|
9
|
+
cors?: {
|
|
10
|
+
/**
|
|
11
|
+
* 허용할 origin 목록 (문자열 배열 또는 함수)
|
|
12
|
+
* 기본값: process.env.CORS_ALLOW_ORIGINS를 쉼표로 분리한 배열, 또는 ['*']
|
|
13
|
+
*/
|
|
14
|
+
origin?: string[] | ((origin: string | undefined, callback: (err: Error | null, allow?: boolean) => void) => void);
|
|
15
|
+
/**
|
|
16
|
+
* Credentials 허용 여부
|
|
17
|
+
* 기본값: true
|
|
18
|
+
*/
|
|
19
|
+
credentials?: boolean;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* JSON body parser 설정
|
|
23
|
+
*/
|
|
24
|
+
json?: {
|
|
25
|
+
/**
|
|
26
|
+
* 최대 body 크기
|
|
27
|
+
* 기본값: '1mb'
|
|
28
|
+
*/
|
|
29
|
+
limit?: string;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* URL encoded body parser 설정
|
|
33
|
+
*/
|
|
34
|
+
urlencoded?: {
|
|
35
|
+
/**
|
|
36
|
+
* extended 옵션
|
|
37
|
+
* 기본값: true
|
|
38
|
+
*/
|
|
39
|
+
extended?: boolean;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* 라우터 설정
|
|
43
|
+
*/
|
|
44
|
+
router?: {
|
|
45
|
+
/**
|
|
46
|
+
* 라우트 파일들이 위치한 디렉토리 경로
|
|
47
|
+
* 기본값: 'src/restapi'
|
|
48
|
+
*/
|
|
49
|
+
directory?: string;
|
|
50
|
+
/**
|
|
51
|
+
* 라우터를 등록할 base path
|
|
52
|
+
* 기본값: '/restapi'
|
|
53
|
+
*/
|
|
54
|
+
basePath?: string;
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Trust proxy 설정
|
|
58
|
+
* 기본값: true
|
|
59
|
+
*/
|
|
60
|
+
trustProxy?: boolean;
|
|
61
|
+
/**
|
|
62
|
+
* 커스텀 미들웨어 함수들
|
|
63
|
+
*/
|
|
64
|
+
middleware?: Array<(req: Request, res: Response, next: NextFunction) => void>;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* 확장된 Express Request 타입
|
|
68
|
+
*/
|
|
69
|
+
export interface ExtendedRequest extends Request {
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* 확장된 Express Response 타입
|
|
73
|
+
*/
|
|
74
|
+
export interface ExtendedResponse extends Response {
|
|
75
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
/**
|
|
3
|
+
* 파일 경로 기준 동적 라우터 생성
|
|
4
|
+
*
|
|
5
|
+
* 디렉토리 구조를 API endpoint로 자동 매핑합니다.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* // 디렉토리 구조:
|
|
9
|
+
* // restapi/
|
|
10
|
+
* // ├── health.ts
|
|
11
|
+
* // ├── user/
|
|
12
|
+
* // │ └── profile.ts
|
|
13
|
+
* // └── admin/
|
|
14
|
+
* // └── users.ts
|
|
15
|
+
*
|
|
16
|
+
* // 결과:
|
|
17
|
+
* // /restapi/health → restapi/health.ts
|
|
18
|
+
* // /restapi/user/profile → restapi/user/profile.ts
|
|
19
|
+
* // /restapi/admin/users → restapi/admin/users.ts
|
|
20
|
+
*
|
|
21
|
+
* @param directory - 라우트 파일들이 위치한 루트 디렉토리
|
|
22
|
+
* @returns Express Router 인스턴스
|
|
23
|
+
*/
|
|
24
|
+
export declare function createRouter(directory: string): express.Router;
|
|
@@ -0,0 +1,86 @@
|
|
|
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.createRouter = createRouter;
|
|
40
|
+
const express_1 = __importDefault(require("express"));
|
|
41
|
+
const fs = __importStar(require("fs"));
|
|
42
|
+
const path = __importStar(require("path"));
|
|
43
|
+
/**
|
|
44
|
+
* 파일 경로 기준 동적 라우터 생성
|
|
45
|
+
*
|
|
46
|
+
* 디렉토리 구조를 API endpoint로 자동 매핑합니다.
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* // 디렉토리 구조:
|
|
50
|
+
* // restapi/
|
|
51
|
+
* // ├── health.ts
|
|
52
|
+
* // ├── user/
|
|
53
|
+
* // │ └── profile.ts
|
|
54
|
+
* // └── admin/
|
|
55
|
+
* // └── users.ts
|
|
56
|
+
*
|
|
57
|
+
* // 결과:
|
|
58
|
+
* // /restapi/health → restapi/health.ts
|
|
59
|
+
* // /restapi/user/profile → restapi/user/profile.ts
|
|
60
|
+
* // /restapi/admin/users → restapi/admin/users.ts
|
|
61
|
+
*
|
|
62
|
+
* @param directory - 라우트 파일들이 위치한 루트 디렉토리
|
|
63
|
+
* @returns Express Router 인스턴스
|
|
64
|
+
*/
|
|
65
|
+
function createRouter(directory) {
|
|
66
|
+
const router = express_1.default.Router();
|
|
67
|
+
const files = fs.readdirSync(directory);
|
|
68
|
+
for (const file of files) {
|
|
69
|
+
const filePath = path.join(directory, file);
|
|
70
|
+
if (fs.statSync(filePath).isDirectory()) {
|
|
71
|
+
// 디렉토리인 경우 재귀적으로 라우터 생성
|
|
72
|
+
const subRouter = createRouter(filePath);
|
|
73
|
+
router.use(`/${file}`, subRouter);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
// 파일인 경우 라우터 생성
|
|
77
|
+
if (file.endsWith('.ts') || file.endsWith('.js')) {
|
|
78
|
+
const routePath = `/${file.replace('.ts', '').replace('.js', '')}`;
|
|
79
|
+
const required = require(filePath);
|
|
80
|
+
const routeModule = required.default || required;
|
|
81
|
+
router.use(routePath, routeModule);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return router;
|
|
86
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hamjimin/xplat-back",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Express + TypeScript 백엔드 스캐폴딩 도구 (zium-backend 기반)",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"bin": {
|
|
8
|
+
"xplat-back": "./bin/xplat-back"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"bin",
|
|
13
|
+
"templates",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc",
|
|
18
|
+
"copy-templates": "mkdir -p dist/cli/templates && cp -r src/cli/templates/* dist/cli/templates/",
|
|
19
|
+
"prebuild": "npm run copy-templates",
|
|
20
|
+
"prepublishOnly": "npm run build",
|
|
21
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"express",
|
|
25
|
+
"typescript",
|
|
26
|
+
"backend",
|
|
27
|
+
"scaffolding",
|
|
28
|
+
"router",
|
|
29
|
+
"api",
|
|
30
|
+
"framework"
|
|
31
|
+
],
|
|
32
|
+
"author": "xplat",
|
|
33
|
+
"license": "ISC",
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"express": "^4.18.2",
|
|
36
|
+
"cookie-parser": "^1.4.6",
|
|
37
|
+
"cors": "^2.8.5"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/express": "^4.17.21",
|
|
41
|
+
"@types/node": "^20.10.0",
|
|
42
|
+
"@types/cookie-parser": "^1.4.6",
|
|
43
|
+
"@types/cors": "^2.8.17",
|
|
44
|
+
"typescript": "^5.3.3"
|
|
45
|
+
}
|
|
46
|
+
}
|