(tagging)= # 태그 기능 Wagtail은 두 가지 Django 모듈을 조합하여 태그 기능을 제공합니다. 1. [django-taggit](https://django-taggit.readthedocs.io/) - 일반적인 용도의 태그 구현을 제공합니다. 2. [django-modelcluster](https://github.com/wagtail/django-modelcluster) - django-taggit의 `TaggableManager` 를 확장하여 미리보기 및 리비전 처리에 필요한 데이터베이스 쓰기 없이 메모리에서 태그 관계를 관리할 수 있게 합니다. ## 페이지 모델에 태그 추가하기 페이지 모델에 태그 기능을 추가하려면, django-taggit의 `Tag` 모델과 페이지 모델 간의 다대다 관계를 설정하기 위해 `TaggedItemBase` 를 상속하는 '중간(through)' 모델을 정의해야 하며, 이 관계를 단일 태그 필드로 표현하기 위해 페이지 모델에 `ClusterTaggableManager` 접근자를 추가해야 합니다. 다음 예제에서는 `BlogPageTag` 모델을 통해 `BlogPage` 에 태그 기능을 설정합니다: ```python # models.py from modelcluster.fields import ParentalKey from modelcluster.contrib.taggit import ClusterTaggableManager from taggit.models import TaggedItemBase class BlogPageTag(TaggedItemBase): content_object = ParentalKey('demo.BlogPage', on_delete=models.CASCADE, related_name='tagged_items') class BlogPage(Page): ... tags = ClusterTaggableManager(through=BlogPageTag, blank=True) promote_panels = Page.promote_panels + [ ... FieldPanel('tags'), ] ``` Wagtail의 관리자 인터페이스는 타입어헤드 태그 자동완성과 친숙한 태그 아이콘을 포함하여 콘텐츠에 태그를 입력하기 위한 멋진 인터페이스를 제공합니다. 이제 뷰와 템플릿에서 다대다 태그 관계를 활용할 수 있습니다. 예를 들어, 블로그의 인덱스 페이지가 태그별로 `BlogPage` 목록을 필터링하기 위해 `?tag=...` 쿼리 매개변수를 받도록 설정할 수 있습니다: ```python from django.shortcuts import render class BlogIndexPage(Page): ... def get_context(self, request): context = super().get_context(request) # 블로그 항목 가져오기 blog_entries = BlogPage.objects.child_of(self).live() # 태그로 필터링 tag = request.GET.get('tag') if tag: blog_entries = blog_entries.filter(tags__name=tag) context['blog_entries'] = blog_entries return context ``` 여기서 `blog_entries.filter(tags__name=tag)` 는 `BlogPage` 의 `tags` 관계를 따라 일치하는 태그 이름을 가진 페이지만 필터링한 후 렌더링을 위해 템플릿에 전달합니다. 이제 `blog_page.html` 템플릿을 업데이트하여 페이지와 관련된 태그 목록을 표시하고 필터링된 인덱스 페이지로 다시 연결되는 링크를 추가할 수 있습니다: ```html+django {% for tag in page.tags.all %} {{ tag }} {% endfor %} ``` `page.tags.all` 을 반복하면 `page` 와 관련된 각 태그가 표시되고, 인덱스로 돌아가는 링크는 `BlogIndexPage` 모델에 추가된 필터 옵션을 활용합니다. Django 쿼리는 `tagged_items` 관련 이름 필드를 사용하여 태그와 관련된 `BlogPage` 객체를 가져올 수도 있습니다. 동일한 접근 방식을 [](snippets)를 통해 관리되는 페이지가 아닌 모델에 태그를 추가하는 데 사용할 수 있습니다. 이 경우, `ClusterTaggableManager` 와 호환되도록 모델이 `modelcluster.models.ClusterableModel` 을 상속해야 합니다. ## 커스텀 태그 모델 위의 예제에서는 새로 생성된 태그가 django-taggit의 기본 `Tag` 모델에 추가되며, 이는 동일한 레시피를 사용하는 다른 모든 모델뿐만 아니라 Wagtail의 이미지 및 문서 모델과도 공유됩니다. 특히 이는 태그 필드의 자동완성 제안이 이전에 다른 모델에 추가된 태그를 포함한다는 것을 의미합니다. 이를 방지하기 위해 `TagBase` 를 상속하는 커스텀 태그 모델과 `ItemBase` 를 상속하는 '중간(through)' 모델을 설정하여 해당 페이지 모델에 대한 독립적인 태그 풀을 제공할 수 있습니다. ```python from django.db import models from modelcluster.contrib.taggit import ClusterTaggableManager from modelcluster.fields import ParentalKey from taggit.models import TagBase, ItemBase class BlogTag(TagBase): class Meta: verbose_name = "블로그 태그" verbose_name_plural = "블로그 태그" class TaggedBlog(ItemBase): tag = models.ForeignKey( BlogTag, related_name="tagged_blogs", on_delete=models.CASCADE ) content_object = ParentalKey( to='demo.BlogPage', on_delete=models.CASCADE, related_name='tagged_items' ) class BlogPage(Page): ... tags = ClusterTaggableManager(through='demo.TaggedBlog', blank=True) ``` 관리자 내에서 태그 필드는 사용 중인 커스텀 태그 모델을 자동으로 인식하고 해당 태그 모델에서 가져온 자동완성 제안을 제공합니다. ## 자유 태그 비활성화하기 기본적으로 태그 필드는 "자유 태그" 방식으로 작동합니다: 편집자는 필드에 원하는 내용을 입력할 수 있으며, 저장할 때 기존 태그로 인식되지 않는 태그 텍스트는 자동으로 생성됩니다. 이 동작을 비활성화하고 편집자가 데이터베이스에 이미 존재하는 태그만 입력할 수 있도록 하려면, 커스텀 태그 모델에 `free_tagging = False` 옵션을 사용할 수 있습니다: ```python from taggit.models import TagBase from wagtail.snippets.models import register_snippet @register_snippet class BlogTag(TagBase): free_tagging = False class Meta: verbose_name = "블로그 태그" verbose_name_plural = "블로그 태그" ``` 여기서는 관리자(및 적절한 권한을 가진 다른 사용자)에게 허용된 태그 세트를 관리하는 인터페이스를 제공하기 위해 `BlogTag` 를 스니펫으로 등록했습니다. `free_tagging = False` 옵션이 설정되면 편집자는 더 이상 태그 필드에 임의의 텍스트를 입력할 수 없으며, 대신 자동완성 드롭다운에서 기존 태그를 선택해야 합니다. ## 스니펫으로 태그 관리하기 프로젝트에서 사용되는 모든 태그를 관리하기 위해 `Tag` 모델을 Wagtail 관리자를 통해 관리할 수 있는 스니펫으로 등록할 수 있습니다. 이를 통해 메인 메뉴 내에 태그를 추가, 편집 또는 삭제할 수 있는 태그 관리 인터페이스를 가질 수 있습니다. 콘텐츠에서 제거된 태그는 `Tag` 모델에서 삭제되지 않으며 여전히 타입어헤드 태그 자동완성에 표시됩니다. 따라서 태그 인터페이스를 갖는 것은 필요하지 않은 태그를 완전히 제거하는 좋은 방법입니다. 태그 인터페이스를 추가하려면 프로젝트의 앱 중 하나에 있는 `wagtail_hooks.py` 파일에 다음 코드 블록을 추가하세요: ```python from wagtail.admin.panels import FieldPanel from wagtail.snippets.models import register_snippet from wagtail.snippets.views.snippets import SnippetViewSet from taggit.models import Tag class TagsSnippetViewSet(SnippetViewSet): panels = [FieldPanel("name")] # name 필드만 표시 model = Tag icon = "tag" # 필요에 따라 변경 add_to_admin_menu = True menu_label = "태그" menu_order = 200 # 3번째 위치에 배치 (000이 1번째, 100이 2번째) list_display = ["name", "slug"] search_fields = ("name",) register_snippet(TagsSnippetViewSet) ``` `Tag` 모델에는 `name` 과 `slug` 필수 필드가 있습니다. 태그를 추가하기로 결정했다면 `name` 필드 패널만 표시하는 것이 좋습니다. `slug` 필드는 `name` 필드가 채워질 때 자동으로 채워지므로 두 필드 모두에 동일한 이름을 입력할 필요가 없기 때문입니다.