""" Standardized Error Response Models 통일된 에러 응답 형식 모든 API 엔드포인트에서 일관된 에러 응답 제공 """ from typing import Optional, Any, Dict from fastapi import HTTPException, status from pydantic import BaseModel class ErrorDetail(BaseModel): """ 에러 상세 정보 Attributes: code: 에러 코드 (예: "TOKEN_001", "VALIDATION_ERROR") message: 사용자 친화적 에러 메시지 field: 에러가 발생한 필드 (선택) details: 추가 정보 (선택) """ code: str message: str field: Optional[str] = None details: Optional[Dict[str, Any]] = None class ErrorResponse(BaseModel): """ 표준 에러 응답 Attributes: success: 항상 False error: 에러 상세 정보 """ success: bool = False error: ErrorDetail def create_error_response( code: str, message: str, status_code: int = status.HTTP_400_BAD_REQUEST, field: Optional[str] = None, details: Optional[Dict[str, Any]] = None ) -> HTTPException: """ 표준 에러 응답 생성 Args: code: 에러 코드 message: 에러 메시지 status_code: HTTP 상태 코드 field: 에러 필드 (선택) details: 추가 정보 (선택) Returns: HTTPException """ return HTTPException( status_code=status_code, detail=ErrorResponse( error=ErrorDetail( code=code, message=message, field=field, details=details ) ).model_dump() ) # 사전 정의된 에러 생성 함수 def bad_request_error( message: str, code: str = "BAD_REQUEST", field: Optional[str] = None ) -> HTTPException: """400 Bad Request 에러""" return create_error_response( code=code, message=message, status_code=status.HTTP_400_BAD_REQUEST, field=field ) def unauthorized_error( message: str = "인증이 필요합니다", code: str = "UNAUTHORIZED" ) -> HTTPException: """401 Unauthorized 에러""" return create_error_response( code=code, message=message, status_code=status.HTTP_401_UNAUTHORIZED ) def forbidden_error( message: str = "접근 권한이 없습니다", code: str = "FORBIDDEN" ) -> HTTPException: """403 Forbidden 에러""" return create_error_response( code=code, message=message, status_code=status.HTTP_403_FORBIDDEN ) def not_found_error( resource: str = "리소스", code: str = "NOT_FOUND" ) -> HTTPException: """404 Not Found 에러""" return create_error_response( code=code, message=f"{resource}를 찾을 수 없습니다", status_code=status.HTTP_404_NOT_FOUND ) def conflict_error( message: str, code: str = "CONFLICT" ) -> HTTPException: """409 Conflict 에러""" return create_error_response( code=code, message=message, status_code=status.HTTP_409_CONFLICT ) def rate_limit_error( retry_after: int = 60, code: str = "RATE_LIMIT_EXCEEDED" ) -> HTTPException: """429 Too Many Requests 에러""" return create_error_response( code=code, message="요청 한도를 초과했습니다. 잠시 후 다시 시도해주세요.", status_code=status.HTTP_429_TOO_MANY_REQUESTS, details={"retry_after": retry_after} ) def internal_server_error( message: str = "서버 내부 오류가 발생했습니다", code: str = "INTERNAL_ERROR", details: Optional[Dict[str, Any]] = None ) -> HTTPException: """500 Internal Server Error""" return create_error_response( code=code, message=message, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, details=details ) def service_unavailable_error( message: str = "서비스를 일시적으로 사용할 수 없습니다", code: str = "SERVICE_UNAVAILABLE" ) -> HTTPException: """503 Service Unavailable 에러""" return create_error_response( code=code, message=message, status_code=status.HTTP_503_SERVICE_UNAVAILABLE ) # 도메인별 에러 코드 class ErrorCodes: """에러 코드 상수""" # 인증/권한 UNAUTHORIZED = "UNAUTHORIZED" FORBIDDEN = "FORBIDDEN" TOKEN_EXPIRED = "TOKEN_EXPIRED" # 토큰 관련 TOKEN_BALANCE_FAILED = "TOKEN_001" TOKEN_PRODUCT_NOT_FOUND = "TOKEN_002" TOKEN_INSUFFICIENT = "TOKEN_003" TOKEN_CHARGE_FAILED = "TOKEN_004" TOKEN_BONUS_FAILED = "TOKEN_005" PAYMENT_RECORD_FAILED = "TOKEN_006" # 스토리 관련 STORY_NOT_FOUND = "STORY_001" STORY_CREATE_FAILED = "STORY_002" STORY_UPDATE_FAILED = "STORY_003" # 코스 관련 COURSE_NOT_FOUND = "COURSE_001" COURSE_CREATE_FAILED = "COURSE_002" COURSE_GENERATION_FAILED = "COURSE_003" # 소셜 관련 LIKE_FAILED = "SOCIAL_001" REVIEW_CREATE_FAILED = "SOCIAL_002" REVIEW_DELETE_FAILED = "SOCIAL_003" # 데이터 검증 VALIDATION_ERROR = "VALIDATION_ERROR" INVALID_UUID = "INVALID_UUID" INVALID_COORDINATES = "INVALID_COORDINATES" # 일반 NOT_FOUND = "NOT_FOUND" CONFLICT = "CONFLICT" BAD_REQUEST = "BAD_REQUEST" RATE_LIMIT_EXCEEDED = "RATE_LIMIT_EXCEEDED" INTERNAL_ERROR = "INTERNAL_ERROR" SERVICE_UNAVAILABLE = "SERVICE_UNAVAILABLE"