Files
TK-FB-Project/synology_deployment/api/utils/errorHandler.js
Hyungi Ahn a9bce9d20b fix: 캘린더 모달 중복 카드 문제 및 삭제 권한 개선
- monthly_worker_status 조회 시 GROUP BY로 중복 데이터 합산
- 작업보고서 삭제 권한을 그룹장 이상으로 제한 (admin, system, group_leader)
- 중복 데이터 정리를 위한 마이그레이션 SQL 추가 (009_fix_duplicate_monthly_status.sql)
- synology_deployment 버전에도 동일 수정 적용
2025-12-02 13:08:44 +09:00

144 lines
4.0 KiB
JavaScript

// 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
};