@fuzionx/framework 0.1.42 → 0.1.44

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 (80) hide show
  1. package/README.md +501 -501
  2. package/bin/fx.js +12 -12
  3. package/cli/db-sync.js +100 -100
  4. package/cli/index.js +494 -494
  5. package/cli/templates/make/app/controllers/HomeController.js +14 -14
  6. package/cli/templates/make/app/routes/api.js +7 -7
  7. package/cli/templates/make/app/routes/web.js +5 -5
  8. package/cli/templates/make/app/views/default/errors/404.html +11 -11
  9. package/cli/templates/make/app/views/default/errors/500.html +14 -14
  10. package/cli/templates/make/app/views/default/layouts/main.html +22 -22
  11. package/cli/templates/make/app/views/default/pages/home.html +11 -11
  12. package/cli/templates/make/controller.js.tpl +40 -40
  13. package/cli/templates/make/event.js.tpl +8 -8
  14. package/cli/templates/make/job.js.tpl +10 -10
  15. package/cli/templates/make/middleware.js.tpl +10 -10
  16. package/cli/templates/make/model.js.tpl +15 -15
  17. package/cli/templates/make/service.js.tpl +15 -15
  18. package/cli/templates/make/task.js.tpl +15 -15
  19. package/cli/templates/make/test.js.tpl +7 -7
  20. package/cli/templates/make/worker.js.tpl +14 -14
  21. package/cli/templates/make/ws.js.tpl +18 -18
  22. package/index.js +67 -67
  23. package/lib/core/AppError.js +46 -46
  24. package/lib/core/Application.js +1006 -1006
  25. package/lib/core/AutoLoader.js +227 -227
  26. package/lib/core/Base.js +64 -64
  27. package/lib/core/Config.js +331 -228
  28. package/lib/core/Context.js +484 -484
  29. package/lib/database/ConnectionManager.js +208 -208
  30. package/lib/database/MariaModel.js +29 -29
  31. package/lib/database/Model.js +247 -247
  32. package/lib/database/ModelRegistry.js +72 -72
  33. package/lib/database/MongoModel.js +232 -232
  34. package/lib/database/Pagination.js +37 -37
  35. package/lib/database/PostgreModel.js +29 -29
  36. package/lib/database/QueryBuilder.js +172 -172
  37. package/lib/database/SQLiteModel.js +27 -27
  38. package/lib/database/SqlModel.js +257 -257
  39. package/lib/database/SqlQueryBuilder.js +332 -332
  40. package/lib/helpers/CryptoHelper.js +48 -48
  41. package/lib/helpers/FileHelper.js +61 -61
  42. package/lib/helpers/HashHelper.js +39 -39
  43. package/lib/helpers/I18nHelper.js +174 -174
  44. package/lib/helpers/Logger.js +108 -108
  45. package/lib/helpers/MediaHelper.js +84 -84
  46. package/lib/http/Controller.js +34 -34
  47. package/lib/http/ErrorHandler.js +136 -136
  48. package/lib/http/Middleware.js +43 -43
  49. package/lib/http/Router.js +109 -109
  50. package/lib/http/Validation.js +125 -125
  51. package/lib/middleware/apiAuth.js +79 -79
  52. package/lib/middleware/auth.js +42 -42
  53. package/lib/middleware/bodyParser.js +19 -19
  54. package/lib/middleware/cors.js +47 -47
  55. package/lib/middleware/csrf.js +32 -32
  56. package/lib/middleware/index.js +13 -13
  57. package/lib/middleware/session.js +27 -27
  58. package/lib/middleware/theme.js +20 -20
  59. package/lib/realtime/RoomManager.js +85 -85
  60. package/lib/realtime/WsHandler.js +107 -107
  61. package/lib/schedule/Job.js +38 -38
  62. package/lib/schedule/Queue.js +103 -103
  63. package/lib/schedule/Scheduler.js +171 -171
  64. package/lib/schedule/Task.js +39 -39
  65. package/lib/schedule/WorkerPool.js +225 -225
  66. package/lib/services/EventBus.js +94 -94
  67. package/lib/services/Service.js +261 -261
  68. package/lib/services/Storage.js +112 -112
  69. package/lib/utilities/ArrUtil.js +112 -112
  70. package/lib/utilities/DateUtil.js +98 -98
  71. package/lib/utilities/FunctionUtil.js +119 -119
  72. package/lib/utilities/NumUtil.js +75 -75
  73. package/lib/utilities/ObjectUtil.js +170 -170
  74. package/lib/utilities/PaginationUtil.js +81 -81
  75. package/lib/utilities/StrUtil.js +105 -105
  76. package/lib/utilities/index.js +18 -18
  77. package/lib/view/OpenAPI.js +231 -231
  78. package/lib/view/View.js +83 -83
  79. package/package.json +2 -2
  80. package/testing/index.js +232 -232
