@mandujs/core 0.7.0 → 0.7.2

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.
@@ -1,552 +1,220 @@
1
- /**
2
- * Mandu Filling - 만두소 🥟
3
- * 체이닝 API로 비즈니스 로직 정의
4
- */
5
-
6
- import { ManduContext, NEXT_SYMBOL, ValidationError } from "./context";
7
- import { AuthenticationError, AuthorizationError } from "./auth";
8
- import { ErrorClassifier, formatErrorResponse, ErrorCode } from "../error";
9
- import { createContract, type ContractDefinition, type ContractInstance } from "../contract";
10
- import {
11
- type LifecycleStore,
12
- type OnRequestHandler,
13
- type BeforeHandleHandler,
14
- type AfterHandleHandler,
15
- type OnErrorHandler,
16
- type AfterResponseHandler,
17
- createLifecycleStore,
18
- executeLifecycle,
19
- } from "../runtime/lifecycle";
20
-
21
- /** Handler function type */
22
- export type Handler = (ctx: ManduContext) => Response | Promise<Response>;
23
-
24
- /** Guard function type - returns next() or Response */
25
- export type Guard = (ctx: ManduContext) => symbol | Response | Promise<symbol | Response>;
26
-
27
- /** HTTP methods */
28
- export type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS";
29
-
30
- /** Loader function type - SSR 데이터 로딩 */
31
- export type Loader<T = unknown> = (ctx: ManduContext) => T | Promise<T>;
32
-
33
- /** Loader 실행 옵션 */
34
- export interface LoaderOptions<T = unknown> {
35
- /** 타임아웃 (ms), 기본값 5000 */
36
- timeout?: number;
37
- /** 타임아웃 또는 에러 시 반환할 fallback 데이터 */
38
- fallback?: T;
39
- }
40
-
41
- /** Loader 타임아웃 에러 */
42
- export class LoaderTimeoutError extends Error {
43
- constructor(timeout: number) {
44
- super(`Loader timed out after ${timeout}ms`);
45
- this.name = "LoaderTimeoutError";
46
- }
47
- }
48
-
49
- interface FillingConfig<TLoaderData = unknown> {
50
- handlers: Map<HttpMethod, Handler>;
51
- guards: Guard[];
52
- methodGuards: Map<HttpMethod, Guard[]>;
53
- loader?: Loader<TLoaderData>;
54
- lifecycle: LifecycleStore;
55
- }
56
-
57
- /**
58
- * Mandu Filling Builder
59
- * @example
60
- * ```typescript
61
- * export default Mandu.filling()
62
- * .guard(authCheck)
63
- * .get(ctx => ctx.ok({ message: 'Hello!' }))
64
- * .post(ctx => ctx.created({ id: 1 }))
65
- * ```
66
- *
67
- * @example with loader
68
- * ```typescript
69
- * export default Mandu.filling<{ todos: Todo[] }>()
70
- * .loader(async (ctx) => {
71
- * const todos = await db.todos.findMany();
72
- * return { todos };
73
- * })
74
- * .get(ctx => ctx.ok(ctx.get('loaderData')))
75
- * ```
76
- */
77
- export class ManduFilling<TLoaderData = unknown> {
78
- private config: FillingConfig<TLoaderData> = {
79
- handlers: new Map(),
80
- guards: [],
81
- methodGuards: new Map(),
82
- lifecycle: createLifecycleStore(),
83
- };
84
-
85
- // ============================================
86
- // 🥟 SSR Loader
87
- // ============================================
88
-
89
- /**
90
- * Define SSR data loader
91
- * 페이지 렌더링 전 서버에서 데이터를 로드합니다.
92
- * 로드된 데이터는 클라이언트로 전달되어 hydration에 사용됩니다.
93
- *
94
- * @example
95
- * ```typescript
96
- * .loader(async (ctx) => {
97
- * const todos = await db.todos.findMany();
98
- * return { todos, user: ctx.get('user') };
99
- * })
100
- * ```
101
- */
102
- loader(loaderFn: Loader<TLoaderData>): this {
103
- this.config.loader = loaderFn;
104
- return this;
105
- }
106
-
107
- /**
108
- * Execute loader and return data
109
- * @internal Used by SSR runtime
110
- * @param ctx ManduContext
111
- * @param options Loader 실행 옵션 (timeout, fallback)
112
- */
113
- async executeLoader(
114
- ctx: ManduContext,
115
- options: LoaderOptions<TLoaderData> = {}
116
- ): Promise<TLoaderData | undefined> {
117
- if (!this.config.loader) {
118
- return undefined;
119
- }
120
-
121
- const { timeout = 5000, fallback } = options;
122
-
123
- try {
124
- const loaderPromise = Promise.resolve(this.config.loader(ctx));
125
-
126
- const timeoutPromise = new Promise<never>((_, reject) => {
127
- setTimeout(() => reject(new LoaderTimeoutError(timeout)), timeout);
128
- });
129
-
130
- return await Promise.race([loaderPromise, timeoutPromise]);
131
- } catch (error) {
132
- if (fallback !== undefined) {
133
- console.warn(
134
- `[Mandu] Loader failed, using fallback:`,
135
- error instanceof Error ? error.message : String(error)
136
- );
137
- return fallback;
138
- }
139
- throw error;
140
- }
141
- }
142
-
143
- /**
144
- * Check if loader is defined
145
- */
146
- hasLoader(): boolean {
147
- return !!this.config.loader;
148
- }
149
-
150
- // ============================================
151
- // 🥟 HTTP Method Handlers
152
- // ============================================
153
-
154
- /** Handle GET requests */
155
- get(handler: Handler): this {
156
- this.config.handlers.set("GET", handler);
157
- return this;
158
- }
159
-
160
- /** Handle POST requests */
161
- post(handler: Handler): this {
162
- this.config.handlers.set("POST", handler);
163
- return this;
164
- }
165
-
166
- /** Handle PUT requests */
167
- put(handler: Handler): this {
168
- this.config.handlers.set("PUT", handler);
169
- return this;
170
- }
171
-
172
- /** Handle PATCH requests */
173
- patch(handler: Handler): this {
174
- this.config.handlers.set("PATCH", handler);
175
- return this;
176
- }
177
-
178
- /** Handle DELETE requests */
179
- delete(handler: Handler): this {
180
- this.config.handlers.set("DELETE", handler);
181
- return this;
182
- }
183
-
184
- /** Handle HEAD requests */
185
- head(handler: Handler): this {
186
- this.config.handlers.set("HEAD", handler);
187
- return this;
188
- }
189
-
190
- /** Handle OPTIONS requests */
191
- options(handler: Handler): this {
192
- this.config.handlers.set("OPTIONS", handler);
193
- return this;
194
- }
195
-
196
- /** Handle all methods with single handler */
197
- all(handler: Handler): this {
198
- const methods: HttpMethod[] = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
199
- methods.forEach((method) => this.config.handlers.set(method, handler));
200
- return this;
201
- }
202
-
203
- // ============================================
204
- // 🥟 Guards (만두 찜기)
205
- // ============================================
206
-
207
- /**
208
- * Add guard for all methods or specific methods
209
- * @example
210
- * .guard(authCheck) // all methods
211
- * .guard(authCheck, 'POST', 'PUT') // specific methods
212
- */
213
- guard(guardFn: Guard, ...methods: HttpMethod[]): this {
214
- if (methods.length === 0) {
215
- // Apply to all methods
216
- this.config.guards.push(guardFn);
217
- } else {
218
- // Apply to specific methods
219
- methods.forEach((method) => {
220
- const guards = this.config.methodGuards.get(method) || [];
221
- guards.push(guardFn);
222
- this.config.methodGuards.set(method, guards);
223
- });
224
- }
225
- return this;
226
- }
227
-
228
- /** Alias for guard - more semantic for middleware */
229
- use(guardFn: Guard, ...methods: HttpMethod[]): this {
230
- return this.guard(guardFn, ...methods);
231
- }
232
-
233
- // ============================================
234
- // 🥟 Lifecycle Hooks (Elysia 스타일)
235
- // ============================================
236
-
237
- /**
238
- * 요청 시작 시 실행
239
- * @example
240
- * ```typescript
241
- * .onRequest((ctx) => {
242
- * console.log('Request:', ctx.req.method, ctx.req.url);
243
- * })
244
- * ```
245
- */
246
- onRequest(fn: OnRequestHandler): this {
247
- this.config.lifecycle.onRequest.push({ fn, scope: "local" });
248
- return this;
249
- }
250
-
251
- /**
252
- * 핸들러 전 실행 (Guard 역할)
253
- * Response 반환 시 체인 중단
254
- * @example
255
- * ```typescript
256
- * .beforeHandle((ctx) => {
257
- * if (!ctx.get('user')) {
258
- * return ctx.unauthorized();
259
- * }
260
- * // void 반환 시 계속 진행
261
- * })
262
- * ```
263
- */
264
- beforeHandle(fn: BeforeHandleHandler): this {
265
- this.config.lifecycle.beforeHandle.push({ fn, scope: "local" });
266
- return this;
267
- }
268
-
269
- /**
270
- * 핸들러 후 실행 (응답 변환)
271
- * @example
272
- * ```typescript
273
- * .afterHandle((ctx, response) => {
274
- * // 응답 헤더 추가
275
- * response.headers.set('X-Request-Id', crypto.randomUUID());
276
- * return response;
277
- * })
278
- * ```
279
- */
280
- afterHandle(fn: AfterHandleHandler): this {
281
- this.config.lifecycle.afterHandle.push({ fn, scope: "local" });
282
- return this;
283
- }
284
-
285
- /**
286
- * 에러 발생 시 실행
287
- * Response 반환 시 에러 응답으로 사용
288
- * @example
289
- * ```typescript
290
- * .onError((ctx, error) => {
291
- * console.error('Error:', error);
292
- * return ctx.json({ error: error.message }, 500);
293
- * })
294
- * ```
295
- */
296
- onError(fn: OnErrorHandler): this {
297
- this.config.lifecycle.onError.push({ fn, scope: "local" });
298
- return this;
299
- }
300
-
301
- /**
302
- * 응답 후 실행 (비동기, 응답에 영향 없음)
303
- * 로깅, 메트릭 수집 등에 사용
304
- * @example
305
- * ```typescript
306
- * .afterResponse((ctx) => {
307
- * console.log('Response sent:', ctx.req.url);
308
- * metrics.increment('requests');
309
- * })
310
- * ```
311
- */
312
- afterResponse(fn: AfterResponseHandler): this {
313
- this.config.lifecycle.afterResponse.push({ fn, scope: "local" });
314
- return this;
315
- }
316
-
317
- // ============================================
318
- // 🥟 Execution
319
- // ============================================
320
-
321
- /**
322
- * Handle incoming request
323
- * Called by generated route handler
324
- * @param request The incoming request
325
- * @param params URL path parameters
326
- * @param routeContext Route context for error reporting
327
- */
328
- async handle(
329
- request: Request,
330
- params: Record<string, string> = {},
331
- routeContext?: { routeId: string; pattern: string }
332
- ): Promise<Response> {
333
- const ctx = new ManduContext(request, params);
334
- const method = request.method.toUpperCase() as HttpMethod;
335
-
336
- try {
337
- // Run global guards
338
- for (const guard of this.config.guards) {
339
- const result = await guard(ctx);
340
- if (result !== NEXT_SYMBOL) {
341
- return result as Response;
342
- }
343
- if (!ctx.shouldContinue) {
344
- const response = ctx.getResponse();
345
- if (!response) {
346
- throw new Error("Guard set shouldContinue=false but no response was provided");
347
- }
348
- return response;
349
- }
350
- }
351
-
352
- // Run method-specific guards
353
- const methodGuards = this.config.methodGuards.get(method) || [];
354
- for (const guard of methodGuards) {
355
- const result = await guard(ctx);
356
- if (result !== NEXT_SYMBOL) {
357
- return result as Response;
358
- }
359
- if (!ctx.shouldContinue) {
360
- const response = ctx.getResponse();
361
- if (!response) {
362
- throw new Error("Guard set shouldContinue=false but no response was provided");
363
- }
364
- return response;
365
- }
366
- }
367
-
368
- // Get handler for method
369
- const handler = this.config.handlers.get(method);
370
- if (!handler) {
371
- return ctx.json(
372
- {
373
- status: "error",
374
- message: `Method ${method} not allowed`,
375
- allowed: Array.from(this.config.handlers.keys()),
376
- },
377
- 405
378
- );
379
- }
380
-
381
- // Execute handler
382
- return await handler(ctx);
383
- } catch (error) {
384
- // Handle authentication errors
385
- if (error instanceof AuthenticationError) {
386
- return ctx.json(
387
- {
388
- errorType: "AUTH_ERROR",
389
- code: "AUTHENTICATION_REQUIRED",
390
- message: error.message,
391
- summary: "인증 필요 - 로그인 후 다시 시도하세요",
392
- timestamp: new Date().toISOString(),
393
- },
394
- 401
395
- );
396
- }
397
-
398
- // Handle authorization errors
399
- if (error instanceof AuthorizationError) {
400
- return ctx.json(
401
- {
402
- errorType: "AUTH_ERROR",
403
- code: "ACCESS_DENIED",
404
- message: error.message,
405
- summary: "권한 없음 - 접근 권한이 부족합니다",
406
- requiredRoles: error.requiredRoles,
407
- timestamp: new Date().toISOString(),
408
- },
409
- 403
410
- );
411
- }
412
-
413
- // Handle validation errors with enhanced error format
414
- if (error instanceof ValidationError) {
415
- return ctx.json(
416
- {
417
- errorType: "LOGIC_ERROR",
418
- code: ErrorCode.SLOT_VALIDATION_ERROR,
419
- message: "Validation failed",
420
- summary: "입력 검증 실패 - 요청 데이터 확인 필요",
421
- fix: {
422
- file: routeContext ? `spec/slots/${routeContext.routeId}.slot.ts` : "spec/slots/",
423
- suggestion: "요청 데이터가 스키마와 일치하는지 확인하세요",
424
- },
425
- route: routeContext,
426
- errors: error.errors,
427
- timestamp: new Date().toISOString(),
428
- },
429
- 400
430
- );
431
- }
432
-
433
- // Handle other errors with error classification
434
- const classifier = new ErrorClassifier(null, routeContext);
435
- const manduError = classifier.classify(error);
436
-
437
- console.error(`[Mandu] ${manduError.errorType}:`, manduError.message);
438
-
439
- const response = formatErrorResponse(manduError, {
440
- isDev: process.env.NODE_ENV !== "production",
441
- });
442
-
443
- return ctx.json(response, 500);
444
- }
445
- }
446
-
447
- /**
448
- * Get list of registered methods
449
- */
450
- getMethods(): HttpMethod[] {
451
- return Array.from(this.config.handlers.keys());
452
- }
453
-
454
- /**
455
- * Check if method is registered
456
- */
457
- hasMethod(method: HttpMethod): boolean {
458
- return this.config.handlers.has(method);
459
- }
460
- }
461
-
462
- /**
463
- * Mandu namespace with factory methods
464
- */
465
- export const Mandu = {
466
- /**
467
- * Create a new filling (slot logic builder)
468
- * @example
469
- * ```typescript
470
- * import { Mandu } from '@mandujs/core'
471
- *
472
- * export default Mandu.filling()
473
- * .get(ctx => ctx.ok({ message: 'Hello!' }))
474
- * ```
475
- *
476
- * @example with loader data type
477
- * ```typescript
478
- * import { Mandu } from '@mandujs/core'
479
- *
480
- * interface LoaderData {
481
- * todos: Todo[];
482
- * user: User | null;
483
- * }
484
- *
485
- * export default Mandu.filling<LoaderData>()
486
- * .loader(async (ctx) => {
487
- * const todos = await db.todos.findMany();
488
- * return { todos, user: null };
489
- * })
490
- * .get(ctx => ctx.ok(ctx.get('loaderData')))
491
- * ```
492
- */
493
- filling<TLoaderData = unknown>(): ManduFilling<TLoaderData> {
494
- return new ManduFilling<TLoaderData>();
495
- },
496
-
497
- /**
498
- * Create an API contract (schema-first definition)
499
- *
500
- * Contract-first 방식으로 API 스키마를 정의합니다.
501
- * 정의된 스키마는 다음에 활용됩니다:
502
- * - TypeScript 타입 추론 (Slot에서 자동 완성)
503
- * - 런타임 요청/응답 검증
504
- * - OpenAPI 문서 자동 생성
505
- * - Guard의 Contract-Slot 일관성 검사
506
- *
507
- * @example
508
- * ```typescript
509
- * import { z } from "zod";
510
- * import { Mandu } from "@mandujs/core";
511
- *
512
- * const UserSchema = z.object({
513
- * id: z.string().uuid(),
514
- * email: z.string().email(),
515
- * name: z.string().min(2),
516
- * });
517
- *
518
- * export default Mandu.contract({
519
- * description: "사용자 관리 API",
520
- * tags: ["users"],
521
- *
522
- * request: {
523
- * GET: {
524
- * query: z.object({
525
- * page: z.coerce.number().default(1),
526
- * limit: z.coerce.number().default(10),
527
- * }),
528
- * },
529
- * POST: {
530
- * body: UserSchema.omit({ id: true }),
531
- * },
532
- * },
533
- *
534
- * response: {
535
- * 200: z.object({ data: z.array(UserSchema) }),
536
- * 201: z.object({ data: UserSchema }),
537
- * 400: z.object({ error: z.string() }),
538
- * },
539
- * });
540
- * ```
541
- */
542
- contract<T extends ContractDefinition>(definition: T): T & ContractInstance {
543
- return createContract(definition);
544
- },
545
-
546
- /**
547
- * Create context manually (for testing)
548
- */
549
- context(request: Request, params?: Record<string, string>): ManduContext {
550
- return new ManduContext(request, params);
551
- },
552
- };
1
+ /**
2
+ * Mandu Filling - 만두소 🥟
3
+ * 체이닝 API로 비즈니스 로직 정의
4
+ */
5
+
6
+ import { ManduContext, ValidationError } from "./context";
7
+ import { AuthenticationError, AuthorizationError } from "./auth";
8
+ import { ErrorClassifier, formatErrorResponse, ErrorCode } from "../error";
9
+ import { createContract, type ContractDefinition, type ContractInstance } from "../contract";
10
+ import {
11
+ type LifecycleStore,
12
+ type OnRequestHandler,
13
+ type BeforeHandleHandler,
14
+ type AfterHandleHandler,
15
+ type OnErrorHandler,
16
+ type AfterResponseHandler,
17
+ createLifecycleStore,
18
+ executeLifecycle,
19
+ } from "../runtime/lifecycle";
20
+
21
+ /** Handler function type */
22
+ export type Handler = (ctx: ManduContext) => Response | Promise<Response>;
23
+
24
+ /** HTTP methods */
25
+ export type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS";
26
+
27
+ /** Loader function type - SSR 데이터 로딩 */
28
+ export type Loader<T = unknown> = (ctx: ManduContext) => T | Promise<T>;
29
+
30
+ /** Loader 실행 옵션 */
31
+ export interface LoaderOptions<T = unknown> {
32
+ /** 타임아웃 (ms), 기본값 5000 */
33
+ timeout?: number;
34
+ /** 타임아웃 또는 에러 반환할 fallback 데이터 */
35
+ fallback?: T;
36
+ }
37
+
38
+ /** Loader 타임아웃 에러 */
39
+ export class LoaderTimeoutError extends Error {
40
+ constructor(timeout: number) {
41
+ super(`Loader timed out after ${timeout}ms`);
42
+ this.name = "LoaderTimeoutError";
43
+ }
44
+ }
45
+
46
+ interface FillingConfig<TLoaderData = unknown> {
47
+ handlers: Map<HttpMethod, Handler>;
48
+ loader?: Loader<TLoaderData>;
49
+ lifecycle: LifecycleStore;
50
+ }
51
+
52
+ export class ManduFilling<TLoaderData = unknown> {
53
+ private config: FillingConfig<TLoaderData> = {
54
+ handlers: new Map(),
55
+ lifecycle: createLifecycleStore(),
56
+ };
57
+
58
+ loader(loaderFn: Loader<TLoaderData>): this {
59
+ this.config.loader = loaderFn;
60
+ return this;
61
+ }
62
+
63
+ async executeLoader(
64
+ ctx: ManduContext,
65
+ options: LoaderOptions<TLoaderData> = {}
66
+ ): Promise<TLoaderData | undefined> {
67
+ if (!this.config.loader) {
68
+ return undefined;
69
+ }
70
+ const { timeout = 5000, fallback } = options;
71
+ try {
72
+ const loaderPromise = Promise.resolve(this.config.loader(ctx));
73
+ const timeoutPromise = new Promise<never>((_, reject) => {
74
+ setTimeout(() => reject(new LoaderTimeoutError(timeout)), timeout);
75
+ });
76
+ return await Promise.race([loaderPromise, timeoutPromise]);
77
+ } catch (error) {
78
+ if (fallback !== undefined) {
79
+ console.warn(`[Mandu] Loader failed, using fallback:`, error instanceof Error ? error.message : String(error));
80
+ return fallback;
81
+ }
82
+ throw error;
83
+ }
84
+ }
85
+
86
+ hasLoader(): boolean {
87
+ return !!this.config.loader;
88
+ }
89
+
90
+ get(handler: Handler): this {
91
+ this.config.handlers.set("GET", handler);
92
+ return this;
93
+ }
94
+
95
+ post(handler: Handler): this {
96
+ this.config.handlers.set("POST", handler);
97
+ return this;
98
+ }
99
+
100
+ put(handler: Handler): this {
101
+ this.config.handlers.set("PUT", handler);
102
+ return this;
103
+ }
104
+
105
+ patch(handler: Handler): this {
106
+ this.config.handlers.set("PATCH", handler);
107
+ return this;
108
+ }
109
+
110
+ delete(handler: Handler): this {
111
+ this.config.handlers.set("DELETE", handler);
112
+ return this;
113
+ }
114
+
115
+ head(handler: Handler): this {
116
+ this.config.handlers.set("HEAD", handler);
117
+ return this;
118
+ }
119
+
120
+ options(handler: Handler): this {
121
+ this.config.handlers.set("OPTIONS", handler);
122
+ return this;
123
+ }
124
+
125
+ all(handler: Handler): this {
126
+ const methods: HttpMethod[] = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
127
+ methods.forEach((method) => this.config.handlers.set(method, handler));
128
+ return this;
129
+ }
130
+
131
+ onRequest(fn: OnRequestHandler): this {
132
+ this.config.lifecycle.onRequest.push({ fn, scope: "local" });
133
+ return this;
134
+ }
135
+
136
+ beforeHandle(fn: BeforeHandleHandler): this {
137
+ this.config.lifecycle.beforeHandle.push({ fn, scope: "local" });
138
+ return this;
139
+ }
140
+
141
+ afterHandle(fn: AfterHandleHandler): this {
142
+ this.config.lifecycle.afterHandle.push({ fn, scope: "local" });
143
+ return this;
144
+ }
145
+
146
+ onError(fn: OnErrorHandler): this {
147
+ this.config.lifecycle.onError.push({ fn, scope: "local" });
148
+ return this;
149
+ }
150
+
151
+ afterResponse(fn: AfterResponseHandler): this {
152
+ this.config.lifecycle.afterResponse.push({ fn, scope: "local" });
153
+ return this;
154
+ }
155
+
156
+ async handle(
157
+ request: Request,
158
+ params: Record<string, string> = {},
159
+ routeContext?: { routeId: string; pattern: string }
160
+ ): Promise<Response> {
161
+ const ctx = new ManduContext(request, params);
162
+ const method = request.method.toUpperCase() as HttpMethod;
163
+ const handler = this.config.handlers.get(method);
164
+ if (!handler) {
165
+ return ctx.json({ status: "error", message: `Method ${method} not allowed`, allowed: Array.from(this.config.handlers.keys()) }, 405);
166
+ }
167
+ const lifecycleWithDefaults = this.createLifecycleWithDefaults(routeContext);
168
+ return executeLifecycle(lifecycleWithDefaults, ctx, async () => handler(ctx));
169
+ }
170
+
171
+ private createLifecycleWithDefaults(routeContext?: { routeId: string; pattern: string }): LifecycleStore {
172
+ const lifecycle: LifecycleStore = {
173
+ onRequest: [...this.config.lifecycle.onRequest],
174
+ onParse: [...this.config.lifecycle.onParse],
175
+ beforeHandle: [...this.config.lifecycle.beforeHandle],
176
+ afterHandle: [...this.config.lifecycle.afterHandle],
177
+ mapResponse: [...this.config.lifecycle.mapResponse],
178
+ afterResponse: [...this.config.lifecycle.afterResponse],
179
+ onError: [...this.config.lifecycle.onError],
180
+ };
181
+ const defaultErrorHandler: OnErrorHandler = (ctx, error) => {
182
+ if (error instanceof AuthenticationError) {
183
+ return ctx.json({ errorType: "AUTH_ERROR", code: "AUTHENTICATION_REQUIRED", message: error.message, summary: "인증 필요 - 로그인 후 다시 시도하세요", timestamp: new Date().toISOString() }, 401);
184
+ }
185
+ if (error instanceof AuthorizationError) {
186
+ return ctx.json({ errorType: "AUTH_ERROR", code: "ACCESS_DENIED", message: error.message, summary: "권한 없음 - 접근 권한이 부족합니다", requiredRoles: error.requiredRoles, timestamp: new Date().toISOString() }, 403);
187
+ }
188
+ if (error instanceof ValidationError) {
189
+ return ctx.json({ errorType: "LOGIC_ERROR", code: ErrorCode.SLOT_VALIDATION_ERROR, message: "Validation failed", summary: "입력 검증 실패 - 요청 데이터 확인 필요", fix: { file: routeContext ? `spec/slots/${routeContext.routeId}.slot.ts` : "spec/slots/", suggestion: "요청 데이터가 스키마와 일치하는지 확인하세요" }, route: routeContext, errors: error.errors, timestamp: new Date().toISOString() }, 400);
190
+ }
191
+ const classifier = new ErrorClassifier(null, routeContext);
192
+ const manduError = classifier.classify(error);
193
+ console.error(`[Mandu] ${manduError.errorType}:`, manduError.message);
194
+ const response = formatErrorResponse(manduError, { isDev: process.env.NODE_ENV !== "production" });
195
+ return ctx.json(response, 500);
196
+ };
197
+ lifecycle.onError.push({ fn: defaultErrorHandler, scope: "local" });
198
+ return lifecycle;
199
+ }
200
+
201
+ getMethods(): HttpMethod[] {
202
+ return Array.from(this.config.handlers.keys());
203
+ }
204
+
205
+ hasMethod(method: HttpMethod): boolean {
206
+ return this.config.handlers.has(method);
207
+ }
208
+ }
209
+
210
+ export const Mandu = {
211
+ filling<TLoaderData = unknown>(): ManduFilling<TLoaderData> {
212
+ return new ManduFilling<TLoaderData>();
213
+ },
214
+ contract<T extends ContractDefinition>(definition: T): T & ContractInstance {
215
+ return createContract(definition);
216
+ },
217
+ context(request: Request, params?: Record<string, string>): ManduContext {
218
+ return new ManduContext(request, params);
219
+ },
220
+ };