Programming

; develop a program

Framework/Django

[Django] 장고(Django) 서비스 개발 - 검색

Clloud_ 2022. 12. 25. 23:04
반응형

 

이번 포스팅에서는 장고를 사용하여 게시판 서비스 개발에 필요한 검색에 대하여 공부를 해보고자 한다.

 


검색 기능

질문답변에 대한 데이터가 계속 쌓여가는 게시판에 검색기능은 필수라고 할 수 있다.

검색의 대상은 제목, 질문의 내용, 질문 작성자, 답변의 내용, 답변 작성자 정도로 하면 적당할 것이다.

 

즉, "파이썬"이라고 검색을 하면 "파이썬" 이라는 문자열이 제목, 내용, 질문 작성자, 답변, 답변 작성자에 존재하는지 찾아보고 그 결과를 화면에 보여주어야 한다.

이런 조건으로 검색하려면 질문 목록을 조회하는 부분을 다음처럼 수정해야 한다.

지금은 실제 파일을 수정하지 말고 지금은 눈으로만 살펴본다.

 

from django.db.models import Q

kw = request.GET.get('kw', '')  # 검색어

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()

 

위 코드의 Q함수는 OR조건으로 데이터를 조회하기 위해 사용하는 함수이다.

제목과 내용 그리고 글쓴이를 OR 조건으로 검색하기 위해 사용했다.

그리고 filter 함수 뒤에 사용된 distinct는 조회 결과에 중복이 있을 경우 중복을 제거하여 리턴하는 함수이다.

 

하나의 질문에는 여러 개의 답변이 있을 수 있다.

이때 여러 개의 답변이 검색 조건에 해당될 때 동일한 질문이 중복으로 조회될 수 있다.

이런 이유로 중복된 질문을 제거하기 위해 distinct를 사용했다.

 


GET

위 코드에서 kw는 화면으로부터 전달받은 검색어이다.

그리고 kw 값은 다음처럼 POST가 아닌 GET으로 읽도록 했다.

kw = request.GET.get('kw', '')  # 검색어

 

즉, kw는 다음처럼 GET 방식으로 전달되어야 index 함수에서 읽을 수 있다.

http://localhost:8000/?kw=파이썬&page=1

 

kw를 GET이 아닌 POST 방식으로 전달하는 방법은 추천하지 않는다.

kw를 POST 방식으로 전달한다면 page 파라미터도 역시 POST방식으로 전달해야 한다.

kw는 POST로 전달하고 page는 GET으로 전달하는 방법은 존재하지 않는다.

 

만약 GET이 아닌 POST 방식으로 검색과 페이징을 처리한다면 웹 브라우저에서 "새로고침" 또는 "뒤로 가기"를 했을 때 "만료된 페이지입니다."라는 오류를 종종 만나게 될 것이다.

POST 방식은 동일한 POST 요청이 발생할 경우 중복 요청을 방지하기 위해 "만료된 페이지입니다." 라는 오류를 발생시키기 때문이다.

2페이지에서 3페이지로 갔다가 뒤로 가기를 했을 때 2페이지로 가는 것이 아니라 오류가 발생한다면 엉망이 될 것이다. 

 

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

 


검색 화면

이제 화면에서 kw와 page를 동시에 GET방식으로 호출하는 방법에 대해서 알아보려 한다..

 

검색 창

먼저 question_list.html 템플릿에 검색어를 입력할 수 있는 텍스트창을 다음과 같이 추가한다.

 

[파일명: projects\mysite\templates\pybo\question_list.html]

<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>
    <!-- 페이징처리 시작 -->
    (... 생략 ...)
    <!-- 페이징처리 끝 -->
    # <a href="{% url 'pybo:question_create' %}" class="btn btn-primary">질문 등록하기</a>
</div>
</html>

 

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

맨 밑에 있던 "질문 등록하기" 버튼은 검색 창의 좌측으로 이동했다.

그리고 자바 스크립트에서 이 텍스트창에 입력된 값을 읽기 위해 다음처럼 id 속성을 추가한 점을 주목해야 한다.

 


검색 폼

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

 

[파일명: projects\mysite\templates\pybo\question_list.html]

(... 생략 ...)
    <!-- 페이징처리 끝 -->
</div>
<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>
{% endblock %}

 

