@fuzionx/framework 0.1.29 → 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 +90 -109
- package/cli/templates/{app/fuzionx → make/app}/controllers/HomeController.js +1 -0
- package/cli/templates/{app/tester → make/app}/views/default/errors/404.html +0 -4
- package/cli/templates/{app/fuzionx/views/default/errors/404.html → make/app/views/default/errors/500.html} +1 -2
- package/cli/templates/make/app/views/default/pages/home.html +11 -0
- package/lib/core/Application.js +4 -1
- package/package.json +2 -2
- package/cli/templates/app/.env.example.tpl +0 -14
- package/cli/templates/app/.gitignore.tpl +0 -4
- package/cli/templates/app/app.js.tpl +0 -6
- package/cli/templates/app/database/models/User.js +0 -9
- package/cli/templates/app/fuzionx/views/default/errors/500.html +0 -14
- package/cli/templates/app/fuzionx/views/default/pages/home.html +0 -188
- package/cli/templates/app/fuzionx.yaml.tpl +0 -202
- package/cli/templates/app/locales/en.json +0 -52
- package/cli/templates/app/locales/ko.json +0 -52
- package/cli/templates/app/package.json.tpl +0 -16
- package/cli/templates/app/shared/events/userEvents.js +0 -10
- package/cli/templates/app/shared/jobs/CleanupJob.js +0 -18
- package/cli/templates/app/shared/jobs/EmailTask.js +0 -17
- package/cli/templates/app/shared/jobs/VideoPreviewTask.js +0 -47
- package/cli/templates/app/shared/workers/heavy.js +0 -18
- package/cli/templates/app/tester/controllers/FileController.js +0 -288
- package/cli/templates/app/tester/controllers/HomeController.js +0 -36
- package/cli/templates/app/tester/controllers/UserController.js +0 -43
- package/cli/templates/app/tester/middleware/RequestLogger.js +0 -13
- package/cli/templates/app/tester/routes/api.js +0 -397
- package/cli/templates/app/tester/routes/web.js +0 -8
- package/cli/templates/app/tester/services/UserService.js +0 -52
- package/cli/templates/app/tester/views/default/errors/500.html +0 -14
- package/cli/templates/app/tester/views/default/layouts/main.html +0 -82
- package/cli/templates/app/tester/views/default/pages/home.html +0 -56
- package/cli/templates/app/tester/views/default/pages/i18n.html +0 -104
- package/cli/templates/app/tester/views/default/pages/upload.html +0 -149
- package/cli/templates/app/tester/views/default/pages/websocket.html +0 -239
- package/cli/templates/app/tester/views/default/partials/footer.html +0 -8
- package/cli/templates/app/tester/views/default/partials/header.html +0 -20
- package/cli/templates/app/tester/ws/ChatHandler.js +0 -98
- /package/cli/templates/{app/fuzionx/routes/api.js.tpl → make/app/routes/api.js} +0 -0
- /package/cli/templates/{app/fuzionx/routes/web.js.tpl → make/app/routes/web.js} +0 -0
- /package/cli/templates/{app/fuzionx → make/app}/views/default/layouts/main.html +0 -0
package/cli/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* CLI — fx 명령어 핸들러
|
|
3
3
|
*
|
|
4
|
-
* fx
|
|
4
|
+
* fx make:app <name> 앱 디렉토리 생성
|
|
5
5
|
* fx make:controller <Name> 컨트롤러 생성
|
|
6
6
|
* fx make:service <Name> 서비스 생성
|
|
7
7
|
* fx make:model <Name> 모델 생성
|
|
@@ -120,24 +120,9 @@ export async function makeFile(type, name, baseDir = '.', appName) {
|
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
123
|
-
//
|
|
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/.env.example.tpl', dest: '.env.example' },
|
|
131
|
-
{ tpl: 'app/.env.example.tpl', dest: '.env' },
|
|
132
|
-
{ tpl: 'app/.gitignore.tpl', dest: '.gitignore' },
|
|
133
|
-
];
|
|
134
|
-
|
|
135
|
-
/** fuzionx 앱 템플릿 (.tpl 파일은 치환, 그 외는 직접 복사) */
|
|
136
|
-
const FUZIONX_TPL_FILES = [
|
|
137
|
-
{ tpl: 'app/fuzionx/routes/web.js.tpl', dest: 'app/fuzionx/routes/web.js' },
|
|
138
|
-
{ tpl: 'app/fuzionx/routes/api.js.tpl', dest: 'app/fuzionx/routes/api.js' },
|
|
139
|
-
];
|
|
140
|
-
|
|
141
126
|
/** 디렉토리 재귀 복사 (템플릿 → 프로젝트) */
|
|
142
127
|
async function copyDirRecursive(src, dst) {
|
|
143
128
|
await fs.mkdir(dst, { recursive: true });
|
|
@@ -153,91 +138,6 @@ async function copyDirRecursive(src, dst) {
|
|
|
153
138
|
}
|
|
154
139
|
}
|
|
155
140
|
|
|
156
|
-
/** 스캐폴딩 디렉토리 (빈 디렉토리 + .gitkeep) */
|
|
157
|
-
const APP_DIRS = [
|
|
158
|
-
// fuzionx 앱 (개발자 작업 영역)
|
|
159
|
-
'app/fuzionx/services',
|
|
160
|
-
'app/fuzionx/middleware',
|
|
161
|
-
'app/fuzionx/ws',
|
|
162
|
-
// tester 앱 (빈 폴더가 아닌 것은 copyDirRecursive로 처리)
|
|
163
|
-
// 인프라
|
|
164
|
-
'database/migrations',
|
|
165
|
-
'database/seeds',
|
|
166
|
-
'storage/logs',
|
|
167
|
-
'storage/uploads',
|
|
168
|
-
'public/css',
|
|
169
|
-
'public/js',
|
|
170
|
-
'tests',
|
|
171
|
-
];
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* fx new <name> — 앱 스캐폴딩 (fuzionx + tester 2-app 구조)
|
|
175
|
-
* @param {string} name - 프로젝트 이름
|
|
176
|
-
* @param {string} [targetDir] - 기본: ./<name>
|
|
177
|
-
*/
|
|
178
|
-
export async function createApp(name, targetDir) {
|
|
179
|
-
const dir = targetDir || path.resolve(name);
|
|
180
|
-
const vars = {
|
|
181
|
-
name,
|
|
182
|
-
dbName: name.replace(/-/g, '_'),
|
|
183
|
-
};
|
|
184
|
-
|
|
185
|
-
// 빈 디렉토리 생성 (.gitkeep 포함)
|
|
186
|
-
for (const d of APP_DIRS) {
|
|
187
|
-
const fullDir = path.join(dir, d);
|
|
188
|
-
await fs.mkdir(fullDir, { recursive: true });
|
|
189
|
-
const files = await fs.readdir(fullDir).catch(() => []);
|
|
190
|
-
if (files.length === 0) {
|
|
191
|
-
await fs.writeFile(path.join(fullDir, '.gitkeep'), '');
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// 루트 템플릿 파일 생성 (.tpl 치환)
|
|
196
|
-
for (const { tpl, dest } of APP_FILES) {
|
|
197
|
-
const content = await loadTemplate(tpl, vars);
|
|
198
|
-
const fullPath = path.join(dir, dest);
|
|
199
|
-
await fs.mkdir(path.dirname(fullPath), { recursive: true });
|
|
200
|
-
await fs.writeFile(fullPath, content);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// fuzionx 앱 — .tpl 파일 (치환 필요)
|
|
204
|
-
for (const { tpl, dest } of FUZIONX_TPL_FILES) {
|
|
205
|
-
const content = await loadTemplate(tpl, vars);
|
|
206
|
-
const fullPath = path.join(dir, dest);
|
|
207
|
-
await fs.mkdir(path.dirname(fullPath), { recursive: true });
|
|
208
|
-
await fs.writeFile(fullPath, content);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// fuzionx 앱 — 직접 복사 (controllers, views)
|
|
212
|
-
const fuzionxSrc = path.join(TPL_DIR, 'app/fuzionx');
|
|
213
|
-
await copyDirRecursive(
|
|
214
|
-
path.join(fuzionxSrc, 'controllers'),
|
|
215
|
-
path.join(dir, 'app/fuzionx/controllers'),
|
|
216
|
-
);
|
|
217
|
-
await copyDirRecursive(
|
|
218
|
-
path.join(fuzionxSrc, 'views'),
|
|
219
|
-
path.join(dir, 'app/fuzionx/views'),
|
|
220
|
-
);
|
|
221
|
-
|
|
222
|
-
// tester 앱 — 전체 복사
|
|
223
|
-
const testerSrc = path.join(TPL_DIR, 'app/tester');
|
|
224
|
-
await copyDirRecursive(testerSrc, path.join(dir, 'app/tester'));
|
|
225
|
-
|
|
226
|
-
// shared — 전체 복사
|
|
227
|
-
const sharedSrc = path.join(TPL_DIR, 'app/shared');
|
|
228
|
-
await copyDirRecursive(sharedSrc, path.join(dir, 'shared'));
|
|
229
|
-
|
|
230
|
-
// database/models — 복사
|
|
231
|
-
const dbSrc = path.join(TPL_DIR, 'app/database');
|
|
232
|
-
await copyDirRecursive(dbSrc, path.join(dir, 'database'));
|
|
233
|
-
|
|
234
|
-
// locales — 복사
|
|
235
|
-
const localesSrc = path.join(TPL_DIR, 'app/locales');
|
|
236
|
-
await copyDirRecursive(localesSrc, path.join(dir, 'locales'));
|
|
237
|
-
|
|
238
|
-
return dir;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
141
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
242
142
|
// CLI 엔트리
|
|
243
143
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
@@ -249,14 +149,29 @@ export async function createApp(name, targetDir) {
|
|
|
249
149
|
export async function run(args) {
|
|
250
150
|
const [command, ...rest] = args;
|
|
251
151
|
|
|
152
|
+
// ── fx new → create-fuzionx 안내 ──
|
|
252
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') {
|
|
253
160
|
const name = rest[0];
|
|
254
|
-
if (!name) { console.error('Usage: fx
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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`);
|
|
260
175
|
return;
|
|
261
176
|
}
|
|
262
177
|
|
|
@@ -284,6 +199,69 @@ export async function run(args) {
|
|
|
284
199
|
return;
|
|
285
200
|
}
|
|
286
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
|
+
|
|
287
265
|
// ── fx test — 테스트 실행 ──
|
|
288
266
|
if (command === 'test') {
|
|
289
267
|
const { execSync } = await import('node:child_process');
|
|
@@ -357,7 +335,8 @@ export async function run(args) {
|
|
|
357
335
|
}
|
|
358
336
|
|
|
359
337
|
console.log(`
|
|
360
|
-
|
|
338
|
+
npx create-fuzionx <name> Create new project
|
|
339
|
+
fx make:app <name> Create new app directory structure
|
|
361
340
|
fx make:controller <Name> --app= Create controller (app-specific)
|
|
362
341
|
fx make:service <Name> --app= Create service (app-specific)
|
|
363
342
|
fx make:model <Name> Create model (database/models)
|
|
@@ -369,6 +348,8 @@ export async function run(args) {
|
|
|
369
348
|
fx make:worker <Name> Create worker (shared/workers)
|
|
370
349
|
fx make:test <Name> Create test
|
|
371
350
|
fx dev Start dev server (--watch)
|
|
351
|
+
fx stop Stop server (graceful)
|
|
352
|
+
fx restart Restart server (graceful)
|
|
372
353
|
fx test Run tests
|
|
373
354
|
fx routes Print route table
|
|
374
355
|
fx config Print fuzionx.yaml
|
|
@@ -6,10 +6,6 @@
|
|
|
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
|
-
{% 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
9
|
<a href="/" style="display:inline-block;margin-top:24px;padding:12px 24px;background:#e74c3c;color:#fff;text-decoration:none;border-radius:6px;">홈으로 돌아가기</a>
|
|
14
10
|
</div>
|
|
15
11
|
{% endblock %}
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
{% extends "layouts/main.html" %}
|
|
2
2
|
|
|
3
|
-
{% block title %}
|
|
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 %}
|
package/lib/core/Application.js
CHANGED
|
@@ -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';
|
|
@@ -427,10 +428,12 @@ export default class Application {
|
|
|
427
428
|
|
|
428
429
|
if (!this._booted) await this.boot();
|
|
429
430
|
|
|
430
|
-
//
|
|
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');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fuzionx/framework",
|
|
3
|
-
"version": "0.1.
|
|
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.
|
|
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,9 +0,0 @@
|
|
|
1
|
-
import { MariaModel } from '@fuzionx/framework';
|
|
2
|
-
|
|
3
|
-
export default class User extends MariaModel {
|
|
4
|
-
static table = 'users';
|
|
5
|
-
static connection = 'main';
|
|
6
|
-
static softDelete = true;
|
|
7
|
-
static hidden = ['password'];
|
|
8
|
-
static fillable = ['name', 'email', 'password', 'role'];
|
|
9
|
-
}
|
|
@@ -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>
|