@fuzionx/framework 0.1.8 → 0.1.9

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/cli/db-sync.js ADDED
@@ -0,0 +1,99 @@
1
+ /**
2
+ * db:sync — 모델 기반 스키마 SQL 생성
3
+ *
4
+ * 사용: fx db:sync [--dry-run]
5
+ *
6
+ * 모델의 static 멤버(table, primaryKey, timestamps, softDelete)에서
7
+ * CREATE TABLE SQL을 생성합니다. 실제 마이그레이션 시스템이 아닌
8
+ * 빠른 프로토타이핑용 도구입니다.
9
+ *
10
+ * @see docs/framework/02-database-orm.md
11
+ */
12
+ import { promises as fs } from 'node:fs';
13
+ import path from 'node:path';
14
+
15
+ /**
16
+ * 모델 클래스에서 CREATE TABLE SQL 생성
17
+ * @param {typeof import('../lib/Model.js').default} ModelClass
18
+ * @returns {string}
19
+ */
20
+ export function generateCreateTable(ModelClass) {
21
+ const table = ModelClass.table;
22
+ const pk = ModelClass.primaryKey || 'id';
23
+ const timestamps = ModelClass.timestamps !== false;
24
+ const softDelete = ModelClass.softDelete || false;
25
+
26
+ if (!table) return '';
27
+
28
+ const columns = [];
29
+
30
+ // 기본키
31
+ columns.push(` \`${pk}\` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY`);
32
+
33
+ // timestamps
34
+ if (timestamps) {
35
+ columns.push(` \`created_at\` DATETIME DEFAULT CURRENT_TIMESTAMP`);
36
+ columns.push(` \`updated_at\` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP`);
37
+ }
38
+
39
+ // softDelete
40
+ if (softDelete) {
41
+ columns.push(` \`deleted_at\` DATETIME DEFAULT NULL`);
42
+ }
43
+
44
+ return `CREATE TABLE IF NOT EXISTS \`${table}\` (\n${columns.join(',\n')}\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;\n`;
45
+ }
46
+
47
+ /**
48
+ * 디렉토리의 모든 모델에서 SQL 생성
49
+ * @param {string} modelsDir
50
+ * @returns {Promise<string>}
51
+ */
52
+ export async function generateSchema(modelsDir) {
53
+ const sqls = [];
54
+
55
+ try {
56
+ const entries = await fs.readdir(modelsDir, { withFileTypes: true });
57
+ const files = entries
58
+ .filter(e => e.isFile() && e.name.endsWith('.js') && !e.name.startsWith('.'))
59
+ .map(e => path.join(modelsDir, e.name));
60
+
61
+ for (const file of files) {
62
+ const mod = await import(file);
63
+ const ModelClass = mod.default;
64
+ if (ModelClass?.table) {
65
+ sqls.push(generateCreateTable(ModelClass));
66
+ }
67
+ }
68
+ } catch {
69
+ // 디렉토리 없음
70
+ }
71
+
72
+ return sqls.join('\n');
73
+ }
74
+
75
+ /**
76
+ * CLI 엔트리
77
+ */
78
+ export async function run(args) {
79
+ const dryRun = args.includes('--dry-run');
80
+ const modelsDir = path.resolve('app/models');
81
+
82
+ console.log(`[db:sync] Scanning ${modelsDir}...`);
83
+
84
+ const sql = await generateSchema(modelsDir);
85
+
86
+ if (!sql) {
87
+ console.log('[db:sync] No models found.');
88
+ return;
89
+ }
90
+
91
+ if (dryRun) {
92
+ console.log('\n-- DRY RUN (preview only) --\n');
93
+ console.log(sql);
94
+ } else {
95
+ console.log('\n-- Generated SQL --\n');
96
+ console.log(sql);
97
+ console.log('-- Execute this SQL against your database to sync schema --');
98
+ }
99
+ }
package/cli/fx.js ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { run } from './index.js';
3
+ run(process.argv.slice(2));
package/cli/index.js ADDED
@@ -0,0 +1,329 @@
1
+ /**
2
+ * CLI — fx 명령어 핸들러
3
+ *
4
+ * fx new <name> 앱 스캐폴딩
5
+ * fx make:controller <Name> 컨트롤러 생성
6
+ * fx make:service <Name> 서비스 생성
7
+ * fx make:model <Name> 모델 생성
8
+ * fx make:middleware <Name> 미들웨어 생성
9
+ * fx make:job <Name> Job 생성
10
+ * fx make:task <Name> Task 생성
11
+ * fx make:ws <Name> WsHandler 생성
12
+ * fx make:event <Name> 이벤트 핸들러 생성
13
+ * fx make:worker <Name> Worker 생성
14
+ * fx make:test <Name> 테스트 생성
15
+ *
16
+ * @see docs/framework/16-cli.md
17
+ */
18
+ import { promises as fs } from 'node:fs';
19
+ import path from 'node:path';
20
+ import { fileURLToPath } from 'node:url';
21
+
22
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
23
+ const TPL_DIR = path.join(__dirname, 'templates');
24
+
25
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
26
+ // Template Engine
27
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
28
+
29
+ /**
30
+ * 간단 템플릿 치환 — {{varName}} → value
31
+ * @param {string} template
32
+ * @param {object} vars - { Name, name, nameLower, tableName, dbName }
33
+ * @returns {string}
34
+ */
35
+ function render(template, vars) {
36
+ return template.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? '');
37
+ }
38
+
39
+ /**
40
+ * 템플릿 파일 읽기 + 치환
41
+ * @param {string} relativePath - templates/ 기준 상대 경로
42
+ * @param {object} vars
43
+ * @returns {Promise<string>}
44
+ */
45
+ async function loadTemplate(relativePath, vars = {}) {
46
+ const content = await fs.readFile(path.join(TPL_DIR, relativePath), 'utf-8');
47
+ return render(content, vars);
48
+ }
49
+
50
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
51
+ // fx make:* — 코드 생성
52
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
53
+
54
+ const TYPE_DIRS = {
55
+ controller: 'controllers',
56
+ service: 'services',
57
+ model: 'models',
58
+ middleware: 'middleware',
59
+ job: 'jobs',
60
+ task: 'jobs',
61
+ ws: 'ws',
62
+ event: 'events',
63
+ worker: 'workers',
64
+ test: 'tests',
65
+ };
66
+
67
+ const TYPE_SUFFIXES = {
68
+ controller: 'Controller',
69
+ service: 'Service',
70
+ model: '',
71
+ middleware: 'Middleware',
72
+ job: 'Job',
73
+ task: '',
74
+ ws: 'Handler',
75
+ event: '',
76
+ worker: '',
77
+ test: '.test',
78
+ };
79
+
80
+ /**
81
+ * fx make:<type> <Name> — 코드 생성
82
+ * @param {string} type - 'controller', 'service', 'model', 등
83
+ * @param {string} name - PascalCase 이름 (e.g. 'User')
84
+ * @param {string} [baseDir='.'] - 프로젝트 루트
85
+ * @returns {Promise<string>} - 생성된 파일 경로
86
+ */
87
+ export async function makeFile(type, name, baseDir = '.') {
88
+ const dir = TYPE_DIRS[type];
89
+ if (!dir) throw new Error(`Unknown type: ${type}`);
90
+
91
+ const suffix = TYPE_SUFFIXES[type];
92
+ const fileName = `${name}${suffix}.js`;
93
+ const filePath = path.join(baseDir, dir, fileName);
94
+
95
+ const vars = {
96
+ Name: name,
97
+ name: name,
98
+ nameLower: name.toLowerCase(),
99
+ tableName: name.toLowerCase() + 's',
100
+ };
101
+
102
+ const content = await loadTemplate(`make/${type}.js.tpl`, vars);
103
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
104
+ await fs.writeFile(filePath, content);
105
+
106
+ return filePath;
107
+ }
108
+
109
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
110
+ // fx new <name> — 앱 스캐폴딩
111
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
112
+
113
+ const APP_FILES = [
114
+ { tpl: 'app/package.json.tpl', dest: 'package.json' },
115
+ { tpl: 'app/fuzionx.yaml.tpl', dest: 'fuzionx.yaml' },
116
+ { tpl: 'app/app.js.tpl', dest: 'app.js' },
117
+ { tpl: 'app/routes/web.js.tpl', dest: 'routes/web.js' },
118
+ { tpl: 'app/routes/api.js.tpl', dest: 'routes/api.js' },
119
+ { tpl: 'app/.env.example.tpl', dest: '.env.example' },
120
+ { tpl: 'app/.env.example.tpl', dest: '.env' },
121
+ { tpl: 'app/.gitignore.tpl', dest: '.gitignore' },
122
+ ];
123
+
124
+ /** 스캐폴딩 디렉토리 (16-cli.md) */
125
+ const APP_DIRS = [
126
+ 'controllers',
127
+ 'models',
128
+ 'services',
129
+ 'middleware',
130
+ 'ws',
131
+ 'jobs',
132
+ 'events',
133
+ 'workers',
134
+ 'migrations',
135
+ 'seeds',
136
+ 'storage/logs',
137
+ 'storage/uploads',
138
+ 'public/css',
139
+ 'public/js',
140
+ 'locales',
141
+ 'tests',
142
+ ];
143
+
144
+ /**
145
+ * fx new <name> — 앱 스캐폴딩
146
+ * @param {string} name - 프로젝트 이름
147
+ * @param {string} [targetDir] - 기본: ./<name>
148
+ */
149
+ export async function createApp(name, targetDir) {
150
+ const dir = targetDir || path.resolve(name);
151
+ const vars = {
152
+ name,
153
+ dbName: name.replace(/-/g, '_'),
154
+ };
155
+
156
+ // 디렉토리 생성
157
+ for (const d of APP_DIRS) {
158
+ await fs.mkdir(path.join(dir, d), { recursive: true });
159
+ await fs.writeFile(path.join(dir, d, '.gitkeep'), '');
160
+ }
161
+
162
+ // 템플릿 파일 생성
163
+ for (const { tpl, dest } of APP_FILES) {
164
+ const content = await loadTemplate(tpl, vars);
165
+ const fullPath = path.join(dir, dest);
166
+ await fs.mkdir(path.dirname(fullPath), { recursive: true });
167
+ await fs.writeFile(fullPath, content);
168
+ }
169
+
170
+ // HomeController 복사 (16-cli.md)
171
+ const hcSrc = path.join(TPL_DIR, 'app/controllers/HomeController.js');
172
+ const hcDst = path.join(dir, 'controllers/HomeController.js');
173
+ await fs.copyFile(hcSrc, hcDst);
174
+
175
+ // Views — views/{theme}/ 구조 (03, 16-cli.md)
176
+ const viewsSrc = path.join(TPL_DIR, 'app/views/default');
177
+ const viewsDst = path.join(dir, 'views/default');
178
+
179
+ // layouts/main.html
180
+ const layoutDir = path.join(viewsDst, 'layouts');
181
+ await fs.mkdir(layoutDir, { recursive: true });
182
+ await fs.copyFile(path.join(viewsSrc, 'layouts/main.html'), path.join(layoutDir, 'main.html'));
183
+
184
+ // pages/home.html
185
+ const pagesDir = path.join(viewsDst, 'pages');
186
+ await fs.mkdir(pagesDir, { recursive: true });
187
+ await fs.copyFile(path.join(viewsSrc, 'pages/home.html'), path.join(pagesDir, 'home.html'));
188
+
189
+ // errors/404.html, 500.html
190
+ const errDir = path.join(viewsDst, 'errors');
191
+ await fs.mkdir(errDir, { recursive: true });
192
+ await fs.copyFile(path.join(viewsSrc, 'errors/404.html'), path.join(errDir, '404.html'));
193
+ await fs.copyFile(path.join(viewsSrc, 'errors/500.html'), path.join(errDir, '500.html'));
194
+
195
+ return dir;
196
+ }
197
+
198
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
199
+ // CLI 엔트리
200
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
201
+
202
+ /**
203
+ * CLI 엔트리 (fx 명령어)
204
+ * @param {string[]} args - process.argv.slice(2)
205
+ */
206
+ export async function run(args) {
207
+ const [command, ...rest] = args;
208
+
209
+ if (command === 'new') {
210
+ const name = rest[0];
211
+ if (!name) { console.error('Usage: fx new <name>'); process.exit(1); }
212
+ const dir = await createApp(name);
213
+ console.log(`✅ Created ${name} at ${dir}`);
214
+ console.log(`\n cd ${name}`);
215
+ console.log(` npm install`);
216
+ console.log(` npm run dev\n`);
217
+ return;
218
+ }
219
+
220
+ if (command?.startsWith('make:')) {
221
+ const type = command.replace('make:', '');
222
+ const name = rest[0];
223
+ if (!name) { console.error(`Usage: fx ${command} <Name>`); process.exit(1); }
224
+ const file = await makeFile(type, name);
225
+ console.log(`✅ Created ${file}`);
226
+ return;
227
+ }
228
+
229
+ // ── fx dev — 개발 서버 (Node --watch) ──
230
+ if (command === 'dev') {
231
+ const { execSync } = await import('node:child_process');
232
+ const port = rest.find(a => a.startsWith('--port='))?.split('=')[1] || '';
233
+ const entry = 'app.js';
234
+ const env = port ? `PORT=${port} ` : '';
235
+ console.log(`🚀 Starting dev server...`);
236
+ try {
237
+ execSync(`${env}node --watch ${entry}`, { stdio: 'inherit', cwd: process.cwd() });
238
+ } catch {}
239
+ return;
240
+ }
241
+
242
+ // ── fx test — 테스트 실행 ──
243
+ if (command === 'test') {
244
+ const { execSync } = await import('node:child_process');
245
+ try {
246
+ execSync('npx vitest run', { stdio: 'inherit', cwd: process.cwd() });
247
+ } catch { process.exit(1); }
248
+ return;
249
+ }
250
+
251
+ // ── fx routes — 라우트 테이블 출력 ──
252
+ if (command === 'routes') {
253
+ try {
254
+ const appMod = await import(path.resolve('app.js'));
255
+ const app = appMod.default || appMod.app;
256
+ if (!app?._router) { console.error('Cannot load app routes'); return; }
257
+ const routes = app._router.getRoutes();
258
+ console.log('');
259
+ console.log(' METHOD PATH HANDLER MIDDLEWARE');
260
+ console.log(' ────── ───── ──────── ──────────');
261
+ for (const r of routes) {
262
+ const method = r.method.padEnd(8);
263
+ const rPath = r.path.padEnd(22);
264
+ const handler = r.handler?.__handler__
265
+ ? `${r.handler.controller?.name || ''}.${r.handler.method}`
266
+ : (typeof r.handler === 'function' ? r.handler.name || 'anonymous' : '');
267
+ const mw = (r.middleware || []).join(', ');
268
+ console.log(` ${method} ${rPath} ${handler.padEnd(26)} ${mw}`);
269
+ }
270
+ console.log('');
271
+ } catch (err) { console.error('Failed to load routes:', err.message); }
272
+ return;
273
+ }
274
+
275
+ // ── fx config — fuzionx.yaml 출력 ──
276
+ if (command === 'config') {
277
+ try {
278
+ const configPath = path.resolve('fuzionx.yaml');
279
+ const content = await fs.readFile(configPath, 'utf-8');
280
+ console.log('\n📄 fuzionx.yaml:\n');
281
+ console.log(content);
282
+ } catch (err) { console.error('Cannot read fuzionx.yaml:', err.message); }
283
+ return;
284
+ }
285
+
286
+ // ── fx db:sync — 모델 ↔ DB 스키마 diff ──
287
+ if (command === 'db:sync') {
288
+ try {
289
+ const modelsDir = path.resolve('models');
290
+ const files = await fs.readdir(modelsDir);
291
+ const jsFiles = files.filter(f => f.endsWith('.js'));
292
+ console.log('\n📊 Model Schema Status:\n');
293
+ for (const file of jsFiles) {
294
+ const mod = await import(path.join(modelsDir, file));
295
+ const Model = mod.default;
296
+ if (!Model) continue;
297
+ const name = path.basename(file, '.js');
298
+ const table = Model.table || name.toLowerCase() + 's';
299
+ const cols = Object.keys(Model.columns || {});
300
+ const status = rest.includes('--apply') ? '✅ synced' : '⏳ pending';
301
+ console.log(` ${name.padEnd(20)} table: ${table.padEnd(20)} columns: ${cols.length} ${status}`);
302
+ }
303
+ if (!rest.includes('--apply')) {
304
+ console.log('\n Run with --apply to sync changes to database.');
305
+ }
306
+ console.log('');
307
+ } catch (err) { console.error('db:sync error:', err.message); }
308
+ return;
309
+ }
310
+
311
+ console.log(`
312
+ fx new <name> Create new app
313
+ fx make:controller <Name> Create controller
314
+ fx make:service <Name> Create service
315
+ fx make:model <Name> Create model
316
+ fx make:middleware <Name> Create middleware
317
+ fx make:job <Name> Create job
318
+ fx make:task <Name> Create task
319
+ fx make:ws <Name> Create WsHandler
320
+ fx make:event <Name> Create event handler
321
+ fx make:worker <Name> Create worker (worker_threads)
322
+ fx make:test <Name> Create test
323
+ fx dev Start dev server (--watch)
324
+ fx test Run tests
325
+ fx routes Print route table
326
+ fx config Print fuzionx.yaml
327
+ fx db:sync Sync models → DB (--apply)
328
+ `);
329
+ }
@@ -0,0 +1,14 @@
1
+ # .env — 이 파일을 .env로 복사 후 값을 채우세요
2
+
3
+ # Database
4
+ DB_HOST=127.0.0.1
5
+ DB_PORT=3306
6
+ DB_USER=root
7
+ DB_PASSWORD=
8
+ DB_NAME={{dbName}}
9
+
10
+ # Auth
11
+ JWT_SECRET=
12
+
13
+ # Redis (세션/큐 공용)
14
+ REDIS_URL=
@@ -0,0 +1,4 @@
1
+ .env
2
+ node_modules/
3
+ storage/logs/
4
+ storage/uploads/
@@ -0,0 +1,14 @@
1
+ import { Application } from '@fuzionx/framework';
2
+ import webRoutes from './routes/web.js';
3
+ import apiRoutes from './routes/api.js';
4
+
5
+ const app = new Application({ configPath: './fuzionx.yaml' });
6
+
7
+ app.routes(webRoutes);
8
+ app.routes(apiRoutes);
9
+
10
+ await app.boot();
11
+
12
+ app.listen(49080, () => {
13
+ console.log('🚀 FuzionX running on http://localhost:49080');
14
+ });
@@ -0,0 +1,13 @@
1
+ import { Controller } from '@fuzionx/framework';
2
+
3
+ /**
4
+ * Home 컨트롤러.
5
+ * routes/web.js에서 라우트를 등록하세요.
6
+ */
7
+ export default class HomeController extends Controller {
8
+
9
+ /** 홈 페이지 */
10
+ async index(ctx) {
11
+ ctx.render('pages/home');
12
+ }
13
+ }
@@ -0,0 +1,30 @@
1
+ # FuzionX Configuration
2
+ bridge:
3
+ port: 49080
4
+ workers: 4
5
+ worker_timeout: 30
6
+
7
+ rate_limit:
8
+ enabled: true
9
+ per_ip: 1000
10
+
11
+ database:
12
+ main:
13
+ driver: sqlite
14
+ path: ./storage/database.sqlite
15
+
16
+ app:
17
+ name: '{{name}}'
18
+ environment: development
19
+ auth:
20
+ secret: 'change-me-in-production'
21
+ accessTtl: '15m'
22
+ i18n:
23
+ default_locale: 'ko'
24
+ fallback: 'en'
25
+ docs:
26
+ enabled: true
27
+ path: '/docs'
28
+
29
+ themes:
30
+ default: 'default'
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "{{name}}",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "fx dev",
7
+ "test": "vitest run"
8
+ },
9
+ "dependencies": {
10
+ "@fuzionx/framework": "^0.1.0"
11
+ },
12
+ "devDependencies": {
13
+ "vitest": "^3.0.0"
14
+ }
15
+ }
@@ -0,0 +1,7 @@
1
+ export default (r) => {
2
+ r.group('/api', (r) => {
3
+ r.get('/health', (ctx) => {
4
+ ctx.json({ status: 'ok', timestamp: Date.now() });
5
+ });
6
+ });
7
+ };
@@ -0,0 +1,5 @@
1
+ export default (r) => {
2
+ r.get('/', (ctx) => {
3
+ ctx.render('home');
4
+ });
5
+ };
@@ -0,0 +1,15 @@
1
+ {% extends "layouts/main.html" %}
2
+
3
+ {% block title %}404 — 찾을 수 없습니다{% endblock %}
4
+
5
+ {% block content %}
6
+ <div style="text-align:center;padding:80px 20px;">
7
+ <h1 style="font-size:72px;color:#e74c3c;">{{ error.code }}</h1>
8
+ <p style="font-size:20px;margin:16px 0;">{{ error.message }}</p>
9
+ <p style="color:#888;">요청: {{ request.url }}</p>
10
+ {% if config.debug %}
11
+ <pre style="text-align:left;max-width:600px;margin:24px auto;background:#f5f5f5;padding:16px;border-radius:8px;overflow:auto;">{{ error.stack }}</pre>
12
+ {% endif %}
13
+ <a href="/" style="display:inline-block;margin-top:24px;padding:12px 24px;background:#e74c3c;color:#fff;text-decoration:none;border-radius:6px;">홈으로 돌아가기</a>
14
+ </div>
15
+ {% endblock %}
@@ -0,0 +1,14 @@
1
+ {% extends "layouts/main.html" %}
2
+
3
+ {% block title %}500 — 서버 오류{% endblock %}
4
+
5
+ {% block content %}
6
+ <div style="text-align:center;padding:80px 20px;">
7
+ <h1 style="font-size:72px;color:#e74c3c;">500</h1>
8
+ <p style="font-size:20px;margin:16px 0;">{{ error.message | default(value='Internal Server Error') }}</p>
9
+ {% if config.debug and error.stack %}
10
+ <pre style="text-align:left;max-width:600px;margin:24px auto;background:#f5f5f5;padding:16px;border-radius:8px;overflow:auto;">{{ error.stack }}</pre>
11
+ {% endif %}
12
+ <a href="/" style="display:inline-block;margin-top:24px;padding:12px 24px;background:#e74c3c;color:#fff;text-decoration:none;border-radius:6px;">홈으로 돌아가기</a>
13
+ </div>
14
+ {% endblock %}
@@ -0,0 +1,22 @@
1
+ <!DOCTYPE html>
2
+ <html lang="{{ locale | default(value='ko') }}">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>{% block title %}{{ config.app.name | default(value='FuzionX') }}{% endblock %}</title>
7
+ <style>
8
+ * { margin: 0; padding: 0; box-sizing: border-box; }
9
+ body { font-family: system-ui, -apple-system, sans-serif; line-height: 1.6; color: #333; }
10
+ </style>
11
+ {% block head %}{% endblock %}
12
+ </head>
13
+ <body>
14
+ {% include "partials/header.html" ignore missing %}
15
+
16
+ <main>
17
+ {% block content %}{% endblock %}
18
+ </main>
19
+
20
+ {% include "partials/footer.html" ignore missing %}
21
+ </body>
22
+ </html>
@@ -0,0 +1,188 @@
1
+ <!DOCTYPE html>
2
+ <html lang="ko">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>FuzionX</title>
7
+ <style>
8
+ * { margin: 0; padding: 0; box-sizing: border-box; }
9
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;800&display=swap');
10
+
11
+ body {
12
+ font-family: 'Inter', system-ui, -apple-system, sans-serif;
13
+ min-height: 100vh;
14
+ display: flex;
15
+ align-items: center;
16
+ justify-content: center;
17
+ background: linear-gradient(135deg, #0f0c29 0%, #1a1a3e 40%, #24243e 100%);
18
+ color: #e0e0e0;
19
+ overflow: hidden;
20
+ }
21
+
22
+ .container {
23
+ text-align: center;
24
+ z-index: 1;
25
+ animation: fadeInUp 0.8s ease-out;
26
+ }
27
+
28
+ .logo {
29
+ font-size: 4rem;
30
+ font-weight: 800;
31
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
32
+ -webkit-background-clip: text;
33
+ background-clip: text;
34
+ -webkit-text-fill-color: transparent;
35
+ letter-spacing: -2px;
36
+ margin-bottom: 0.5rem;
37
+ }
38
+
39
+ .subtitle {
40
+ font-size: 1.1rem;
41
+ font-weight: 300;
42
+ color: rgba(255, 255, 255, 0.5);
43
+ margin-bottom: 2.5rem;
44
+ letter-spacing: 2px;
45
+ }
46
+
47
+ .card {
48
+ background: rgba(255, 255, 255, 0.05);
49
+ backdrop-filter: blur(20px);
50
+ border: 1px solid rgba(255, 255, 255, 0.1);
51
+ border-radius: 16px;
52
+ padding: 2rem 3rem;
53
+ max-width: 480px;
54
+ margin: 0 auto 2rem;
55
+ }
56
+
57
+ .version {
58
+ display: inline-block;
59
+ background: linear-gradient(135deg, #667eea, #764ba2);
60
+ color: white;
61
+ padding: 4px 14px;
62
+ border-radius: 20px;
63
+ font-size: 0.75rem;
64
+ font-weight: 600;
65
+ letter-spacing: 1px;
66
+ margin-bottom: 1.5rem;
67
+ }
68
+
69
+ .features {
70
+ display: grid;
71
+ grid-template-columns: 1fr 1fr;
72
+ gap: 1rem;
73
+ text-align: left;
74
+ margin-top: 1.5rem;
75
+ }
76
+
77
+ .feature {
78
+ display: flex;
79
+ align-items: center;
80
+ gap: 8px;
81
+ font-size: 0.85rem;
82
+ color: rgba(255, 255, 255, 0.7);
83
+ }
84
+
85
+ .feature span {
86
+ font-size: 1.1rem;
87
+ }
88
+
89
+ .links {
90
+ display: flex;
91
+ gap: 1rem;
92
+ justify-content: center;
93
+ margin-top: 1rem;
94
+ }
95
+
96
+ .links a {
97
+ color: rgba(255, 255, 255, 0.6);
98
+ text-decoration: none;
99
+ font-size: 0.85rem;
100
+ padding: 8px 20px;
101
+ border: 1px solid rgba(255, 255, 255, 0.15);
102
+ border-radius: 8px;
103
+ transition: all 0.3s ease;
104
+ }
105
+
106
+ .links a:hover {
107
+ color: #fff;
108
+ border-color: #667eea;
109
+ background: rgba(102, 126, 234, 0.1);
110
+ transform: translateY(-2px);
111
+ }
112
+
113
+ .hint {
114
+ margin-top: 2rem;
115
+ font-size: 0.75rem;
116
+ color: rgba(255, 255, 255, 0.3);
117
+ }
118
+
119
+ .hint code {
120
+ background: rgba(255, 255, 255, 0.08);
121
+ padding: 2px 8px;
122
+ border-radius: 4px;
123
+ font-family: 'JetBrains Mono', monospace;
124
+ }
125
+
126
+ /* Background orbs */
127
+ .orb {
128
+ position: fixed;
129
+ border-radius: 50%;
130
+ filter: blur(80px);
131
+ opacity: 0.3;
132
+ animation: float 8s ease-in-out infinite;
133
+ }
134
+ .orb-1 { width: 400px; height: 400px; background: #667eea; top: -100px; right: -100px; }
135
+ .orb-2 { width: 300px; height: 300px; background: #764ba2; bottom: -80px; left: -80px; animation-delay: -4s; }
136
+ .orb-3 { width: 200px; height: 200px; background: #f093fb; top: 50%; left: 60%; animation-delay: -2s; }
137
+
138
+ @keyframes fadeInUp {
139
+ from { opacity: 0; transform: translateY(30px); }
140
+ to { opacity: 1; transform: translateY(0); }
141
+ }
142
+
143
+ @keyframes float {
144
+ 0%, 100% { transform: translate(0, 0); }
145
+ 50% { transform: translate(30px, -30px); }
146
+ }
147
+
148
+ @media (max-width: 600px) {
149
+ .logo { font-size: 2.5rem; }
150
+ .card { padding: 1.5rem; margin: 0 1rem; }
151
+ .features { grid-template-columns: 1fr; }
152
+ }
153
+ </style>
154
+ </head>
155
+ <body>
156
+ <div class="orb orb-1"></div>
157
+ <div class="orb orb-2"></div>
158
+ <div class="orb orb-3"></div>
159
+
160
+ <div class="container">
161
+ <h1 class="logo">FuzionX</h1>
162
+ <p class="subtitle">HIGH-PERFORMANCE NODE.JS FRAMEWORK</p>
163
+
164
+ <div class="card">
165
+ <div class="version">v0.1.0 · POWERED BY RUST</div>
166
+
167
+ <div class="features">
168
+ <div class="feature"><span>⚡</span> 500K+ RPS</div>
169
+ <div class="feature"><span>🦀</span> Rust N-API Bridge</div>
170
+ <div class="feature"><span>🎯</span> MVC Architecture</div>
171
+ <div class="feature"><span>🔌</span> WebSocket</div>
172
+ <div class="feature"><span>🗄️</span> Multi-DB ORM</div>
173
+ <div class="feature"><span>🔐</span> Auth & Session</div>
174
+ <div class="feature"><span>📡</span> Event System</div>
175
+ <div class="feature"><span>⏰</span> Job Scheduler</div>
176
+ </div>
177
+ </div>
178
+
179
+ <div class="links">
180
+ <a href="https://github.com/saytohenry/fuzionx">GitHub</a>
181
+ <a href="/docs">API Docs</a>
182
+ <a href="/api/health">Health Check</a>
183
+ </div>
184
+
185
+ <p class="hint">Edit <code>routes/web.js</code> to get started</p>
186
+ </div>
187
+ </body>
188
+ </html>
@@ -0,0 +1,40 @@
1
+ import { Controller } from '@fuzionx/framework';
2
+
3
+ /**
4
+ * {{Name}} 컨트롤러.
5
+ * routes/ 파일에서 라우트를 등록하세요.
6
+ */
7
+ export default class {{Name}}Controller extends Controller {
8
+
9
+ /** 목록 조회 */
10
+ async index(ctx) {
11
+ const items = await this.db.{{Name}}.paginate(ctx.query.page || 1, 20);
12
+ ctx.json(items);
13
+ }
14
+
15
+ /** 상세 조회 */
16
+ async show(ctx) {
17
+ const item = await this.db.{{Name}}.findOrFail(ctx.params.id);
18
+ ctx.json(item);
19
+ }
20
+
21
+ /** 생성 */
22
+ async store(ctx) {
23
+ const item = await this.db.{{Name}}.create(ctx.body);
24
+ ctx.status(201).json(item);
25
+ }
26
+
27
+ /** 수정 */
28
+ async update(ctx) {
29
+ const item = await this.db.{{Name}}.findOrFail(ctx.params.id);
30
+ await item.update(ctx.body);
31
+ ctx.json(item);
32
+ }
33
+
34
+ /** 삭제 */
35
+ async destroy(ctx) {
36
+ const item = await this.db.{{Name}}.findOrFail(ctx.params.id);
37
+ await item.delete();
38
+ ctx.status(204).end();
39
+ }
40
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * {{Name}} 이벤트 핸들러
3
+ */
4
+ export default function register(app) {
5
+ app.on('{{nameLower}}:created', (data, meta) => {
6
+ // 이벤트 처리 로직
7
+ });
8
+ }
@@ -0,0 +1,10 @@
1
+ import { Job } from '@fuzionx/framework';
2
+
3
+ export default class {{Name}}Job extends Job {
4
+ static schedule = 'daily:02:00';
5
+ static timeout = 30000;
6
+
7
+ async handle() {
8
+ // 정기 실행 로직
9
+ }
10
+ }
@@ -0,0 +1,10 @@
1
+ import { Middleware } from '@fuzionx/framework';
2
+
3
+ export default class {{Name}}Middleware extends Middleware {
4
+ static name = '{{nameLower}}';
5
+
6
+ async handle(ctx, next) {
7
+ // TODO: implement
8
+ await next();
9
+ }
10
+ }
@@ -0,0 +1,15 @@
1
+ import { SQLiteModel } from '@fuzionx/framework';
2
+
3
+ export default class {{Name}} extends SQLiteModel {
4
+ static table = '{{tableName}}';
5
+ static timestamps = true;
6
+ static hidden = [];
7
+
8
+ static columns = {
9
+ id: { type: 'increments' },
10
+ name: { type: 'string', length: 100 },
11
+ };
12
+
13
+ // 관계 정의
14
+ // posts() { return this.hasMany('Post'); }
15
+ }
@@ -0,0 +1,15 @@
1
+ import { Service } from '@fuzionx/framework';
2
+
3
+ export default class {{Name}}Service extends Service {
4
+ async findAll() {
5
+ return this.db.{{Name}}.query().paginate();
6
+ }
7
+
8
+ async findById(id) {
9
+ return this.db.{{Name}}.findOrFail(id);
10
+ }
11
+
12
+ async create(data) {
13
+ return this.db.{{Name}}.create(data);
14
+ }
15
+ }
@@ -0,0 +1,15 @@
1
+ import { Task } from '@fuzionx/framework';
2
+
3
+ export default class {{Name}} extends Task {
4
+ static queue = 'default';
5
+ static retries = 3;
6
+ static retryDelay = 5000;
7
+
8
+ async handle(data) {
9
+ // 비동기 작업 로직
10
+ }
11
+
12
+ async failed(data, error) {
13
+ this.logger.error('작업 최종 실패', { error: error.message });
14
+ }
15
+ }
@@ -0,0 +1,7 @@
1
+ import { describe, it, expect } from 'vitest';
2
+
3
+ describe('{{Name}}', () => {
4
+ it('should work', () => {
5
+ expect(true).toBe(true);
6
+ });
7
+ });
@@ -0,0 +1,14 @@
1
+ /**
2
+ * {{Name}} Worker — CPU-heavy 작업 처리
3
+ *
4
+ * 사용: this.worker.run('{{nameLower}}', data)
5
+ */
6
+ import { parentPort, workerData } from 'node:worker_threads';
7
+
8
+ async function handle(data) {
9
+ // TODO: CPU-heavy 로직 구현
10
+ return data;
11
+ }
12
+
13
+ const result = await handle(workerData);
14
+ parentPort.postMessage(result);
@@ -0,0 +1,18 @@
1
+ import { WsHandler } from '@fuzionx/framework';
2
+
3
+ export default class {{Name}}Handler extends WsHandler {
4
+ static namespace = '/{{nameLower}}';
5
+ static middleware = [];
6
+
7
+ static events(e) {
8
+ // e.on('message', this.handleMessage);
9
+ }
10
+
11
+ async onConnect(socket) {
12
+ this.logger.info('{{Name}} connected:', socket.sessionId);
13
+ }
14
+
15
+ async onDisconnect(socket, code, reason) {
16
+ this.logger.info('{{Name}} disconnected:', socket.sessionId);
17
+ }
18
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fuzionx/framework",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "type": "module",
5
5
  "description": "Full-stack MVC framework built on @fuzionx/core — Controller, Service, Model, Middleware, DI, EventBus",
6
6
  "main": "index.js",
@@ -34,7 +34,7 @@
34
34
  "url": "https://github.com/saytohenry/fuzionx"
35
35
  },
36
36
  "dependencies": {
37
- "@fuzionx/core": "^0.1.8",
37
+ "@fuzionx/core": "^0.1.9",
38
38
  "better-sqlite3": "^12.8.0",
39
39
  "knex": "^3.2.5",
40
40
  "mongoose": "^9.3.2",
@@ -46,6 +46,8 @@
46
46
  },
47
47
  "files": [
48
48
  "index.js",
49
+ "bin/",
50
+ "cli/",
49
51
  "lib/",
50
52
  "testing/"
51
53
  ]