@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 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,2 @@
1
+ #!/usr/bin/env node
2
+ require('../dist/cli/index.js');
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -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>;
@@ -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
+ }
@@ -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,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -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
+ }