Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

uvicorn app.main:app --reload

앞부분에 cd app이라고 되어있긴 한데 앞부분(cd app)을 지우던지 여기서 app.main:appmain:app으로 해야할 지 정해야할 것 같습니다.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그리고 동적포트라서
/app 인경우
python main.py or uvicorn main:app --host 0.0.0.0 --port 39722 --reload 이렇게 가야 되던데 그렇지 않나요?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. 이 부분 원래 리드미에 적혀있는 대로 cd app 해서 진행하니 app 내부에 생기는 문제가 발생하더라고요.
    그래서 현재 부분으로 수정했습니다.
    cd app 부분이 clone한 프로젝트로 이해하고 넘어갔는데 아닌가요??

  2. 저 부분 포트를 지정해줄 필요가 있나요? main.py에서 지정해주기 때문에 main 위치만 지정해주면 되는 거 아닌가요??

Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@

```bash
git clone https://github.com/Queryus/QGenie_api.git
cd app # 복제된 저장소 디렉토리로 이동
cd QGenie_api

```

Expand Down Expand Up @@ -88,7 +88,7 @@

```bash
poetry shell
uvicorn main:app --reload
uvicorn app.main:app --reload
```

또는 Poetry Run을 사용하여 직접 실행할 수 있습니다.
Expand Down
7 changes: 6 additions & 1 deletion app/api/api_router.py
Original file line number Diff line number Diff line change
@@ -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"])
48 changes: 48 additions & 0 deletions app/api/test_api.py
Original file line number Diff line number Diff line change
@@ -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:<port>/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("의도적으로 발생시킨 타입 에러입니다.")

48 changes: 48 additions & 0 deletions app/core/exceptions.py
Original file line number Diff line number Diff line change
@@ -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,
)
42 changes: 42 additions & 0 deletions app/core/status.py
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. 이렇게 상태코드랑 비즈니스 코드를 나누는건 어떤 이점이 있는건가요? (그냥 궁금)
  2. 메시지 포맷팅부분은 이해가 되지않아 설명 부탁드립니다. 나의 AI는 저렇게 하면 안된다고 나오네요.
    이 코드는 메시지에 포맷 자리가 정확히 있어야만 인자를 넣을 수 있어요. 없는데 넣으면 터집니다. 이렇게요!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. 상태코드를 나누는 이유는 각각의 역할과 책임을 분리하고 관리 용이성, 프론트와 명확한 소통 때문이 나눈 것 입니다.
  2. 이해하지 못했습니다..

Original file line number Diff line number Diff line change
@@ -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

13 changes: 12 additions & 1 deletion app/main.py
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이제 앞으로 #테스트 라우터 부분처럼 작성해야하는거죠?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분은 라우터 pr과 합쳐지면 바뀌어야합니다.

Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down
29 changes: 29 additions & 0 deletions app/schemas/response.py
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

자, 여기까지의 흐름이
클라이언트 요청 → API 실행 -> API 내부에서 ResponseMessage.success(...) 호출 -> CommonCode에서 상태코드/메시지 가져옴 (code.code, code.get_message) -> ResponseMessage FastAPI 응답으로 리턴
맞나요?

Original file line number Diff line number Diff line change
@@ -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
)