@fuzionx/framework 0.1.8 → 0.1.20
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 +99 -0
- package/cli/fx.js +3 -0
- package/cli/index.js +329 -0
- package/cli/templates/app/.env.example.tpl +14 -0
- package/cli/templates/app/.gitignore.tpl +4 -0
- package/cli/templates/app/app.js.tpl +14 -0
- package/cli/templates/app/controllers/HomeController.js +13 -0
- package/cli/templates/app/fuzionx.yaml.tpl +32 -0
- package/cli/templates/app/package.json.tpl +15 -0
- package/cli/templates/app/routes/api.js.tpl +7 -0
- package/cli/templates/app/routes/web.js.tpl +5 -0
- package/cli/templates/app/views/default/errors/404.html +15 -0
- package/cli/templates/app/views/default/errors/500.html +14 -0
- package/cli/templates/app/views/default/layouts/main.html +22 -0
- package/cli/templates/app/views/default/pages/home.html +188 -0
- package/cli/templates/make/controller.js.tpl +40 -0
- package/cli/templates/make/event.js.tpl +8 -0
- package/cli/templates/make/job.js.tpl +10 -0
- package/cli/templates/make/middleware.js.tpl +10 -0
- package/cli/templates/make/model.js.tpl +15 -0
- package/cli/templates/make/service.js.tpl +15 -0
- package/cli/templates/make/task.js.tpl +15 -0
- package/cli/templates/make/test.js.tpl +7 -0
- package/cli/templates/make/worker.js.tpl +14 -0
- package/cli/templates/make/ws.js.tpl +18 -0
- package/lib/core/Application.js +164 -5
- package/lib/core/AutoLoader.js +46 -0
- package/lib/core/Config.js +103 -0
- package/lib/core/Context.js +5 -11
- package/lib/view/View.js +31 -20
- package/package.json +4 -2
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
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
|
+
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,32 @@
|
|
|
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
|
+
default: main
|
|
13
|
+
connections:
|
|
14
|
+
main:
|
|
15
|
+
driver: sqlite
|
|
16
|
+
database: ./storage/database.sqlite
|
|
17
|
+
|
|
18
|
+
app:
|
|
19
|
+
name: '{{name}}'
|
|
20
|
+
environment: development
|
|
21
|
+
auth:
|
|
22
|
+
secret: 'change-me-in-production'
|
|
23
|
+
accessTtl: '15m'
|
|
24
|
+
i18n:
|
|
25
|
+
default_locale: 'ko'
|
|
26
|
+
fallback: 'en'
|
|
27
|
+
docs:
|
|
28
|
+
enabled: true
|
|
29
|
+
path: '/docs'
|
|
30
|
+
|
|
31
|
+
themes:
|
|
32
|
+
default: 'default'
|
|
@@ -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>
|