@fuzionx/framework 0.1.28 → 0.1.30

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/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * CLI — fx 명령어 핸들러
3
3
  *
4
- * fx new <name> 스캐폴딩
4
+ * fx make:app <name> 디렉토리 생성
5
5
  * fx make:controller <Name> 컨트롤러 생성
6
6
  * fx make:service <Name> 서비스 생성
7
7
  * fx make:model <Name> 모델 생성
@@ -93,7 +93,7 @@ const TYPE_SUFFIXES = {
93
93
  export async function makeFile(type, name, baseDir = '.', appName) {
94
94
  let dir;
95
95
  if (APP_TYPE_DIRS[type]) {
96
- if (!appName) throw new Error(`--app option required for make:${type} (e.g. fx make:${type} ${name} --app=backend)`);
96
+ if (!appName) throw new Error(`--app option required for make:${type} (e.g. fx make:${type} ${name} --app=fuzionx)`);
97
97
  dir = `app/${appName}/${APP_TYPE_DIRS[type]}`;
98
98
  } else if (SHARED_TYPE_DIRS[type]) {
99
99
  dir = SHARED_TYPE_DIRS[type];
@@ -120,109 +120,22 @@ export async function makeFile(type, name, baseDir = '.', appName) {
120
120
  }
121
121
 
122
122
  // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
123
- // fx new <name> — 앱 스캐폴딩
123
+ // 유틸리티
124
124
  // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
125
125
 
126
- const APP_FILES = [
127
- { tpl: 'app/package.json.tpl', dest: 'package.json' },
128
- { tpl: 'app/fuzionx.yaml.tpl', dest: 'fuzionx.yaml' },
129
- { tpl: 'app/app.js.tpl', dest: 'app.js' },
130
- { tpl: 'app/routes/web.js.tpl', dest: 'app/backend/routes/web.js' },
131
- { tpl: 'app/routes/api.js.tpl', dest: 'app/backend/routes/api.js' },
132
- { tpl: 'app/.env.example.tpl', dest: '.env.example' },
133
- { tpl: 'app/.env.example.tpl', dest: '.env' },
134
- { tpl: 'app/.gitignore.tpl', dest: '.gitignore' },
135
- ];
136
-
137
- /** 스캐폴딩 디렉토리 (multi-app 구조) */
138
- const APP_DIRS = [
139
- // 앱별
140
- 'app/backend/controllers',
141
- 'app/backend/routes',
142
- 'app/backend/services',
143
- 'app/backend/middleware',
144
- 'app/backend/views/default/pages',
145
- 'app/backend/views/default/layouts',
146
- 'app/backend/views/default/errors',
147
- 'app/backend/ws',
148
- // 공유
149
- 'database/models',
150
- 'database/migrations',
151
- 'database/seeds',
152
- 'shared/events',
153
- 'shared/jobs',
154
- 'shared/workers',
155
- // 인프라
156
- 'storage/logs',
157
- 'storage/uploads',
158
- 'public/css',
159
- 'public/js',
160
- 'locales',
161
- 'tests',
162
- ];
163
-
164
- /**
165
- * fx new <name> — 앱 스캐폴딩
166
- * @param {string} name - 프로젝트 이름
167
- * @param {string} [targetDir] - 기본: ./<name>
168
- */
169
- export async function createApp(name, targetDir) {
170
- const dir = targetDir || path.resolve(name);
171
- const vars = {
172
- name,
173
- dbName: name.replace(/-/g, '_'),
174
- };
175
-
176
- // 디렉토리 생성
177
- for (const d of APP_DIRS) {
178
- await fs.mkdir(path.join(dir, d), { recursive: true });
179
- // .gitkeep 는 리프 원래 파일이 없는 디렉토리에만
180
- const files = await fs.readdir(path.join(dir, d)).catch(() => []);
181
- if (files.length === 0) {
182
- await fs.writeFile(path.join(dir, d, '.gitkeep'), '');
126
+ /** 디렉토리 재귀 복사 (템플릿 → 프로젝트) */
127
+ async function copyDirRecursive(src, dst) {
128
+ await fs.mkdir(dst, { recursive: true });
129
+ const entries = await fs.readdir(src, { withFileTypes: true });
130
+ for (const entry of entries) {
131
+ const srcPath = path.join(src, entry.name);
132
+ const dstPath = path.join(dst, entry.name);
133
+ if (entry.isDirectory()) {
134
+ await copyDirRecursive(srcPath, dstPath);
135
+ } else {
136
+ await fs.copyFile(srcPath, dstPath);
183
137
  }
184
138
  }
185
-
186
- // 템플릿 파일 생성
187
- for (const { tpl, dest } of APP_FILES) {
188
- const content = await loadTemplate(tpl, vars);
189
- const fullPath = path.join(dir, dest);
190
- await fs.mkdir(path.dirname(fullPath), { recursive: true });
191
- await fs.writeFile(fullPath, content);
192
- }
193
-
194
- // HomeController 복사
195
- const hcSrc = path.join(TPL_DIR, 'app/controllers/HomeController.js');
196
- const hcDst = path.join(dir, 'app/backend/controllers/HomeController.js');
197
- await fs.copyFile(hcSrc, hcDst);
198
-
199
- // Views — app/backend/views/{theme}/ 구조
200
- const viewsSrc = path.join(TPL_DIR, 'app/views/default');
201
- const viewsDst = path.join(dir, 'app/backend/views/default');
202
-
203
- // layouts/main.html
204
- await fs.copyFile(
205
- path.join(viewsSrc, 'layouts/main.html'),
206
- path.join(viewsDst, 'layouts/main.html'),
207
- );
208
-
209
- // pages/home.html
210
- await fs.copyFile(
211
- path.join(viewsSrc, 'pages/home.html'),
212
- path.join(viewsDst, 'pages/home.html'),
213
- );
214
-
215
- // errors/404.html, 500.html
216
- await fs.copyFile(
217
- path.join(viewsSrc, 'errors/404.html'),
218
- path.join(viewsDst, 'errors/404.html'),
219
- );
220
- await fs.copyFile(
221
- path.join(viewsSrc, 'errors/500.html'),
222
- path.join(viewsDst, 'errors/500.html'),
223
- );
224
-
225
- return dir;
226
139
  }
227
140
 
228
141
  // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
@@ -236,14 +149,29 @@ export async function createApp(name, targetDir) {
236
149
  export async function run(args) {
237
150
  const [command, ...rest] = args;
238
151
 
152
+ // ── fx new → create-fuzionx 안내 ──
239
153
  if (command === 'new') {
154
+ console.log(`\n 프로젝트 생성은 create-fuzionx를 사용하세요:\n`);
155
+ console.log(` npx create-fuzionx <name>\n`);
156
+ return;
157
+ }
158
+
159
+ if (command === 'make:app') {
240
160
  const name = rest[0];
241
- if (!name) { console.error('Usage: fx new <name>'); process.exit(1); }
242
- const dir = await createApp(name);
243
- console.log(`✅ Created ${name} at ${dir}`);
244
- console.log(`\n cd ${name}`);
245
- console.log(` npm install`);
246
- console.log(` npm run dev\n`);
161
+ if (!name) { console.error('Usage: fx make:app <appName>'); process.exit(1); }
162
+ const appDir = path.join('.', 'app', name);
163
+ // 디렉토리 생성
164
+ for (const d of ['services', 'middleware', 'ws']) {
165
+ const fullDir = path.join(appDir, d);
166
+ await fs.mkdir(fullDir, { recursive: true });
167
+ await fs.writeFile(path.join(fullDir, '.gitkeep'), '');
168
+ }
169
+ // 기본 파일 복사 (HomeController, routes, views)
170
+ const appTplDir = path.join(TPL_DIR, 'make/app');
171
+ await copyDirRecursive(appTplDir, appDir);
172
+ console.log(`✅ Created app/${name}/`);
173
+ console.log(`\n fuzionx.yaml의 apps에 추가하세요:`);
174
+ console.log(` "127.0.0.1:49080": ${name}\n`);
247
175
  return;
248
176
  }
249
177
 
@@ -271,6 +199,69 @@ export async function run(args) {
271
199
  return;
272
200
  }
273
201
 
202
+ // ── fx stop — 서버 종료 ──
203
+ if (command === 'stop') {
204
+ const pidFile = path.resolve('fuzionx.pid');
205
+ try {
206
+ const pid = parseInt(await fs.readFile(pidFile, 'utf-8'), 10);
207
+ console.log(`🛑 Stopping PID ${pid}...`);
208
+ process.kill(pid, 'SIGTERM');
209
+ for (let i = 0; i < 50; i++) {
210
+ await new Promise(r => setTimeout(r, 100));
211
+ try { process.kill(pid, 0); } catch { break; }
212
+ }
213
+ await fs.unlink(pidFile).catch(() => {});
214
+ console.log('✅ Server stopped.');
215
+ } catch (err) {
216
+ if (err.code === 'ENOENT') {
217
+ console.log('⚠️ fuzionx.pid 없음 — 서버가 실행 중이 아닙니다.');
218
+ } else if (err.code === 'ESRCH') {
219
+ await fs.unlink(pidFile).catch(() => {});
220
+ console.log('⚠️ PID 프로세스가 이미 종료됨. PID 파일 삭제.');
221
+ } else {
222
+ console.error('❌ 종료 실패:', err.message);
223
+ }
224
+ }
225
+ return;
226
+ }
227
+
228
+ // ── fx restart — 서버 재시작 ──
229
+ if (command === 'restart') {
230
+ const pidFile = path.resolve('fuzionx.pid');
231
+ try {
232
+ const pid = parseInt(await fs.readFile(pidFile, 'utf-8'), 10);
233
+ console.log(`🔄 Stopping PID ${pid}...`);
234
+ process.kill(pid, 'SIGTERM');
235
+ for (let i = 0; i < 50; i++) {
236
+ await new Promise(r => setTimeout(r, 100));
237
+ try { process.kill(pid, 0); } catch { break; }
238
+ }
239
+ } catch (err) {
240
+ if (err.code === 'ENOENT') {
241
+ console.log('⚠️ fuzionx.pid 없음 — 새로 시작합니다.');
242
+ } else if (err.code === 'ESRCH') {
243
+ console.log('⚠️ PID 프로세스가 이미 종료됨.');
244
+ } else {
245
+ console.error('❌ 종료 실패:', err.message);
246
+ }
247
+ }
248
+ // 백그라운드로 재시작
249
+ const { spawn } = await import('node:child_process');
250
+ const logDir = path.resolve('storage/logs');
251
+ await fs.mkdir(logDir, { recursive: true });
252
+ const logFile = path.join(logDir, 'server.log');
253
+ const { openSync } = await import('node:fs');
254
+ const out = openSync(logFile, 'a');
255
+ const child = spawn('node', ['app.js'], {
256
+ cwd: process.cwd(),
257
+ detached: true,
258
+ stdio: ['ignore', out, out],
259
+ });
260
+ child.unref();
261
+ console.log(`🚀 Restarted (PID ${child.pid}) — logs: storage/logs/server.log`);
262
+ return;
263
+ }
264
+
274
265
  // ── fx test — 테스트 실행 ──
275
266
  if (command === 'test') {
276
267
  const { execSync } = await import('node:child_process');
@@ -344,7 +335,8 @@ export async function run(args) {
344
335
  }
345
336
 
346
337
  console.log(`
347
- fx new <name> Create new app (multi-app structure)
338
+ npx create-fuzionx <name> Create new project
339
+ fx make:app <name> Create new app directory structure
348
340
  fx make:controller <Name> --app= Create controller (app-specific)
349
341
  fx make:service <Name> --app= Create service (app-specific)
350
342
  fx make:model <Name> Create model (database/models)
@@ -356,6 +348,8 @@ export async function run(args) {
356
348
  fx make:worker <Name> Create worker (shared/workers)
357
349
  fx make:test <Name> Create test
358
350
  fx dev Start dev server (--watch)
351
+ fx stop Stop server (graceful)
352
+ fx restart Restart server (graceful)
359
353
  fx test Run tests
360
354
  fx routes Print route table
361
355
  fx config Print fuzionx.yaml
@@ -7,6 +7,7 @@ import { Controller } from '@fuzionx/framework';
7
7
  export default class HomeController extends Controller {
8
8
 
9
9
  /** 홈 페이지 */
10
+ static index;
10
11
  async index(ctx) {
11
12
  ctx.render('home');
12
13
  }
@@ -0,0 +1,11 @@
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
+ <a href="/" style="display:inline-block;margin-top:24px;padding:12px 24px;background:#e74c3c;color:#fff;text-decoration:none;border-radius:6px;">홈으로 돌아가기</a>
10
+ </div>
11
+ {% endblock %}
@@ -1,12 +1,11 @@
1
1
  {% extends "layouts/main.html" %}
2
2
 
3
- {% block title %}404찾을 수 없습니다{% endblock %}
3
+ {% block title %}500서버 오류{% endblock %}
4
4
 
5
5
  {% block content %}
6
6
  <div style="text-align:center;padding:80px 20px;">
7
7
  <h1 style="font-size:72px;color:#e74c3c;">{{ error.code }}</h1>
8
8
  <p style="font-size:20px;margin:16px 0;">{{ error.message }}</p>
9
- <p style="color:#888;">요청: {{ request.url }}</p>
10
9
  {% if config.debug %}
11
10
  <pre style="text-align:left;max-width:600px;margin:24px auto;background:#f5f5f5;padding:16px;border-radius:8px;overflow:auto;">{{ error.stack }}</pre>
12
11
  {% endif %}
@@ -0,0 +1,11 @@
1
+ {% extends "layouts/main.html" %}
2
+
3
+ {% block title %}Welcome{% endblock %}
4
+
5
+ {% block content %}
6
+ <div style="text-align:center;padding:80px 20px;">
7
+ <h1 style="font-size:48px;color:#333;">🚀 Welcome</h1>
8
+ <p style="font-size:18px;color:#666;margin:16px 0;">Your new app is ready.</p>
9
+ <p style="color:#999;font-size:14px;">Edit <code style="background:#f0f0f0;padding:2px 8px;border-radius:4px;">routes/web.js</code> to get started</p>
10
+ </div>
11
+ {% endblock %}
@@ -8,6 +8,7 @@
8
8
  * @see docs/framework/04-bootstrap-lifecycle.md
9
9
  */
10
10
  import path from 'node:path';
11
+ import { promises as fs } from 'node:fs';
11
12
  import Config from './Config.js';
12
13
  import Context from './Context.js';
13
14
  import Router from '../http/Router.js';
@@ -370,7 +371,7 @@ export default class Application {
370
371
  _getDefaultAppName() {
371
372
  const appsConfig = this.config.get('apps') || {};
372
373
  const values = Object.values(appsConfig);
373
- return values[0] || 'backend';
374
+ return values[0] || 'fuzionx';
374
375
  }
375
376
 
376
377
  /**
@@ -427,10 +428,12 @@ export default class Application {
427
428
 
428
429
  if (!this._booted) await this.boot();
429
430
 
430
- // 포트 사용 여부 확인 — primary에서만 (워커는 SO_REUSEPORT로 공유)
431
+ // PID 파일 생성 (primary)
431
432
  const cluster = await import('node:cluster');
432
433
  if (!cluster.default?.isWorker) {
433
434
  await this._checkPort(port);
435
+ const pidPath = path.join(this.baseDir, 'fuzionx.pid');
436
+ await fs.writeFile(pidPath, String(process.pid));
434
437
  }
435
438
 
436
439
  await this.emit('ready');
@@ -53,12 +53,12 @@ export default class Logger {
53
53
  _log(level, message, context) {
54
54
  if (this._levels[level] > this._minLevel) return;
55
55
 
56
- const msg = context
57
- ? `${message} ${JSON.stringify(context)}`
58
- : message;
56
+ // Error 객체면 전체 스택 포함
57
+ const isErr = message instanceof Error;
58
+ const text = isErr ? (message.stack || `${message}`) : `${message}`;
59
+ const msg = context ? `${text} ${JSON.stringify(context)}` : text;
59
60
 
60
61
  // ── Bridge N-API 위임 ──
61
- // Core logger.js 참조: bridge.logInfo(target, msg), bridge.logWarn(target, msg, location?)
62
62
  if (this._bridge) {
63
63
  try {
64
64
  if (level === 'info' && typeof this._bridge.logInfo === 'function') {
@@ -88,13 +88,13 @@ export default class Logger {
88
88
  timestamp,
89
89
  level,
90
90
  target: this._prefix,
91
- message,
91
+ message: msg,
92
92
  ...(context || {}),
93
93
  };
94
94
  console[level === 'debug' ? 'log' : level](JSON.stringify(entry));
95
95
  } else {
96
96
  const levelTag = level.toUpperCase().padEnd(5);
97
- const fullMsg = `${timestamp} ${levelTag} [${this._prefix}] ${message}`;
97
+ const fullMsg = `${timestamp} ${levelTag} [${this._prefix}] ${msg}`;
98
98
  if (context && Object.keys(context).length > 0) {
99
99
  console[level === 'debug' ? 'log' : level](fullMsg, context);
100
100
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fuzionx/framework",
3
- "version": "0.1.28",
3
+ "version": "0.1.30",
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.28",
37
+ "@fuzionx/core": "^0.1.30",
38
38
  "better-sqlite3": "^12.8.0",
39
39
  "knex": "^3.2.5",
40
40
  "mongoose": "^9.3.2",
@@ -1,14 +0,0 @@
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=
@@ -1,4 +0,0 @@
1
- .env
2
- node_modules/
3
- storage/logs/
4
- storage/uploads/
@@ -1,6 +0,0 @@
1
- import { Application } from '@fuzionx/framework';
2
-
3
- const app = new Application({ configPath: './fuzionx.yaml' });
4
-
5
- await app.boot();
6
- await app.listen();
@@ -1,202 +0,0 @@
1
- # ═══════════════════════════════════════════════
2
- # fuzionx.yaml — FuzionX 전체 설정 레퍼런스
3
- # ═══════════════════════════════════════════════
4
-
5
- # ── Bridge (Rust 엔진) ──────────────────────────
6
- bridge:
7
- port: 49080
8
- workers: 0 # 0 = CPU 코어 수 자동
9
- worker_timeout: 30 # 워커 요청 타임아웃 (초)
10
-
11
- # ── 보안: CORS ──
12
- cors:
13
- enabled: false
14
- origins:
15
- - "*"
16
-
17
- # ── 보안: Rate Limit ──
18
- rate_limit:
19
- enabled: true
20
- per_ip: 1000 # IP당 초당 요청 수
21
-
22
- # ── 보안: HSTS ──
23
- hsts:
24
- enabled: false
25
- max_age: 31536000 # 1년 (초)
26
-
27
- # ── 보안: CSP ──
28
- csp:
29
- enabled: false
30
- directives:
31
- - "default-src 'self'"
32
-
33
- # ── 보안: IP Filter ──
34
- ip_filter:
35
- enabled: false
36
- whitelist: [] # CIDR 형식
37
- blacklist: []
38
-
39
- # ── 세션 ──
40
- session:
41
- enabled: false
42
- store: memory # memory | redis
43
- ttl: 3600 # 세션 TTL (초)
44
- cookie_name: fuzionx.sid
45
- # redis_url: "redis://localhost:6379"
46
-
47
- # ── 국제화 (i18n) ──
48
- i18n:
49
- enabled: false
50
- default_locale: ko
51
- locale_dir: ./locales
52
- auto_complete: false
53
- locales:
54
- - ko
55
- - en
56
-
57
- # ── WebSocket ──
58
- websocket:
59
- enabled: false
60
- path: /ws
61
- check_interval: 60 # 헬스체크 간격 (초)
62
- timeout: 60 # 타임아웃 (초)
63
-
64
- # ── Hub (멀티서버 동기화) ──
65
- hub:
66
- enabled: false
67
- url: "ws://127.0.0.1:9100/ws/bridge"
68
- group: "{{name}}"
69
-
70
- # ── 파일 업로드 ──
71
- upload:
72
- dir: /tmp
73
- max_size: "1gb"
74
- max_files: 20
75
- allowed_types:
76
- - image/jpeg
77
- - image/png
78
- - image/webp
79
- - image/gif
80
- - video/mp4
81
- - video/webm
82
- - application/pdf
83
- # watermark: ./assets/watermark.png
84
- # watermark_opacity: 50
85
-
86
- # ── 정적 파일 ──
87
- static:
88
- - url: /public
89
- path: ./public
90
-
91
- # ── 로깅 ──
92
- logging:
93
- level: info # error | warn | info | debug
94
- intercept_console: true # console.* 가로채기
95
- file:
96
- enabled: false
97
- path: ./logs/app.log
98
-
99
- # ── 액세스 로그 ──
100
- access_log:
101
- enabled: false
102
- path: ./logs/access.log
103
- error_path: ./logs/error.log
104
-
105
- # ── 트래픽 캡처 ──
106
- traffic_capture:
107
- enabled: false
108
- max_body_size: "64KB"
109
- exclude_content_types: []
110
- loki:
111
- enabled: false
112
- url: ""
113
- labels: []
114
- fields: []
115
- batch_size: 100
116
- flush_interval: "2s"
117
- clickhouse:
118
- enabled: false
119
- url: ""
120
- database: fuzionx
121
- table: traffic
122
- format: full # metadata | headers_only | full
123
- batch_size: 100
124
- flush_interval: "1s"
125
-
126
- # ── ASP 암호화 ──
127
- asp:
128
- enabled: false
129
- master_secret: "${ASP_SECRET:change-me-in-production}"
130
- header_signal: Ruxy-Enc-Mode
131
-
132
- # ── Database ────────────────────────────────────
133
- database:
134
- default: main
135
- connections:
136
- main:
137
- driver: sqlite # sqlite | mariadb | postgres | mongodb
138
- database: ./storage/database.sqlite
139
- # host: "127.0.0.1"
140
- # port: 3306
141
- # user: root
142
- # password: ""
143
- # charset: utf8mb4
144
- # pool:
145
- # min: 2
146
- # max: 10
147
-
148
- # ── App (프레임워크) ────────────────────────────
149
- app:
150
- name: '{{name}}'
151
- environment: development # development | production
152
-
153
- asp:
154
- enabled: false
155
-
156
- # 인증
157
- auth:
158
- secret: '${JWT_SECRET:change-me-in-production}'
159
- accessTtl: '15m'
160
- # refreshTtl: '7d'
161
- # bcrypt_rounds: 12
162
-
163
- # 국제화 (프레임워크 편의 설정)
164
- i18n:
165
- default_locale: 'ko'
166
- fallback: 'en'
167
-
168
- # Swagger/OpenAPI
169
- docs:
170
- enabled: true
171
- path: '/docs'
172
-
173
- # 테마
174
- themes:
175
- default: 'default'
176
-
177
- # 스토리지
178
- # storage:
179
- # driver: local
180
- # local:
181
- # path: ./storage
182
- # url_prefix: /storage
183
-
184
- # 스케줄러
185
- # scheduler:
186
- # enabled: true
187
- # lock:
188
- # driver: null # null | redis
189
- # redis_url: ""
190
- # prefix: "myapp:lock"
191
-
192
- # 큐
193
- # queue:
194
- # driver: memory # memory | redis
195
- # redis_url: ""
196
- # prefix: "myapp:queue"
197
-
198
- # ── Multi-App (도메인 → 앱 라우팅) ────────────────
199
- apps:
200
- "localhost:49080": backend
201
- # "admin.example.com": backend
202
- # "example.com": frontend
@@ -1,15 +0,0 @@
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.28"
11
- },
12
- "devDependencies": {
13
- "vitest": "^3.0.0"
14
- }
15
- }
@@ -1,14 +0,0 @@
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 %}
@@ -1,188 +0,0 @@
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>