import type { GeneratedMap } from "../generator/generate"; import type { ManduError, RouteContext, ErrorType } from "./types"; import { ErrorCode, ERROR_MESSAGES, ERROR_SUMMARIES } from "./types"; import { StackTraceAnalyzer, type StackFrame } from "./stack-analyzer"; /** * ValidationError 타입 체크 (filling/context.ts에서 정의) */ function isValidationError(error: unknown): error is { errors: unknown[] } { return ( error !== null && typeof error === "object" && "errors" in error && Array.isArray((error as { errors: unknown[] }).errors) ); } /** * 에러 분류기 */ export class ErrorClassifier { private analyzer: StackTraceAnalyzer; private routeContext?: RouteContext; private isDev: boolean; constructor( generatedMap: GeneratedMap | null = null, routeContext?: RouteContext, rootDir: string = process.cwd() ) { this.analyzer = new StackTraceAnalyzer(generatedMap, rootDir); this.routeContext = routeContext; this.isDev = process.env.NODE_ENV !== "production"; } /** * 에러를 ManduError로 분류 */ classify(error: unknown): ManduError { // ValidationError 체크 if (isValidationError(error)) { return this.createValidationError(error); } // 일반 Error 객체 if (error instanceof Error) { return this.classifyError(error); } // 그 외 (문자열, 숫자 등) return this.createUnknownError(error); } /** * ValidationError 처리 */ private createValidationError(_error: { errors: unknown[] }): ManduError { const slotFile = this.findSlotFile(); return { errorType: "LOGIC_ERROR", code: ErrorCode.SLOT_VALIDATION_ERROR, message: ERROR_MESSAGES[ErrorCode.SLOT_VALIDATION_ERROR], summary: ERROR_SUMMARIES[ErrorCode.SLOT_VALIDATION_ERROR], fix: { file: slotFile || "spec/slots/", suggestion: "요청 데이터가 스키마와 일치하는지 확인하세요", }, route: this.routeContext, timestamp: new Date().toISOString(), }; } /** * Error 객체 분류 */ private classifyError(error: Error): ManduError { const frames = this.analyzer.parseStack(error.stack); const source = this.analyzer.determineErrorSource(frames); const blameFrame = this.analyzer.findBlameFrame(frames); let errorType: ErrorType; let code: ErrorCode; let fixFile: string; let suggestion: string; switch (source) { case "slot": errorType = "LOGIC_ERROR"; code = ErrorCode.SLOT_RUNTIME_ERROR; fixFile = blameFrame?.file || this.findSlotFile() || "spec/slots/"; suggestion = this.generateSlotSuggestion(error); break; case "spec": errorType = "SPEC_ERROR"; code = ErrorCode.SPEC_VALIDATION_ERROR; fixFile = blameFrame?.file || ".mandu/routes.manifest.json"; suggestion = "Spec 파일의 JSON 구문 또는 스키마를 확인하세요"; break; case "generated": // Generated 파일 에러 → Slot으로 매핑 시도 if (blameFrame) { const slotLocation = this.analyzer.mapToSlotLocation(blameFrame.file, blameFrame.line); if (slotLocation) { errorType = "LOGIC_ERROR"; code = ErrorCode.SLOT_RUNTIME_ERROR; fixFile = slotLocation.file; suggestion = this.generateSlotSuggestion(error); break; } } // 매핑 실패 시 프레임워크 버그로 처리 errorType = "FRAMEWORK_BUG"; code = ErrorCode.FRAMEWORK_INTERNAL; fixFile = blameFrame?.file || "packages/core/"; suggestion = "Generated 파일에서 예기치 않은 오류 발생. 버그 리포트를 등록해주세요."; break; case "framework": errorType = "FRAMEWORK_BUG"; code = this.determineFrameworkCode(blameFrame); fixFile = blameFrame?.file || "packages/core/"; suggestion = "Mandu 프레임워크 내부 오류입니다. GitHub 이슈를 등록해주세요."; break; default: // Unknown → 보수적으로 LOGIC_ERROR로 분류 errorType = "LOGIC_ERROR"; code = ErrorCode.SLOT_HANDLER_ERROR; fixFile = this.findSlotFile() || "spec/slots/"; suggestion = this.generateSlotSuggestion(error); } const manduError: ManduError = { errorType, code, message: error.message, summary: this.generateSummary(code, blameFrame), fix: { file: fixFile, suggestion, line: blameFrame?.line, }, route: this.routeContext, timestamp: new Date().toISOString(), }; // 개발 모드에서 디버그 정보 추가 if (this.isDev && error.stack) { manduError.debug = { stack: error.stack, originalError: error.message, generatedFile: this.analyzer.isGeneratedFile(blameFrame?.file || "") ? blameFrame?.file : undefined, }; } return manduError; } /** * 알 수 없는 타입의 에러 처리 */ private createUnknownError(error: unknown): ManduError { const message = typeof error === "string" ? error : String(error); return { errorType: "LOGIC_ERROR", code: ErrorCode.SLOT_HANDLER_ERROR, message, summary: `핸들러 오류 - ${this.findSlotFile() || "slot"} 파일 확인 필요`, fix: { file: this.findSlotFile() || "spec/slots/", suggestion: "핸들러에서 throw된 값을 확인하세요", }, route: this.routeContext, timestamp: new Date().toISOString(), }; } /** * 라우트 컨텍스트에서 Slot 파일 경로 찾기 */ private findSlotFile(): string | null { if (!this.routeContext?.id) return null; return `spec/slots/${this.routeContext.id}.slot.ts`; } /** * Slot 에러에 대한 제안 생성 */ private generateSlotSuggestion(error: Error): string { const message = error.message.toLowerCase(); if (message.includes("undefined") || message.includes("null")) { return "null/undefined 처리를 확인하세요. ctx.body() 결과나 파라미터 값이 없을 수 있습니다."; } if (message.includes("is not a function")) { return "호출하려는 함수가 정의되어 있는지 확인하세요."; } if (message.includes("cannot read") || message.includes("cannot access")) { return "객체의 속성에 접근하기 전에 객체가 존재하는지 확인하세요."; } if (message.includes("import") || message.includes("module")) { return "import 경로와 모듈 이름이 올바른지 확인하세요."; } return "slot 파일의 로직을 검토하세요."; } /** * 프레임워크 에러 코드 결정 */ private determineFrameworkCode(frame: StackFrame | null): ErrorCode { if (!frame) return ErrorCode.FRAMEWORK_INTERNAL; const file = frame.file.toLowerCase(); if (file.includes("generator") || file.includes("generate")) { return ErrorCode.FRAMEWORK_GENERATOR_ERROR; } if (file.includes("ssr") || file.includes("render")) { return ErrorCode.FRAMEWORK_SSR_ERROR; } if (file.includes("router") || file.includes("routing")) { return ErrorCode.FRAMEWORK_ROUTER_ERROR; } return ErrorCode.FRAMEWORK_INTERNAL; } /** * 요약 메시지 생성 */ private generateSummary(code: ErrorCode, frame: StackFrame | null): string { const baseSummary = ERROR_SUMMARIES[code] || "오류 발생"; if (frame?.file) { const shortFile = frame.file.split("/").pop() || frame.file; return `${baseSummary} (${shortFile}:${frame.line || "?"})`; } return baseSummary; } } /** * 특정 에러 타입에 대한 ManduError 생성 헬퍼 */ export function createSpecError( code: ErrorCode, message: string, file: string = ".mandu/routes.manifest.json", suggestion?: string ): ManduError { return { errorType: "SPEC_ERROR", code, message, summary: ERROR_SUMMARIES[code] || message, fix: { file, suggestion: suggestion || ERROR_MESSAGES[code] || "Spec 파일을 확인하세요", }, timestamp: new Date().toISOString(), }; } export function createLogicError( code: ErrorCode, message: string, slotFile: string, routeContext?: RouteContext, suggestion?: string ): ManduError { return { errorType: "LOGIC_ERROR", code, message, summary: ERROR_SUMMARIES[code] || message, fix: { file: slotFile, suggestion: suggestion || ERROR_MESSAGES[code] || "Slot 파일을 확인하세요", }, route: routeContext, timestamp: new Date().toISOString(), }; } export function createFrameworkBug( code: ErrorCode, message: string, file?: string ): ManduError { return { errorType: "FRAMEWORK_BUG", code, message, summary: ERROR_SUMMARIES[code] || message, fix: { file: file || "packages/core/", suggestion: "Mandu 프레임워크 내부 오류입니다. GitHub 이슈를 등록해주세요.", }, timestamp: new Date().toISOString(), }; }