refactor: API 서버 구조 개선 및 표준화
- 통합 에러 처리 시스템 구축: * utils/errorHandler.js: ApiError 클래스 및 에러 미들웨어 * 데이터베이스, 유효성 검사, 권한 에러 표준화 * 비동기 함수 래퍼 (asyncHandler) 추가 - 응답 포맷터 시스템 구축: * utils/responseFormatter.js: 일관된 API 응답 형식 * 성공, 페이지네이션, 인증, 파일업로드 등 전용 포맷터 * Express 응답 확장 미들웨어 - 유효성 검사 시스템 구축: * utils/validator.js: 스키마 기반 유효성 검사 * 필수 필드, 타입, 길이, 형식 검사 함수들 * 일반적인 스키마 정의 (사용자, 프로젝트, 작업보고서 등) - 코드 정리 및 표준화: * 삭제된 테이블 참조 제거 (work_report_audit_log 등) * 대문자 테이블명을 소문자로 통일 (Users -> users) * authController.js에 새로운 유틸리티 적용 예시 - 미들웨어 통합: * index.js에 에러 핸들러 및 응답 포맷터 적용 * 헬스체크 엔드포인트 개선
This commit is contained in:
143
api.hyungi.net/utils/errorHandler.js
Normal file
143
api.hyungi.net/utils/errorHandler.js
Normal file
@@ -0,0 +1,143 @@
|
||||
// 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
|
||||
};
|
||||
Reference in New Issue
Block a user