(api_ninja)= # Django Ninja 설정 방법 Wagtail은 Django REST Framework 기반의 [내장 API 모듈](api)을 제공하지만, 다른 API 프레임워크를 사용하는 것도 가능합니다. 여기서는 Python 타입 힌트와 [Pydantic](https://docs.pydantic.dev/latest/)을 기반으로 구축된 API 프레임워크인 [Django Ninja](https://django-ninja.dev/) 사용법에 대한 정보를 제공하며, OpenAPI 스키마에 대한 내장 지원을 포함합니다. ## 기본 설정 `django-ninja` 를 설치합니다. 선택적으로 `INSTALLED_APPS` 에 `ninja` 를 추가하여 OpenAPI 문서 뷰어를 사용할 때 외부에서 정적 파일을 로드하지 않도록 할 수 있습니다. ### API 생성 프로젝트 루트의 기존 `urls.py` 파일 옆에 새로운 `api.py` 모듈을 만들고 라우터를 인스턴스화합니다. ```python # api.py from typing import Literal from django.http import HttpRequest from django.shortcuts import get_object_or_404 from ninja import Field, ModelSchema, NinjaAPI from wagtail.models import Page api = NinjaAPI() ``` 다음으로, Django가 요청을 API로 라우팅할 수 있도록 URL을 등록합니다. 이것이 작동하는지 테스트하려면 `/api/docs` 로 이동하여 OpenAPI 문서를 표시합니다 (아직 사용 가능한 엔드포인트는 없음). ```python # urls.py from .api import api urlpatterns = [ ... path("api/", api.urls), ... # api 라인이 기본 Wagtail 페이지 제공 경로 위에 오도록 합니다. path("", include(wagtail_urls)), ] ``` ### 첫 번째 엔드포인트 사이트의 모든 페이지 목록을 반환하는 간단한 엔드포인트를 만듭니다. `@api.get` 작업 데코레이터를 사용하여 엔드포인트가 사용 가능한 경로와 응답 형식을 정의합니다. 여기서는 우리가 만드는 사용자 정의 스키마를 사용합니다. ```python # api.py class BasePageSchema(ModelSchema): url: str = Field(None, alias="get_url") class Config: model = Page model_fields = [ "id", "title", "slug", ] @api.get("/pages/", response=list[BasePageSchema]) def list_pages(request: "HttpRequest"): return Page.objects.live().public().exclude(id=1) ``` 우리의 사용자 정의 `BasePageSchema` 는 두 가지 기술을 결합합니다: [Django 모델에서 스키마 생성](https://django-ninja.dev/guides/response/django-pydantic/) 및 [별칭](https://django-ninja.dev/guides/response/#aliases)을 사용한 계산된 필드. 여기서는 별칭을 사용하여 페이지 URL을 검색합니다. 또한 엔드포인트에 `child_of: int = None` 매개변수를 추가하여 부모별로 페이지를 필터링할 수 있습니다. ```python @api.get("/pages/", response=list[BasePageSchema]) def list_pages(request: "HttpRequest", child_of: int = None): if child_of: return get_object_or_404(Page, id=child_of).get_children().live().public() # 페이지 트리 루트를 제외합니다. return Page.objects.live().public().exclude(id=1) ``` Ninja는 `list_pages` 함수의 모든 매개변수를 쿼리 매개변수로 처리합니다. 제공된 타입 힌트를 사용하여 값을 구문 분석하고 유효성을 검사하며 OpenAPI 스키마를 생성합니다. ### 사용자 정의 페이지 필드 추가 다음으로, 특정 유형의 단일 페이지를 반환하는 "상세" API 엔드포인트를 추가해 보겠습니다. Ninja의 [경로 매개변수](https://django-ninja.dev/guides/input/path-params/)를 사용하여 `page_id` 를 검색할 수 있습니다. 또한 특정 페이지 유형에 대한 새 스키마를 만듭니다. 여기서는 `BasePageSchema` 를 기반으로 하는 `BlogPage` 입니다. ```python from blog.models import BlogPage class BlogPageSchema(BasePageSchema, ModelSchema): class Config(BasePageSchema.Config): model = BlogPage model_fields = [ "intro", ] @api.get("/pages/{page_id}/", response=BlogPageSchema) def get_page(request: "HttpRequest", page_id: int): return get_object_or_404(Page, id=page_id).specific ``` 이것은 잘 작동하며, 엔드포인트는 이제 일반 `Page` 필드와 `BlogPage` 소개를 반환합니다. 그러나 모든 페이지 콘텐츠가 API를 통해 제공되는 사이트의 경우 모든 페이지 유형에 대해 새 엔드포인트를 만드는 것이 지루해질 수 있습니다. ### 여러 스키마 결합 응답이 여러 페이지 유형을 반환할 수 있음을 반영하기 위해 타입 힌트 유니온 구문을 사용하여 여러 스키마를 결합합니다. 이를 통해 동일한 엔드포인트에서 다른 페이지 유형을 반환할 수 있습니다. 다음은 `HomePage` 유형에 대한 추가 스키마가 있는 예입니다. ```python from home.models import HomePage class HomePageSchema(BasePageSchema, ModelSchema): class Config(BasePageSchema.Config): model = HomePage @api.get("/pages/{page_id}/", response=BlogPageSchema | HomePageSchema) def get_page(request: "HttpRequest", page_id: int): return get_object_or_404(Page, id=page_id).specific ``` 이것으로, 우리는 여전히 주어진 페이지에 사용할 스키마를 결정할 방법이 없습니다. 페이지 유형별로 이 작업을 수행하고 싶으므로 스키마에 추가 `content_type` 클래스 속성 주석을 추가합니다. - `BasePageSchema` 의 경우, 모든 페이지 유형이 이 기반을 사용할 수 있으므로 `content_type: str` 을 정의합니다. - `HomePageSchema` 의 경우, `content_type: Literal["homepage"]` 를 설정합니다. - 그리고 `BlogPageSchema` 의 경우, `content_type: Literal["blogpage"]` 를 설정합니다. 이제 남은 것은 `BasePageSchema` 에 [리졸버](https://django-ninja.dev/guides/response/#resolvers) 계산 필드를 추가하여 각 페이지에 대한 올바른 콘텐츠 유형을 반환하는 것입니다. 다음은 `BasePageSchema` 의 최종 버전입니다. ```python class BasePageSchema(ModelSchema): url: str = Field(None, alias="get_url") content_type: str @staticmethod def resolve_content_type(page: Page) -> str: return page.specific_class._meta.model_name class Config: model = Page model_fields = [ "id", "title", "slug", ] ``` 이것으로 Pydantic은 `get_page` 에서 반환된 페이지 데이터를 `response` 유니온의 스키마 중 하나에 따라 유효성을 검사할 수 있습니다. 그런 다음 특정 스키마에 따라 데이터를 직렬화합니다. ### 중첩된 데이터 페이지 스키마가 별도의 모델에 있는 데이터를 참조하는 경우 새 엔드포인트를 만드는 대신 페이지 스키마에 직접 데이터를 추가할 수 있습니다. 다음은 블로그 페이지 작성자(`ParentalManyToManyField` 가 있는 스니펫)를 추가하는 예입니다. ```python class BlogPageSchema(BasePageSchema, ModelSchema): content_type: Literal["blogpage"] authors: list[str] = [] class Config(BasePageSchema.Config): model = BlogPage model_fields = [ "intro", ] @staticmethod def resolve_authors(page: BlogPage, context) -> list[str]: return [author.name for author in page.authors.all()] ``` `BlogPage` 클래스에 작성자 이름을 직접 검색하는 메서드가 있는 경우 `Field` 클래스를 사용하여 이 작업을 수행할 수도 있습니다: `authors: list[str] = Field([], alias="get_author_names")`. ### API의 리치 텍스트 Wagtail의 리치 텍스트 필드는 [](../../extending/rich_text_internals)에 설명된 특정 내부 형식을 사용합니다. 스키마에 `str` 로 추가할 수 있지만, API가 페이지 및 이미지에 대한 참조가 URL로 대체되는 "표시" 표현을 제공하는 것이 더 유용한 경우가 많습니다. 이것은 [Ninja 리졸버](https://django-ninja.dev/guides/response/#resolvers)로도 수행할 수 있습니다. 다음은 `HomePageSchema` 의 예입니다. ```python from wagtail.rich_text import expand_db_html class HomePageSchema(BasePageSchema, ModelSchema): content_type: Literal["homepage"] body: str class Config(BasePageSchema.Config): model = HomePage @staticmethod def resolve_body(page: HomePage, context) -> str: return expand_db_html(page.body) ``` 여기서 `body` 는 `str` 로 정의되고 리졸버는 `expand_db_html` 함수를 사용하여 내부 표현을 HTML로 변환합니다. ### API의 이미지 이미지에 대해서도 비슷한 기술을 사용할 수 있으며, 리졸버와 별칭을 결합하여 데이터를 생성합니다. [`get_renditions()` 메서드](image_renditions_multiple)를 사용하여 형식이 지정된 이미지를 검색하고 사용자 정의 `RenditionSchema` 를 사용하여 API 표현을 정의합니다. ```python from wagtail.images.models import AbstractRendition class RenditionSchema(ModelSchema): # 속성에 대해 Field / alias API를 사용해야 합니다. url: str = Field(None, alias="file.url") alt: str = Field(None, alias="alt") class Config: model = AbstractRendition model_fields = [ "width", "height", ] ``` `BlogPageSchema` 에서 이미지 필드를 `main_image: list[RenditionSchema] = []` 로 정의합니다. 그런 다음 이에 대한 리졸버를 추가합니다. ```python @staticmethod def resolve_main_image(page: BlogPage) -> list[AbstractRendition]: filters = [ "fill-800x600|format-webp", "fill-800x600", ] if image := page.main_image(): return image.get_renditions(*filters).values() return [] ``` JSON에서 `main_image` 는 이제 배열로 표시되며, 각 항목은 `url`, `alt`, `width` 및 `height` 속성을 가진 객체입니다. ## OpenAPI 문서 Django Ninja는 정의된 작업 및 스키마를 기반으로 [OpenAPI](https://swagger.io/specification/) 문서를 자동으로 생성합니다. 여기에는 브라우저에서 직접 API를 시험해 볼 수 있는 지원 기능이 있는 문서 뷰어도 포함됩니다. 위의 예제를 사용하여 `/api/docs` 에서 문서에 액세스해 볼 수 있습니다. 이 기능을 최대한 활용하려면 지원되는 [작업 매개변수](https://django-ninja.dev/reference/operations-parameters/)를 고려하십시오.