// utils/errorHandler.js - 통합 에러 처리 유틸리티 /** * 표준화된 에러 응답 생성 */ class ApiError extends Error { constructor(message, statusCode = 500, errorCode = null) { super(message); this.statusCode = statusCode; this.errorCode = errorCode; this.timestamp = new Date().toISOString(); } } /** * 에러 응답 포맷터 */ const formatErrorResponse = (error, req = null) => { const response = { success: false, error: error.message || '알 수 없는 오류가 발생했습니다.', timestamp: error.timestamp || new Date().toISOString() }; // 개발 환경에서만 상세 정보 포함 if (process.env.NODE_ENV === 'development') { response.stack = error.stack; response.errorCode = error.errorCode; if (req) { response.requestInfo = { method: req.method, url: req.originalUrl, userAgent: req.get('User-Agent'), ip: req.ip }; } } return response; }; /** * 데이터베이스 에러 처리 */ const handleDatabaseError = (error, operation = 'database operation') => { console.error(`[DB Error] ${operation}:`, error); // 일반적인 DB 에러 코드 매핑 const errorMappings = { 'ER_DUP_ENTRY': { message: '중복된 데이터입니다.', statusCode: 409 }, 'ER_NO_REFERENCED_ROW_2': { message: '참조된 데이터가 존재하지 않습니다.', statusCode: 400 }, 'ER_ROW_IS_REFERENCED_2': { message: '다른 데이터에서 참조되고 있어 삭제할 수 없습니다.', statusCode: 409 }, 'ER_BAD_FIELD_ERROR': { message: '잘못된 필드명입니다.', statusCode: 400 }, 'ER_NO_SUCH_TABLE': { message: '테이블이 존재하지 않습니다.', statusCode: 500 }, 'ECONNREFUSED': { message: '데이터베이스 연결에 실패했습니다.', statusCode: 503 } }; const mapping = errorMappings[error.code] || errorMappings[error.errno]; if (mapping) { throw new ApiError(mapping.message, mapping.statusCode, error.code); } // 기본 에러 throw new ApiError(`${operation} 중 오류가 발생했습니다.`, 500, error.code); }; /** * 유효성 검사 에러 처리 */ const handleValidationError = (field, value, rule) => { const message = `${field} 필드가 유효하지 않습니다. (값: ${value}, 규칙: ${rule})`; throw new ApiError(message, 400, 'VALIDATION_ERROR'); }; /** * 권한 에러 처리 */ const handleAuthorizationError = (requiredLevel, userLevel) => { const message = `접근 권한이 부족합니다. (필요: ${requiredLevel}, 현재: ${userLevel})`; throw new ApiError(message, 403, 'AUTHORIZATION_ERROR'); }; /** * 리소스 없음 에러 처리 */ const handleNotFoundError = (resource, identifier = null) => { const message = identifier ? `${resource}(${identifier})을(를) 찾을 수 없습니다.` : `${resource}을(를) 찾을 수 없습니다.`; throw new ApiError(message, 404, 'NOT_FOUND'); }; /** * Express 에러 핸들러 미들웨어 */ const errorMiddleware = (error, req, res, next) => { // ApiError가 아닌 경우 변환 if (!(error instanceof ApiError)) { error = new ApiError(error.message || '서버 내부 오류', 500); } const response = formatErrorResponse(error, req); // 로깅 if (error.statusCode >= 500) { console.error('[Server Error]', { message: error.message, stack: error.stack, url: req.originalUrl, method: req.method, user: req.user?.username || 'anonymous' }); } else { console.warn('[Client Error]', { message: error.message, url: req.originalUrl, method: req.method, user: req.user?.username || 'anonymous' }); } res.status(error.statusCode).json(response); }; /** * 비동기 함수 래퍼 (에러 자동 처리) */ const asyncHandler = (fn) => { return (req, res, next) => { Promise.resolve(fn(req, res, next)).catch(next); }; }; module.exports = { ApiError, formatErrorResponse, handleDatabaseError, handleValidationError, handleAuthorizationError, handleNotFoundError, errorMiddleware, asyncHandler };