장고 - 상위, 하위 카테고리 생성 및 기능 구현
- 이번 포스트에서는 상위, 하위 카테고리를 생성하여 카테고리 클릭 시, 해당 페이지로 이동하는 방법에 대해 학습한 내용을 다룰 것이다.
- 이 기능에서 가장 중요한 것은, 상위 카테고리 클릭 시, 상위/하위 카테고리에 해당하는 모든 포스트들이 조회가 되어야 한다는 점이다.
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
35from 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
8from 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
23class 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
6from 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
15TEMPLATES = [
{
'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>
Posted