자격증 & 문제풀이/Django-Project

[Django] 게임 목록(검색, 필터, 가격필터 기능 및 페이징 기능 구현 그리고 최저가 기능 구현)

YSY^ 2020. 7. 20. 00:52

이제 대망의 게임목록 페이지를 만드는 시간입니다.

게임목록페이지에서는 게임리스트를 보여줌과 동시에 게임검색기능, 장르필터기능, 가격필티기능을 구현하고 페이징기능까지 구현할것입니다.

게임검색과 필터 기능의 핵심은 검색을 했을시 game_search페이지로 넘어간다는 것입니다.

즉, 원래의 게임목록 페이지와 검색후 페이지는 다른 페이지입니다.

 

■ views.py(리스트 부분)

 

모든 게임 데이터 가지고 오기(all_games) -> 페이징기능구현 -> 필터에서 쓸 장르리스트 가져오기 -> 최저가기능 순으로 구현되었습니다.

이 페이징, 필터 및 검색, 최저가 기능을 context라는 dictionary에 담아서 game_list.html에서 호출됩니다.

▶ 게임 리스트에서의 최저가 노출

  1. view안에서 model에 정의된 5가지 퍼블리셔들을 하나의 리스트로 만들고 for문을 통해 min값을 탐색.

  2. 게임 이름과 최저가를 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">
                      &nbsp;&nbsp;&nbsp;
                      <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>
                    &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
                    <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 %}
          &nbsp;
          <!--페이지 번호 링크-->
          {% for page in current_page_group_range %}
              &nbsp;
          <!-- 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 %}
              &nbsp;
          {% endfor %}
          <!--다음 페이지 그룹-->
          &nbsp;
          {% 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">
                      &nbsp;&nbsp;&nbsp;
                      <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>
                    &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
                    <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 %}
      &nbsp;
      <!--페이지 번호 링크-->
      {% for page in current_page_group_range %}
          &nbsp;
      <!-- 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 %}
          &nbsp;
      {% endfor %}
      <!--다음 페이지 그룹-->
      &nbsp;
      {% 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페이지로 줄었다.)

 

 

https://coupa.ng/bQ4xVH

 

이엠텍 지포스 RTX 3070 BLACK Edition OC D6 8GB 그래픽카드

COUPANG

www.coupang.com

 

728x90
반응형