GET 방식으로 요청해야 하므로 method 속성에 "get"을 설정했다.

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

이전에 요청했던 kw와 page의 값은 index 함수로부터 전달될 것이다.

action 속성은 '폼이 전송되는 URL'이므로 질문 목록 URL인 {% url 'index' %}를 지정했다.

 

index 함수에서 kw, page 값을 전달하는 부분은 조금 후에 진행한다.

 


페이징

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

 

[파일명: projects\mysite\templates\pybo\question_list.html]

(... 생략 ...)
<!-- 페이징처리 시작 -->
<ul class="pagination justify-content-center">
    <!-- 이전페이지 -->
    {% if question_list.has_previous %}
    <li class="page-item">
        <a class="page-link" data-page="{{ question_list.previous_page_number }}"
           href="javascript:void(0)">이전</a>
    </li>
    {% else %}
    <li class="page-item disabled">
        <a class="page-link" tabindex="-1" aria-disabled="true"
           href="javascript:void(0)">이전</a>
    </li>
    {% endif %}
    <!-- 페이지리스트 -->
    {% for page_number in question_list.paginator.page_range %}
    {% if page_number >= question_list.number|add:-5 and page_number <= question_list.number|add:5 %}
    {% if page_number == question_list.number %}
    <li class="page-item active" aria-current="page">
        <a class="page-link" data-page="{{ page_number }}"
           href="javascript:void(0)">{{ page_number }}</a>
    </li>
    {% else %}
    <li class="page-item">
        <a class="page-link" data-page="{{ page_number }}"
           href="javascript:void(0)">{{ page_number }}</a>
    </li>
    {% endif %}
    {% endif %}
    {% endfor %}
    <!-- 다음페이지 -->
    {% if question_list.has_next %}
    <li class="page-item">
        <a class="page-link" data-page="{{ question_list.next_page_number }}"
           href="javascript:void(0)">다음</a>
    </li>
    {% else %}
    <li class="page-item disabled">
        <a class="page-link" tabindex="-1" aria-disabled="true"
           href="javascript:void(0)">다음</a>
    </li>
    {% endif %}
</ul>
<!-- 페이징처리 끝 -->
(... 생략 ...)

 

모든 페이지 링크를 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>

 


검색 스크립트

그리고 page, kw 파라미터를 동시에 요청할 수 있는 자바스크립트를 다음과 같이 추가한다.

 

[파일명: projects\mysite\templates\pybo\question_list.html]

(... 생략 ...)
{% endblock %}
{% 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 %}

 

위에 추가한 자바스크립트 코드를 자세히 볼 때, 만약 다음과 같이 class 속성값으로 "page-link"라는 값을 가지고 있는 링크를 클릭하면

<a class="page-link" data-page="{{ question_list.previous_page_number }}" href="javascript:void(0)">이전</a>

 

이 링크의 data-page 속성값을 읽어 searchForm의 page 필드에 설정하여 searchForm을 요청하도록 다음과 같은 스크립트를 추가했다.

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();
    });
});

 

그리고 검색버튼을 클릭하면 검색어 텍스트창에 입력된 값을 searchForm의 kw 필드에 설정하여 searchForm을 요청하도록 다음과 같은 스크립트를 추가했다.

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();
});

 

검색버튼을 클릭하는 경우는 새로운 검색에 해당되므로 page에 항상 1을 설정하여 요청하도록 했다.

 


검색 함수

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

 

[파일명: projects\mysite\pybo\views\base_views.py]

(... 생략 ...)
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)

(... 생략 ...)

 

Q함수 내에 사용된 subject__icontains=kw의 의미는 제목에 kw 문자열이 포함되었는지를 의미한다. answer__author__username__icontains 은 좀 복잡해 보이는데 "답변을 작성한 사람의 이름에 포함되는가?"라는 의미를 갖는다. filter 함수에서 모델 속성에 접근하기 위해서는 이처럼 __ (언더바 두개) 를 이용하여 하위 속성에 접근할 수 있다.

※ subject__contains=kw 대신 subject__icontains=kw을 사용하면 대소문자를 가리지 않고 찾아 준다.

 

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

 


검색 확인

이제 검색창에 "마크다운"이라는 검색어로 조회하면 다음과 같이 해당 문장이 있는 게시물만 조회된다.

 

 


반응형