diff --git a/README.md b/README.md index 2f093e9..3c87aee 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ ```bash git clone https://github.com/Queryus/QGenie_api.git - cd app # 복제된 저장소 디렉토리로 이동 + cd QGenie_api ``` @@ -88,7 +88,7 @@ ```bash poetry shell - uvicorn main:app --reload + uvicorn app.main:app --reload ``` 또는 Poetry Run을 사용하여 직접 실행할 수 있습니다. diff --git a/app/api/api_router.py b/app/api/api_router.py index 813de73..4c92561 100644 --- a/app/api/api_router.py +++ b/app/api/api_router.py @@ -1,5 +1,10 @@ from fastapi import APIRouter +from app.api import test_api api_router = APIRouter() -# api_router.include_router(connect_driver.router, prefix="/connections", tags=["Driver"]) +# 테스트 라우터 +api_router.include_router(test_api.router, prefix="/test", tags=["Test"]) + +# 라우터 +# api_router.include_router(connect_driver.router, prefix="/connections", tags=["Driver"]) \ No newline at end of file diff --git a/app/api/test_api.py b/app/api/test_api.py new file mode 100644 index 0000000..c3ae9c0 --- /dev/null +++ b/app/api/test_api.py @@ -0,0 +1,48 @@ +from fastapi import APIRouter + +from app.schemas.response import ResponseMessage +from app.core.exceptions import APIException +from app.core.status import CommonCode + +router = APIRouter() + +@router.get("", response_model=ResponseMessage, summary="타입 변환을 이용한 성공/실패/버그 테스트") +def simple_test(mode: str): + """ + curl 테스트 시 아래 명령어 사용 + curl -i -X GET "http://localhost:/api/test?mode=1" + curl -i -X GET "http://localhost:8000/api/test?mode=1" + + 쿼리 파라미터 'mode' 값에 따라 다른 응답을 반환합니다. + + - **mode=1**: 성공 응답 (200 OK) + - **mode=2**: 커스텀 성공 응답 (200 OK) + - **mode=기타 숫자**: 예상된 실패 (404 Not Found) + - **mode=문자열**: 예상치 못한 서버 버그 (500 Internal Server Error) + """ + try: + # 1. 입력받은 mode를 정수(int)로 변환 시도 + mode_int = int(mode) + + # 2. 정수로 변환 성공 시, 값에 따라 분기 + if mode_int == 1: + # 기본 성공 코드(SUCCESS)로 응답 + return ResponseMessage.success( + value={"detail": "기본 성공 테스트입니다."} + ) + elif mode_int == 2: + # 커스텀 성공 코드(CREATED)로 응답 + return ResponseMessage.success( + value={"detail": "커스텀 성공 코드(CREATED) 테스트입니다."}, + code=CommonCode.CREATED + ) + else: + # 그 외 숫자는 '데이터 없음' 오류로 처리 + raise APIException(CommonCode.NO_SEARCH_DATA) + + except ValueError: + # 3. 정수로 변환 실패 시 (문자열이 들어온 경우) + # 예상치 못한 버그를 강제로 발생시킵니다. + # 이 에러는 generic_exception_handler가 처리하게 됩니다. + raise TypeError("의도적으로 발생시킨 타입 에러입니다.") + diff --git a/app/core/exceptions.py b/app/core/exceptions.py new file mode 100644 index 0000000..ea68b2a --- /dev/null +++ b/app/core/exceptions.py @@ -0,0 +1,48 @@ +import traceback +from fastapi import Request, status +from fastapi.responses import JSONResponse +from app.core.status import CommonCode + + +class APIException(Exception): + """ + API 로직 내에서 발생하는 모든 예상된 오류에 사용할 기본 예외 클래스입니다. + """ + def __init__(self, code: CommonCode, *args): + self.code_enum = code + self.message = code.get_message(*args) + super().__init__(self.message) + +async def api_exception_handler(request: Request, exc: APIException): + """ + APIException이 발생했을 때, 이를 감지하여 표준화된 JSON 오류 응답을 반환합니다. + """ + return JSONResponse( + status_code=exc.code_enum.http_status, + content={ + "code": exc.code_enum.code, + "message": exc.message, + "data": None + } + ) + +async def generic_exception_handler(request: Request, exc: Exception): + """ + 처리되지 않은 모든 예외를 잡아, 일관된 500 서버 오류를 반환합니다. + """ + # 운영 환경에서는 파일 로그나 모니터링 시스템으로 보내야 합니다. + print("="*20, "UNEXPECTED ERROR", "="*20) + traceback.print_exc() + print("="*50) + + # 사용자에게는 간단한 500 에러 메시지만 보여줍니다. + error_response = { + "code": CommonCode.FAIL.code, + "message": CommonCode.FAIL.message, + "data": None + } + + return JSONResponse( + status_code=CommonCode.FAIL.http_status, + content=error_response, + ) \ No newline at end of file diff --git a/app/core/status.py b/app/core/status.py new file mode 100644 index 0000000..790717d --- /dev/null +++ b/app/core/status.py @@ -0,0 +1,42 @@ +from enum import Enum +from fastapi import status + +class CommonCode(Enum): + """ + 애플리케이션의 모든 상태 코드를 중앙에서 관리합니다. + 각 멤버는 (HTTP 상태 코드, 고유 비즈니스 코드, 기본 메시지) 튜플을 값으로 가집니다. + 상태 코드 참고: https://developer.mozilla.org/ko/docs/Web/HTTP/Status + """ + + # ================================== + # 성공 (Success) - 2xx + # ================================== + SUCCESS = (status.HTTP_200_OK, "2000", "성공적으로 처리되었습니다.") + CREATED = (status.HTTP_201_CREATED, "2001", "성공적으로 생성되었습니다.") + SUCCESS_DB_CONNECT = (status.HTTP_200_OK, "2002", "디비 연결을 성공하였습니다.") + + # ================================== + # 클라이언트 오류 (Client Error) - 4xx + # ================================== + NO_VALUE = (status.HTTP_400_BAD_REQUEST, "4000", "필수 값이 존재하지 않습니다.") + DUPLICATION = (status.HTTP_409_CONFLICT, "4001", "이미 존재하는 데이터입니다.") + NO_SEARCH_DATA = (status.HTTP_404_NOT_FOUND, "4002", "요청한 데이터를 찾을 수 없습니다.") + + # ================================== + # 서버 오류 (Server Error) - 5xx + # ================================== + FAIL = (status.HTTP_500_INTERNAL_SERVER_ERROR, "9999", "서버 처리 중 오류가 발생했습니다.") + + + def __init__(self, http_status: int, code: str, message: str): + """Enum 멤버가 생성될 때 각 값을 속성으로 할당합니다.""" + self.http_status = http_status + self.code = code + self.message = message + + def get_message(self, *args) -> str: + """ + 메시지 포맷팅이 필요한 경우, 인자를 받아 완성된 메시지를 반환합니다. + """ + return self.message % args if args else self.message + diff --git a/app/main.py b/app/main.py index 5c37c8b..a8e0b41 100644 --- a/app/main.py +++ b/app/main.py @@ -7,9 +7,20 @@ from app.core.port import get_available_port # 동적 포트 할당 from app.api.api_router import api_router + +from app.core.exceptions import ( + APIException, + api_exception_handler, + generic_exception_handler +) + app = FastAPI() -# 헬스 체크 라우터 +# 전역 예외 처리기 등록 +app.add_exception_handler(APIException, api_exception_handler) +app.add_exception_handler(Exception, generic_exception_handler) + +# 라우터 app.include_router(health.router) app.include_router(api_router, prefix="/api") diff --git a/app/schemas/response.py b/app/schemas/response.py new file mode 100644 index 0000000..c57d50a --- /dev/null +++ b/app/schemas/response.py @@ -0,0 +1,29 @@ +from typing import Generic, TypeVar, Optional +from pydantic import BaseModel, Field +from app.core.status import CommonCode + +T = TypeVar('T') + +class ResponseMessage(BaseModel, Generic[T]): + """ + 모든 API 응답에 사용될 공용 스키마입니다. + """ + code: str = Field(..., description="응답을 나타내는 고유 상태 코드") + message: str = Field(..., description="응답 메시지") + data: Optional[T] = Field(None, description="반환될 실제 데이터") + + @classmethod + def success( + cls, + value: Optional[T] = None, + code: CommonCode = CommonCode.SUCCESS, + *args + ) -> "ResponseMessage[T]": + """ + 성공 응답을 생성하는 팩토리 메서드입니다. + """ + return cls( + code=code.code, + message=code.get_message(*args), + data=value + )