IT/Linux

[Python] django 게시판 검색 기능 구현

액트 2023. 1. 16. 12:12

 

검색기능

게시판 검색 기능입니다.

검색 대상

  • 제목
  • 질문 본문 내용
  • 질문 작성자
  • 답변 본문 내용
  • 답변 작성자

 

1. 검색 화면

맨 밑에 있던 기존 "질문 등록하기" 버튼을 상단 오른쪽으로 옮기고 줄에 검색창을 만들려고 합니다.

question_list.html 템플릿 페이징처리 하단에 기존 작성된 질문 등록하기 코드를 삭제합니다. 

<a href="{% url 'pybo:question_create' %}" class="btn btn-primary">질문 등록하기</a>

상단에 검색어를 입력할 수 있는 텍스트창을 다음과 같이 추가합니다.

{% extends 'base.html' %}
{% load pybo_filter %}    <!-- 템플릿 필터 함수를 사용하기 위한 파일 로드 -->
{% block content %}
<div class="container my-3">
	<!-- 아래 추가 -->
    <div class="row my-3">
        <div class="col-6">
            <a href="{% url 'pybo:question_create' %}" class="btn btn-primary">질문 등록하기</a>
        </div>
        <div class="col-6">
            <div class="input-group">
                <input type="text" id="search_kw" class="form-control" value="{{ kw|default_if_none:'' }}">
                <div class="input-group-append">
                    <button class="btn btn-outline-secondary" type="button" id="btn_search">찾기</button>"
                </div>
            </div>
        </div>
    </div>
    <!-- 아래 추가 끝 -->
    <table class="table">
  	...생략...
    </table>
    <!-- 페이징처리 시작 -->
    <ul class="pagination justify-content-center">
    ...생략...
    </ul>
    <!-- 페이징처리 끝 -->
</div>
 {% endblock %}

<table> 태그 상단 우측에 검색어를 입력할 수 있는 텍스트창을 생성하였습니다. 

자바스크립트에서 이 텍스트창에 입력된 값을 읽기 위해 다음처럼 id 속성을 추가하였습니다.

<input type="text" id="search_kw" class="form-control" value="{{ kw|default_if_none:'' }}">

여기서 kw가 검색어입니다.

 

2. 검색 폼

page와 kw를 동시에 GET으로 요청할 수 있는 searchForm을 다음과 같이 추가합니다.

    <ul class="pagination justify-content-center">
	... 생략 ...
    </ul>
    <!-- 페이징처리 끝 -->
</div>
<!-- searchForm 추가 -->
<form id="searchForm" method="get" action="{% url 'index' %}">
    <input type="hidden" id="kw" name="kw" value="{{ kw|default_if_none:'' }}">
    <input type="hidden" id="page" name="page" value="{{ page }}">
</form>
<!-- searchForm 추가 끝 -->
 {% endblock %}

GET방식으로 요청해야 하므로 method 속성에 "get"을 설정합니다.  action 속성은 '폼이 전송되는 URL' 이므로 질문 목록 URL인 {% url 'index' %} 를 지정했습니다.

kw와 page는 이전에 요청했던 값을 기억하고 있어야 하므로 value에 값을 대입했습니다. 

kw(검색어)를 GET이 아닌 POST 방식으로 전달하게 된다면 웹 브라우저에서 "새로고침" 또는 "뒤로가기"를 했을 때 "만료된 페이지입니다."라는 오류가 발생하게 됩니다.

이유는 POST방식은 동일한 POST 요청이 발생할 경우 중복 요청을 방지하기 위해 "만료된 페이지입니다."라는 오류를 발생시키기 때문입니다. 즉, 2페이지에서 3페이지로 갔다가 뒤로가기를 했을 때 2페이지로 가는 것이 아니라 "만료된 페이지입니다."라고 화면에 보여지게 됩니다.

이러한 이유로 여러 파라미터를 조합하여 게시물 목록을 조회할 때는 GET 방식을 사용하는 것이 좋습니다.

 

