@fuzionx/framework 0.1.71 → 0.1.72
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
CHANGED
|
@@ -852,7 +852,18 @@ export default class Application {
|
|
|
852
852
|
})
|
|
853
853
|
.catch((err) => {
|
|
854
854
|
console.error(`[FX] Unhandled controller error: ${ctx.method} ${ctx.path}`, err);
|
|
855
|
-
|
|
855
|
+
try {
|
|
856
|
+
this._handleContextError(err, ctx);
|
|
857
|
+
} catch (handlerErr) {
|
|
858
|
+
// 에러 핸들링 자체 실패 → 최소한의 500 응답 보장
|
|
859
|
+
console.error(`[FX] Error handler failed:`, handlerErr);
|
|
860
|
+
try {
|
|
861
|
+
ctx._statusCode = 500;
|
|
862
|
+
ctx._body = JSON.stringify({ error: { message: err.message, status: 500 } });
|
|
863
|
+
ctx._headers['Content-Type'] = 'application/json';
|
|
864
|
+
ctx._sent = true;
|
|
865
|
+
} catch { /* ctx 조작도 실패하면 포기 */ }
|
|
866
|
+
}
|
|
856
867
|
this._sendContextResponse(rawReq.requestId, ctx);
|
|
857
868
|
});
|
|
858
869
|
|
|
@@ -1251,20 +1262,29 @@ export default class Application {
|
|
|
1251
1262
|
|
|
1252
1263
|
/**
|
|
1253
1264
|
* Context 에러 핸들링 — ctx에 에러 응답 설정
|
|
1265
|
+
*
|
|
1266
|
+
* 커스텀 에러 핸들러(useError)는 부가 작업(알림 등)용으로,
|
|
1267
|
+
* 기본 에러 응답 설정(ErrorHandler.handle)은 항상 실행된다.
|
|
1268
|
+
*
|
|
1254
1269
|
* @param {Error} err - 발생한 에러
|
|
1255
1270
|
* @param {object} ctx - Context 인스턴스
|
|
1256
1271
|
* @private
|
|
1257
1272
|
*/
|
|
1258
1273
|
_handleContextError(err, ctx) {
|
|
1259
|
-
// 커스텀 에러 핸들러
|
|
1274
|
+
// 커스텀 에러 핸들러 실행 (fire-and-forget — 알림, 로깅 등 부가 작업)
|
|
1260
1275
|
for (const { path: ePath, handler } of this._errorHandlers) {
|
|
1261
1276
|
if (!ePath || ctx.path?.startsWith(ePath)) {
|
|
1262
1277
|
try {
|
|
1263
|
-
handler(err, ctx);
|
|
1264
|
-
|
|
1265
|
-
|
|
1278
|
+
const result = handler(err, ctx);
|
|
1279
|
+
// async 핸들러 → 에러 무시 (알림 실패가 응답에 영향 X)
|
|
1280
|
+
if (result && typeof result.then === 'function') {
|
|
1281
|
+
result.catch(() => {});
|
|
1282
|
+
}
|
|
1283
|
+
} catch { /* 커스텀 핸들러 sync 에러 무시 */ }
|
|
1266
1284
|
}
|
|
1267
1285
|
}
|
|
1286
|
+
|
|
1287
|
+
// 기본 에러 응답 설정 (항상 실행 — ctx에 status/body 세팅)
|
|
1268
1288
|
this._errorHandler.handle(err, ctx);
|
|
1269
1289
|
|
|
1270
1290
|
// 'error' 이벤트 발행 — 앱 레벨에서 ErrorLog 기록용 (fire-and-forget)
|
package/lib/http/ErrorHandler.js
CHANGED
|
@@ -34,8 +34,14 @@ export default class ErrorHandler {
|
|
|
34
34
|
this.logger(`[${status}] ${ctx.method} ${ctx.path} — ${err.message}`);
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
// JSON 응답
|
|
38
|
-
|
|
37
|
+
// JSON 응답 판단 — 요청 헤더 기반 (경로 하드코딩 X)
|
|
38
|
+
// 1. Accept 헤더에 application/json 포함 (SPA fetch/axios 기본)
|
|
39
|
+
// 2. Content-Type이 JSON (JSON 요청 본문)
|
|
40
|
+
// 3. X-Requested-With: XMLHttpRequest (AJAX 요청)
|
|
41
|
+
// 4. /api 경로 (fallback — 헤더 없는 경우 대비)
|
|
42
|
+
const accept = ctx.get('accept') || '';
|
|
43
|
+
const wantsJson = accept.includes('application/json')
|
|
44
|
+
|| (!accept.includes('text/html') && ctx.get('x-requested-with') === 'XMLHttpRequest')
|
|
39
45
|
|| ctx.is('json')
|
|
40
46
|
|| ctx.path?.startsWith('/api');
|
|
41
47
|
|
|
@@ -117,22 +123,134 @@ export default class ErrorHandler {
|
|
|
117
123
|
} catch {}
|
|
118
124
|
}
|
|
119
125
|
|
|
120
|
-
// 3. 프레임워크 내장 에러 페이지 (XSS 방지)
|
|
126
|
+
// 3. 프레임워크 내장 에러 페이지 (모던 디자인 — XSS 방지)
|
|
121
127
|
const esc = (s) => String(s)
|
|
122
128
|
.replace(/&/g, '&').replace(/</g, '<')
|
|
123
129
|
.replace(/>/g, '>').replace(/"/g, '"');
|
|
124
130
|
|
|
125
|
-
|
|
131
|
+
// 상태 코드별 제목/이모지/색상 분기
|
|
132
|
+
const statusMap = {
|
|
133
|
+
400: { emoji: '⚠️', label: 'Bad Request', desc: '잘못된 요청입니다.', color: '#f59e0b' },
|
|
134
|
+
401: { emoji: '🔒', label: 'Unauthorized', desc: '인증이 필요합니다.', color: '#6366f1' },
|
|
135
|
+
403: { emoji: '🚫', label: 'Forbidden', desc: '접근 권한이 없습니다.', color: '#ef4444' },
|
|
136
|
+
404: { emoji: '🔍', label: 'Not Found', desc: '요청한 페이지를 찾을 수 없습니다.', color: '#8b5cf6' },
|
|
137
|
+
500: { emoji: '💥', label: 'Server Error', desc: '서버에서 오류가 발생했습니다.', color: '#ef4444' },
|
|
138
|
+
502: { emoji: '🌐', label: 'Bad Gateway', desc: '게이트웨이 오류가 발생했습니다.', color: '#f97316' },
|
|
139
|
+
503: { emoji: '🔧', label: 'Service Unavailable', desc: '서비스를 일시적으로 이용할 수 없습니다.', color: '#f59e0b' },
|
|
140
|
+
};
|
|
141
|
+
const info = statusMap[status] || {
|
|
142
|
+
emoji: status >= 500 ? '💥' : '⚠️',
|
|
143
|
+
label: status >= 500 ? 'Server Error' : 'Error',
|
|
144
|
+
desc: esc(err.message),
|
|
145
|
+
color: status >= 500 ? '#ef4444' : '#f59e0b',
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// 개발 모드: 스택 트레이스 블록
|
|
149
|
+
const stackBlock = this.isDev && err.stack
|
|
150
|
+
? `<details class="stack" open>
|
|
151
|
+
<summary>Stack Trace</summary>
|
|
152
|
+
<pre>${esc(err.stack)}</pre>
|
|
153
|
+
</details>` : '';
|
|
154
|
+
|
|
155
|
+
// 개발 모드: 요청 정보
|
|
156
|
+
const reqBlock = this.isDev
|
|
157
|
+
? `<div class="req-info">
|
|
158
|
+
<span class="method">${esc(ctx.method)}</span>
|
|
159
|
+
<span class="path">${esc(ctx.url)}</span>
|
|
160
|
+
</div>` : '';
|
|
126
161
|
|
|
127
162
|
ctx.status(status).html(
|
|
128
|
-
`<!DOCTYPE html
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
163
|
+
`<!DOCTYPE html>
|
|
164
|
+
<html lang="ko">
|
|
165
|
+
<head>
|
|
166
|
+
<meta charset="utf-8">
|
|
167
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
168
|
+
<title>${status} — ${esc(info.label)}</title>
|
|
169
|
+
<style>
|
|
170
|
+
*{margin:0;padding:0;box-sizing:border-box}
|
|
171
|
+
:root{--bg:#0f0f11;--card:#18181b;--border:#27272a;--text:#fafafa;--sub:#a1a1aa;--accent:${info.color}}
|
|
172
|
+
@media(prefers-color-scheme:light){
|
|
173
|
+
:root{--bg:#f8f9fa;--card:#fff;--border:#e5e7eb;--text:#18181b;--sub:#71717a}
|
|
174
|
+
}
|
|
175
|
+
body{
|
|
176
|
+
font-family:'Inter',system-ui,-apple-system,'Segoe UI',sans-serif;
|
|
177
|
+
background:var(--bg);color:var(--text);
|
|
178
|
+
min-height:100vh;display:flex;align-items:center;justify-content:center;
|
|
179
|
+
padding:20px;
|
|
180
|
+
}
|
|
181
|
+
.container{
|
|
182
|
+
max-width:520px;width:100%;text-align:center;
|
|
183
|
+
}
|
|
184
|
+
.status-code{
|
|
185
|
+
font-size:120px;font-weight:800;line-height:1;
|
|
186
|
+
background:linear-gradient(135deg,var(--accent),color-mix(in srgb,var(--accent) 60%,#fff));
|
|
187
|
+
-webkit-background-clip:text;-webkit-text-fill-color:transparent;
|
|
188
|
+
background-clip:text;
|
|
189
|
+
letter-spacing:-4px;margin-bottom:8px;
|
|
190
|
+
animation:fadeIn .5s ease-out;
|
|
191
|
+
}
|
|
192
|
+
.emoji{font-size:48px;margin-bottom:16px;display:block;animation:bounce .6s ease-out}
|
|
193
|
+
.card{
|
|
194
|
+
background:var(--card);border:1px solid var(--border);
|
|
195
|
+
border-radius:16px;padding:40px 32px;
|
|
196
|
+
box-shadow:0 4px 24px rgba(0,0,0,.12);
|
|
197
|
+
animation:slideUp .4s ease-out;
|
|
198
|
+
}
|
|
199
|
+
h1{font-size:18px;font-weight:600;margin-bottom:8px;color:var(--text)}
|
|
200
|
+
.desc{color:var(--sub);font-size:14px;line-height:1.6;margin-bottom:24px}
|
|
201
|
+
.home-btn{
|
|
202
|
+
display:inline-flex;align-items:center;gap:6px;
|
|
203
|
+
padding:10px 24px;border-radius:8px;font-size:14px;font-weight:500;
|
|
204
|
+
background:var(--accent);color:#fff;
|
|
205
|
+
text-decoration:none;transition:all .2s;
|
|
206
|
+
border:none;cursor:pointer;
|
|
207
|
+
}
|
|
208
|
+
.home-btn:hover{opacity:.85;transform:translateY(-1px)}
|
|
209
|
+
.req-info{
|
|
210
|
+
margin-top:20px;padding:10px 16px;
|
|
211
|
+
background:color-mix(in srgb,var(--bg) 80%,var(--card));
|
|
212
|
+
border-radius:8px;font-size:12px;font-family:'Menlo','Consolas',monospace;
|
|
213
|
+
color:var(--sub);display:flex;gap:8px;justify-content:center;align-items:center;
|
|
214
|
+
}
|
|
215
|
+
.method{
|
|
216
|
+
background:var(--accent);color:#fff;padding:2px 8px;border-radius:4px;
|
|
217
|
+
font-weight:600;font-size:11px;
|
|
218
|
+
}
|
|
219
|
+
.stack{
|
|
220
|
+
margin-top:20px;text-align:left;
|
|
221
|
+
}
|
|
222
|
+
.stack summary{
|
|
223
|
+
font-size:12px;color:var(--sub);cursor:pointer;margin-bottom:8px;
|
|
224
|
+
font-weight:500;
|
|
225
|
+
}
|
|
226
|
+
.stack pre{
|
|
227
|
+
background:color-mix(in srgb,var(--bg) 80%,var(--card));
|
|
228
|
+
border:1px solid var(--border);border-radius:8px;
|
|
229
|
+
padding:16px;font-size:11px;line-height:1.5;
|
|
230
|
+
overflow-x:auto;color:var(--sub);font-family:'Menlo','Consolas',monospace;
|
|
231
|
+
max-height:300px;overflow-y:auto;
|
|
232
|
+
}
|
|
233
|
+
.footer{margin-top:32px;font-size:11px;color:var(--sub)}
|
|
234
|
+
@keyframes fadeIn{from{opacity:0;transform:scale(.9)}to{opacity:1;transform:scale(1)}}
|
|
235
|
+
@keyframes slideUp{from{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}
|
|
236
|
+
@keyframes bounce{0%{transform:scale(0)}50%{transform:scale(1.2)}100%{transform:scale(1)}}
|
|
237
|
+
</style>
|
|
238
|
+
</head>
|
|
239
|
+
<body>
|
|
240
|
+
<div class="container">
|
|
241
|
+
<div class="card">
|
|
242
|
+
<span class="emoji">${info.emoji}</span>
|
|
243
|
+
<div class="status-code">${status}</div>
|
|
244
|
+
<h1>${esc(info.label)}</h1>
|
|
245
|
+
<p class="desc">${esc(info.desc)}</p>
|
|
246
|
+
<a href="/" class="home-btn">← 홈으로</a>
|
|
247
|
+
${reqBlock}
|
|
248
|
+
${stackBlock}
|
|
249
|
+
</div>
|
|
250
|
+
<p class="footer">FuzionX Framework</p>
|
|
251
|
+
</div>
|
|
252
|
+
</body>
|
|
253
|
+
</html>`
|
|
136
254
|
);
|
|
137
255
|
}
|
|
138
256
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fuzionx/framework",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.72",
|
|
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",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"@aws-sdk/client-s3": "^3.1028.0",
|
|
38
|
-
"@fuzionx/core": "^0.1.
|
|
38
|
+
"@fuzionx/core": "^0.1.72",
|
|
39
39
|
"better-sqlite3": "^12.8.0",
|
|
40
40
|
"knex": "^3.2.5",
|
|
41
41
|
"mongoose": "^9.3.2",
|