@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.
Files changed (41) hide show
  1. package/cli/index.js +90 -109
  2. package/cli/templates/{app/fuzionx → make/app}/controllers/HomeController.js +1 -0
  3. package/cli/templates/{app/tester → make/app}/views/default/errors/404.html +0 -4
  4. package/cli/templates/{app/fuzionx/views/default/errors/404.html → make/app/views/default/errors/500.html} +1 -2
  5. package/cli/templates/make/app/views/default/pages/home.html +11 -0
  6. package/lib/core/Application.js +4 -1
  7. package/package.json +2 -2
  8. package/cli/templates/app/.env.example.tpl +0 -14
  9. package/cli/templates/app/.gitignore.tpl +0 -4
  10. package/cli/templates/app/app.js.tpl +0 -6
  11. package/cli/templates/app/database/models/User.js +0 -9
  12. package/cli/templates/app/fuzionx/views/default/errors/500.html +0 -14
  13. package/cli/templates/app/fuzionx/views/default/pages/home.html +0 -188
  14. package/cli/templates/app/fuzionx.yaml.tpl +0 -202
  15. package/cli/templates/app/locales/en.json +0 -52
  16. package/cli/templates/app/locales/ko.json +0 -52
  17. package/cli/templates/app/package.json.tpl +0 -16
  18. package/cli/templates/app/shared/events/userEvents.js +0 -10
  19. package/cli/templates/app/shared/jobs/CleanupJob.js +0 -18
  20. package/cli/templates/app/shared/jobs/EmailTask.js +0 -17
  21. package/cli/templates/app/shared/jobs/VideoPreviewTask.js +0 -47
  22. package/cli/templates/app/shared/workers/heavy.js +0 -18
  23. package/cli/templates/app/tester/controllers/FileController.js +0 -288
  24. package/cli/templates/app/tester/controllers/HomeController.js +0 -36
  25. package/cli/templates/app/tester/controllers/UserController.js +0 -43
  26. package/cli/templates/app/tester/middleware/RequestLogger.js +0 -13
  27. package/cli/templates/app/tester/routes/api.js +0 -397
  28. package/cli/templates/app/tester/routes/web.js +0 -8
  29. package/cli/templates/app/tester/services/UserService.js +0 -52
  30. package/cli/templates/app/tester/views/default/errors/500.html +0 -14
  31. package/cli/templates/app/tester/views/default/layouts/main.html +0 -82
  32. package/cli/templates/app/tester/views/default/pages/home.html +0 -56
  33. package/cli/templates/app/tester/views/default/pages/i18n.html +0 -104
  34. package/cli/templates/app/tester/views/default/pages/upload.html +0 -149
  35. package/cli/templates/app/tester/views/default/pages/websocket.html +0 -239
  36. package/cli/templates/app/tester/views/default/partials/footer.html +0 -8
  37. package/cli/templates/app/tester/views/default/partials/header.html +0 -20
  38. package/cli/templates/app/tester/ws/ChatHandler.js +0 -98
  39. /package/cli/templates/{app/fuzionx/routes/api.js.tpl → make/app/routes/api.js} +0 -0
  40. /package/cli/templates/{app/fuzionx/routes/web.js.tpl → make/app/routes/web.js} +0 -0
  41. /package/cli/templates/{app/fuzionx → make/app}/views/default/layouts/main.html +0 -0
