@fuzionx/framework 0.1.45 → 0.1.46
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/lib/core/Application.js +425 -138
- package/lib/core/Context.js +540 -236
- package/lib/middleware/auth.js +1 -1
- package/lib/middleware/csrf.js +1 -1
- package/lib/middleware/session.js +5 -4
- package/package.json +2 -2
package/lib/core/Context.js
CHANGED
|
@@ -1,72 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Context — 요청/응답 통합 API
|
|
2
|
+
* Context — 요청/응답 통합 API (고성능 버전)
|
|
3
3
|
*
|
|
4
4
|
* Bridge rawReq → Context 변환.
|
|
5
5
|
* 미들웨어/핸들러에 전달되는 단일 객체.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* 최적화:
|
|
8
|
+
* - 프로토타입 기반 메서드 공유 (프로토타입에 메서드 정의 → 함수 생성 0)
|
|
9
|
+
* - Lazy 프로퍼티: session, cookies, locale, t() 는 접근 시에만 생성
|
|
10
|
+
* - 공유 인스턴스(sync용) + Object.create(async용) 패턴
|
|
11
|
+
* - _toFusionResponse()에서 공유 응답 객체 재사용
|
|
9
12
|
*
|
|
10
13
|
* @see docs/framework/06-context.md
|
|
11
|
-
* @see packages/fuzionx/lib/context.js (Core SessionProto 참조)
|
|
12
14
|
*/
|
|
13
15
|
import AppError, { ValidationError } from '../core/AppError.js';
|
|
14
16
|
import { readFileSync } from 'node:fs';
|
|
15
17
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
* @param {import('./Application.js').default} app
|
|
20
|
-
*/
|
|
21
|
-
constructor(rawReq, app) {
|
|
22
|
-
// ── 요청 프로퍼티 ──
|
|
23
|
-
this.method = rawReq.method || 'GET';
|
|
24
|
-
this.url = rawReq.url || '/';
|
|
25
|
-
this.path = rawReq.path || this.url.split('?')[0];
|
|
26
|
-
this.query = rawReq.query || {};
|
|
27
|
-
this.params = rawReq.params || {};
|
|
28
|
-
this.headers = rawReq.headers || {};
|
|
29
|
-
this.body = rawReq.body || null;
|
|
30
|
-
this.ip = rawReq.remoteIp || '';
|
|
31
|
-
this.files = rawReq.files || null;
|
|
32
|
-
this.uploadError = rawReq.uploadError || null;
|
|
33
|
-
this.formFields = rawReq.formFields || null;
|
|
34
|
-
this.handlerId = rawReq.handlerId;
|
|
35
|
-
|
|
36
|
-
// 파생 속성 (06-context.md)
|
|
37
|
-
this.protocol = this.headers['x-forwarded-proto'] || 'http';
|
|
38
|
-
this.host = this.headers['host'] || '';
|
|
39
|
-
|
|
40
|
-
// ── 프레임워크 주입 ──
|
|
41
|
-
this.app = app;
|
|
42
|
-
this.user = null;
|
|
43
|
-
this._sessionId = rawReq.sessionId || null;
|
|
44
|
-
this._rawSession = rawReq.session || {};
|
|
45
|
-
// ── Cookie (parseCookies 먼저 — _detectLocale에서 참조) ──
|
|
46
|
-
this.cookies = this._parseCookies();
|
|
47
|
-
|
|
48
|
-
this.session = this._createSession(rawReq, app);
|
|
49
|
-
this.locale = this._detectLocale(rawReq);
|
|
50
|
-
|
|
51
|
-
// i18n — ctx.t() + ctx.t.all() (18-i18n.md)
|
|
52
|
-
this.t = this._createT();
|
|
53
|
-
|
|
54
|
-
// ── lazy 캐시 ──
|
|
55
|
-
this._json = undefined;
|
|
56
|
-
this._setCookies = []; // Set-Cookie 헤더 배열
|
|
57
|
-
|
|
58
|
-
// ── 응답 상태 ──
|
|
59
|
-
this._statusCode = 200;
|
|
60
|
-
this._headers = {};
|
|
61
|
-
this._body = '';
|
|
62
|
-
this._sent = false;
|
|
63
|
-
}
|
|
18
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
19
|
+
// Context 프로토타입 — 모든 메서드를 여기에 정의
|
|
20
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
64
21
|
|
|
65
|
-
|
|
66
|
-
// Request 유틸
|
|
67
|
-
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
22
|
+
const ContextProto = {
|
|
68
23
|
|
|
69
|
-
|
|
24
|
+
// ── Request 유틸 ──
|
|
25
|
+
|
|
26
|
+
/** lazy JSON 파싱 — body가 이미 object면 그대로 반환 */
|
|
70
27
|
get getJson() {
|
|
71
28
|
if (this._json !== undefined) return this._json;
|
|
72
29
|
if (typeof this.body === 'object' && this.body !== null) {
|
|
@@ -78,14 +35,22 @@ export default class Context {
|
|
|
78
35
|
this._json = null;
|
|
79
36
|
}
|
|
80
37
|
return this._json;
|
|
81
|
-
}
|
|
38
|
+
},
|
|
82
39
|
|
|
83
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* 헤더 값 가져오기 (case-insensitive)
|
|
42
|
+
* @param {string} name - 헤더 이름
|
|
43
|
+
* @returns {string|undefined}
|
|
44
|
+
*/
|
|
84
45
|
get(name) {
|
|
85
46
|
return this.headers[name.toLowerCase()];
|
|
86
|
-
}
|
|
47
|
+
},
|
|
87
48
|
|
|
88
|
-
/**
|
|
49
|
+
/**
|
|
50
|
+
* Content-Type 확인
|
|
51
|
+
* @param {string} type - 'json', 'html', 'form', 'multipart'
|
|
52
|
+
* @returns {boolean}
|
|
53
|
+
*/
|
|
89
54
|
is(type) {
|
|
90
55
|
const ct = this.get('content-type') || '';
|
|
91
56
|
const map = {
|
|
@@ -95,7 +60,7 @@ export default class Context {
|
|
|
95
60
|
multipart: 'multipart/form-data',
|
|
96
61
|
};
|
|
97
62
|
return ct.includes(map[type] || type);
|
|
98
|
-
}
|
|
63
|
+
},
|
|
99
64
|
|
|
100
65
|
/**
|
|
101
66
|
* Accept 협상 (06-context.md)
|
|
@@ -115,16 +80,66 @@ export default class Context {
|
|
|
115
80
|
if (accept.includes(mime) || accept.includes('*/*')) return type;
|
|
116
81
|
}
|
|
117
82
|
return false;
|
|
118
|
-
}
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
// ── Lazy 프로퍼티: cookies ──
|
|
86
|
+
|
|
87
|
+
/** lazy 쿠키 파싱 — 최초 접근 시에만 파싱 */
|
|
88
|
+
get cookies() {
|
|
89
|
+
if (this._cookies !== undefined) return this._cookies;
|
|
90
|
+
this._cookies = _parseCookies(this.headers);
|
|
91
|
+
return this._cookies;
|
|
92
|
+
},
|
|
119
93
|
|
|
120
|
-
//
|
|
121
|
-
|
|
122
|
-
|
|
94
|
+
// ── Lazy 프로퍼티: session ──
|
|
95
|
+
|
|
96
|
+
/** lazy 세션 객체 — 최초 접근 시에만 생성 */
|
|
97
|
+
get session() {
|
|
98
|
+
if (this._session !== undefined) return this._session;
|
|
99
|
+
this._session = _createSession(this._rawReq, this.app);
|
|
100
|
+
return this._session;
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
// ── Lazy 프로퍼티: locale ──
|
|
104
|
+
|
|
105
|
+
/** lazy locale 감지 — 최초 접근 시에만 계산 */
|
|
106
|
+
get locale() {
|
|
107
|
+
if (this._locale !== undefined) return this._locale;
|
|
108
|
+
this._locale = _detectLocale(this);
|
|
109
|
+
return this._locale;
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
// ── Lazy 프로퍼티: t (i18n) ──
|
|
113
|
+
|
|
114
|
+
/** lazy i18n 함수 — 최초 접근 시에만 생성 */
|
|
115
|
+
get t() {
|
|
116
|
+
if (this._t !== undefined) return this._t;
|
|
117
|
+
this._t = _createT(this);
|
|
118
|
+
return this._t;
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
// ── Lazy 프로퍼티: protocol, host ──
|
|
122
|
+
|
|
123
|
+
/** 프로토콜 (x-forwarded-proto 우선) */
|
|
124
|
+
get protocol() {
|
|
125
|
+
return this.headers['x-forwarded-proto'] || 'http';
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
/** 호스트 헤더 */
|
|
129
|
+
get host() {
|
|
130
|
+
return this.headers['host'] || '';
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
// ── Cookie 읽기/쓰기 ──
|
|
123
134
|
|
|
124
135
|
/**
|
|
125
136
|
* 쿠키 읽기/쓰기
|
|
126
|
-
* @overload cookie(name) →
|
|
127
|
-
* @overload cookie(name, value, opts?) →
|
|
137
|
+
* @overload cookie(name) → 값 반환
|
|
138
|
+
* @overload cookie(name, value, opts?) → Set-Cookie 추가
|
|
139
|
+
* @param {string} name - 쿠키 이름
|
|
140
|
+
* @param {string} [value] - 쿠키 값
|
|
141
|
+
* @param {object} [opts] - 쿠키 옵션
|
|
142
|
+
* @returns {string|null|this}
|
|
128
143
|
*/
|
|
129
144
|
cookie(name, value, opts) {
|
|
130
145
|
if (arguments.length === 1) {
|
|
@@ -142,41 +157,46 @@ export default class Context {
|
|
|
142
157
|
if (o.sameSite) str += `; SameSite=${o.sameSite}`;
|
|
143
158
|
this._setCookies.push(str);
|
|
144
159
|
return this;
|
|
145
|
-
}
|
|
160
|
+
},
|
|
146
161
|
|
|
147
|
-
/**
|
|
162
|
+
/**
|
|
163
|
+
* 쿠키 삭제
|
|
164
|
+
* @param {string} name - 쿠키 이름
|
|
165
|
+
* @param {object} [opts] - 쿠키 옵션
|
|
166
|
+
*/
|
|
148
167
|
clearCookie(name, opts) {
|
|
149
168
|
return this.cookie(name, '', { ...opts, maxAge: 0 });
|
|
150
|
-
}
|
|
169
|
+
},
|
|
151
170
|
|
|
152
|
-
|
|
153
|
-
_parseCookies() {
|
|
154
|
-
const header = this.headers?.cookie || this.headers?.Cookie || '';
|
|
155
|
-
if (!header) return {};
|
|
156
|
-
const result = {};
|
|
157
|
-
for (const pair of header.split(';')) {
|
|
158
|
-
const [k, ...v] = pair.trim().split('=');
|
|
159
|
-
if (k) result[k.trim()] = decodeURIComponent(v.join('='));
|
|
160
|
-
}
|
|
161
|
-
return result;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
165
|
-
// Response 메서드
|
|
166
|
-
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
171
|
+
// ── Response 메서드 ──
|
|
167
172
|
|
|
173
|
+
/**
|
|
174
|
+
* 상태 코드 설정
|
|
175
|
+
* @param {number} code - HTTP 상태 코드
|
|
176
|
+
* @returns {this}
|
|
177
|
+
*/
|
|
168
178
|
status(code) {
|
|
169
179
|
this._statusCode = code;
|
|
170
180
|
return this;
|
|
171
|
-
}
|
|
181
|
+
},
|
|
172
182
|
|
|
183
|
+
/**
|
|
184
|
+
* JSON 응답
|
|
185
|
+
* @param {object} data - 응답 데이터
|
|
186
|
+
* @returns {this}
|
|
187
|
+
*/
|
|
173
188
|
json(data) {
|
|
174
189
|
this._body = JSON.stringify(data);
|
|
175
190
|
this._headers['Content-Type'] = 'application/json';
|
|
176
191
|
this._sent = true;
|
|
177
192
|
return this;
|
|
178
|
-
}
|
|
193
|
+
},
|
|
179
194
|
|
|
195
|
+
/**
|
|
196
|
+
* 텍스트/HTML 응답
|
|
197
|
+
* @param {string|object} data - 응답 데이터
|
|
198
|
+
* @returns {this}
|
|
199
|
+
*/
|
|
180
200
|
send(data) {
|
|
181
201
|
if (typeof data === 'object' && data !== null) {
|
|
182
202
|
return this.json(data);
|
|
@@ -187,57 +207,92 @@ export default class Context {
|
|
|
187
207
|
}
|
|
188
208
|
this._sent = true;
|
|
189
209
|
return this;
|
|
190
|
-
}
|
|
210
|
+
},
|
|
191
211
|
|
|
212
|
+
/**
|
|
213
|
+
* HTML 응답 (send 위임)
|
|
214
|
+
* @param {string} content - HTML 문자열
|
|
215
|
+
* @returns {this}
|
|
216
|
+
*/
|
|
192
217
|
html(content) {
|
|
218
|
+
this._headers['Content-Type'] = 'text/html; charset=utf-8';
|
|
193
219
|
return this.send(content);
|
|
194
|
-
}
|
|
220
|
+
},
|
|
195
221
|
|
|
222
|
+
/**
|
|
223
|
+
* 텍스트 응답
|
|
224
|
+
* @param {string} content - 텍스트 문자열
|
|
225
|
+
* @returns {this}
|
|
226
|
+
*/
|
|
196
227
|
text(content) {
|
|
197
228
|
this._body = String(content);
|
|
198
229
|
this._headers['Content-Type'] = 'text/plain; charset=utf-8';
|
|
199
230
|
this._sent = true;
|
|
200
231
|
return this;
|
|
201
|
-
}
|
|
232
|
+
},
|
|
202
233
|
|
|
234
|
+
/**
|
|
235
|
+
* 리다이렉트 응답
|
|
236
|
+
* @param {string} url - 리다이렉트 URL
|
|
237
|
+
* @param {number} [code=302] - 상태 코드
|
|
238
|
+
* @returns {this}
|
|
239
|
+
*/
|
|
203
240
|
redirect(url, code = 302) {
|
|
204
241
|
this._statusCode = code;
|
|
205
242
|
this._headers['Location'] = url;
|
|
206
243
|
this._body = '';
|
|
207
244
|
this._sent = true;
|
|
208
245
|
return this;
|
|
209
|
-
}
|
|
246
|
+
},
|
|
210
247
|
|
|
248
|
+
/** Referer 기반 리다이렉트 */
|
|
211
249
|
back() {
|
|
212
250
|
const referer = this.get('referer') || '/';
|
|
213
251
|
return this.redirect(referer);
|
|
214
|
-
}
|
|
252
|
+
},
|
|
215
253
|
|
|
254
|
+
/** 빈 응답으로 종료 */
|
|
216
255
|
end() {
|
|
217
256
|
this._body = '';
|
|
218
257
|
this._sent = true;
|
|
219
258
|
return this;
|
|
220
|
-
}
|
|
259
|
+
},
|
|
221
260
|
|
|
261
|
+
/**
|
|
262
|
+
* 응답 헤더 설정
|
|
263
|
+
* @param {string} key - 헤더 키
|
|
264
|
+
* @param {string} value - 헤더 값
|
|
265
|
+
* @returns {this}
|
|
266
|
+
*/
|
|
222
267
|
header(key, value) {
|
|
223
268
|
this._headers[key] = value;
|
|
224
269
|
return this;
|
|
225
|
-
}
|
|
270
|
+
},
|
|
226
271
|
|
|
227
|
-
/**
|
|
272
|
+
/**
|
|
273
|
+
* setHeader alias (06-context.md)
|
|
274
|
+
* @param {string} key - 헤더 키
|
|
275
|
+
* @param {string} value - 헤더 값
|
|
276
|
+
* @returns {this}
|
|
277
|
+
*/
|
|
228
278
|
setHeader(key, value) {
|
|
229
279
|
return this.header(key, value);
|
|
230
|
-
}
|
|
280
|
+
},
|
|
231
281
|
|
|
282
|
+
/**
|
|
283
|
+
* 에러 throw (AppError)
|
|
284
|
+
* @param {number} statusCode - HTTP 상태 코드
|
|
285
|
+
* @param {string} message - 에러 메시지
|
|
286
|
+
*/
|
|
232
287
|
error(statusCode, message) {
|
|
233
288
|
throw new AppError(message, statusCode);
|
|
234
|
-
}
|
|
289
|
+
},
|
|
235
290
|
|
|
236
291
|
/**
|
|
237
292
|
* 파일 다운로드 (06-context.md)
|
|
238
|
-
* Bridge는 문자열 body만 지원하므로 파일 내용을 직접 읽어 base64 전송.
|
|
239
293
|
* @param {string} filePath - 절대 경로
|
|
240
|
-
* @param {string} [filename]
|
|
294
|
+
* @param {string} [filename] - 파일명
|
|
295
|
+
* @returns {this}
|
|
241
296
|
*/
|
|
242
297
|
download(filePath, filename) {
|
|
243
298
|
const name = filename || filePath.split('/').pop();
|
|
@@ -254,14 +309,13 @@ export default class Context {
|
|
|
254
309
|
}
|
|
255
310
|
this._sent = true;
|
|
256
311
|
return this;
|
|
257
|
-
}
|
|
312
|
+
},
|
|
258
313
|
|
|
259
314
|
/**
|
|
260
315
|
* 스트리밍 응답 (06-context.md)
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
* @
|
|
264
|
-
* @param {string} [contentType='application/octet-stream']
|
|
316
|
+
* @param {ReadableStream|Buffer|string} readableStream - 스트림 데이터
|
|
317
|
+
* @param {string} [contentType='application/octet-stream'] - Content-Type
|
|
318
|
+
* @returns {Promise<this>}
|
|
265
319
|
*/
|
|
266
320
|
async stream(readableStream, contentType = 'application/octet-stream') {
|
|
267
321
|
this._headers['Content-Type'] = contentType;
|
|
@@ -281,12 +335,15 @@ export default class Context {
|
|
|
281
335
|
}
|
|
282
336
|
this._sent = true;
|
|
283
337
|
return this;
|
|
284
|
-
}
|
|
338
|
+
},
|
|
285
339
|
|
|
286
|
-
//
|
|
287
|
-
// Validate
|
|
288
|
-
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
340
|
+
// ── Validate ──
|
|
289
341
|
|
|
342
|
+
/**
|
|
343
|
+
* Joi 스키마 검증
|
|
344
|
+
* @param {object} schema - Joi 스키마 인스턴스
|
|
345
|
+
* @returns {object} 검증된 데이터
|
|
346
|
+
*/
|
|
290
347
|
validate(schema) {
|
|
291
348
|
if (schema && typeof schema.validate === 'function') {
|
|
292
349
|
const { error, value } = schema.validate(this.body, { abortEarly: false, stripUnknown: true });
|
|
@@ -300,36 +357,16 @@ export default class Context {
|
|
|
300
357
|
return value;
|
|
301
358
|
}
|
|
302
359
|
return this.body;
|
|
303
|
-
}
|
|
360
|
+
},
|
|
304
361
|
|
|
305
|
-
//
|
|
306
|
-
// i18n (Bridge 연동)
|
|
307
|
-
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
308
|
-
|
|
309
|
-
/** @private Setup i18n t() function with .all() support */
|
|
310
|
-
_createT() {
|
|
311
|
-
const self = this;
|
|
312
|
-
const t = (key, vars) => {
|
|
313
|
-
const locale = vars?.locale || self.locale;
|
|
314
|
-
if (self.app?.i18n) {
|
|
315
|
-
return self.app.i18n.translate(locale, key, vars);
|
|
316
|
-
}
|
|
317
|
-
return vars?.default || key;
|
|
318
|
-
};
|
|
319
|
-
// ctx.t.all() — 현재 locale 전체 번역 데이터 (18-i18n.md)
|
|
320
|
-
t.all = () => {
|
|
321
|
-
if (self.app?.i18n?.getAll) {
|
|
322
|
-
return self.app.i18n.getAll(self.locale);
|
|
323
|
-
}
|
|
324
|
-
return {};
|
|
325
|
-
};
|
|
326
|
-
return t;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
330
|
-
// View 렌더링
|
|
331
|
-
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
362
|
+
// ── View 렌더링 ──
|
|
332
363
|
|
|
364
|
+
/**
|
|
365
|
+
* 템플릿 렌더링 (Bridge Tera SSR)
|
|
366
|
+
* @param {string} view - 뷰 이름
|
|
367
|
+
* @param {object} [data] - 템플릿 변수
|
|
368
|
+
* @returns {this}
|
|
369
|
+
*/
|
|
333
370
|
render(view, data) {
|
|
334
371
|
// 글로벌 변수 주입 (03-views-templates.md)
|
|
335
372
|
const bridge = this.app?._bridge;
|
|
@@ -349,11 +386,11 @@ export default class Context {
|
|
|
349
386
|
}
|
|
350
387
|
|
|
351
388
|
const globals = {
|
|
352
|
-
session: this.
|
|
389
|
+
session: this._rawReq?.session || {},
|
|
353
390
|
auth: { user: this.user },
|
|
354
391
|
config: this.app?.config?._raw || {},
|
|
355
392
|
request: { url: this.url, method: this.method, path: this.path, ip: this.ip },
|
|
356
|
-
csrf_token: this.
|
|
393
|
+
csrf_token: (this._rawReq?.session)?._csrfToken || '',
|
|
357
394
|
flash: this.session?.getFlash() || {},
|
|
358
395
|
theme: this.theme || this.app?.config?.get('themes.default', 'default') || 'default',
|
|
359
396
|
locale: this.locale,
|
|
@@ -371,114 +408,381 @@ export default class Context {
|
|
|
371
408
|
|
|
372
409
|
const html = viewEngine.render(view, globals);
|
|
373
410
|
return this.html(html);
|
|
374
|
-
}
|
|
411
|
+
},
|
|
412
|
+
|
|
413
|
+
// ── 응답 변환 ──
|
|
375
414
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
//
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
// Flash 소비 후 세션 저장소에 즉시 반영
|
|
391
|
-
if (hadFlash && sessionId && bridge?.sessionSet) {
|
|
392
|
-
try { bridge.sessionSet(sessionId, { ...data }); } catch {}
|
|
415
|
+
/**
|
|
416
|
+
* Bridge 응답 포맷 { status, body, headers }
|
|
417
|
+
* sync 경로: 공유 응답 객체(_sharedFusionResponse)를 재사용하여 할당 0
|
|
418
|
+
* async 경로: 새 객체 생성
|
|
419
|
+
* @returns {{ status: number, body: string, headers: object }}
|
|
420
|
+
*/
|
|
421
|
+
_toFusionResponse() {
|
|
422
|
+
// Set-Cookie 헤더 병합
|
|
423
|
+
if (this._setCookies.length > 0) {
|
|
424
|
+
this._headers['Set-Cookie'] = this._setCookies;
|
|
425
|
+
}
|
|
426
|
+
// async(비공유) 인스턴스면 새 객체 반환 → race condition 방지
|
|
427
|
+
if (this !== _sharedCtx) {
|
|
428
|
+
return { status: this._statusCode, body: this._body, headers: this._headers };
|
|
393
429
|
}
|
|
430
|
+
// sync(공유) 인스턴스 → 사전 할당된 응답 객체 재사용
|
|
431
|
+
_sharedFusionResponse.status = this._statusCode;
|
|
432
|
+
_sharedFusionResponse.body = this._body;
|
|
433
|
+
_sharedFusionResponse.headers = this._headers;
|
|
434
|
+
return _sharedFusionResponse;
|
|
435
|
+
},
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
439
|
+
// Private 헬퍼 함수 (프로토타입 외부)
|
|
440
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
394
441
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
},
|
|
442
|
+
/**
|
|
443
|
+
* 쿠키 헤더 파싱 — 공통 함수
|
|
444
|
+
* @param {object} headers - 요청 헤더
|
|
445
|
+
* @returns {object} 파싱된 쿠키 맵
|
|
446
|
+
*/
|
|
447
|
+
function _parseCookies(headers) {
|
|
448
|
+
const header = headers?.cookie || headers?.Cookie || '';
|
|
449
|
+
if (!header) return {};
|
|
450
|
+
const result = {};
|
|
451
|
+
for (const pair of header.split(';')) {
|
|
452
|
+
const [k, ...v] = pair.trim().split('=');
|
|
453
|
+
if (k) result[k.trim()] = decodeURIComponent(v.join('='));
|
|
454
|
+
}
|
|
455
|
+
return result;
|
|
456
|
+
}
|
|
411
457
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
458
|
+
/**
|
|
459
|
+
* 세션 프록시 객체 생성 — lazy 시점에만 호출
|
|
460
|
+
* @param {object} rawReq - 원시 요청 객체
|
|
461
|
+
* @param {import('./Application.js').default} app - Application 인스턴스
|
|
462
|
+
* @returns {object} 세션 프록시 (get/set/destroy/renew/flash/getFlash)
|
|
463
|
+
*/
|
|
464
|
+
function _createSession(rawReq, app) {
|
|
465
|
+
// Core req의 session은 SessionProto 인스턴스 → _data 프로퍼티 추출
|
|
466
|
+
const rawSession = rawReq?.session;
|
|
467
|
+
const data = (rawSession?._data !== undefined) ? rawSession._data : (rawSession || {});
|
|
468
|
+
// Flash 추출 + 세션에서 삭제 (one-time read)
|
|
469
|
+
const flash = { ...(data._flash || {}) };
|
|
470
|
+
const hadFlash = !!data._flash;
|
|
471
|
+
delete data._flash;
|
|
472
|
+
const sessionId = rawReq?.sessionId || null;
|
|
473
|
+
const bridge = app?._bridge || null;
|
|
474
|
+
|
|
475
|
+
// Flash 소비 후 세션 저장소에 즉시 반영
|
|
476
|
+
if (hadFlash && sessionId && bridge?.sessionSet) {
|
|
477
|
+
try { bridge.sessionSet(sessionId, { ...data }); } catch {}
|
|
478
|
+
}
|
|
419
479
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
480
|
+
return {
|
|
481
|
+
/** 세션 값 조회 */
|
|
482
|
+
get: (key) => key ? (data[key] ?? null) : { ...data },
|
|
483
|
+
|
|
484
|
+
/** 세션 값 설정 — Bridge sessionSet 연동 */
|
|
485
|
+
set: (key, value) => {
|
|
486
|
+
data[key] = value;
|
|
487
|
+
if (sessionId && bridge?.sessionSet) {
|
|
488
|
+
try { bridge.sessionSet(sessionId, { ...data }); }
|
|
489
|
+
catch (e) { console.error('[Session] sessionSet failed:', e?.message || e); }
|
|
490
|
+
}
|
|
491
|
+
},
|
|
492
|
+
|
|
493
|
+
/** 세션 삭제 — Bridge sessionDestroy 연동 */
|
|
494
|
+
destroy: () => {
|
|
495
|
+
Object.keys(data).forEach(k => delete data[k]);
|
|
496
|
+
if (sessionId && bridge?.sessionDestroy) {
|
|
497
|
+
try { bridge.sessionDestroy(sessionId); } catch {}
|
|
498
|
+
}
|
|
499
|
+
},
|
|
500
|
+
|
|
501
|
+
/** 세션 갱신 — Bridge sessionRenew 연동 */
|
|
502
|
+
renew: () => {
|
|
503
|
+
if (sessionId && bridge?.sessionRenew) {
|
|
504
|
+
try { return bridge.sessionRenew(sessionId); } catch {}
|
|
505
|
+
}
|
|
506
|
+
return null;
|
|
507
|
+
},
|
|
508
|
+
|
|
509
|
+
/** 플래시 메시지 설정 */
|
|
510
|
+
flash: (key, value) => {
|
|
511
|
+
if (!data._flash) data._flash = {};
|
|
512
|
+
data._flash[key] = value;
|
|
513
|
+
if (sessionId && bridge?.sessionSet) {
|
|
514
|
+
try { bridge.sessionSet(sessionId, { ...data }); } catch {}
|
|
515
|
+
}
|
|
516
|
+
},
|
|
517
|
+
|
|
518
|
+
/** 플래시 메시지 조회 (1회 후 삭제) */
|
|
519
|
+
getFlash: (key) => {
|
|
520
|
+
if (key) {
|
|
521
|
+
const val = flash[key];
|
|
522
|
+
delete flash[key];
|
|
523
|
+
return val ?? null;
|
|
524
|
+
}
|
|
525
|
+
const all = { ...flash };
|
|
526
|
+
Object.keys(flash).forEach(k => delete flash[k]);
|
|
527
|
+
return all;
|
|
528
|
+
},
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* locale 감지 (5단계 우선순위) — 06-context.md
|
|
534
|
+
* @param {object} ctx - Context 인스턴스
|
|
535
|
+
* @returns {string} 감지된 locale
|
|
536
|
+
*/
|
|
537
|
+
function _detectLocale(ctx) {
|
|
538
|
+
if (ctx._rawReq?.locale) return ctx._rawReq.locale;
|
|
539
|
+
if (ctx.query?.lang) return ctx.query.lang;
|
|
540
|
+
// 쿠키 fuzionx.lang (lazy cookie 접근)
|
|
541
|
+
const cookieLang = ctx.cookies?.['fuzionx.lang'];
|
|
542
|
+
if (cookieLang) return cookieLang;
|
|
543
|
+
if (ctx._rawReq?.session?.locale) return ctx._rawReq.session.locale;
|
|
544
|
+
const al = ctx.headers?.['accept-language'];
|
|
545
|
+
if (al) {
|
|
546
|
+
const first = al.split(',')[0]?.split('-')[0];
|
|
547
|
+
if (first) return first;
|
|
450
548
|
}
|
|
549
|
+
return ctx.app?.config?.get('app.i18n.default_locale', 'ko') || 'ko';
|
|
550
|
+
}
|
|
451
551
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
const first = al.split(',')[0]?.split('-')[0];
|
|
463
|
-
if (first) return first;
|
|
552
|
+
/**
|
|
553
|
+
* i18n 번역 함수 생성 — lazy 시점에만 호출
|
|
554
|
+
* @param {object} ctx - Context 인스턴스
|
|
555
|
+
* @returns {Function} t(key, vars) + t.all()
|
|
556
|
+
*/
|
|
557
|
+
function _createT(ctx) {
|
|
558
|
+
const t = (key, vars) => {
|
|
559
|
+
const locale = vars?.locale || ctx.locale;
|
|
560
|
+
if (ctx.app?.i18n) {
|
|
561
|
+
return ctx.app.i18n.translate(locale, key, vars);
|
|
464
562
|
}
|
|
465
|
-
return
|
|
466
|
-
}
|
|
563
|
+
return vars?.default || key;
|
|
564
|
+
};
|
|
565
|
+
// ctx.t.all() — 현재 locale 전체 번역 데이터 (18-i18n.md)
|
|
566
|
+
t.all = () => {
|
|
567
|
+
if (ctx.app?.i18n?.getAll) {
|
|
568
|
+
return ctx.app.i18n.getAll(ctx.locale);
|
|
569
|
+
}
|
|
570
|
+
return {};
|
|
571
|
+
};
|
|
572
|
+
return t;
|
|
573
|
+
}
|
|
467
574
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
575
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
576
|
+
// 공유 인스턴스 (sync 경로용 — 할당 0)
|
|
577
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
578
|
+
|
|
579
|
+
/** 사전 할당된 응답 객체 (sync 경로에서 재사용) */
|
|
580
|
+
const _sharedFusionResponse = { status: 200, body: '', headers: null };
|
|
581
|
+
|
|
582
|
+
/** 사전 할당된 응답 헤더 객체 (sync 경로에서 재사용) */
|
|
583
|
+
const _sharedRespHeaders = {};
|
|
584
|
+
|
|
585
|
+
/** 사전 할당된 Set-Cookie 배열 (sync 경로에서 재사용) */
|
|
586
|
+
const _sharedSetCookies = [];
|
|
587
|
+
|
|
588
|
+
/** 사전 할당된 Context 싱글톤 (sync 경로 전용 재사용) */
|
|
589
|
+
const _sharedCtx = Object.create(ContextProto);
|
|
590
|
+
_sharedCtx.method = '';
|
|
591
|
+
_sharedCtx.url = '';
|
|
592
|
+
_sharedCtx.path = '';
|
|
593
|
+
_sharedCtx.query = null;
|
|
594
|
+
_sharedCtx.params = null;
|
|
595
|
+
_sharedCtx.headers = null;
|
|
596
|
+
_sharedCtx.body = null;
|
|
597
|
+
_sharedCtx.ip = '';
|
|
598
|
+
_sharedCtx.files = null;
|
|
599
|
+
_sharedCtx.uploadError = null;
|
|
600
|
+
_sharedCtx.formFields = null;
|
|
601
|
+
_sharedCtx.handlerId = 0;
|
|
602
|
+
_sharedCtx.user = null;
|
|
603
|
+
_sharedCtx.app = null;
|
|
604
|
+
_sharedCtx.appName = null;
|
|
605
|
+
_sharedCtx.theme = null;
|
|
606
|
+
_sharedCtx._sessionId = null;
|
|
607
|
+
_sharedCtx._rawReq = null;
|
|
608
|
+
_sharedCtx._json = undefined;
|
|
609
|
+
_sharedCtx._cookies = undefined;
|
|
610
|
+
_sharedCtx._session = undefined;
|
|
611
|
+
_sharedCtx._locale = undefined;
|
|
612
|
+
_sharedCtx._t = undefined;
|
|
613
|
+
_sharedCtx._setCookies = _sharedSetCookies;
|
|
614
|
+
_sharedCtx._statusCode = 200;
|
|
615
|
+
_sharedCtx._headers = _sharedRespHeaders;
|
|
616
|
+
_sharedCtx._body = '';
|
|
617
|
+
_sharedCtx._sent = false;
|
|
618
|
+
_sharedCtx._res = null; // Core res 직접 참조용 (createContextFromReq에서 설정)
|
|
619
|
+
|
|
620
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
621
|
+
// Context 팩토리 함수
|
|
622
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
471
623
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
624
|
+
/**
|
|
625
|
+
* Context 인스턴스 생성 — 팩토리 패턴
|
|
626
|
+
*
|
|
627
|
+
* @param {object} rawReq - Bridge에서 전달된 raw 요청 객체
|
|
628
|
+
* @param {import('./Application.js').default} app - Application 인스턴스
|
|
629
|
+
* @param {boolean} [reusable=true] - true면 공유 인스턴스 재사용 (sync용), false면 새로 생성 (async용)
|
|
630
|
+
* @returns {object} Context 인스턴스
|
|
631
|
+
*/
|
|
632
|
+
export function createContext(rawReq, app, reusable = true) {
|
|
633
|
+
let ctx;
|
|
634
|
+
if (reusable) {
|
|
635
|
+
// sync 핸들러: 공유 인스턴스 리셋 후 재사용 (할당 0)
|
|
636
|
+
ctx = _sharedCtx;
|
|
637
|
+
// 이전 헤더 키 제거 (무조건 리셋)
|
|
638
|
+
const keys = Object.keys(_sharedRespHeaders);
|
|
639
|
+
for (let i = 0; i < keys.length; i++) {
|
|
640
|
+
delete _sharedRespHeaders[keys[i]];
|
|
477
641
|
}
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
642
|
+
ctx._headers = _sharedRespHeaders;
|
|
643
|
+
// Set-Cookie 배열 리셋
|
|
644
|
+
_sharedSetCookies.length = 0;
|
|
645
|
+
ctx._setCookies = _sharedSetCookies;
|
|
646
|
+
} else {
|
|
647
|
+
// async 핸들러: 새 인스턴스 (응답 전까지 유지 필요)
|
|
648
|
+
ctx = Object.create(ContextProto);
|
|
649
|
+
ctx._setCookies = [];
|
|
650
|
+
ctx._headers = {};
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// ── 요청 프로퍼티 (핫 경로 — 즉시 할당) ──
|
|
654
|
+
ctx.method = rawReq.method || 'GET';
|
|
655
|
+
ctx.url = rawReq.url || '/';
|
|
656
|
+
ctx.path = rawReq.path || ctx.url.split('?')[0];
|
|
657
|
+
ctx.query = rawReq.query || {};
|
|
658
|
+
ctx.params = rawReq.params || {};
|
|
659
|
+
ctx.headers = rawReq.headers || {};
|
|
660
|
+
ctx.body = rawReq.body || null;
|
|
661
|
+
ctx.ip = rawReq.remoteIp || '';
|
|
662
|
+
ctx.files = rawReq.files || null;
|
|
663
|
+
ctx.uploadError = rawReq.uploadError || null;
|
|
664
|
+
ctx.formFields = rawReq.formFields || null;
|
|
665
|
+
ctx.handlerId = rawReq.handlerId;
|
|
666
|
+
|
|
667
|
+
// ── 프레임워크 주입 ──
|
|
668
|
+
ctx.app = app;
|
|
669
|
+
ctx.user = null;
|
|
670
|
+
ctx._sessionId = rawReq.sessionId || null;
|
|
671
|
+
ctx._rawReq = rawReq;
|
|
672
|
+
|
|
673
|
+
// ── lazy 캐시 초기화 (접근 시에만 생성) ──
|
|
674
|
+
ctx._json = undefined;
|
|
675
|
+
ctx._cookies = undefined; // get cookies() 에서 lazy 파싱
|
|
676
|
+
ctx._session = undefined; // get session() 에서 lazy 생성
|
|
677
|
+
ctx._locale = undefined; // get locale() 에서 lazy 감지
|
|
678
|
+
ctx._t = undefined; // get t() 에서 lazy 생성
|
|
679
|
+
|
|
680
|
+
// ── 응답 상태 리셋 ──
|
|
681
|
+
ctx._statusCode = 200;
|
|
682
|
+
ctx._body = '';
|
|
683
|
+
ctx._sent = false;
|
|
684
|
+
|
|
685
|
+
// ── 앱/테마 (디스패칭 후 설정) ──
|
|
686
|
+
ctx.appName = null;
|
|
687
|
+
ctx.theme = null;
|
|
688
|
+
|
|
689
|
+
return ctx;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
693
|
+
// Core req/res 직접 참조 팩토리 (최적화: 중간 객체 제거)
|
|
694
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* Core req/res에서 직접 Context 생성 — 중간 객체 리터럴 할당 제거
|
|
698
|
+
*
|
|
699
|
+
* ⚡ 최적화 핵심:
|
|
700
|
+
* - Core req 프로퍼티를 직접 참조 (14-프로퍼티 중간 객체 힙 할당 제거)
|
|
701
|
+
* - sync 경로: ctx._headers = res._headers (헤더 객체 공유 → _copyToRes 불필요)
|
|
702
|
+
* - async 경로: 독립 인스턴스 (기존 createContext와 동일)
|
|
703
|
+
*
|
|
704
|
+
* @param {object} req - Core의 req 객체 (createReq로 생성된)
|
|
705
|
+
* @param {object|null} res - Core의 res 객체 (sync: 직접 참조, async: null)
|
|
706
|
+
* @param {import('./Application.js').default} app - Application 인스턴스
|
|
707
|
+
* @param {boolean} [reusable=true] - true면 공유 인스턴스 (sync용)
|
|
708
|
+
* @returns {object} Context 인스턴스
|
|
709
|
+
*/
|
|
710
|
+
export function createContextFromReq(req, res, app, reusable = true) {
|
|
711
|
+
let ctx;
|
|
712
|
+
if (reusable && res) {
|
|
713
|
+
// sync 핸들러: 공유 인스턴스 + Core res 헤더 공유 (할당 0)
|
|
714
|
+
ctx = _sharedCtx;
|
|
715
|
+
// Core res의 헤더 객체를 직접 공유 → 핸들러가 ctx._headers에 쓰면 res._headers에 반영
|
|
716
|
+
ctx._headers = res._headers;
|
|
717
|
+
// Set-Cookie 배열 리셋
|
|
718
|
+
_sharedSetCookies.length = 0;
|
|
719
|
+
ctx._setCookies = _sharedSetCookies;
|
|
720
|
+
ctx._res = res; // Core res 참조 저장
|
|
721
|
+
} else {
|
|
722
|
+
// async 핸들러: 새 인스턴스 (응답 전까지 유지 필요)
|
|
723
|
+
ctx = Object.create(ContextProto);
|
|
724
|
+
ctx._setCookies = [];
|
|
725
|
+
ctx._headers = {};
|
|
726
|
+
ctx._res = null;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// ── Core req 프로퍼티 직접 참조 (중간 객체 없이) ──
|
|
730
|
+
ctx.method = req.method || 'GET';
|
|
731
|
+
ctx.url = req.url || '/';
|
|
732
|
+
ctx.path = req.path || ctx.url.split('?')[0];
|
|
733
|
+
ctx.query = req.query || {};
|
|
734
|
+
ctx.params = req.params || {};
|
|
735
|
+
ctx.headers = req.headers || {};
|
|
736
|
+
ctx.body = req.body || req.json || null; // Core req는 body 또는 lazy json getter
|
|
737
|
+
ctx.ip = req.ip || ''; // Core req는 ip (remoteIp 아님)
|
|
738
|
+
ctx.files = req.files || null;
|
|
739
|
+
ctx.uploadError = null;
|
|
740
|
+
ctx.formFields = req.formFields || null;
|
|
741
|
+
ctx.handlerId = req.handlerId;
|
|
742
|
+
|
|
743
|
+
// ── 프레임워크 주입 ──
|
|
744
|
+
ctx.app = app;
|
|
745
|
+
ctx.user = null;
|
|
746
|
+
ctx._sessionId = req.sessionId || null;
|
|
747
|
+
ctx._rawReq = req; // Core의 req 직접 참조
|
|
748
|
+
|
|
749
|
+
// ── lazy 캐시 초기화 (접근 시에만 생성) ──
|
|
750
|
+
ctx._json = undefined;
|
|
751
|
+
ctx._cookies = undefined;
|
|
752
|
+
ctx._session = undefined;
|
|
753
|
+
ctx._locale = undefined;
|
|
754
|
+
ctx._t = undefined;
|
|
755
|
+
|
|
756
|
+
// ── 응답 상태 리셋 ──
|
|
757
|
+
ctx._statusCode = 200;
|
|
758
|
+
ctx._body = '';
|
|
759
|
+
ctx._sent = false;
|
|
760
|
+
|
|
761
|
+
// ── 앱/테마 (디스패칭 후 설정) ──
|
|
762
|
+
ctx.appName = null;
|
|
763
|
+
ctx.theme = null;
|
|
764
|
+
|
|
765
|
+
return ctx;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
769
|
+
// 하위 호환: default export = Context 클래스
|
|
770
|
+
// 기존 코드에서 `new Context(rawReq, app)` 사용 시 호환
|
|
771
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
772
|
+
|
|
773
|
+
/**
|
|
774
|
+
* Context 래거시 호환 클래스
|
|
775
|
+
* 기존 코드에서 `new Context(rawReq, app)` 형태로 사용 시 호환성 유지를 위해 제공.
|
|
776
|
+
* 내부에서 createContext(rawReq, app, false)를 호출하여 async-safe 인스턴스를 반환.
|
|
777
|
+
*/
|
|
778
|
+
export default class Context {
|
|
779
|
+
/**
|
|
780
|
+
* @param {object} rawReq - Bridge에서 전달된 raw 요청 객체
|
|
781
|
+
* @param {import('./Application.js').default} app - Application 인스턴스
|
|
782
|
+
*/
|
|
783
|
+
constructor(rawReq, app) {
|
|
784
|
+
// 래거시 호환: new Context() → createContext(false) 위임
|
|
785
|
+
// 새 인스턴스(async-safe)를 생성하여 프록시 반환
|
|
786
|
+
return createContext(rawReq, app, false);
|
|
483
787
|
}
|
|
484
788
|
}
|