3. 페이징

기존 페이징 처리하는 부분도 ?page=1 처럼 직접 파라미터를 코딩하는 방식에서 값을 읽어 폼에 설정할 수 있도록 다음처럼 변경해야 합니다.

모든 페이지 링크를 href 속성에 직접 입력하는 대신 data-page 속성으로 값을 읽을 수 있도록 변경합니다.

즉, 다음과 같은 링크를 

<a class="page-link" href="?page={{ question_list.previous_page_number }}">이전</a>

다음처럼 수정합니다.

<a class="page-link" data-page="{{ question_list.previous_page_number }}" href="javascript:void(0)">이전</a>
<!-- 페이징처리 시작 -->
<ul class="pagination justify-content-center">
    ...생략...
        <a class="page-link" data-page="{{ question_list.previous_page_number }}"
            href="javascript:void(0)">이전</a>
    ...생략...
        <a class="page-link" data-page="{{ page_number }}" href="javascript:void(0)">{{ page_number }}</a>
    ...생략...
        <a class="page-link" data-page="{{ page_number }}" href="javascript:void(0)">{{ page_number }}</a>
    ...생략...
        <a class="page-link" data-page="{{ question_list.next_page_number }}"
            href="javascript:void(0)">다음</a>
    ...생략...
<!-- 페이징처리 끝 -->

 

4. 검색 스크립트

page. kw 파라미터를 동시에 요청할 수 있는 자바스크립트를 하단에 다음과 같이 추가합니다.

{% block script %}
<script type='text/javascript'>
const page_elements = document.getElementsByClassName("page-link");
Array.from(page_elements).forEach(function(element) {
    element.addEventListener('click', function() {
        document.getElementById('page').value = this.dataset.page;
        document.getElementById('searchForm').submit();
    });
});
const btn_search = document.getElementById("btn_search");
btn_search.addEventListener('click', function() {
    document.getElementById('kw').value = document.getElementById('search_kw').value;
    document.getElementById('page').value = 1;  // 검색버튼을 클릭할 경우 1페이지부터 조회한다.
    document.getElementById('searchForm').submit();
});
</script>
{% endblock %}

 

5. 검색 함수

이제 화면에서 요청한 검색어가 질문 목록 조회에 적용될 수 있도록 base_views.py의 index 함수를 다음처럼 수정합니다.

...생략 ...
from django.db.models import Q



def index(request):
    page = request.GET.get('page', '1') # 페이지
    kw = request.GET.get('kw', '')  # 검색어
    question_list = Question.objects.order_by('-create_date')
    if kw:
        question_list = question_list.filter(
            Q(subject__icontains=kw) |  # 제목 검색
            Q(content__icontains=kw) |  # 내용 검색
            Q(answer__content__icontains=kw) |  # 답변 내용 검색
            Q(author__username__icontains=kw) |  # 질문 글쓴이 검색
            Q(answer__author__username__icontains=kw)  # 답변 글쓴이 검색
        ).distinct()
    paginator = Paginator(question_list, 10) # 페이지당 10개씩 보여주기
    page_obj = paginator.get_page(page)
    context = {'question_list': page_obj, 'page': page, 'kw': kw}
    return render(request, 'pybo/question_list.html', context)
    ...생략...

filter 함수에서 모델 속성에 접근하기 위해서는 __ (언더바 두개)를 사용하여 하위 속성에 접근할 수 있습니다.

Q함수내에 사용된 subject__icontains=kw의 의미는 제목에 kw 문자열이 포함되었는지를 의미합니다.

subject__contains=kw 대신 subject__icontains=kw를 사용하면 대소문자를 가리지 않고 검색합니다.

answer__author__username__icontains는 "답변을 작성한 사람의 이름에 포함되는가"? 라는 의미를 갖습니다.

그리고 page와 kw를 템플릿에 전달하기 위해 context 딕셔너리에 추가합니다.

 

6. 검색 확인