@@ -1,288 +0,0 @@
1
- import { Controller } from '@fuzionx/framework';
2
- import path from 'node:path';
3
-
4
- export default class FileController extends Controller {
5
-
6
- /** 단일 파일 업로드 */
7
- static upload;
8
- async upload(ctx) {
9
- const file = ctx.files?.[0];
10
- if (!file) {
11
- ctx.status(400).json({ error: '파일이 필요합니다' });
12
- return;
13
- }
14
-
15
- const destPath = `uploads/${Date.now()}_${file.originalName}`;
16
- const url = await ctx.app.storage.put(destPath, file.tempPath);
17
-
18
- ctx.json({
19
- success: true,
20
- file: {
21
- url,
22
- originalName: file.originalName,
23
- mimeType: file.mimeType,
24
- size: file.size,
25
- destPath,
26
- },
27
- });
28
- }
29
-
30
- /** 다중 파일 업로드 */
31
- static uploadMultiple;
32
- async uploadMultiple(ctx) {
33
- const files = ctx.files || [];
34
- if (files.length === 0) {
35
- ctx.status(400).json({ error: '파일이 필요합니다' });
36
- return;
37
- }
38
-
39
- const results = [];
40
- for (const file of files) {
41
- const destPath = `uploads/${Date.now()}_${file.originalName}`;
42
- const url = await ctx.app.storage.put(destPath, file.tempPath);
43
- results.push({
44
- url,
45
- originalName: file.originalName,
46
- mimeType: file.mimeType,
47
- size: file.size,
48
- });
49
- }
50
-
51
- ctx.json({ success: true, files: results, count: results.length });
52
- }
53
-
54
- /** 이미지 리사이즈 (Bridge Rust) */
55
- static resize;
56
- async resize(ctx) {
57
- const file = ctx.files?.[0];
58
- if (!file) {
59
- ctx.status(400).json({ error: '이미지 파일이 필요합니다' });
60
- return;
61
- }
62
-
63
- const width = Number(ctx.query?.width) || 200;
64
- const height = Number(ctx.query?.height) || 200;
65
- const format = ctx.query?.format || 'webp';
66
- const quality = Number(ctx.query?.quality) || 80;
67
-
68
- try {
69
- const ext = format === 'jpeg' ? 'jpg' : format;
70
- const outName = `resized/${Date.now()}_${width}x${height}.${ext}`;
71
- const outPath = path.resolve(ctx.app.storage._basePath || './storage', outName);
72
-
73
- await ctx.app.file.ensureDir(path.dirname(outPath));
74
- ctx.app.media.resize(file.tempPath, outPath, width, height, format, quality);
75
-
76
- const size = await ctx.app.file.size(outPath);
77
- ctx.json({
78
- success: true,
79
- original: { name: file.originalName, size: file.size },
80
- resized: { path: outName, width, height, format, quality, size },
81
- });
82
- } catch (err) {
83
- ctx.status(500).json({ error: err.message });
84
- }
85
- }
86
-
87
- /** 이미지 다중 리사이즈 */
88
- static resizeMultiple;
89
- async resizeMultiple(ctx) {
90
- const file = ctx.files?.[0];
91
- if (!file) {
92
- ctx.status(400).json({ error: '이미지 파일이 필요합니다' });
93
- return;
94
- }
95
-
96
- try {
97
- const outDir = path.resolve(ctx.app.storage._basePath || './storage', 'resized');
98
- await ctx.app.file.ensureDir(outDir);
99
- const baseName = `img_${Date.now()}`;
100
-
101
- const specs = [
102
- { width: 800, height: 600, format: 'webp', suffix: 'large' },
103
- { width: 400, height: 300, format: 'webp', suffix: 'medium' },
104
- { width: 200, height: 200, format: 'webp', suffix: 'thumb' },
105
- ];
106
-
107
- ctx.app.media.resizeMultiple(file.tempPath, outDir, baseName, specs);
108
-
109
- ctx.json({
110
- success: true,
111
- original: { name: file.originalName, size: file.size },
112
- outputs: specs.map(s => ({
113
- path: `resized/${baseName}_${s.suffix}.${s.format}`,
114
- width: s.width, height: s.height, format: s.format,
115
- })),
116
- });
117
- } catch (err) {
118
- ctx.status(500).json({ error: err.message });
119
- }
120
- }
121
-
122
- /** WebP 변환 */
123
- static toWebp;
124
- async toWebp(ctx) {
125
- const file = ctx.files?.[0];
126
- if (!file) {
127
- ctx.status(400).json({ error: '이미지 파일이 필요합니다' });
128
- return;
129
- }
130
-
131
- try {
132
- const outName = `converted/${Date.now()}.webp`;
133
- const outPath = path.resolve(ctx.app.storage._basePath || './storage', outName);
134
- await ctx.app.file.ensureDir(path.dirname(outPath));
135
-
136
- const quality = Number(ctx.query?.quality) || 80;
137
- ctx.app.media.toWebp(file.tempPath, outPath, quality);
138
-
139
- // 워터마크 자동 적용 (config에 경로가 있으면)
140
- const wmPath = ctx.app.config.get('bridge.upload.watermark');
141
- if (wmPath) {
142
- const wmOpacity = ctx.app.config.get('bridge.upload.watermark_opacity', 50);
143
- ctx.app.media.applyWatermark(outPath, wmPath, wmOpacity, 'webp', quality);
144
- }
145
-
146
- const size = await ctx.app.file.size(outPath);
147
- ctx.json({
148
- success: true,
149
- original: { name: file.originalName, size: file.size, type: file.mimeType },
150
- webp: { path: outName, quality, size },
151
- });
152
- } catch (err) {
153
- ctx.status(500).json({ error: err.message });
154
- }
155
- }
156
-
157
- /** 이미지 정보 조회 */
158
- static imageInfo;
159
- async imageInfo(ctx) {
160
- const file = ctx.files?.[0];
161
- if (!file) {
162
- ctx.status(400).json({ error: '이미지 파일이 필요합니다' });
163
- return;
164
- }
165
-
166
- try {
167
- const info = ctx.app.media.imageInfo(file.tempPath);
168
- ctx.json({
169
- success: true,
170
- originalName: file.originalName,
171
- info: typeof info === 'string' ? JSON.parse(info) : info,
172
- });
173
- } catch (err) {
174
- ctx.status(500).json({ error: err.message });
175
- }
176
- }
177
-
178
- /** 비디오 썸네일 추출 */
179
- static videoThumbnail;
180
- async videoThumbnail(ctx) {
181
- const file = ctx.files?.[0];
182
- if (!file) {
183
- ctx.status(400).json({ error: '비디오 파일이 필요합니다' });
184
- return;
185
- }
186
-
187
- try {
188
- const outName = `thumbnails/${Date.now()}_thumb.jpeg`;
189
- const outPath = path.resolve(ctx.app.storage._basePath || './storage', outName);
190
- await ctx.app.file.ensureDir(path.dirname(outPath));
191
-
192
- const atSeconds = Number(ctx.query?.at) || 300;
193
- const width = Number(ctx.query?.width) || 640;
194
- ctx.app.media.videoThumbnail(file.tempPath, outPath, atSeconds, width, 'jpeg');
195
-
196
- // 워터마크 자동 적용 (config에 경로가 있으면)
197
- const wmPath = ctx.app.config.get('bridge.upload.watermark');
198
- if (wmPath) {
199
- const wmOpacity = ctx.app.config.get('bridge.upload.watermark_opacity', 50);
200
- ctx.app.media.applyWatermark(outPath, wmPath, wmOpacity, 'jpeg', 90);
201
- }
202
-
203
- ctx.json({
204
- success: true,
205
- original: { name: file.originalName, type: file.mimeType },
206
- thumbnail: { path: outName, atSeconds, width },
207
- });
208
- } catch (err) {
209
- ctx.status(500).json({ error: err.message });
210
- }
211
- }
212
-
213
- /** 비디오 미리보기 (Queue로 백그라운드 처리) */
214
- static videoPreview;
215
- async videoPreview(ctx) {
216
- const file = ctx.files?.[0];
217
- if (!file) {
218
- ctx.status(400).json({ error: '비디오 파일이 필요합니다' });
219
- return;
220
- }
221
-
222
- try {
223
- const interval = Number(ctx.query?.interval) || 5;
224
- const width = Number(ctx.query?.width) || 320;
225
- const cols = Number(ctx.query?.cols) || 10;
226
-
227
- const basePath = ctx.app.storage._basePath || './storage';
228
- const jobId = Date.now().toString(36);
229
- const thumbDir = path.resolve(basePath, `previews/${jobId}`);
230
- const sheetPath = path.resolve(basePath, `previews/${jobId}_sheet.jpg`);
231
-
232
- // 임시 파일을 영구 경로로 복사 (요청 종료 시 temp 삭제되므로)
233
- const { promises: fs } = await import('node:fs');
234
- const permPath = path.resolve(basePath, `previews/${jobId}_src${path.extname(file.originalName)}`);
235
- await fs.mkdir(path.dirname(permPath), { recursive: true });
236
- await fs.copyFile(file.tempPath, permPath);
237
-
238
- // Queue로 백그라운드 디스패치
239
- ctx.app.dispatch('VideoPreviewTask', {
240
- filePath: permPath,
241
- outputDir: thumbDir,
242
- sheetPath,
243
- interval,
244
- width,
245
- cols,
246
- });
247
-
248
- ctx.json({
249
- success: true,
250
- jobId,
251
- status: 'processing',
252
- message: `${interval}초 간격으로 썸네일 추출 중 (백그라운드)`,
253
- original: { name: file.originalName, type: file.mimeType },
254
- output: { thumbDir, sheetPath },
255
- });
256
- } catch (err) {
257
- ctx.status(500).json({ error: err.message });
258
- }
259
- }
260
-
261
- /** Storage 파일 목록 */
262
- static listFiles;
263
- async listFiles(ctx) {
264
- const { promises: fs } = await import('node:fs');
265
- const basePath = ctx.app.storage._basePath || './storage';
266
- const subDir = ctx.query?.dir || 'uploads';
267
- const dirPath = path.resolve(basePath, subDir);
268
-
269
- try {
270
- const entries = await fs.readdir(dirPath, { withFileTypes: true });
271
- const files = [];
272
- for (const e of entries) {
273
- if (e.isFile() && !e.name.startsWith('.')) {
274
- const stat = await fs.stat(path.join(dirPath, e.name));
275
- files.push({
276
- name: e.name,
277
- size: stat.size,
278
- modified: stat.mtime.toISOString(),
279
- path: `${subDir}/${e.name}`,
280
- });
281
- }
282
- }
283
- ctx.json({ dir: subDir, files, count: files.length });
284
- } catch {
285
- ctx.json({ dir: subDir, files: [], count: 0 });
286
- }
287
- }
288
- }
@@ -1,36 +0,0 @@
1
- import { Controller } from '@fuzionx/framework';
2
-
3
- export default class HomeController extends Controller {
4
-
5
- /** 홈 페이지 (대시보드) */
6
- static index;
7
- async index(ctx) {
8
- ctx.render('home', {
9
- title: 'FuzionX Test Dashboard',
10
- appName: ctx.app?.config?.get('app.name') || 'FuzionX',
11
- });
12
- }
13
-
14
- /** WebSocket 테스트 페이지 */
15
- static websocket;
16
- async websocket(ctx) {
17
- ctx.render('websocket', {
18
- title: 'WebSocket Test',
19
- });
20
- }
21
-
22
- /** Upload & Media 테스트 페이지 */
23
- static upload;
24
- async upload(ctx) {
25
- ctx.render('upload', {
26
- title: 'Upload & Media Test',
27
- });
28
- }
29
-
30
- /** i18n 테스트 페이지 */
31
- async i18n(ctx) {
32
- ctx.render('i18n', {
33
- title: 'i18n Test',
34
- });
35
- }
36
- }
@@ -1,43 +0,0 @@
1
- import { Controller } from '@fuzionx/framework';
2
-
3
- export default class UserController extends Controller {
4
-
5
- /** 사용자 목록 (Service 캐시 활용) */
6
- static index;
7
- async index(ctx) {
8
- const userService = this.service('UserService');
9
- const users = await userService.getUsers();
10
- ctx.json({ users, total: users.length, cached: true });
11
- }
12
-
13
- /** 사용자 상세 */
14
- static show;
15
- async show(ctx) {
16
- const { id } = ctx.params;
17
- ctx.json({ id: Number(id), name: 'Alice', email: 'alice@test.com', role: 'admin' });
18
- }
19
-
20
- /** 사용자 생성 (Service + 이벤트 발행) */
21
- static store;
22
- async store(ctx) {
23
- const userService = this.service('UserService');
24
- const user = await userService.register(ctx.body);
25
- ctx.status(201).json(user);
26
- }
27
-
28
- /** 사용자 수정 */
29
- static update;
30
- async update(ctx) {
31
- const { id } = ctx.params;
32
- const body = ctx.body;
33
- ctx.json({ id: Number(id), ...body, updatedAt: new Date().toISOString() });
34
- }
35
-
36
- /** 사용자 삭제 (이벤트 발행) */
37
- static destroy;
38
- async destroy(ctx) {
39
- const { id } = ctx.params;
40
- this.emit('user:deleted', { id: Number(id) });
41
- ctx.status(204).end();
42
- }
43
- }
@@ -1,13 +0,0 @@
1
- import { Middleware } from '@fuzionx/framework';
2
-
3
- /** 요청 로깅 미들웨어 */
4
- export default class RequestLogger extends Middleware {
5
- static alias = 'requestLogger';
6
-
7
- async handle(ctx, next) {
8
- const start = Date.now();
9
- await next();
10
- const ms = Date.now() - start;
11
- console.log(`[RequestLogger] ${ctx.method} ${ctx.path} → ${ms}ms`);
12
- }
13
- }