@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 +102 -108
- package/cli/templates/{app → make/app}/controllers/HomeController.js +1 -0
- package/cli/templates/make/app/views/default/errors/404.html +11 -0
- package/cli/templates/{app/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 +5 -2
- package/lib/helpers/Logger.js +6 -6
- 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/fuzionx.yaml.tpl +0 -202
- package/cli/templates/app/package.json.tpl +0 -15
- package/cli/templates/app/views/default/errors/500.html +0 -14
- package/cli/templates/app/views/default/pages/home.html +0 -188
- /package/cli/templates/{app/routes/api.js.tpl → make/app/routes/api.js} +0 -0
- /package/cli/templates/{app/routes/web.js.tpl → make/app/routes/web.js} +0 -0
- /package/cli/templates/{app → 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> 모델 생성
|
|
@@ -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=
|
|
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
|
-
//
|
|
123
|
+
// 유틸리티
|
|
124
124
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
125
125
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
|
|
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
|
|
@@ -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 %}
|
package/cli/templates/{app/views/default/errors/404.html → make/app/views/default/errors/500.html}
RENAMED
|
@@ -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';
|
|
@@ -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] || '
|
|
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
|
-
//
|
|
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/lib/helpers/Logger.js
CHANGED
|
@@ -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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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}] ${
|
|
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.
|
|
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,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,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>
|
|
File without changes
|
|
File without changes
|
|
File without changes
|