@fuzionx/framework 0.1.2 → 0.1.5
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/README.md +496 -0
- package/lib/core/Application.js +15 -1
- package/lib/view/View.js +29 -29
- package/package.json +2 -2
package/README.md
ADDED
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
# @fuzionx/framework
|
|
2
|
+
|
|
3
|
+
> Laravel-inspired full-stack MVC framework powered by Rust N-API bridge — **500K+ RPS**
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
@fuzionx/framework ← npm install (개발자가 설치)
|
|
7
|
+
└── @fuzionx/core ← HTTP 엔진 (libuv Fusion, WebSocket, 세션, 크립토)
|
|
8
|
+
└── @fuzionx/bridge ← Rust N-API 네이티브 모듈
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# 프로젝트 생성
|
|
17
|
+
npx create-fuzionx my-app
|
|
18
|
+
cd my-app
|
|
19
|
+
npm install
|
|
20
|
+
|
|
21
|
+
# 개발 서버
|
|
22
|
+
npx fx dev # --watch 자동 재시작
|
|
23
|
+
|
|
24
|
+
# 또는 직접 실행
|
|
25
|
+
node app.js
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
서버가 `http://localhost:49080` 에서 시작됩니다.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## 프로젝트 구조
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
my-app/
|
|
36
|
+
├── fuzionx.yaml # 서버 + DB + 세션 설정
|
|
37
|
+
├── .env # 환경변수
|
|
38
|
+
├── app.js # 엔트리포인트
|
|
39
|
+
│
|
|
40
|
+
├── routes/ # 라우트 정의
|
|
41
|
+
│ ├── api.js
|
|
42
|
+
│ └── web.js
|
|
43
|
+
│
|
|
44
|
+
├── controllers/ # HTTP 컨트롤러 (자동 스캔)
|
|
45
|
+
│ └── UserController.js
|
|
46
|
+
│
|
|
47
|
+
├── models/ # ORM 모델 (자동 스캔)
|
|
48
|
+
│ └── User.js
|
|
49
|
+
│
|
|
50
|
+
├── services/ # 비즈니스 로직 (자동 스캔)
|
|
51
|
+
│ └── UserService.js
|
|
52
|
+
│
|
|
53
|
+
├── middleware/ # 미들웨어 클래스 (자동 스캔)
|
|
54
|
+
│ └── AuthMiddleware.js
|
|
55
|
+
│
|
|
56
|
+
├── ws/ # WebSocket 핸들러 (자동 스캔)
|
|
57
|
+
├── jobs/ # 스케줄러/큐 Job (자동 스캔)
|
|
58
|
+
├── events/ # 이벤트 리스너
|
|
59
|
+
│
|
|
60
|
+
├── views/ # 템플릿 (Tera SSR / Bridge 렌더링)
|
|
61
|
+
│ └── default/
|
|
62
|
+
│ ├── layouts/
|
|
63
|
+
│ │ └── main.html
|
|
64
|
+
│ └── pages/
|
|
65
|
+
│ └── home.html
|
|
66
|
+
│
|
|
67
|
+
├── locales/ # i18n 번역 파일
|
|
68
|
+
│ ├── ko.yaml
|
|
69
|
+
│ └── en.yaml
|
|
70
|
+
│
|
|
71
|
+
├── storage/ # 로그, 업로드
|
|
72
|
+
└── public/ # 정적 파일
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## 엔트리포인트 (app.js)
|
|
78
|
+
|
|
79
|
+
```javascript
|
|
80
|
+
import { Application } from '@fuzionx/framework';
|
|
81
|
+
import apiRoutes from './routes/api.js';
|
|
82
|
+
import webRoutes from './routes/web.js';
|
|
83
|
+
|
|
84
|
+
const app = new Application({ configPath: './fuzionx.yaml' });
|
|
85
|
+
|
|
86
|
+
// 라우트 등록
|
|
87
|
+
app.routes(apiRoutes);
|
|
88
|
+
app.routes(webRoutes);
|
|
89
|
+
|
|
90
|
+
// 서비스 등록 (Lazy 싱글톤)
|
|
91
|
+
app.register('mailer', () => new MailService(app.config.get('app.mail')));
|
|
92
|
+
|
|
93
|
+
// 라이프사이클 훅
|
|
94
|
+
app.on('booted', async () => {
|
|
95
|
+
console.log(`DB 연결 완료`);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// 서버 시작
|
|
99
|
+
app.listen(49080, () => {
|
|
100
|
+
console.log('🚀 FuzionX running on http://localhost:49080');
|
|
101
|
+
});
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## 라우팅 & 컨트롤러
|
|
107
|
+
|
|
108
|
+
### 라우트 파일
|
|
109
|
+
|
|
110
|
+
```javascript
|
|
111
|
+
// routes/api.js
|
|
112
|
+
import UserController from '../controllers/UserController.js';
|
|
113
|
+
import Joi from 'joi';
|
|
114
|
+
|
|
115
|
+
export default (r) => {
|
|
116
|
+
r.group('/api', { middleware: ['apiAuth'] }, (r) => {
|
|
117
|
+
r.group('/users', (r) => {
|
|
118
|
+
r.get('/', UserController.index);
|
|
119
|
+
r.get('/:id', UserController.show);
|
|
120
|
+
r.post('/', UserController.store, {
|
|
121
|
+
validate: { body: Joi.object({
|
|
122
|
+
name: Joi.string().min(2).required(),
|
|
123
|
+
email: Joi.string().email().required(),
|
|
124
|
+
}) }
|
|
125
|
+
});
|
|
126
|
+
r.put('/:id', UserController.update);
|
|
127
|
+
r.delete('/:id', UserController.destroy);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
};
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
```javascript
|
|
134
|
+
// routes/web.js
|
|
135
|
+
import HomeController from '../controllers/HomeController.js';
|
|
136
|
+
|
|
137
|
+
export default (r) => {
|
|
138
|
+
r.get('/', HomeController.index);
|
|
139
|
+
r.get('/about', HomeController.about);
|
|
140
|
+
};
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### 컨트롤러
|
|
144
|
+
|
|
145
|
+
```javascript
|
|
146
|
+
// controllers/UserController.js
|
|
147
|
+
import { Controller } from '@fuzionx/framework';
|
|
148
|
+
|
|
149
|
+
export default class UserController extends Controller {
|
|
150
|
+
|
|
151
|
+
async index(ctx) {
|
|
152
|
+
const users = await this.db.User.paginate(ctx.query.page || 1, 20);
|
|
153
|
+
ctx.json(users);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async show(ctx) {
|
|
157
|
+
const user = await this.db.User.findOrFail(ctx.params.id);
|
|
158
|
+
ctx.json(user);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async store(ctx) {
|
|
162
|
+
const user = await this.service('UserService').register(ctx.body);
|
|
163
|
+
ctx.status(201).json(user);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async update(ctx) {
|
|
167
|
+
const user = await this.db.User.findOrFail(ctx.params.id);
|
|
168
|
+
await user.update(ctx.body);
|
|
169
|
+
ctx.json(user);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async destroy(ctx) {
|
|
173
|
+
await this.service('UserService').deactivate(ctx.params.id);
|
|
174
|
+
ctx.status(204).end();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
미들웨어는 `글로벌 → 그룹 → 라우트별` 3단계로 적용됩니다.
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## Context (`ctx`)
|
|
184
|
+
|
|
185
|
+
모든 핸들러, 미들웨어에 전달되는 통합 객체:
|
|
186
|
+
|
|
187
|
+
### Request
|
|
188
|
+
|
|
189
|
+
```javascript
|
|
190
|
+
ctx.method // 'GET', 'POST', ...
|
|
191
|
+
ctx.url // '/users/42?page=1'
|
|
192
|
+
ctx.path // '/users/42'
|
|
193
|
+
ctx.params // { id: '42' }
|
|
194
|
+
ctx.query // { page: '1' }
|
|
195
|
+
ctx.body // JSON 자동 파싱된 요청 바디
|
|
196
|
+
ctx.headers // 요청 헤더
|
|
197
|
+
ctx.ip // 클라이언트 IP
|
|
198
|
+
ctx.user // 인증된 사용자 (미들웨어에서 설정)
|
|
199
|
+
ctx.session // 세션
|
|
200
|
+
ctx.cookies // 파싱된 쿠키 객체
|
|
201
|
+
ctx.locale // 현재 locale ('ko', 'en')
|
|
202
|
+
ctx.files // 업로드된 파일 메타데이터 배열
|
|
203
|
+
|
|
204
|
+
ctx.get(header) // 헤더 값
|
|
205
|
+
ctx.is(type) // Content-Type 확인
|
|
206
|
+
ctx.accepts('json','html') // Accept 협상
|
|
207
|
+
ctx.t(key, vars) // i18n 번역
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Response
|
|
211
|
+
|
|
212
|
+
```javascript
|
|
213
|
+
ctx.json(data) // JSON 응답
|
|
214
|
+
ctx.status(201).json(data) // 상태 코드 + JSON
|
|
215
|
+
ctx.send(html) // HTML 응답
|
|
216
|
+
ctx.text(string) // text/plain
|
|
217
|
+
ctx.render('home', { title }) // 템플릿 렌더링 (Rust Bridge SSR)
|
|
218
|
+
ctx.redirect('/login') // 302 리다이렉트
|
|
219
|
+
ctx.redirect('/new', 301) // 301 영구 리다이렉트
|
|
220
|
+
ctx.back() // Referer로 리다이렉트
|
|
221
|
+
ctx.error(404, 'Not found') // 에러 응답
|
|
222
|
+
ctx.download(filePath) // 파일 다운로드
|
|
223
|
+
ctx.stream(readable) // 스트림 응답
|
|
224
|
+
ctx.setHeader(key, value) // 헤더 설정
|
|
225
|
+
ctx.cookie(name, value, opts) // 쿠키 설정
|
|
226
|
+
ctx.end() // 응답 종료
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
231
|
+
## Model (ORM)
|
|
232
|
+
|
|
233
|
+
```javascript
|
|
234
|
+
// models/User.js
|
|
235
|
+
import { Model } from '@fuzionx/framework';
|
|
236
|
+
|
|
237
|
+
export default class User extends Model {
|
|
238
|
+
static table = 'users';
|
|
239
|
+
static timestamps = true;
|
|
240
|
+
static hidden = ['password'];
|
|
241
|
+
|
|
242
|
+
static columns = {
|
|
243
|
+
id: { type: 'increments' },
|
|
244
|
+
name: { type: 'string', length: 100 },
|
|
245
|
+
email: { type: 'string', length: 150, unique: true },
|
|
246
|
+
password: { type: 'string', length: 255 },
|
|
247
|
+
active: { type: 'boolean', default: true },
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
posts() { return this.hasMany('Post'); }
|
|
251
|
+
profile() { return this.hasOne('Profile'); }
|
|
252
|
+
|
|
253
|
+
static findByEmail(email) {
|
|
254
|
+
return this.where('email', email).first();
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
**지원 드라이버**: SQLite · MariaDB · PostgreSQL · MongoDB
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
## Service
|
|
264
|
+
|
|
265
|
+
```javascript
|
|
266
|
+
// services/UserService.js
|
|
267
|
+
import { Service } from '@fuzionx/framework';
|
|
268
|
+
|
|
269
|
+
export default class UserService extends Service {
|
|
270
|
+
|
|
271
|
+
async register(data) {
|
|
272
|
+
if (await this.db.User.findByEmail(data.email)) {
|
|
273
|
+
throw this.error('이미 존재하는 이메일');
|
|
274
|
+
}
|
|
275
|
+
const user = await this.transaction(async (trx) => {
|
|
276
|
+
const user = await this.db.User.create(data, { trx });
|
|
277
|
+
await this.db.Profile.create({ userId: user.id }, { trx });
|
|
278
|
+
return user;
|
|
279
|
+
});
|
|
280
|
+
this.emit('user:created', user);
|
|
281
|
+
return user;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
---
|
|
287
|
+
|
|
288
|
+
## Middleware
|
|
289
|
+
|
|
290
|
+
```javascript
|
|
291
|
+
// middleware/AuthMiddleware.js
|
|
292
|
+
import { Middleware } from '@fuzionx/framework';
|
|
293
|
+
|
|
294
|
+
export default class AuthMiddleware extends Middleware {
|
|
295
|
+
static name = 'auth';
|
|
296
|
+
|
|
297
|
+
handle(ctx, next) {
|
|
298
|
+
const token = ctx.headers.authorization?.replace('Bearer ', '');
|
|
299
|
+
if (!token) return ctx.error(401, 'Unauthorized');
|
|
300
|
+
ctx.user = this.service('AuthService').verify(token);
|
|
301
|
+
next();
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### 내장 미들웨어
|
|
307
|
+
|
|
308
|
+
| 이름 | 기능 | 처리 |
|
|
309
|
+
|------|------|------|
|
|
310
|
+
| `bodyParser` | JSON/Form 바디 파싱 | JS |
|
|
311
|
+
| `cors` | CORS 헤더 | Rust |
|
|
312
|
+
| `csrf` | CSRF 토큰 검증 | JS |
|
|
313
|
+
| `session` | 세션 로드/저장 | Rust |
|
|
314
|
+
| `rateLimit` | 요청 제한 | Rust |
|
|
315
|
+
| `static` | 정적 파일 서빙 | Rust |
|
|
316
|
+
|
|
317
|
+
---
|
|
318
|
+
|
|
319
|
+
## WebSocket
|
|
320
|
+
|
|
321
|
+
```javascript
|
|
322
|
+
// ws/ChatHandler.js
|
|
323
|
+
import { WsHandler } from '@fuzionx/framework';
|
|
324
|
+
|
|
325
|
+
export default class ChatHandler extends WsHandler {
|
|
326
|
+
static namespace = '/chat';
|
|
327
|
+
static middleware = ['auth'];
|
|
328
|
+
|
|
329
|
+
static events(e) {
|
|
330
|
+
e.on('chat', this.handleChat);
|
|
331
|
+
e.on('typing', this.handleTyping);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
handleChat(socket, data) {
|
|
335
|
+
socket.to(`room:${data.roomId}`).send({
|
|
336
|
+
type: 'chat',
|
|
337
|
+
data: { user: socket.user, message: data.message },
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
---
|
|
344
|
+
|
|
345
|
+
## 설정 (`fuzionx.yaml`)
|
|
346
|
+
|
|
347
|
+
```yaml
|
|
348
|
+
bridge:
|
|
349
|
+
port: 49080
|
|
350
|
+
workers: 4 # 0 = CPU 수 자동
|
|
351
|
+
cors:
|
|
352
|
+
enabled: true
|
|
353
|
+
origins: ["*"]
|
|
354
|
+
rate_limit:
|
|
355
|
+
enabled: true
|
|
356
|
+
per_ip: 1000
|
|
357
|
+
session:
|
|
358
|
+
enabled: true
|
|
359
|
+
store: memory # memory | redis
|
|
360
|
+
ttl: 3600
|
|
361
|
+
logging:
|
|
362
|
+
level: info
|
|
363
|
+
|
|
364
|
+
database:
|
|
365
|
+
main:
|
|
366
|
+
driver: sqlite
|
|
367
|
+
path: ./storage/database.sqlite
|
|
368
|
+
|
|
369
|
+
app:
|
|
370
|
+
name: 'My App'
|
|
371
|
+
environment: ${NODE_ENV:development}
|
|
372
|
+
auth:
|
|
373
|
+
secret: ${JWT_SECRET:change-me}
|
|
374
|
+
accessTtl: '15m'
|
|
375
|
+
i18n:
|
|
376
|
+
default_locale: 'ko'
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
### 환경변수 치환
|
|
380
|
+
|
|
381
|
+
```yaml
|
|
382
|
+
host: ${DB_HOST:127.0.0.1} # 없으면 기본값 사용
|
|
383
|
+
password: ${DB_PASSWORD} # 필수 — 없으면 부트 시 에러
|
|
384
|
+
redis_url: ${REDIS_URL:} # 선택 — 없으면 빈 문자열
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
### Config 접근
|
|
388
|
+
|
|
389
|
+
```javascript
|
|
390
|
+
app.config.get('bridge.port') // 49080
|
|
391
|
+
app.config.get('app.auth.secret') // JWT secret
|
|
392
|
+
app.config.get('app.payment.api_key', null) // 커스텀 설정 + 기본값
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
---
|
|
396
|
+
|
|
397
|
+
## CLI
|
|
398
|
+
|
|
399
|
+
### 프로젝트 생성 (`create-fuzionx`)
|
|
400
|
+
|
|
401
|
+
```bash
|
|
402
|
+
npx create-fuzionx my-app # 새 프로젝트 스캐폴딩
|
|
403
|
+
cd my-app
|
|
404
|
+
npm install
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
### 프로젝트 내 명령어 (`fx`)
|
|
408
|
+
|
|
409
|
+
```bash
|
|
410
|
+
npx fx make:controller User # CRUD 컨트롤러
|
|
411
|
+
npx fx make:model User # ORM 모델
|
|
412
|
+
npx fx make:service User # 서비스
|
|
413
|
+
npx fx make:middleware Auth # 미들웨어
|
|
414
|
+
npx fx make:job Cleanup # 스케줄 Job
|
|
415
|
+
npx fx make:task SendEmail # 큐 Task
|
|
416
|
+
npx fx make:ws Chat # WebSocket 핸들러
|
|
417
|
+
npx fx make:event user # 이벤트 핸들러
|
|
418
|
+
npx fx make:worker Heavy # Worker (worker_threads)
|
|
419
|
+
npx fx make:test User # 테스트
|
|
420
|
+
|
|
421
|
+
# DB
|
|
422
|
+
npx fx db:sync # 모델 ↔ DB 스키마 diff
|
|
423
|
+
npx fx db:sync --apply # 안전 변경 적용
|
|
424
|
+
|
|
425
|
+
# 개발
|
|
426
|
+
npx fx dev # 개발 서버 (--watch)
|
|
427
|
+
npx fx dev --port=4000 # 포트 지정
|
|
428
|
+
npx fx test # 테스트 실행 (vitest)
|
|
429
|
+
npx fx routes # 라우트 테이블 출력
|
|
430
|
+
npx fx config # 설정 파싱 결과 출력
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
---
|
|
434
|
+
|
|
435
|
+
## 부트스트랩 순서
|
|
436
|
+
|
|
437
|
+
```
|
|
438
|
+
app.js 실행
|
|
439
|
+
├── 1. Config 로드 fuzionx.yaml 파싱
|
|
440
|
+
├── 2. .env 로드 환경변수 주입
|
|
441
|
+
├── 3. Bridge 부트 Rust 네이티브 초기화
|
|
442
|
+
│ ★ emit('booting')
|
|
443
|
+
├── 4. DB 연결 Knex/Mongoose
|
|
444
|
+
├── 5. 모델 로드 models/ 자동 스캔
|
|
445
|
+
│ ★ emit('booted')
|
|
446
|
+
├── 6. i18n 로드 locales/ 번역 파일
|
|
447
|
+
├── 7~12. 자동 스캔 services, middleware, controllers,
|
|
448
|
+
│ ws, jobs, events
|
|
449
|
+
│ ★ emit('ready')
|
|
450
|
+
└── 13. 서버 시작 startFusionServer(port)
|
|
451
|
+
★ emit('listening')
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
### 라이프사이클 훅
|
|
455
|
+
|
|
456
|
+
| 훅 | 시점 | 용도 |
|
|
457
|
+
|---|------|------|
|
|
458
|
+
| `booting` | Config 로드 직후 | 서비스 등록, 환경 설정 |
|
|
459
|
+
| `booted` | DB + 모델 로드 완료 | 캐시 워밍 |
|
|
460
|
+
| `ready` | 모든 초기화 완료 | 준비 로그 |
|
|
461
|
+
| `listening` | 서버 시작 완료 | 포트 표시 |
|
|
462
|
+
| `shutdown` | SIGTERM/SIGINT | 리소스 정리 |
|
|
463
|
+
|
|
464
|
+
---
|
|
465
|
+
|
|
466
|
+
## 에러 처리
|
|
467
|
+
|
|
468
|
+
| 에러 타입 | HTTP | JSON | HTML |
|
|
469
|
+
|-----------|:----:|------|------|
|
|
470
|
+
| ValidationError | 422 | `{ error, fields }` | flash + redirect |
|
|
471
|
+
| ServiceError | 400/403/404 | `{ error }` | 테마 에러 페이지 |
|
|
472
|
+
| Error (dev) | 500 | `{ error, stack }` | 에러 페이지 + 스택 |
|
|
473
|
+
| Error (prod) | 500 | `{ error }` | 에러 페이지 |
|
|
474
|
+
|
|
475
|
+
---
|
|
476
|
+
|
|
477
|
+
## 주요 특징
|
|
478
|
+
|
|
479
|
+
- ⚡ **500K+ RPS** — Rust N-API Bridge (libuv + SO_REUSEPORT)
|
|
480
|
+
- 🦀 **Rust 네이티브** — HTTP, 세션, CORS, Rate Limit, 암호화 모두 Rust 처리
|
|
481
|
+
- 🎯 **MVC 아키텍처** — Controller, Service, Model 분리
|
|
482
|
+
- 📦 **자동 스캔** — controllers/, models/, services/ 등 자동 로드
|
|
483
|
+
- 🔌 **WebSocket** — 네임스페이스 기반 이벤트 라우팅
|
|
484
|
+
- 🗄️ **Multi-DB ORM** — SQLite, MariaDB, PostgreSQL, MongoDB
|
|
485
|
+
- 🔐 **Auth & Session** — JWT + 세션 (Memory/Redis)
|
|
486
|
+
- 📡 **EventBus** — 도메인 이벤트 + Hub 연동 (멀티서버)
|
|
487
|
+
- ⏰ **Scheduler & Queue** — Job (cron), Task (큐)
|
|
488
|
+
- 🌍 **i18n** — 다국어 지원, `ctx.t()`, locale 자동 감지
|
|
489
|
+
- 🎨 **Tera SSR** — Rust 기반 템플릿 렌더링
|
|
490
|
+
- 📄 **OpenAPI** — 자동 Swagger 문서 생성
|
|
491
|
+
|
|
492
|
+
---
|
|
493
|
+
|
|
494
|
+
## License
|
|
495
|
+
|
|
496
|
+
MIT
|
package/lib/core/Application.js
CHANGED
|
@@ -34,7 +34,10 @@ let FuzionXApp;
|
|
|
34
34
|
try {
|
|
35
35
|
const core = await import('@fuzionx/core');
|
|
36
36
|
FuzionXApp = core.FuzionXApp || core.RuxyApp;
|
|
37
|
-
} catch {
|
|
37
|
+
} catch (e) {
|
|
38
|
+
console.error('[fuzionx-framework] ⚠️ @fuzionx/core 로드 실패 — Bridge 없이 테스트 모드로 실행됩니다:',
|
|
39
|
+
e.message);
|
|
40
|
+
}
|
|
38
41
|
|
|
39
42
|
export default class Application {
|
|
40
43
|
/**
|
|
@@ -304,6 +307,17 @@ export default class Application {
|
|
|
304
307
|
: undefined,
|
|
305
308
|
port,
|
|
306
309
|
});
|
|
310
|
+
// FuzionXApp 생성 후 Bridge 전파 — boot() 시점에는 null이었음
|
|
311
|
+
this._bridge = this._coreApp._bridge;
|
|
312
|
+
if (this._bridge) {
|
|
313
|
+
if (this._view) this._view._bridge = this._bridge;
|
|
314
|
+
if (this.logger) this.logger._bridge = this._bridge;
|
|
315
|
+
if (this.crypto) this.crypto._bridge = this._bridge;
|
|
316
|
+
if (this.hash) this.hash._bridge = this._bridge;
|
|
317
|
+
if (this.media) this.media._bridge = this._bridge;
|
|
318
|
+
if (this.file) this.file._bridge = this._bridge;
|
|
319
|
+
if (this.i18n) this.i18n._bridge = this._bridge;
|
|
320
|
+
}
|
|
307
321
|
}
|
|
308
322
|
this._registerBridgeRoutes(this._coreApp);
|
|
309
323
|
this._coreApp.listen(port, callback);
|
package/lib/view/View.js
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* View — 뷰 렌더링 (
|
|
2
|
+
* View — 뷰 렌더링 (Bridge SSR)
|
|
3
3
|
*
|
|
4
4
|
* ctx.theme에 따른 테마 경로 해석.
|
|
5
|
-
* Bridge
|
|
5
|
+
* Bridge renderTemplateFile N-API로 렌더링.
|
|
6
6
|
*
|
|
7
7
|
* @see docs/framework/03-views-templates.md
|
|
8
8
|
*/
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
|
|
9
11
|
export default class View {
|
|
10
12
|
/**
|
|
11
13
|
* @param {object} opts
|
|
12
14
|
* @param {string} opts.viewsPath - 뷰 파일 루트 경로
|
|
13
15
|
* @param {string} [opts.theme='default'] - 기본 테마
|
|
14
|
-
* @param {object} [opts.bridge] - Bridge
|
|
15
|
-
* @param {object} [opts.globals] - 모든 뷰에 주입되는 전역 변수
|
|
16
|
+
* @param {object} [opts.bridge] - Bridge N-API 인스턴스
|
|
16
17
|
*/
|
|
17
18
|
constructor(opts = {}) {
|
|
18
19
|
this.viewsPath = opts.viewsPath || '';
|
|
19
20
|
this.theme = opts.theme || 'default';
|
|
20
21
|
this._bridge = opts.bridge || null;
|
|
21
|
-
this._globals = opts.globals || {};
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
/**
|
|
@@ -27,46 +27,46 @@ export default class View {
|
|
|
27
27
|
* @param {*} value
|
|
28
28
|
*/
|
|
29
29
|
share(key, value) {
|
|
30
|
+
this._globals = this._globals || {};
|
|
30
31
|
this._globals[key] = value;
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
/**
|
|
34
|
-
* 템플릿 렌더링
|
|
35
|
+
* 템플릿 렌더링 — Bridge renderTemplateFile N-API 사용
|
|
35
36
|
*
|
|
36
37
|
* 테마 경로 해석 (03-views-templates.md):
|
|
37
|
-
* '
|
|
38
|
+
* 'home' → {viewsPath}/{theme}/pages/home.html
|
|
38
39
|
*
|
|
39
|
-
* @param {string} template - 'users/index' 등
|
|
40
|
+
* @param {string} template - 'home', 'users/index' 등
|
|
40
41
|
* @param {object} [data] - 템플릿 변수
|
|
41
|
-
* @param {string} [theme] - 테마 오버라이드
|
|
42
|
+
* @param {string} [theme] - 테마 오버라이드
|
|
42
43
|
* @returns {string} - 렌더링된 HTML
|
|
43
44
|
*/
|
|
44
45
|
render(template, data = {}, theme) {
|
|
45
46
|
const activeTheme = theme || data.theme || this.theme;
|
|
46
47
|
const mergedData = { ...this._globals, ...data, theme: activeTheme };
|
|
47
48
|
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
} catch {
|
|
54
|
-
// SSR 실패 시 폴백
|
|
55
|
-
}
|
|
56
|
-
}
|
|
49
|
+
// 파일 경로 해석: pages/{name}.html 우선
|
|
50
|
+
const candidates = [
|
|
51
|
+
join(this.viewsPath, activeTheme, 'pages', `${template}.html`),
|
|
52
|
+
join(this.viewsPath, activeTheme, `${template}.html`),
|
|
53
|
+
];
|
|
57
54
|
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
|
|
55
|
+
// Bridge renderTemplateFile N-API 호출
|
|
56
|
+
if (this._bridge && typeof this._bridge.renderTemplateFile === 'function') {
|
|
57
|
+
const contextJson = JSON.stringify(mergedData);
|
|
61
58
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
59
|
+
for (const filePath of candidates) {
|
|
60
|
+
try {
|
|
61
|
+
return this._bridge.renderTemplateFile(filePath, contextJson);
|
|
62
|
+
} catch {
|
|
63
|
+
// 파일 없음 → 다음 후보
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
throw new Error(`View '${template}' not found. Searched: ${candidates.join(', ')}`);
|
|
69
68
|
}
|
|
70
|
-
|
|
69
|
+
|
|
70
|
+
throw new Error(`Bridge not available — cannot render view '${template}'`);
|
|
71
71
|
}
|
|
72
72
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fuzionx/framework",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
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.5",
|
|
38
38
|
"better-sqlite3": "^12.8.0",
|
|
39
39
|
"knex": "^3.2.5",
|
|
40
40
|
"mongoose": "^9.3.2",
|