@fuzionx/framework 0.1.43 → 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 -331
  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,231 +1,231 @@
1
- /**
2
- * OpenAPI — OpenAPI 3.0 spec 빌더 + Swagger UI
3
- *
4
- * 라우트 정의 + Joi 스키마에서 OpenAPI spec 자동 생성.
5
- *
6
- * @see docs/framework/21-openapi.md
7
- */
8
-
9
- /**
10
- * Joi 스키마를 JSON Schema로 변환 (간단 버전)
11
- * @param {object} joiSchema
12
- * @returns {object}
13
- */
14
- function joiToJsonSchema(joiSchema) {
15
- if (!joiSchema) return {};
16
-
17
- // Joi는 describe()로 메타 추출 가능
18
- if (typeof joiSchema.describe === 'function') {
19
- return convertJoiDescribe(joiSchema.describe());
20
- }
21
-
22
- // 이미 plain object면 그대로
23
- return joiSchema;
24
- }
25
-
26
- function convertJoiDescribe(desc) {
27
- if (!desc) return {};
28
-
29
- if (desc.type === 'object') {
30
- const properties = {};
31
- const required = [];
32
-
33
- if (desc.keys) {
34
- for (const [key, child] of Object.entries(desc.keys)) {
35
- properties[key] = convertJoiDescribe(child);
36
- if (child.flags?.presence === 'required') {
37
- required.push(key);
38
- }
39
- }
40
- }
41
-
42
- return {
43
- type: 'object',
44
- properties,
45
- ...(required.length ? { required } : {}),
46
- };
47
- }
48
-
49
- const result = { type: desc.type || 'string' };
50
-
51
- if (desc.flags?.description) result.description = desc.flags.description;
52
- if (desc.rules) {
53
- for (const rule of desc.rules) {
54
- if (rule.name === 'min') result.minLength = rule.args.limit;
55
- if (rule.name === 'max') result.maxLength = rule.args.limit;
56
- }
57
- }
58
- if (desc.allow) result.enum = desc.allow;
59
- if (desc.flags?.default !== undefined) result.default = desc.flags.default;
60
-
61
- return result;
62
- }
63
-
64
- export default class OpenAPI {
65
- /**
66
- * @param {object} opts
67
- * @param {string} [opts.title='FuzionX API']
68
- * @param {string} [opts.version='1.0.0']
69
- * @param {string} [opts.description]
70
- * @param {Array} [opts.servers]
71
- */
72
- constructor(opts = {}) {
73
- this.title = opts.title || 'FuzionX API';
74
- this.version = opts.version || '1.0.0';
75
- this.description = opts.description || '';
76
- this.servers = opts.servers || [];
77
- this._spec = null;
78
- }
79
-
80
- /**
81
- * 라우트 목록에서 OpenAPI spec 빌드
82
- * @param {Array} routes - Router.getRoutes() 결과
83
- * @returns {object} - OpenAPI 3.0 spec
84
- */
85
- build(routes) {
86
- const paths = {};
87
-
88
- for (const route of routes) {
89
- if (!route.docs) continue;
90
-
91
- const openApiPath = route.path.replace(/:(\w+)/g, '{$1}');
92
-
93
- if (!paths[openApiPath]) paths[openApiPath] = {};
94
-
95
- const method = route.method.toLowerCase();
96
- const operation = {
97
- summary: route.docs.summary || '',
98
- tags: route.docs.tags || [],
99
- };
100
-
101
- if (route.docs.description) {
102
- operation.description = route.docs.description;
103
- }
104
- if (route.docs.deprecated) {
105
- operation.deprecated = true;
106
- }
107
-
108
- // parameters (query + params)
109
- const parameters = [];
110
-
111
- if (route.validate?.params) {
112
- const schema = joiToJsonSchema(route.validate.params);
113
- if (schema.properties) {
114
- for (const [name, prop] of Object.entries(schema.properties)) {
115
- parameters.push({
116
- name,
117
- in: 'path',
118
- required: true,
119
- schema: prop,
120
- });
121
- }
122
- }
123
- }
124
-
125
- if (route.validate?.query) {
126
- const schema = joiToJsonSchema(route.validate.query);
127
- if (schema.properties) {
128
- for (const [name, prop] of Object.entries(schema.properties)) {
129
- parameters.push({
130
- name,
131
- in: 'query',
132
- required: schema.required?.includes(name) || false,
133
- schema: prop,
134
- });
135
- }
136
- }
137
- }
138
-
139
- if (parameters.length) operation.parameters = parameters;
140
-
141
- // requestBody (body)
142
- if (route.validate?.body) {
143
- const schema = joiToJsonSchema(route.validate.body);
144
- operation.requestBody = {
145
- content: {
146
- 'application/json': { schema },
147
- },
148
- };
149
- }
150
-
151
- // responses
152
- operation.responses = route.docs.responses || { '200': { description: 'OK' } };
153
-
154
- // security
155
- if (route.middleware?.includes('auth') || route.middleware?.includes('apiAuth')) {
156
- const scheme = route.middleware.includes('apiAuth') ? 'bearer' : 'session';
157
- operation.security = [{ [scheme]: [] }];
158
- }
159
-
160
- paths[openApiPath][method] = operation;
161
- }
162
-
163
- this._spec = {
164
- openapi: '3.0.3',
165
- info: {
166
- title: this.title,
167
- version: this.version,
168
- ...(this.description ? { description: this.description } : {}),
169
- },
170
- ...(this.servers.length ? { servers: this.servers } : {}),
171
- paths,
172
- components: {
173
- securitySchemes: {
174
- bearer: {
175
- type: 'http',
176
- scheme: 'bearer',
177
- bearerFormat: 'JWT',
178
- },
179
- session: {
180
- type: 'apiKey',
181
- in: 'cookie',
182
- name: 'sid',
183
- },
184
- },
185
- },
186
- };
187
-
188
- return this._spec;
189
- }
190
-
191
- /**
192
- * 캐시된 spec 반환 (JSON)
193
- */
194
- toJSON() {
195
- return this._spec;
196
- }
197
-
198
- /**
199
- * YAML 포맷 (간단 변환)
200
- */
201
- toYAML() {
202
- return jsonToSimpleYaml(this._spec);
203
- }
204
- }
205
-
206
- function jsonToSimpleYaml(obj, indent = 0) {
207
- const spaces = ' '.repeat(indent);
208
- let result = '';
209
-
210
- for (const [key, value] of Object.entries(obj)) {
211
- if (value === null || value === undefined) continue;
212
-
213
- if (Array.isArray(value)) {
214
- result += `${spaces}${key}:\n`;
215
- for (const item of value) {
216
- if (typeof item === 'object') {
217
- result += `${spaces} -\n${jsonToSimpleYaml(item, indent + 4)}`;
218
- } else {
219
- result += `${spaces} - ${item}\n`;
220
- }
221
- }
222
- } else if (typeof value === 'object') {
223
- result += `${spaces}${key}:\n${jsonToSimpleYaml(value, indent + 2)}`;
224
- } else {
225
- const v = typeof value === 'string' ? `'${value}'` : value;
226
- result += `${spaces}${key}: ${v}\n`;
227
- }
228
- }
229
-
230
- return result;
231
- }
1
+ /**
2
+ * OpenAPI — OpenAPI 3.0 spec 빌더 + Swagger UI
3
+ *
4
+ * 라우트 정의 + Joi 스키마에서 OpenAPI spec 자동 생성.
5
+ *
6
+ * @see docs/framework/21-openapi.md
7
+ */
8
+
9
+ /**
10
+ * Joi 스키마를 JSON Schema로 변환 (간단 버전)
11
+ * @param {object} joiSchema
12
+ * @returns {object}
13
+ */
14
+ function joiToJsonSchema(joiSchema) {
15
+ if (!joiSchema) return {};
16
+
17
+ // Joi는 describe()로 메타 추출 가능
18
+ if (typeof joiSchema.describe === 'function') {
19
+ return convertJoiDescribe(joiSchema.describe());
20
+ }
21
+
22
+ // 이미 plain object면 그대로
23
+ return joiSchema;
24
+ }
25
+
26
+ function convertJoiDescribe(desc) {
27
+ if (!desc) return {};
28
+
29
+ if (desc.type === 'object') {
30
+ const properties = {};
31
+ const required = [];
32
+
33
+ if (desc.keys) {
34
+ for (const [key, child] of Object.entries(desc.keys)) {
35
+ properties[key] = convertJoiDescribe(child);
36
+ if (child.flags?.presence === 'required') {
37
+ required.push(key);
38
+ }
39
+ }
40
+ }
41
+
42
+ return {
43
+ type: 'object',
44
+ properties,
45
+ ...(required.length ? { required } : {}),
46
+ };
47
+ }
48
+
49
+ const result = { type: desc.type || 'string' };
50
+
51
+ if (desc.flags?.description) result.description = desc.flags.description;
52
+ if (desc.rules) {
53
+ for (const rule of desc.rules) {
54
+ if (rule.name === 'min') result.minLength = rule.args.limit;
55
+ if (rule.name === 'max') result.maxLength = rule.args.limit;
56
+ }
57
+ }
58
+ if (desc.allow) result.enum = desc.allow;
59
+ if (desc.flags?.default !== undefined) result.default = desc.flags.default;
60
+
61
+ return result;
62
+ }
63
+
64
+ export default class OpenAPI {
65
+ /**
66
+ * @param {object} opts
67
+ * @param {string} [opts.title='FuzionX API']
68
+ * @param {string} [opts.version='1.0.0']
69
+ * @param {string} [opts.description]
70
+ * @param {Array} [opts.servers]
71
+ */
72
+ constructor(opts = {}) {
73
+ this.title = opts.title || 'FuzionX API';
74
+ this.version = opts.version || '1.0.0';
75
+ this.description = opts.description || '';
76
+ this.servers = opts.servers || [];
77
+ this._spec = null;
78
+ }
79
+
80
+ /**
81
+ * 라우트 목록에서 OpenAPI spec 빌드
82
+ * @param {Array} routes - Router.getRoutes() 결과
83
+ * @returns {object} - OpenAPI 3.0 spec
84
+ */
85
+ build(routes) {
86
+ const paths = {};
87
+
88
+ for (const route of routes) {
89
+ if (!route.docs) continue;
90
+
91
+ const openApiPath = route.path.replace(/:(\w+)/g, '{$1}');
92
+
93
+ if (!paths[openApiPath]) paths[openApiPath] = {};
94
+
95
+ const method = route.method.toLowerCase();
96
+ const operation = {
97
+ summary: route.docs.summary || '',
98
+ tags: route.docs.tags || [],
99
+ };
100
+
101
+ if (route.docs.description) {
102
+ operation.description = route.docs.description;
103
+ }
104
+ if (route.docs.deprecated) {
105
+ operation.deprecated = true;
106
+ }
107
+
108
+ // parameters (query + params)
109
+ const parameters = [];
110
+
111
+ if (route.validate?.params) {
112
+ const schema = joiToJsonSchema(route.validate.params);
113
+ if (schema.properties) {
114
+ for (const [name, prop] of Object.entries(schema.properties)) {
115
+ parameters.push({
116
+ name,
117
+ in: 'path',
118
+ required: true,
119
+ schema: prop,
120
+ });
121
+ }
122
+ }
123
+ }
124
+
125
+ if (route.validate?.query) {
126
+ const schema = joiToJsonSchema(route.validate.query);
127
+ if (schema.properties) {
128
+ for (const [name, prop] of Object.entries(schema.properties)) {
129
+ parameters.push({
130
+ name,
131
+ in: 'query',
132
+ required: schema.required?.includes(name) || false,
133
+ schema: prop,
134
+ });
135
+ }
136
+ }
137
+ }
138
+
139
+ if (parameters.length) operation.parameters = parameters;
140
+
141
+ // requestBody (body)
142
+ if (route.validate?.body) {
143
+ const schema = joiToJsonSchema(route.validate.body);
144
+ operation.requestBody = {
145
+ content: {
146
+ 'application/json': { schema },
147
+ },
148
+ };
149
+ }
150
+
151
+ // responses
152
+ operation.responses = route.docs.responses || { '200': { description: 'OK' } };
153
+
154
+ // security
155
+ if (route.middleware?.includes('auth') || route.middleware?.includes('apiAuth')) {
156
+ const scheme = route.middleware.includes('apiAuth') ? 'bearer' : 'session';
157
+ operation.security = [{ [scheme]: [] }];
158
+ }
159
+
160
+ paths[openApiPath][method] = operation;
161
+ }
162
+
163
+ this._spec = {
164
+ openapi: '3.0.3',
165
+ info: {
166
+ title: this.title,
167
+ version: this.version,
168
+ ...(this.description ? { description: this.description } : {}),
169
+ },
170
+ ...(this.servers.length ? { servers: this.servers } : {}),
171
+ paths,
172
+ components: {
173
+ securitySchemes: {
174
+ bearer: {
175
+ type: 'http',
176
+ scheme: 'bearer',
177
+ bearerFormat: 'JWT',
178
+ },
179
+ session: {
180
+ type: 'apiKey',
181
+ in: 'cookie',
182
+ name: 'sid',
183
+ },
184
+ },
185
+ },
186
+ };
187
+
188
+ return this._spec;
189
+ }
190
+
191
+ /**
192
+ * 캐시된 spec 반환 (JSON)
193
+ */
194
+ toJSON() {
195
+ return this._spec;
196
+ }
197
+
198
+ /**
199
+ * YAML 포맷 (간단 변환)
200
+ */
201
+ toYAML() {
202
+ return jsonToSimpleYaml(this._spec);
203
+ }
204
+ }
205
+
206
+ function jsonToSimpleYaml(obj, indent = 0) {
207
+ const spaces = ' '.repeat(indent);
208
+ let result = '';
209
+
210
+ for (const [key, value] of Object.entries(obj)) {
211
+ if (value === null || value === undefined) continue;
212
+
213
+ if (Array.isArray(value)) {
214
+ result += `${spaces}${key}:\n`;
215
+ for (const item of value) {
216
+ if (typeof item === 'object') {
217
+ result += `${spaces} -\n${jsonToSimpleYaml(item, indent + 4)}`;
218
+ } else {
219
+ result += `${spaces} - ${item}\n`;
220
+ }
221
+ }
222
+ } else if (typeof value === 'object') {
223
+ result += `${spaces}${key}:\n${jsonToSimpleYaml(value, indent + 2)}`;
224
+ } else {
225
+ const v = typeof value === 'string' ? `'${value}'` : value;
226
+ result += `${spaces}${key}: ${v}\n`;
227
+ }
228
+ }
229
+
230
+ return result;
231
+ }
package/lib/view/View.js CHANGED
@@ -1,83 +1,83 @@
1
- /**
2
- * View — 뷰 렌더링 (Bridge Tera SSR)
3
- *
4
- * Bridge의 ssrRenderFile N-API로 Tera 파일 기반 렌더링.
5
- * {% extends %}, {% block %}, {% include %}, {{ t(key="...") }} 완전 지원.
6
- *
7
- * Bridge 없으면 반드시 에러 — 폴백 없음.
8
- *
9
- * @see docs/framework/03-views-templates.md
10
- */
11
- import { join } from 'node:path';
12
-
13
- export default class View {
14
- /**
15
- * @param {object} opts
16
- * @param {string} opts.viewsPath - 뷰 파일 루트 경로
17
- * @param {string} [opts.theme='default'] - 기본 테마
18
- * @param {object} [opts.bridge] - Bridge N-API 인스턴스
19
- */
20
- constructor(opts = {}) {
21
- this.viewsPath = opts.viewsPath || '';
22
- this.theme = opts.theme || 'default';
23
- this._bridge = opts.bridge || null;
24
- }
25
-
26
- /**
27
- * 전역 변수 추가
28
- * @param {string} key
29
- * @param {*} value
30
- */
31
- share(key, value) {
32
- this._globals = this._globals || {};
33
- this._globals[key] = value;
34
- }
35
-
36
- /**
37
- * 템플릿 렌더링 — Bridge ssrRenderFile N-API 사용
38
- *
39
- * 테마 경로 해석 (03-views-templates.md):
40
- * 'home' → ssrRenderFile(viewsPath/{theme}, 'pages/home.html', context, locale)
41
- *
42
- * @param {string} template - 'home', 'users/index' 등
43
- * @param {object} [data] - 템플릿 변수
44
- * @param {string} [theme] - 테마 오버라이드
45
- * @returns {string} - 렌더링된 HTML
46
- */
47
- render(template, data = {}, theme) {
48
- if (!this._bridge) {
49
- throw new Error('Bridge not available — cannot render view');
50
- }
51
-
52
- if (typeof this._bridge.ssrRenderFile !== 'function') {
53
- throw new Error('Bridge ssrRenderFile not available — rebuild with ssr feature');
54
- }
55
-
56
- const activeTheme = theme || data.theme || this.theme;
57
- const mergedData = { ...this._globals, ...data, theme: activeTheme };
58
- const locale = data.locale || 'ko';
59
-
60
- // Tera glob 루트: views/{theme}/
61
- const templateDir = join(this.viewsPath, activeTheme);
62
- const contextJson = JSON.stringify(mergedData);
63
-
64
- // 후보 이름: pages/{template}.html → {template}.html
65
- const candidates = [
66
- `pages/${template}.html`,
67
- `${template}.html`,
68
- ];
69
-
70
- for (const templateName of candidates) {
71
- try {
72
- return this._bridge.ssrRenderFile(templateDir, templateName, contextJson, locale);
73
- } catch (err) {
74
- // 파일 없음 → 다음 후보 (Tera init 에러는 파일 미존재)
75
- if (!err.message?.includes('not found') && !err.message?.includes('init failed')) {
76
- throw err; // 렌더링 에러는 그대로 전파
77
- }
78
- }
79
- }
80
-
81
- throw new Error(`View '${template}' not found in theme '${activeTheme}'. Searched: ${candidates.join(', ')}`);
82
- }
83
- }
1
+ /**
2
+ * View — 뷰 렌더링 (Bridge Tera SSR)
3
+ *
4
+ * Bridge의 ssrRenderFile N-API로 Tera 파일 기반 렌더링.
5
+ * {% extends %}, {% block %}, {% include %}, {{ t(key="...") }} 완전 지원.
6
+ *
7
+ * Bridge 없으면 반드시 에러 — 폴백 없음.
8
+ *
9
+ * @see docs/framework/03-views-templates.md
10
+ */
11
+ import { join } from 'node:path';
12
+
13
+ export default class View {
14
+ /**
15
+ * @param {object} opts
16
+ * @param {string} opts.viewsPath - 뷰 파일 루트 경로
17
+ * @param {string} [opts.theme='default'] - 기본 테마
18
+ * @param {object} [opts.bridge] - Bridge N-API 인스턴스
19
+ */
20
+ constructor(opts = {}) {
21
+ this.viewsPath = opts.viewsPath || '';
22
+ this.theme = opts.theme || 'default';
23
+ this._bridge = opts.bridge || null;
24
+ }
25
+
26
+ /**
27
+ * 전역 변수 추가
28
+ * @param {string} key
29
+ * @param {*} value
30
+ */
31
+ share(key, value) {
32
+ this._globals = this._globals || {};
33
+ this._globals[key] = value;
34
+ }
35
+
36
+ /**
37
+ * 템플릿 렌더링 — Bridge ssrRenderFile N-API 사용
38
+ *
39
+ * 테마 경로 해석 (03-views-templates.md):
40
+ * 'home' → ssrRenderFile(viewsPath/{theme}, 'pages/home.html', context, locale)
41
+ *
42
+ * @param {string} template - 'home', 'users/index' 등
43
+ * @param {object} [data] - 템플릿 변수
44
+ * @param {string} [theme] - 테마 오버라이드
45
+ * @returns {string} - 렌더링된 HTML
46
+ */
47
+ render(template, data = {}, theme) {
48
+ if (!this._bridge) {
49
+ throw new Error('Bridge not available — cannot render view');
50
+ }
51
+
52
+ if (typeof this._bridge.ssrRenderFile !== 'function') {
53
+ throw new Error('Bridge ssrRenderFile not available — rebuild with ssr feature');
54
+ }
55
+
56
+ const activeTheme = theme || data.theme || this.theme;
57
+ const mergedData = { ...this._globals, ...data, theme: activeTheme };
58
+ const locale = data.locale || 'ko';
59
+
60
+ // Tera glob 루트: views/{theme}/
61
+ const templateDir = join(this.viewsPath, activeTheme);
62
+ const contextJson = JSON.stringify(mergedData);
63
+
64
+ // 후보 이름: pages/{template}.html → {template}.html
65
+ const candidates = [
66
+ `pages/${template}.html`,
67
+ `${template}.html`,
68
+ ];
69
+
70
+ for (const templateName of candidates) {
71
+ try {
72
+ return this._bridge.ssrRenderFile(templateDir, templateName, contextJson, locale);
73
+ } catch (err) {
74
+ // 파일 없음 → 다음 후보 (Tera init 에러는 파일 미존재)
75
+ if (!err.message?.includes('not found') && !err.message?.includes('init failed')) {
76
+ throw err; // 렌더링 에러는 그대로 전파
77
+ }
78
+ }
79
+ }
80
+
81
+ throw new Error(`View '${template}' not found in theme '${activeTheme}'. Searched: ${candidates.join(', ')}`);
82
+ }
83
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fuzionx/framework",
3
- "version": "0.1.43",
3
+ "version": "0.1.44",
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.43",
37
+ "@fuzionx/core": "^0.1.44",
38
38
  "better-sqlite3": "^12.8.0",
39
39
  "knex": "^3.2.5",
40
40
  "mongoose": "^9.3.2",