From 1e71b6eff713b05e00c38a3174ef8884f01723e3 Mon Sep 17 00:00:00 2001 From: ChoiseU Date: Fri, 1 Aug 2025 00:12:48 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20response=20=EA=B3=B5=ED=86=B5?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=B6=84=EB=A6=AC=ED=95=B4=EC=84=9C=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EB=B0=8F=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20router=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/api_router.py | 7 +++++- app/api/test_api.py | 47 ++++++++++++++++++++++++++++++++++++++++ app/core/exceptions.py | 48 +++++++++++++++++++++++++++++++++++++++++ app/core/status.py | 42 ++++++++++++++++++++++++++++++++++++ app/main.py | 13 ++++++++++- app/schemas/response.py | 29 +++++++++++++++++++++++++ 6 files changed, 184 insertions(+), 2 deletions(-) create mode 100644 app/api/test_api.py create mode 100644 app/core/exceptions.py create mode 100644 app/core/status.py create mode 100644 app/schemas/response.py diff --git a/app/api/api_router.py b/app/api/api_router.py index 813de73..12c0d1c 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"]) +# 테스트 라우터 +app.include_router(test_api.router, prefix="/api", 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..c6517df --- /dev/null +++ b/app/api/test_api.py @@ -0,0 +1,47 @@ +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("/test", response_model=ResponseMessage, summary="타입 변환을 이용한 성공/실패/버그 테스트") +def simple_test(mode: str): + """ + curl 테스트 시 아래 명령어 사용 + curl -i -X GET "http://localhost:/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 + ) From 547a7cbf0c841fe4e3c47b8a3dd7c3b4d7ec43af Mon Sep 17 00:00:00 2001 From: ChoiseU Date: Fri, 1 Aug 2025 00:13:39 +0900 Subject: [PATCH 2/4] =?UTF-8?q?docs:=20=EA=B0=80=EC=83=81=ED=99=98?= =?UTF-8?q?=EA=B2=BD=20=ED=99=9C=EC=84=B1=ED=99=94=20=EB=B6=80=EB=B6=84=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2f093e9..97279c6 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,7 @@ ```bash poetry shell - uvicorn main:app --reload + uvicorn app.main:app --reload ``` 또는 Poetry Run을 사용하여 직접 실행할 수 있습니다. From d059b5310da50606829be7ffe09bf761b173d463 Mon Sep 17 00:00:00 2001 From: ChoiseU Date: Sat, 2 Aug 2025 15:15:15 +0900 Subject: [PATCH 3/4] =?UTF-8?q?refactor:=20api=20router=20=EA=B3=BC=20?= =?UTF-8?q?=ED=95=A9=EC=B9=98=EB=A9=B4=EC=84=9C=20=EB=B0=9C=EC=83=9D?= =?UTF-8?q?=ED=95=9C=20=EC=95=88=EB=A7=9E=EB=8A=94=20=EB=B6=80=EB=B6=84=20?= =?UTF-8?q?endpoint=20=EB=B6=80=EB=B6=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/api_router.py | 2 +- app/api/test_api.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/api/api_router.py b/app/api/api_router.py index 12c0d1c..4c92561 100644 --- a/app/api/api_router.py +++ b/app/api/api_router.py @@ -4,7 +4,7 @@ api_router = APIRouter() # 테스트 라우터 -app.include_router(test_api.router, prefix="/api", tags=["Test"]) +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 index c6517df..c3ae9c0 100644 --- a/app/api/test_api.py +++ b/app/api/test_api.py @@ -6,11 +6,12 @@ router = APIRouter() -@router.get("/test", response_model=ResponseMessage, summary="타입 변환을 이용한 성공/실패/버그 테스트") +@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' 값에 따라 다른 응답을 반환합니다. From 38c1cbead997bbea0f862df08f7f779a8c6b22f3 Mon Sep 17 00:00:00 2001 From: ChoiseU Date: Sat, 2 Aug 2025 15:21:21 +0900 Subject: [PATCH 4/4] =?UTF-8?q?docs:=20cd=20app=EC=9D=84=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=EC=A0=9D=ED=8A=B8=20=EC=9D=B4=EB=A6=84=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 97279c6..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 ```