@fuzionx/framework 0.1.43 → 0.1.45
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 +501 -501
- package/bin/fx.js +12 -12
- package/cli/db-sync.js +100 -100
- package/cli/index.js +494 -494
- package/cli/templates/make/app/controllers/HomeController.js +14 -14
- package/cli/templates/make/app/routes/api.js +7 -7
- package/cli/templates/make/app/routes/web.js +5 -5
- package/cli/templates/make/app/views/default/errors/404.html +11 -11
- package/cli/templates/make/app/views/default/errors/500.html +14 -14
- package/cli/templates/make/app/views/default/layouts/main.html +22 -22
- package/cli/templates/make/app/views/default/pages/home.html +11 -11
- package/cli/templates/make/controller.js.tpl +40 -40
- package/cli/templates/make/event.js.tpl +8 -8
- package/cli/templates/make/job.js.tpl +10 -10
- package/cli/templates/make/middleware.js.tpl +10 -10
- package/cli/templates/make/model.js.tpl +15 -15
- package/cli/templates/make/service.js.tpl +15 -15
- package/cli/templates/make/task.js.tpl +15 -15
- package/cli/templates/make/test.js.tpl +7 -7
- package/cli/templates/make/worker.js.tpl +14 -14
- package/cli/templates/make/ws.js.tpl +18 -18
- package/index.js +67 -67
- package/lib/core/AppError.js +46 -46
- package/lib/core/Application.js +1006 -1006
- package/lib/core/AutoLoader.js +227 -227
- package/lib/core/Base.js +64 -64
- package/lib/core/Config.js +331 -331
- package/lib/core/Context.js +484 -484
- package/lib/database/ConnectionManager.js +208 -208
- package/lib/database/MariaModel.js +29 -29
- package/lib/database/Model.js +247 -247
- package/lib/database/ModelRegistry.js +72 -72
- package/lib/database/MongoModel.js +232 -232
- package/lib/database/Pagination.js +37 -37
- package/lib/database/PostgreModel.js +29 -29
- package/lib/database/QueryBuilder.js +172 -172
- package/lib/database/SQLiteModel.js +27 -27
- package/lib/database/SqlModel.js +257 -257
- package/lib/database/SqlQueryBuilder.js +332 -332
- package/lib/helpers/CryptoHelper.js +48 -48
- package/lib/helpers/FileHelper.js +61 -61
- package/lib/helpers/HashHelper.js +39 -39
- package/lib/helpers/I18nHelper.js +174 -174
- package/lib/helpers/Logger.js +108 -108
- package/lib/helpers/MediaHelper.js +84 -84
- package/lib/http/Controller.js +34 -34
- package/lib/http/ErrorHandler.js +136 -136
- package/lib/http/Middleware.js +43 -43
- package/lib/http/Router.js +109 -109
- package/lib/http/Validation.js +125 -125
- package/lib/middleware/apiAuth.js +79 -79
- package/lib/middleware/auth.js +42 -42
- package/lib/middleware/bodyParser.js +19 -19
- package/lib/middleware/cors.js +47 -47
- package/lib/middleware/csrf.js +32 -32
- package/lib/middleware/index.js +13 -13
- package/lib/middleware/session.js +27 -27
- package/lib/middleware/theme.js +20 -20
- package/lib/realtime/RoomManager.js +85 -85
- package/lib/realtime/WsHandler.js +107 -107
- package/lib/schedule/Job.js +38 -38
- package/lib/schedule/Queue.js +103 -103
- package/lib/schedule/Scheduler.js +171 -171
- package/lib/schedule/Task.js +39 -39
- package/lib/schedule/WorkerPool.js +225 -225
- package/lib/services/EventBus.js +94 -94
- package/lib/services/Service.js +261 -261
- package/lib/services/Storage.js +112 -112
- package/lib/utilities/ArrUtil.js +112 -112
- package/lib/utilities/DateUtil.js +98 -98
- package/lib/utilities/FunctionUtil.js +119 -119
- package/lib/utilities/NumUtil.js +75 -75
- package/lib/utilities/ObjectUtil.js +170 -170
- package/lib/utilities/PaginationUtil.js +81 -81
- package/lib/utilities/StrUtil.js +105 -105
- package/lib/utilities/index.js +18 -18
- package/lib/view/OpenAPI.js +231 -231
- package/lib/view/View.js +83 -83
- package/package.json +2 -2
- package/testing/index.js +232 -232
package/lib/view/OpenAPI.js
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "0.1.45",
|
|
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.45",
|
|
38
38
|
"better-sqlite3": "^12.8.0",
|
|
39
39
|
"knex": "^3.2.5",
|
|
40
40
|
"mongoose": "^9.3.2",
|