장고 - 상위, 하위 카테고리 생성 및 기능 구현

  • 이번 포스트에서는 상위, 하위 카테고리를 생성하여 카테고리 클릭 시, 해당 페이지로 이동하는 방법에 대해 학습한 내용을 다룰 것이다.
  • 이 기능에서 가장 중요한 것은, 상위 카테고리 클릭 시, 상위/하위 카테고리에 해당하는 모든 포스트들이 조회가 되어야 한다는 점이다.

Category 기능 구현

1. Category 모델 작성

ex) wps_onlineshop 프로젝트

  • 경로 : shop > models.py
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    from django.db import models
    from django.shortcuts import resolve_url
    from ckeditor_uploader.fields import RichTextUploadingField
    class Category(models.Model):
    parent_category = models.ForeignKey('self', on_delete=models.SET_NULL, blank=True, null=True, related_name='sub_categories')
    name = models.CharField(max_length=200)
    slug = models.SlugField(max_length=200, allow_unicode=True, unique=True)
    image = models.ImageField(upload_to='category_images/%Y/%m/%d', blank=True)
    description = RichTextUploadingField(blank=True)

    def __str__(self):
    return self.name

    class Meta:
    ordering = ['name']

    def get_absolute_url(self):
    return resolve_url('product_in_category', self.slug)


    class Product(models.Model):
    category = models.ForeignKey(Category, on_delete=models.SET_NULL, blank=True, null=True, related_name='products')
    name = models.CharField(max_length=200)
    slug = models.SlugField(max_length=200, allow_unicode=True, unique=True)
    image = models.ImageField(upload_to='product_images/%Y/%m/%d')
    description = RichTextUploadingField(blank=True)
    price = models.PositiveIntegerField()
    stock = models.PositiveIntegerField()
    available_display = models.BooleanField(default=True)
    available_order = models.BooleanField(default=True)
    created = models.DateTimeField(auto_now_add=True)
    updated = models.DateField(auto_now=True)

    def __str__(self):
    return "["+self.category.name+"] " + self.name

2. 카테고리 페이지 경로 작성

  • 경로 : shop > urls.py
    1
    2
    3
    4
    5
    6
    7
    8
    from django.urls import path

    from .views import *
    urlpatterns = [
    # url에 특정 카테고리 이름(=slug) 입력 시, 해당 카테고리 포스트로 이동되도록 경로 설정
    path('<slug>/', ProductList.as_view(), name='product_in_category'),
    path('', ProductList.as_view(), name='index'),
    ]

3. 카테고리 클릭시 해당 카테고리별 포스트가 조회되도록 코드 작성

  • 경로 : shop > views.py
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    class ProductList(ListView):
    model = Product
    template_name = 'shop/product_list.html'

    def get_queryset(self):
    # 상위 카테고리를 고르면 하위 카테고리 제품들이 한꺼번에 출력되도록 변경
    queryset = super().get_queryset()
    # url로 '/<slug>/'을 입력했다면 아래 코드 실행
    if 'slug' in self.kwargs:
    category = Category.objects.filter(slug=self.kwargs['slug'])
    if category.exists():
    category |= self.get_category_list(category[0])
    queryset = queryset.filter(category__in=category)
    else:
    queryset = queryset.none()
    return queryset

    # 하위 카테고리가 있으면, 재귀함수로 더이상 하위 카테고리가 없을 때까지 반복하여 categories에 저장
    def get_category_list(self,category):
    categories = category.sub_categories.all()
    for category in categories:
    categories |= self.get_category_list(category)
    return categories

4. 최상위 카테고리를 기준으로 그 하위 카테고리들을 context_processors로 전달하기 위한 코드 작성

  • 경로 : shop > context_processors.py

    1
    2
    3
    4
    5
    6
    from shop.models import Category

    def category(request):
    # id 1번을 'Home' 이라는 최상위 카테고리로 모델 저장하여 그 하위 카테고리들을 categories라는 변수로 반환
    categories = Category.objects.filter(parent_category=Category.objects.get(pk=1)).order_by('name')
    return {'categories':categories}
  • 경로 : config(프로젝트) > settings.py

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    TEMPLATES = [
    {
    'BACKEND': 'django.template.backends.django.DjangoTemplates',
    # 모든 페이지에 적용될 html이 있다면, 그 위치 설정
    'DIRS': [os.path.join(BASE_DIR, 'layout')],
    'APP_DIRS': True,
    'OPTIONS': {
    'context_processors': [
    ...
    # context_processors.py 작성 시, 이 곳에 해당 위치 입력
    'shop.context_processors.category',
    ],
    },
    },
    ]

5. 카테고리가 위치할 header 부분 코드 작성

  • 경로 : layout > base.html

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    <nav class="navbar navbar-expand-lg navbar-light bg-light sticky-top">
    <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarTogglerDemo01"
    aria-controls="navbarTogglerDemo01" aria-expanded="false" aria-label="Toggle navigation">
    <span class="navbar-toggler-icon"></span>
    </button>
    <div class="collapse navbar-collapse" id="navbarTogglerDemo01">

    <ul class="navbar-nav mr-auto mt-2 mt-lg-0">
    <!-- 카테고리 리스트 들어갈 위치 -->
    {% include 'category_list.html' %}
    </ul>
    <form class="form-inline my-2 my-lg-0">
    <input class="form-control mr-sm-2" type="search" placeholder="Search" aria-label="Search">
    <button class="btn btn-outline-success my-2 my-sm-0" type="submit">Search</button>
    </form>
    </div>
    </nav>
  • 경로 : layout > category_list.html

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    <!-- 최상위 카테고리의 모든 하위 카테고리들을 for문으로 실행하여 -->
    {% for category in categories %}
    <!-- 1차 하위 카테고리에 2차 하위 카테고리가 있을 경우 -->
    {% if category.sub_categories.all|length %}
    <!-- bootstrap 프레임워크를 이용하여 dropdown 타입으로 카테고리 표현 -->
    <li class="nav-item dropdown">
    <!-- 카테고리 클릭 시, Category모델에서 설정한 get_absolute_url 페이지로 이동 -->
    <a class="nav-link" href="{{category.get_absolute_url}}" id="menu_category_{{category.id}}" role="button"
    aria-haspopup="true" aria-expanded="false">
    {{category.name}}
    </a>
    <div class="dropdown-menu" aria-labelledby="menu_category_{{category.id}}">
    <!-- 1차 하위 카테고리의 2차 하위카테고리를 for문으로 순서대로 실행 -->
    {% for sub_category in category.sub_categories.all %}
    <!-- 2차 하위카테고리를 dropdown 타입으로 표시하고, 클릭 시 해당 카테고리에 속하는 포스트가 조회되도록 코드 실행 -->
    <a class="dropdown-item" href="{{sub_category.get_absolute_url}}">{{sub_category.name}}</a>
    {% endfor %}
    </div>
    </li>
    <!-- 1차 하위 카테고리에 2차 하위 카테고리가 없는 경우 -->
    {% else %}
    <li class="nav-item">
    <a class="nav-link" href="{{category.get_absolute_url}}">{{category.name}}</a>
    </li>
    {% endif %}
    {% endfor %}
    <!-- dropdown 타입 CSS 설정 -->
    <style>
    .dropdown .dropdown-menu {
    display:none;
    }
    .dropdown:hover .dropdown-menu {
    display: block;
    margin-top: 0;
    }
    </style>