@@ -1,136 +1,136 @@
1
- /**
2
- * ErrorHandler — 기본 에러 핸들러 (JSON/HTML 분기 + 테마 에러 페이지)
3
- *
4
- * @see docs/framework/08-error-handling.md
5
- * @see docs/framework/03-views-templates.md (에러 페이지)
6
- */
7
- import AppError, { ValidationError } from '../core/AppError.js';
8
-
9
- export default class ErrorHandler {
10
- /**
11
- * @param {object} opts
12
- * @param {boolean} [opts.isDev=false] - dev 모드에서 스택 노출
13
- * @param {Function} [opts.logger] - 에러 로깅 함수
14
- * @param {import('./View.js').default} [opts.view] - 뷰 렌더러 (테마 에러 페이지용)
15
- */
16
- constructor(opts = {}) {
17
- this.isDev = opts.isDev ?? false;
18
- this.logger = opts.logger || console.error;
19
- this._view = opts.view || null;
20
- }
21
-
22
- /**
23
- * 에러 처리 — ctx에 응답 세팅
24
- * @param {Error} err
25
- * @param {import('./Context.js').default} ctx
26
- */
27
- handle(err, ctx) {
28
- const status = err.status || 500;
29
-
30
- // 500 에러는 로깅
31
- if (status >= 500) {
32
- this.logger(err);
33
- }
34
-
35
- // JSON 응답 요청이면 JSON으로
36
- const wantsJson = ctx.get('accept')?.includes('application/json')
37
- || ctx.is('json')
38
- || ctx.path?.startsWith('/api');
39
-
40
- if (wantsJson) {
41
- return this._jsonResponse(err, ctx, status);
42
- }
43
-
44
- return this._htmlResponse(err, ctx, status);
45
- }
46
-
47
- /** @private */
48
- _jsonResponse(err, ctx, status) {
49
- const body = {
50
- error: {
51
- message: err.message,
52
- status,
53
- },
54
- };
55
-
56
- // ValidationError → 필드 에러 포함
57
- if (err instanceof ValidationError) {
58
- body.error.fields = err.fields;
59
- }
60
-
61
- // dev 모드에서 스택 노출
62
- if (this.isDev && status >= 500) {
63
- body.error.stack = err.stack;
64
- }
65
-
66
- // 추가 데이터
67
- if (err.data && !(err instanceof ValidationError)) {
68
- body.error.data = err.data;
69
- }
70
-
71
- ctx.status(status).json(body);
72
- }
73
-
74
- /**
75
- * HTML 에러 응답 — 테마 에러 페이지 지원
76
- *
77
- * 검색 순서 (03-views-templates.md):
78
- * 1. views/{theme}/errors/{code}.html
79
- * 2. views/{theme}/errors/default.html
80
- * 3. 프레임워크 내장 에러 페이지
81
- *
82
- * @private
83
- */
84
- _htmlResponse(err, ctx, status) {
85
- const errorData = {
86
- error: {
87
- code: status,
88
- message: err.message,
89
- stack: this.isDev ? err.stack : null,
90
- },
91
- request: { url: ctx.url, method: ctx.method },
92
- config: { debug: this.isDev },
93
- };
94
-
95
- // 테마 에러 페이지 시도 (View 렌더러가 있을 때)
96
- const appEntry = ctx.app?._appRegistry?.get(ctx.appName);
97
- const view = this._view || appEntry?.view;
98
- if (view) {
99
- const theme = ctx.theme || ctx.app?.config?.get('themes.default', 'default') || 'default';
100
- // 1. views/{theme}/errors/{code}.html
101
- try {
102
- const html = view.render(`errors/${status}`, errorData);
103
- if (html && !html.startsWith('<!-- template:')) {
104
- ctx.status(status).html(html);
105
- return;
106
- }
107
- } catch {}
108
- // 2. views/{theme}/errors/default.html
109
- try {
110
- const html = view.render('errors/default', errorData);
111
- if (html && !html.startsWith('<!-- template:')) {
112
- ctx.status(status).html(html);
113
- return;
114
- }
115
- } catch {}
116
- }
117
-
118
- // 3. 프레임워크 내장 에러 페이지 (XSS 방지)
119
- const esc = (s) => String(s)
120
- .replace(/&/g, '&amp;').replace(/</g, '&lt;')
121
- .replace(/>/g, '&gt;').replace(/"/g, '&quot;');
122
-
123
- const title = status >= 500 ? 'Server Error' : esc(err.message);
124
-
125
- ctx.status(status).html(
126
- `<!DOCTYPE html><html><head><meta charset="utf-8"><title>${status}</title>` +
127
- `<style>body{font-family:system-ui,-apple-system,sans-serif;max-width:600px;margin:80px auto;` +
128
- `padding:0 20px;color:#333}h1{color:#e74c3c;}pre{background:#f5f5f5;padding:16px;border-radius:8px;` +
129
- `overflow-x:auto;font-size:13px;}</style></head>` +
130
- `<body><h1>${status} — ${title}</h1>` +
131
- `<p>요청: ${esc(ctx.method)} ${esc(ctx.url)}</p>` +
132
- `${this.isDev && err.stack ? `<pre>${esc(err.stack)}</pre>` : ''}` +
133
- `</body></html>`
134
- );
135
- }
136
- }
1
+ /**
2
+ * ErrorHandler — 기본 에러 핸들러 (JSON/HTML 분기 + 테마 에러 페이지)
3
+ *
4
+ * @see docs/framework/08-error-handling.md
5
+ * @see docs/framework/03-views-templates.md (에러 페이지)
6
+ */
7
+ import AppError, { ValidationError } from '../core/AppError.js';
8
+
9
+ export default class ErrorHandler {
10
+ /**
11
+ * @param {object} opts
12
+ * @param {boolean} [opts.isDev=false] - dev 모드에서 스택 노출
13
+ * @param {Function} [opts.logger] - 에러 로깅 함수
14
+ * @param {import('./View.js').default} [opts.view] - 뷰 렌더러 (테마 에러 페이지용)
15
+ */
16
+ constructor(opts = {}) {
17
+ this.isDev = opts.isDev ?? false;
18
+ this.logger = opts.logger || console.error;
19
+ this._view = opts.view || null;
20
+ }
21
+
22
+ /**
23
+ * 에러 처리 — ctx에 응답 세팅
24
+ * @param {Error} err
25
+ * @param {import('./Context.js').default} ctx
26
+ */
27
+ handle(err, ctx) {
28
+ const status = err.status || 500;
29
+
30
+ // 500 에러는 로깅
31
+ if (status >= 500) {
32
+ this.logger(err);
33
+ }
34
+
35
+ // JSON 응답 요청이면 JSON으로
36
+ const wantsJson = ctx.get('accept')?.includes('application/json')
37
+ || ctx.is('json')
38
+ || ctx.path?.startsWith('/api');
39
+
40
+ if (wantsJson) {
41
+ return this._jsonResponse(err, ctx, status);
42
+ }
43
+
44
+ return this._htmlResponse(err, ctx, status);
45
+ }
46
+
47
+ /** @private */
48
+ _jsonResponse(err, ctx, status) {
49
+ const body = {
50
+ error: {
51
+ message: err.message,
52
+ status,
53
+ },
54
+ };
55
+
56
+ // ValidationError → 필드 에러 포함
57
+ if (err instanceof ValidationError) {
58
+ body.error.fields = err.fields;
59
+ }
60
+
61
+ // dev 모드에서 스택 노출
62
+ if (this.isDev && status >= 500) {
63
+ body.error.stack = err.stack;
64
+ }
65
+
66
+ // 추가 데이터
67
+ if (err.data && !(err instanceof ValidationError)) {
68
+ body.error.data = err.data;
69
+ }
70
+
71
+ ctx.status(status).json(body);
72
+ }
73
+
74
+ /**
75
+ * HTML 에러 응답 — 테마 에러 페이지 지원
76
+ *
77
+ * 검색 순서 (03-views-templates.md):
78
+ * 1. views/{theme}/errors/{code}.html
79
+ * 2. views/{theme}/errors/default.html
80
+ * 3. 프레임워크 내장 에러 페이지
81
+ *
82
+ * @private
83
+ */
84
+ _htmlResponse(err, ctx, status) {
85
+ const errorData = {
86
+ error: {
87
+ code: status,
88
+ message: err.message,
89
+ stack: this.isDev ? err.stack : null,
90
+ },
91
+ request: { url: ctx.url, method: ctx.method },
92
+ config: { debug: this.isDev },
93
+ };
94
+
95
+ // 테마 에러 페이지 시도 (View 렌더러가 있을 때)
96
+ const appEntry = ctx.app?._appRegistry?.get(ctx.appName);
97
+ const view = this._view || appEntry?.view;
98
+ if (view) {
99
+ const theme = ctx.theme || ctx.app?.config?.get('themes.default', 'default') || 'default';
100
+ // 1. views/{theme}/errors/{code}.html
101
+ try {
102
+ const html = view.render(`errors/${status}`, errorData);
103
+ if (html && !html.startsWith('<!-- template:')) {
104
+ ctx.status(status).html(html);
105
+ return;
106
+ }
107
+ } catch {}
108
+ // 2. views/{theme}/errors/default.html
109
+ try {
110
+ const html = view.render('errors/default', errorData);
111
+ if (html && !html.startsWith('<!-- template:')) {
112
+ ctx.status(status).html(html);
113
+ return;
114
+ }
115
+ } catch {}
116
+ }
117
+
118
+ // 3. 프레임워크 내장 에러 페이지 (XSS 방지)
119
+ const esc = (s) => String(s)
120
+ .replace(/&/g, '&amp;').replace(/</g, '&lt;')
121
+ .replace(/>/g, '&gt;').replace(/"/g, '&quot;');
122
+
123
+ const title = status >= 500 ? 'Server Error' : esc(err.message);
124
+
125
+ ctx.status(status).html(
126
+ `<!DOCTYPE html><html><head><meta charset="utf-8"><title>${status}</title>` +
127
+ `<style>body{font-family:system-ui,-apple-system,sans-serif;max-width:600px;margin:80px auto;` +
128
+ `padding:0 20px;color:#333}h1{color:#e74c3c;}pre{background:#f5f5f5;padding:16px;border-radius:8px;` +
129
+ `overflow-x:auto;font-size:13px;}</style></head>` +
130
+ `<body><h1>${status} — ${title}</h1>` +
131
+ `<p>요청: ${esc(ctx.method)} ${esc(ctx.url)}</p>` +
132
+ `${this.isDev && err.stack ? `<pre>${esc(err.stack)}</pre>` : ''}` +
133
+ `</body></html>`
134
+ );
135
+ }
136
+ }
@@ -1,43 +1,43 @@
1
- /**
2
- * Middleware — 기본 미들웨어 클래스
3
- *
4
- * @see docs/framework/12-middleware.md
5
- * @see docs/framework/class-design.mm.md (Middleware)
6
- */
7
- import Base from '../core/Base.js';
8
-
9
- export default class Middleware extends Base {
10
- /** @type {string} 미들웨어 등록 이름 (라우트에서 참조) */
11
- static alias = '';
12
-
13
- /**
14
- * 미들웨어 핸들러 (서브클래스에서 오버라이드)
15
- * @param {import('./Context.js').default} ctx
16
- * @param {Function} next
17
- * @param {...*} params - 파라미터화 미들웨어 인자 ('name:param1:param2')
18
- */
19
- async handle(ctx, next, ...params) {
20
- await next();
21
- }
22
- }
23
-
24
- /**
25
- * 미들웨어 체인 실행기
26
- *
27
- * @param {Array<Function>} middlewares - [(ctx, next) => ...] 배열
28
- * @param {import('./Context.js').default} ctx
29
- * @returns {Promise<void>}
30
- */
31
- export async function runMiddlewareChain(middlewares, ctx) {
32
- let idx = 0;
33
-
34
- async function next(err) {
35
- if (err) throw err;
36
- if (idx >= middlewares.length) return;
37
-
38
- const fn = middlewares[idx++];
39
- await fn(ctx, next);
40
- }
41
-
42
- await next();
43
- }
1
+ /**
2
+ * Middleware — 기본 미들웨어 클래스
3
+ *
4
+ * @see docs/framework/12-middleware.md
5
+ * @see docs/framework/class-design.mm.md (Middleware)
6
+ */
7
+ import Base from '../core/Base.js';
8
+
9
+ export default class Middleware extends Base {
10
+ /** @type {string} 미들웨어 등록 이름 (라우트에서 참조) */
11
+ static alias = '';
12
+
13
+ /**
14
+ * 미들웨어 핸들러 (서브클래스에서 오버라이드)
15
+ * @param {import('./Context.js').default} ctx
16
+ * @param {Function} next
17
+ * @param {...*} params - 파라미터화 미들웨어 인자 ('name:param1:param2')
18
+ */
19
+ async handle(ctx, next, ...params) {
20
+ await next();
21
+ }
22
+ }
23
+
24
+ /**
25
+ * 미들웨어 체인 실행기
26
+ *
27
+ * @param {Array<Function>} middlewares - [(ctx, next) => ...] 배열
28
+ * @param {import('./Context.js').default} ctx
29
+ * @returns {Promise<void>}
30
+ */
31
+ export async function runMiddlewareChain(middlewares, ctx) {
32
+ let idx = 0;
33
+
34
+ async function next(err) {
35
+ if (err) throw err;
36
+ if (idx >= middlewares.length) return;
37
+
38
+ const fn = middlewares[idx++];
39
+ await fn(ctx, next);
40
+ }
41
+
42
+ await next();
43
+ }
@@ -1,109 +1,109 @@
1
- /**
2
- * Router / RouteGroup — 라우트 등록 DSL
3
- *
4
- * @see docs/framework/01-routing-controllers.md
5
- * @see docs/framework/class-design.mm.md (Router/RouteGroup)
6
- */
7
-
8
- export class RouteGroup {
9
- /**
10
- * @param {string} prefix - 그룹 접두사 ('', '/api')
11
- * @param {object} [groupOpts] - 그룹 옵션 { middleware }
12
- */
13
- constructor(prefix = '', groupOpts = {}) {
14
- this._prefix = prefix;
15
- this._groupMiddleware = groupOpts.middleware || [];
16
- this._routes = [];
17
- }
18
-
19
- /**
20
- * @param {string} method
21
- * @param {string} path
22
- * @param {Function|object} handler
23
- * @param {object} [opts]
24
- */
25
- _addRoute(method, path, handler, opts = {}) {
26
- const fullPath = this._prefix + path;
27
- this._routes.push({
28
- method,
29
- path: fullPath,
30
- handler,
31
- middleware: [...this._groupMiddleware, ...(opts.middleware || [])],
32
- validate: opts.validate || null,
33
- upload: opts.upload || null,
34
- docs: opts.docs || null,
35
- });
36
- }
37
-
38
- get(path, handler, opts) { this._addRoute('GET', path, handler, opts); }
39
- post(path, handler, opts) { this._addRoute('POST', path, handler, opts); }
40
- put(path, handler, opts) { this._addRoute('PUT', path, handler, opts); }
41
- patch(path, handler, opts) { this._addRoute('PATCH', path, handler, opts); }
42
- delete(path, handler, opts) { this._addRoute('DELETE', path, handler, opts); }
43
-
44
- /**
45
- * 그룹 — 중첩 라우트
46
- * 인자 순서: (prefix, opts, callback) 또는 (prefix, callback, opts)
47
- * @param {string} prefix
48
- * @param {object|Function} optsOrCallback
49
- * @param {Function|object} [callbackOrOpts]
50
- */
51
- group(prefix, optsOrCallback, callbackOrOpts) {
52
- let opts, callback;
53
- if (typeof optsOrCallback === 'function') {
54
- // group('/api', (r) => {}, opts?)
55
- callback = optsOrCallback;
56
- opts = callbackOrOpts || {};
57
- } else {
58
- // group('/api', { middleware: [...] }, (r) => {})
59
- opts = optsOrCallback || {};
60
- callback = callbackOrOpts;
61
- }
62
-
63
- const subGroup = new RouteGroup(this._prefix + prefix, {
64
- middleware: [...this._groupMiddleware, ...(opts.middleware || [])],
65
- });
66
- callback(subGroup);
67
- this._routes.push(...subGroup._routes);
68
- }
69
-
70
- /**
71
- * RESTful resource 자동 등록
72
- * @param {string} name - 리소스 이름 (복수형, e.g. 'users')
73
- * @param {object} controller - Controller 클래스 (메서드 레퍼런스)
74
- * @param {object} [opts]
75
- */
76
- resource(name, controller, opts = {}) {
77
- const basePath = `/${name}`;
78
- const middleware = opts.middleware || [];
79
- const base = { middleware };
80
-
81
- if (controller.index) this.get(basePath, controller.index, base);
82
- if (controller.store) this.post(basePath, controller.store, base);
83
- if (controller.show) this.get(`${basePath}/:id`, controller.show, base);
84
- if (controller.update) this.put(`${basePath}/:id`, controller.update, base);
85
- if (controller.destroy) this.delete(`${basePath}/:id`, controller.destroy, base);
86
- }
87
- }
88
-
89
- export default class Router extends RouteGroup {
90
- constructor() {
91
- super('');
92
- }
93
-
94
- /**
95
- * 모든 등록된 라우트 반환
96
- * @returns {Array<{method, path, handler, middleware, validate, upload, docs}>}
97
- */
98
- getRoutes() {
99
- return this._routes;
100
- }
101
-
102
- /**
103
- * 라우트 파일 로드 (routes/ callback 등록)
104
- * @param {Function} routeCallback - (r: RouteGroup) => void
105
- */
106
- load(routeCallback) {
107
- routeCallback(this);
108
- }
109
- }
1
+ /**
2
+ * Router / RouteGroup — 라우트 등록 DSL
3
+ *
4
+ * @see docs/framework/01-routing-controllers.md
5
+ * @see docs/framework/class-design.mm.md (Router/RouteGroup)
6
+ */
7
+
8
+ export class RouteGroup {
9
+ /**
10
+ * @param {string} prefix - 그룹 접두사 ('', '/api')
11
+ * @param {object} [groupOpts] - 그룹 옵션 { middleware }
12
+ */
13
+ constructor(prefix = '', groupOpts = {}) {
14
+ this._prefix = prefix;
15
+ this._groupMiddleware = groupOpts.middleware || [];
16
+ this._routes = [];
17
+ }
18
+
19
+ /**
20
+ * @param {string} method
21
+ * @param {string} path
22
+ * @param {Function|object} handler
23
+ * @param {object} [opts]
24
+ */
25
+ _addRoute(method, path, handler, opts = {}) {
26
+ const fullPath = this._prefix + path;
27
+ this._routes.push({
28
+ method,
29
+ path: fullPath,
30
+ handler,
31
+ middleware: [...this._groupMiddleware, ...(opts.middleware || [])],
32
+ validate: opts.validate || null,
33
+ upload: opts.upload || null,
34
+ docs: opts.docs || null,
35
+ });
36
+ }
37
+
38
+ get(path, handler, opts) { this._addRoute('GET', path, handler, opts); }
39
+ post(path, handler, opts) { this._addRoute('POST', path, handler, opts); }
40
+ put(path, handler, opts) { this._addRoute('PUT', path, handler, opts); }
41
+ patch(path, handler, opts) { this._addRoute('PATCH', path, handler, opts); }
42
+ delete(path, handler, opts) { this._addRoute('DELETE', path, handler, opts); }
43
+
44
+ /**
45
+ * 그룹 — 중첩 라우트
46
+ * 인자 순서: (prefix, opts, callback) 또는 (prefix, callback, opts)
47
+ * @param {string} prefix
48
+ * @param {object|Function} optsOrCallback
49
+ * @param {Function|object} [callbackOrOpts]
50
+ */
51
+ group(prefix, optsOrCallback, callbackOrOpts) {
52
+ let opts, callback;
53
+ if (typeof optsOrCallback === 'function') {
54
+ // group('/api', (r) => {}, opts?)
55
+ callback = optsOrCallback;
56
+ opts = callbackOrOpts || {};
57
+ } else {
58
+ // group('/api', { middleware: [...] }, (r) => {})
59
+ opts = optsOrCallback || {};
60
+ callback = callbackOrOpts;
61
+ }
62
+
63
+ const subGroup = new RouteGroup(this._prefix + prefix, {
64
+ middleware: [...this._groupMiddleware, ...(opts.middleware || [])],
65
+ });
66
+ callback(subGroup);
67
+ this._routes.push(...subGroup._routes);
68
+ }
69
+
70
+ /**
71
+ * RESTful resource 자동 등록
72
+ * @param {string} name - 리소스 이름 (복수형, e.g. 'users')
73
+ * @param {object} controller - Controller 클래스 (메서드 레퍼런스)
74
+ * @param {object} [opts]
75
+ */
76
+ resource(name, controller, opts = {}) {
77
+ const basePath = `/${name}`;
78
+ const middleware = opts.middleware || [];
79
+ const base = { middleware };
80
+
81
+ if (controller.index) this.get(basePath, controller.index, base);
82
+ if (controller.store) this.post(basePath, controller.store, base);
83
+ if (controller.show) this.get(`${basePath}/:id`, controller.show, base);
84
+ if (controller.update) this.put(`${basePath}/:id`, controller.update, base);
85
+ if (controller.destroy) this.delete(`${basePath}/:id`, controller.destroy, base);
86
+ }
87
+ }
88
+
89
+ export default class Router extends RouteGroup {
90
+ constructor() {
91
+ super('');
92
+ }
93
+
94
+ /**
95
+ * 모든 등록된 라우트 반환
96
+ * @returns {Array<{method, path, handler, middleware, validate, upload, docs}>}
97
+ */
98
+ getRoutes() {
99
+ return this._routes;
100
+ }
101
+
102
+ /**
103
+ * 라우트 파일 로드 (routes/ callback 등록)
104
+ * @param {Function} routeCallback - (r: RouteGroup) => void
105
+ */
106
+ load(routeCallback) {
107
+ routeCallback(this);
108
+ }
109
+ }