이제 대망의 게임목록 페이지를 만드는 시간입니다.
게임목록페이지에서는 게임리스트를 보여줌과 동시에 게임검색기능, 장르필터기능, 가격필티기능을 구현하고 페이징기능까지 구현할것입니다.
게임검색과 필터 기능의 핵심은 검색을 했을시 game_search페이지로 넘어간다는 것입니다.
즉, 원래의 게임목록 페이지와 검색후 페이지는 다른 페이지입니다.
■ views.py(리스트 부분)
모든 게임 데이터 가지고 오기(all_games) -> 페이징기능구현 -> 필터에서 쓸 장르리스트 가져오기 -> 최저가기능 순으로 구현되었습니다.
이 페이징, 필터 및 검색, 최저가 기능을 context라는 dictionary에 담아서 game_list.html에서 호출됩니다.
▶ 게임 리스트에서의 최저가 노출
-
view안에서 model에 정의된 5가지 퍼블리셔들을 하나의 리스트로 만들고 for문을 통해 min값을 탐색.
-
게임 이름과 최저가를 zip함수로 묶고 이후 html에서 설계한 형태로 반환.
def game_list(request):
paginate_by = 15
context = {}
##########페이징 기능 ########################
context['is_paginated'] = True
# print(context)
all_games = Game.objects.all()
paginator = Paginator(all_games, paginate_by)
page_number_range = 10 #페이지그룹에 속한 페이지 수
# CBV에서 self.request: HttpRequest
# request.POST : POST 방식으로 넘어온 요청파라미터 조회
# request.GET : GET 방식으로 넘어온 요청파라미터 조회
current_page = int(request.GET.get('page', 1))
context['current_page'] = current_page
#시작/끝 index 조회
start_index = int((current_page-1)/page_number_range)*page_number_range
end_index = start_index + page_number_range
# 현재 페이지가 속한 페이지 그룹의 범위
current_page_group_range = paginator.page_range[start_index : end_index]
print("current_page_group_range", current_page_group_range)
start_page = paginator.page(current_page_group_range[0])
end_page = paginator.page(current_page_group_range[-1])
has_previous_page = start_page.has_previous()
has_next_page = end_page.has_next()
context['current_page_group_range'] = current_page_group_range
if has_previous_page: #이전페이지 그룹이 있다면
context['has_previous_page'] = has_previous_page
context['previous_page'] = start_page.previous_page_number
if has_next_page: #다음 페이지 그룹이 있다면
context['has_next_page'] = has_next_page
context['next_page'] = end_page.next_page_number
e = paginate_by * current_page
s = e - paginate_by
print("내용 index", s, e)
game_list = Game.objects.all()[s:e]
########## 장르 리스트(체크박스 용도) ###############
genre_list = Genre_list.objects.all()
context['genre_list'] = genre_list
########## 최저가 ##########
min_price_list = []
for game in game_list:
min_price = [game.steam, game.origin, game.uplay, game.epic_games, game.drmfree]
min_price = min([p for p in min_price if p is not 0])
# print(min_price)
min_price_list.append(min_price)
# context['min_price'] = min_price_list
context['game_lowest_price'] = zip(game_list, min_price_list)
return render(request, 'game/game_list.html', context)
■ views.py(검색(search) 부분)
game_search부분은 game_list에서 쓴 기능을 다 가지고 오며, 검색 및 필터기능이 추가됩니다.
모든 게임 정보 가지고오기(game_list) -> 검색 및 필터 -> 페이징기능구현 -> 필터에서 쓸 장르리스트 가져오기 -> 최저가기능 순으로 구현되었습니다.
▶ 필터를 진행하는 방식
1. game_list.html 에서 값을 입력하면 get(값이 하나인 때: 검색, 가격일 경우)이나 getlist(값이 여러 개: 필터)를 이용해 값을 가지고 온다.
2. 모든 게임리스트를 가지고 오고 필터를 진행한다.
▶ 게임 검색
값이 title나 developer에 포함되어 있다면(icontains) 해당값을 가진 게임만 return한다
▶ 게임 장르 필터
1. for문을 이용해 리스트에 있는 값을 하나하나 SEARCH한 후 해당하는 값을 가진 게임만 return한다.
(즉 필터링은 검색을 여러 번하는 것과 같음)
2. ['adventure','rpg']와 같이 요청하는 장르 값이 여러 개인 때는 getlist를 이용해서 데이터를 가지고 온다.
▶ 게임 가격 필터
1. get으로 필터를 통해 호출한 해당값을 불러 온다.
ex) any price는 원래값이 0, 20000만원 이하는 원래값이 1이다.
2. 가지고 온 값을 if문을 이용하여 조건을 부여하고 해당값을 가진 게임을 return한다.
ex) if price == ‘1’: game_list = game_list.filter(steam__lte=20000) -> 20000원 이하
def game_search(request):
paginate_by = 15
context = {}
game_list = Game.objects.all()
b = request.GET.get('b','') #검색에서 입력한 값이 넘어옴
f = request.GET.getlist('f') #장르 체크박스 입력한 데이터 값들이 넘어옴(getlist -> 여러개 받을 수 있음)
price = request.GET.get('price','')
print(price)
if b: #b(검색창)에서 값이 넘어왔다면
print(b)
game_list = game_list.filter(Q(title__icontains=b)| Q(developer__icontains = b))
if f: #f(체크박스 필터)에서 값이 넘어왔다면
print(f)
query = Q()
for i in f:
query = query | Q(genre__icontains=i)
game_list = game_list.filter(query)
if price == '0':
game_list = game_list.filter(steam__gt=0)
elif price == '1': #price(가격필터)에서 값이 넘어왔다면
game_list = game_list.filter(steam__lte=20000)
elif price == '2':
game_list = game_list.filter(steam__lte=30000)
elif price == '3':
game_list = game_list.filter(steam__lte=40000)
elif price == '4':
game_list = game_list.filter(steam__lte=50000)
else:
game_list = game_list.filter(steam__gt=50000)
context['is_paginated'] = True
paginator = Paginator(game_list, paginate_by)
page_number_range = 10 #페이지그룹에 속한 페이지 수
# CBV에서 self.request: HttpRequest
# request.POST : POST 방식으로 넘어온 요청파라미터 조회
# request.GET : GET 방식으로 넘어온 요청파라미터 조회
current_page = int(request.GET.get('page', 1))
context['current_page'] = current_page
#시작/끝 index 조회
start_index = int((current_page-1)/page_number_range)*page_number_range
end_index = start_index + page_number_range
# 현재 페이지가 속한 페이지 그룹의 범위
current_page_group_range = paginator.page_range[start_index : end_index]
print("current_page_group_range", current_page_group_range)
start_page = paginator.page(current_page_group_range[0])
end_page = paginator.page(current_page_group_range[-1])
has_previous_page = start_page.has_previous()
has_next_page = end_page.has_next()
context['current_page_group_range'] = current_page_group_range
if has_previous_page: #이전페이지 그룹이 있다면
context['has_previous_page'] = has_previous_page
context['previous_page'] = start_page.previous_page_number
if has_next_page: #다음 페이지 그룹이 있다면
context['has_next_page'] = has_next_page
context['next_page'] = end_page.next_page_number
########## 장르 리스트 ###############
genre_list = Genre_list.objects.all()
context['genre_list'] = genre_list
# pricelist = game.objects.all()
# price = min(d,d,d,.)
# context['price']
e = paginate_by * current_page
s = e - paginate_by
print("내용 index", s, e)
game_list = game_list[s:e]
# 최저가
min_price_list = []
for game in game_list:
min_price = [game.steam, game.origin, game.uplay, game.epic_games, game.drmfree]
min_price = min([p for p in min_price if p is not 0])
# print(min_price)
min_price_list.append(min_price)
# context['min_price'] = min_price_list
context['game_lowest_price'] = zip(game_list, min_price_list)
return render(request, 'game/game_search.html', context)
■ urls.py
from django.urls import path
from . import views
app_name = 'game'
urlpatterns = [
path('list/', views.game_list, name='list'),
path('<int:pk>/detail/', views.GameDetailView.as_view(), name='detail'),
path('create/', views.GameCreateView.as_view(), name='create'),
path('<int:pk>/update/',views.GameUpdateView.as_view(), name = 'update'),
path('<int:pk>/delete/', views.game_delete, name='delete'),
path('search/',views.game_search, name = 'search'),
path('<int:pk>/comment/write/', views.comment_write, name="comment_write"),
path('<int:pk>/comment_delete/', views.comment_delete, name="comment_delete"),
path('<int:pk>/like/', views.like, name='like'),
]
■ game_list.html
검색창 -> 게임장르체크박스 -> 게임가격바 -> 게임목록(최저가 기능 포함) -> 페이지 기능 순으로 구현하였다.
게임가격필터 부분에서 바를 움직이면 그에 해당되는 가격을 표시하도록 자바스크립트 부분이 포함되어있다.
{% extends 'base.html' %}
{% block title %} GAME LIST {% endblock title %}
{% block content %}
<link rel="canonical" href="https://getbootstrap.com/docs/4.5/examples/dashboard/">
<link href="dashboard.css" rel="stylesheet">
<div class="container-fluid">
<div class="row">
<nav id="sidebarMenu" class="col-md-3 col-lg-2 d-md-block sidebar collapse" style="background-color: #272727">
<div class="sidebar-sticky pt-3">
<ul class="nav flex-column">
<table class = 'table' style="background-color: #000000; color: #ffffff">
<br>
<thead>
<h5>게임 찾기</h5>
</thead>
<br>
<tbody>
<form class="form-inline my-2 my-md-0" method="get" action="{% url 'game:search' %}">
<input class="form-control" type="text" placeholder="게임 이름, 개발사를 입력하세요" aria-label="Search" name="b" value="{{b}}">
<br>
{% for g in genre_list %}
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="inlineCheckbox3" name="f", value={{g}}>
<class class="form-check-label" for="inlineCheckbox5" style="color: #CDCDCD">{{g}}</class>
</div>
{% endfor %}
<br>
<div class="form-group">
<a href = "#" onclick= "event.stopPropagation()"></a>
<label for="customRange3" style="color: #CDCDCD">가격 범위</label>
<select source="[name=price]">
<option value="0">Any Price</option>
<option value="1">20000원 이하</option>
<option value="2">30000원 이하</option>
<option value="3">40000원 이하</option>
<option value="4">50000원 이하</option>
<option value="5">50000원 이상</option>
</select>
<input type="range" class="custom-range" value = "0" min="0" max="5" step="1" id="customRange3" name="price", value={{price}}>
<!--게임가격필터 바의 위치에 따라 가격이 실시간으로 표시되게 하는 자바스크립트-->
<script>
;(function(){
function emit(target, name) {
var event
if (document.createEvent) {
event = document.createEvent("HTMLEvents");
event.initEvent(name, true, true);
} else {
event = document.createEventObject();
event.eventType = name;
}
event.eventName = name;
if (document.createEvent) {
target.dispatchEvent(event);
} else {
target.fireEvent("on" + event.eventType, event);
}
}
var outputsSelector = "input[type=number][source],select[source]";
function onChange(e) {
var outputs = document.querySelectorAll(outputsSelector)
for (var index = 0; index < outputs.length; index++) {
var item = outputs[index]
var source = document.querySelector(item.getAttribute('source'));
if (source) {
if (item === e.target) {
source.value = item.value
emit(source, 'input')
emit(source, 'change')
}
if (source === e.target) {
item.value = source.value
}
}
}
}
document.addEventListener('change', onChange)
document.addEventListener('input', onChange)
}());
</script>
</div>
<button type='submit' class='btn btn-warning' style="font-weight: bold">검색</button> <!--위에것과 기능은 같음-->
<!-- <button type='reset' class='btn btn-info'>RESET</button> -->
</form>
</tbody>
</table>
<br>
<br>
<table class = 'table' style="background-color: #000000; color: #ffffff">
<thead>
<h5>특별한 선택</h5>
</thead>
<tbody>
<a class="nav-link" href="#" style="color: #CDCDCD">
선택 A
</a>
<a class="nav-link" href="#" style="color: #CDCDCD">
선택 B
</a>
<a class="nav-link" href="#" style="color: #CDCDCD">
선택 C
</a>
</tbody>
</table>
</ul>
</div>
</nav>
<role="main" class="col-md-9 ml-sm-auto col-lg-10 px-md-4">
<br>
<h2>게임 목록</h2>
<br>
<table class = 'table' style="color: #ffffff">
<thead style="background-color: #000000">
<tr align = "center">
<th scope="col"> 게임 이미지 </th>
<th scope="col"> 게임 이름 </th>
<th scope="col"> 최저가 </th>
<th scope="col"> 개발사 </th>
</tr>
</thead>
<tbody style="background-color: #353535">
{% for game, min_price in game_lowest_price %}
<tr align = "center">
<td style="padding:5px">
{% if game.thumbnail %}
<img src="{{game.thumbnail.url}}" class="thumbnails">
{% endif %}
</td>
<td style="padding:5px">
<a href="{% url 'game:detail' game.pk %}" style="color: #FFC107"><br>{{game.title}}</a> <!--DETAIL과 연결하는 부분 -->
</td>
<td style="padding:5px; color: #CDCDCD"><br>
₩ {{min_price}}
</td>
<td style="padding:5px; color: #CDCDCD"><br>
{{game.developer}}
</td>
</tr>
{% endfor %}
</tbody>
</table>
<br>
<br>
<div class="container-fluid text-center">
<!--페이징 처리-->
<!-- Paginator: paginator 변수,
Page: page_obj 변수
페이징 유무 : is_paginated -->
{% if is_paginated %}
<p>
<!--이전 페이지 그룹으로 이동-->
{% if has_previous_page %}
<a href='/game/list?page={{previous_page}}' style="color: #CDCDCD">◀ 이전 페이지</a>
{% endif %}
<!--페이지 번호 링크-->
{% for page in current_page_group_range %}
<!-- page_obj.number: 현재 보고있는 페이지 -->
{% if page == page_obj.number %}
<a style="color: #FFC107">{{page}}</a>
{% else%}
<a href='/game/list?page={{page}}' style="color: #CDCDCD">
{{page}}
</a>
{% endif %}
{% endfor %}
<!--다음 페이지 그룹-->
{% if has_next_page %}
<a href='/game/list/?page={{next_page}}' style="color: #CDCDCD">다음 페이지 ▶</a>
{% endif %}
</p>
{% endif %}
<br>
<br>
<br>
</div>
</role>
</div>
</div>
{% endblock content %}
■ game_search.html
game_search.html은 game_list.html과 구성이 거의 같다
다만 페이징 처리에서 주의할 점이 있다.
<a href='/game/list/?page={{next_page}}' style="color: #CDCDCD">다음 페이지
search에서 위와같이 하게되면 페이지를 넘어갈때 필터가 풀리게 되고 그냥 game_list의 해당페이지로 이동하게 된다.
따라서 다음과 같이 href를 수정해야한다.
<a href='/game/search/?b={{request.GET.b}}&f={{request.GET.f}}&price={{request.GET.price}}&page={{next_page}}' style="color: #CDCDCD">다음 페이지 ▶</a>
이렇게 하면 필터 값이 유지가 되기 때문에 페이지가 바뀌어도 필터된 것이 풀리지 않는다.
{% extends 'base.html' %}
{% block title %} SEARCH RESULT {% endblock title %}
{% block content %}
<link rel="canonical" href="https://getbootstrap.com/docs/4.5/examples/dashboard/">
<link href="dashboard.css" rel="stylesheet">
<div class="container-fluid">
<div class="row">
<nav id="sidebarMenu" class="col-md-3 col-lg-2 d-md-block sidebar collapse" style="background-color: #272727">
<div class="sidebar-sticky pt-3">
<ul class="nav flex-column">
<table class = 'table' style="background-color: #000000; color: #ffffff">
<br>
<thead>
<h5>게임 찾기</h5>
</thead>
<br>
<tbody>
<form class="form-inline my-2 my-md-0" method="get" action="{% url 'game:search' %}">
<input class="form-control" type="text" placeholder="게임 이름, 개발사를 입력하세요" aria-label="Search" name="b" value="{{b}}">
<br>
{% for g in genre_list %}
<div class="form-check form-check-inline">
<input class="form-check-input" type="checkbox" id="inlineCheckbox3" name="f", value={{g}}>
<class class="form-check-label" for="inlineCheckbox5" style="color: #CDCDCD">{{g}}</class>
</div>
{% endfor %}
<br>
<div class="form-group">
<a href = "#" onclick= "event.stopPropagation()"></a>
<label for="customRange3" style="color: #CDCDCD">가격 범위</label>
<select source="[name=price]">
<option value="0">Any Price</option>
<option value="1">20000원 이하</option>
<option value="2">30000원 이하</option>
<option value="3">40000원 이하</option>
<option value="4">50000원 이하</option>
<option value="5">50000원 이상</option>
</select>
<input type="range" class="custom-range" value = "0" min="0" max="5" step="1" id="customRange3" name="price", value={{price}}>
<script>
;(function(){
function emit(target, name) {
var event
if (document.createEvent) {
event = document.createEvent("HTMLEvents");
event.initEvent(name, true, true);
} else {
event = document.createEventObject();
event.eventType = name;
}
event.eventName = name;
if (document.createEvent) {
target.dispatchEvent(event);
} else {
target.fireEvent("on" + event.eventType, event);
}
}
var outputsSelector = "input[type=number][source],select[source]";
function onChange(e) {
var outputs = document.querySelectorAll(outputsSelector)
for (var index = 0; index < outputs.length; index++) {
var item = outputs[index]
var source = document.querySelector(item.getAttribute('source'));
if (source) {
if (item === e.target) {
source.value = item.value
emit(source, 'input')
emit(source, 'change')
}
if (source === e.target) {
item.value = source.value
}
}
}
}
document.addEventListener('change', onChange)
document.addEventListener('input', onChange)
}());
</script>
</div>
<button type='submit' class='btn btn-warning'>검색</button> <!--위에것과 기능은 같음-->
<!-- <button type='reset' class='btn btn-info'>RESET</button> -->
</form>
</tbody>
</table>
<br>
<br>
<table class = 'table' style="background-color: #000000; color: #ffffff">
<thead>
<h5>특별한 선택</h5>
</thead>
<tbody>
<a class="nav-link" href="#" style="color: #CDCDCD">
선택 A
</a>
<a class="nav-link" href="#" style="color: #CDCDCD">
선택 B
</a>
<a class="nav-link" href="#" style="color: #CDCDCD">
선택 C
</a>
</tbody>
</table>
</ul>
</div>
</nav>
<role="main" class="col-md-9 ml-sm-auto col-lg-10 px-md-4">
<br>
<h2>검색 결과</h2>
<br>
<table class = 'table' style="color: #ffffff">
<thead style="background-color: #000000">
<tr align = "center">
<th scope="col"> 게임 이미지 </th>
<th scope="col"> 게임 이름 </th>
<th scope="col"> 최저가 </th>
<th scope="col"> 개발사 </th>
</tr>
</thead>
<tbody style="background-color: #353535">
{% for game, min_price in game_lowest_price %}
<tr align = "center">
<td style="padding:5px">
{% if game.thumbnail %}
<img src="{{game.thumbnail.url}}" class="thumbnails">
{% endif %}
</td>
<td style="padding:5px">
<a href="{% url 'game:detail' game.pk %}" style="color: #FFC107"><br>{{game.title}}</a> <!--DETAIL과 연결하는 부분 -->
</td>
<td style="padding:5px; color: #CDCDCD"><br>
₩ {{min_price}}
</td>
<td style="padding:5px; color: #CDCDCD"><br>
{{game.developer}}
</td>
</tr>
{% endfor %}
</tbody>
</table>
<br>
<br>
<div class="container-fluid text-center">
<!--페이징 처리-->
<!-- Paginator: paginator 변수,
Page: page_obj 변수
페이징 유무 : is_paginated -->
{% if is_paginated %}
<p>
<!--이전 페이지 그룹으로 이동-->
{% if has_previous_page %}
<a href='/game/search?b={{request.GET.b}}&f={{request.GET.f}}&price={{request.GET.price}}&page={{previous_page}}' style="color: #CDCDCD">◀ 이전 페이지</a>
{% endif %}
<!--페이지 번호 링크-->
{% for page in current_page_group_range %}
<!-- page_obj.number: 현재 보고있는 페이지 -->
{% if page == page_obj.number %}
<a style="color: #FFC107">{{page}}</a>
{% else%}
<a href='/game/search?b={{request.GET.b}}&f={{request.GET.f}}&price={{request.GET.price}}&page={{page}}' style="color: #CDCDCD">
{{page}}
</a>
{% endif %}
{% endfor %}
<!--다음 페이지 그룹-->
{% if has_next_page %}
<a href='/game/search/?b={{request.GET.b}}&f={{request.GET.f}}&price={{request.GET.price}}&page={{next_page}}' style="color: #CDCDCD">다음 페이지 ▶</a>
{% endif %}
</p>
{% endif %}
<br>
<br>
<br>
</div>
</role>
</div>
</div>
{% endblock content %}
■ 결과물
▶ 게임목록
▶ 'ba'을 검색하였을때
▶ 'ba'을 검색하고 장르필터('indie','RPG')를 걸었을때(위의 경우보다 한페이지 줄었다. 참고로 indie 장르을 가진 게임들이 굉장히 많다.)
▶ 'ba'을 검색하고 장르필터('indie','RPG')를 걸고 가격필터(40000만원 이하)를 걸었을때(페이지가 2페이지로 줄었다.)
'자격증 & 문제풀이 > Django-Project' 카테고리의 다른 글
[Django] 게임세부정보(game_detail)[댓글, 좋아요 기능] (0) | 2020.07.20 |
---|---|
[Django] Django와 데이터 연동 (0) | 2020.07.19 |
[Django] Game Update와 Delete (0) | 2020.07.19 |
[Django] Game Model과 Game 등록(create) (0) | 2020.07.19 |
[Django] Base.html (0) | 2020.07.19 |