Skip to content

Commit

Permalink
Feature/swagger (#32)
Browse files Browse the repository at this point in the history
* Feat: swagger json 배포

* Docs: FAQ swagger 파일 수정

* Docs: response schema 수정

* Docs: response 모델 수정

* Chore: setup-dev 실행 시 pre-commit을 install 하도록 커맨드 구성

* Docs: swagger 모듈 분리

* Chore: 중복 command 제거
  • Loading branch information
jungmir authored Feb 25, 2025
1 parent 9392a84 commit c10b530
Show file tree
Hide file tree
Showing 16 changed files with 437 additions and 131 deletions.
5 changes: 5 additions & 0 deletions makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ HOST=0.0.0.0
setup:
uv sync
uv run src/manage.py makemigrations

setup-dev: setup
uv run pre-commit install

migration:
Expand All @@ -23,3 +25,6 @@ e2e-test:

test:
uv run src/manage.py test

export-swagger:
uv run src/manage.py spectacular --file swagger/api.yaml
6 changes: 4 additions & 2 deletions src/config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
LOCAL_APPS = [
"config",
"faq",
"core",
"merchandise",
]

Expand All @@ -54,10 +55,11 @@
# rest_framework settings
REST_FRAMEWORK = {
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
"DEFAULT_PAGINATION_CLASS": "core.paginations.PageNumberPagination",
"EXCEPTION_HANDLER": "rest_framework.views.exception_handler",
"PAGE_SIZE_QUERY_PARAM": "page_size",
"PAGE_SIZE": 10,
"MAX_PAGE_SIZE": 100,
"PAGE_SIZE_QUERY_PARAM": "page_size",
}
# spectacular settings
SPECTACULAR_SETTINGS = {
Expand Down
Empty file added src/core/__init__.py
Empty file.
5 changes: 5 additions & 0 deletions src/core/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from django.apps import AppConfig


class CoreConfig(AppConfig):
name = "core"
14 changes: 14 additions & 0 deletions src/core/errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from rest_framework import status
from rest_framework.exceptions import APIException


class BaseException(APIException):
status_code = status.HTTP_200_OK
default_detail = "Internal Server Error"
default_code = "ERROR"


class NotExistException(BaseException):
status_code = status.HTTP_200_OK
default_detail = "Not Exist."
default_code = "Not Exist"
Empty file.
27 changes: 27 additions & 0 deletions src/core/middlewares/exception_handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from django.core.exceptions import PermissionDenied as DjangoPermissionDenied
from django.http import Http404
from rest_framework.exceptions import APIException, NotFound, PermissionDenied
from rest_framework.views import set_rollback

from core.responses.base import BaseResponse


def custom_exception_handler(exc, context):
if isinstance(exc, Http404):
exc = NotFound(*(exc.args))
elif isinstance(exc, DjangoPermissionDenied):
exc = PermissionDenied(*(exc.args))

if isinstance(exc, APIException):
headers = {}
if getattr(exc, "auth_header", None):
headers["WWW-Authenticate"] = exc.auth_header
if getattr(exc, "wait", None):
headers["Retry-After"] = "%d" % exc.wait

error = dict(code=exc.default_code, message=exc.detail)

set_rollback()
return BaseResponse(error=error, headers=headers, status="ERROR")

return None
20 changes: 20 additions & 0 deletions src/core/paginations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from rest_framework.pagination import PageNumberPagination as DRFPageNumberPagination

from .responses.base import BaseResponse


class PageNumberPagination(DRFPageNumberPagination):
page_size = 10
page_size_query_param = "page_size"
max_page_size = 100 # page 단위의 요청 최대 size

def get_paginated_response(self, data):
pagination = {
"count": self.page.paginator.count,
"next": self.get_next_link(),
"previous": self.get_previous_link(),
}
return BaseResponse(data=data, pagination=pagination)

def get_paginated_response_schema(self, schema: dict) -> dict:
return schema
Empty file added src/core/responses/__init__.py
Empty file.
36 changes: 36 additions & 0 deletions src/core/responses/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from typing import Literal

from rest_framework.response import Response
from rest_framework.serializers import Serializer
from rest_framework.status import HTTP_200_OK

from core.responses.serializer import SuccessResponseSerializer


class BaseResponse(Response):
def __init__(
self,
data: dict | list | None = None,
error: dict | None = None,
pagination: dict | None = None,
template_name: str | None = None,
headers: dict | None = None,
exception: bool = False,
content_type: str | None = None,
status: Literal["SUCCESS", "ERROR"] = "SUCCESS",
):
response_format = {
"status": status,
"error": error,
"data": data,
"pagination": pagination,
}
super().__init__(
response_format, HTTP_200_OK, template_name, headers, exception, content_type
)

def __class_getitem__(cls, *args, **kwargs):
return cls

def get_response_schema(self) -> Serializer:
return SuccessResponseSerializer()
66 changes: 66 additions & 0 deletions src/core/responses/serializer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
from rest_framework import serializers


class ErrorSerializer(serializers.Serializer):
code = serializers.CharField()
message = serializers.CharField()

def to_representation(self, instance):
return {
"code": instance.code,
"message": instance.message,
}


class PageNumberPaginationSerializer(serializers.Serializer):
count = serializers.IntegerField()
next = serializers.CharField()
previous = serializers.CharField()

def to_representation(self, instance):
return {
"count": instance.count,
"next": instance.next,
"previous": instance.previous,
}


class SuccessResponseSerializer(serializers.Serializer):
status = serializers.CharField(default="SUCCESS")
data = serializers.DictField(required=False)
pagination = PageNumberPaginationSerializer(required=False, allow_null=True)

def to_representation(self, instance):
return {
"status": instance.status,
"data": instance.data,
"error": None,
"pagination": None,
}


class ListSuccessResponseSerializer(serializers.Serializer):
status = serializers.CharField(default="SUCCESS")
data = serializers.ListField(required=False, child=serializers.DictField())
pagination = PageNumberPaginationSerializer(required=False, allow_null=True)

def to_representation(self, instance):
return {
"status": instance.status,
"data": instance.data,
"error": None,
"pagination": instance.pagination,
}


class ErrorResponseSerializer(serializers.Serializer):
status = serializers.CharField(default="ERROR")
error = ErrorSerializer()

def to_representation(self, instance):
return {
"status": instance.status,
"error": instance.error,
"data": None,
"pagination": None,
}
9 changes: 9 additions & 0 deletions src/core/swagger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from typing import Callable

from drf_spectacular.utils import extend_schema


class SwaggerSchema:
@classmethod
def generate_schema(cls, *args, **kwargs) -> Callable:
return extend_schema(*args, **kwargs)
1 change: 1 addition & 0 deletions src/core/tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Create your tests here.
104 changes: 104 additions & 0 deletions src/faq/swagger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
from drf_spectacular.utils import OpenApiExample, OpenApiResponse

from core.responses.serializer import (
ErrorResponseSerializer,
ListSuccessResponseSerializer,
SuccessResponseSerializer,
)
from core.swagger import SwaggerSchema


class FAQAPIDocs(SwaggerSchema):
@classmethod
def retrieve(cls):
responses = {
"성공": OpenApiResponse(
response=SuccessResponseSerializer,
description="단일 응답 성공",
examples=[
OpenApiExample(
name="FAQ 조회",
value={
"status": "SUCCESS",
"data": {
"id": 1,
"question": "질문",
"answer": "답변",
"created_at": "2021-01-01",
},
},
),
],
),
"에러": OpenApiResponse(
response=ErrorResponseSerializer,
description="응답 에러",
examples=[
OpenApiExample(
name="데이터 없음",
value={
"status": "ERROR",
"error": {"code": "NOT_EXIST", "message": "데이터가 없습니다."},
},
),
],
),
}
return cls.generate_schema(
operation_id="faq_retrieve", description="FAQ 조회", responses=responses
)

@classmethod
def list(cls):
responses = {
"성공": OpenApiResponse(
response=ListSuccessResponseSerializer,
description="다중 응답 성공",
examples=[
OpenApiExample(
name="FAQ 목록 조회 (페이지네이션 있음)",
value={
"status": "SUCCESS",
"data": [
{
"id": 1,
"question": "질문1",
"answer": "답변1",
},
],
"pagination": {
"count": 20,
"next": "http://localhost:8000/api/v1/faqs/?page=2",
"previous": "http://localhost:8000/api/v1/faqs/?page=1",
},
},
),
OpenApiExample(
name="FAQ 목록 조회 (데이터 있음)",
value={
"status": "SUCCESS",
"data": [
{
"id": 1,
"question": "질문1",
"answer": "답변1",
},
],
"pagination": {"count": 1, "next": None, "previous": None},
},
),
OpenApiExample(
name="FAQ 목록 조회 (데이터 없음)",
value={
"status": "SUCCESS",
"data": [],
"pagination": {"count": 0, "next": None, "previous": None},
},
),
],
),
"에러": OpenApiResponse(response=ErrorResponseSerializer, description="응답 에러"),
}
return cls.generate_schema(
operation_id="faq_list", description="모든 FAQ 목록 조회", responses=responses
)
Loading

0 comments on commit c10b530

Please sign in to comment.