검색 기능 구현하기

Posted on 2022-01-04 by GKSRUDTN99
Django로 웹사이트 만들기 Django Search

1. 자바스크립트로 기능 추가하기

이번에는 포스트 목록 페이지와 포스트 상세 페이지 오른쪽에 껍데기만 있던 검색 창이 실제로 동작하도록 검색기능을 구현한다.
검색창에서 사용자가 키보드로 입력하는 것을 기다리고 있다가 'Enter'를 눌렀을 때 동작하게 하려면 자바스크립트가 필요하다.

base.html 수정하여 검색 창 내용을 서버로 전달하기

  • 먼저 base.html에서 'Go!'버튼을 클릭했을 때 검색 창 내용을 서버로 전달하는 기능을 구현한다.
  • input의 내용을 가져올 수 있도록 id를 추가하고, Go!버튼을 눌렀을 때 자바스크립트를 실행하도록 onclick속성을 추가한다.
<!-- Search widget-->
<div class="card mb-4">
    <div class="card-header">Search</div>
    <div class="card-body">
        <div class="input-group">
            <input class="form-control" type="text" placeholder="Enter search term..."
                   aria-label="Enter search term..." aria-describedby="button-search" id="search-input"/>
            <button class="btn btn-primary" id="button-search" type="button" onclick="searchPost()">Go!</button>
        </div>
    </div>
</div>
  • searchPost()함수는 아래쪽 script태그 안에 정의한다.
  • 검색하려는 글자의 수가 두 글자 이상일 때만 검색이 가능하도록 한다.
    • 사용자가 너무 적은 글자를 입력하고 검색을 진행하면 너무 많은 포스트가 가져와지기 때문에 서버에 부하를 줄 수 있고,
    • 사용자가 잘못 입력했을 경우 오랜 시간을 기다려야하는 문제가 발생하기 때문이다.
  • 또한 'Enter'키를 눌렀을 때도 검색이 진행되도록 한다.
<script>
    function searchPost() {
        let searchValue = document.getElementById('search-input').value.trim();
        if (searchValue.length > 1) {
            location.href = "/blog/search/" + searchValue + "/";
        } else {
            alert('검색어(' + searchValue + ')가 너무 짧습니다.');
        }
    };
</script>
{% include 'blog/footer.html' %}
  • 웹 서버를 실행하여 테스트해보면, 한 글자를 입력했을 때는 검색어가 짧다는 알림이 뜨는 것을 확인할 수 있다.
  • 하지만 두 글자 이상을 입력하면 404 Error가 발생하는데, 아직 urls.pyviews.py파일을 수정하지 않았기 때문이다.

2. 검색 기능 구현하기

테스트 코드 만들기

  • 웹 사이트 방문자가 검색창에 '파이썬'을 입력하고 검색하는 상황을 가정한다.
  • setUp() 함수에서 '파이썬'이 들어있는 제목을 가진 포스트가 없으므로, 이를 포함한 포스트를 하나 생성한다.
  • 검색어를 서버에 전달하는 URL은 /blog/search/검색어/로 한다.
  • 검색어로 조회할 대상은 포스트의 제목과 태그로 한다.
    • 기존에 만들어 두었던 post_003의 태그에 '파이썬'이 포함되어 있으므로, '파이썬'을 검색했을 때 포스트는 총 2개가 나와야 한다.
def test_search(self):
    post_about_python = Post.objects.create(
        title="파이썬에 대한 포스트입니다.",
        content="파이썬에 대한 포스트 내용입니다.",
        author=self.user_trump
    )

    response = self.client.get('/blog/search/파이썬/')
    self.assertEqual(response.status_code, 200)
    soup = BeautifulSoup(response.content, 'html.parser')

    main_area = soup.find('div', id='main-area')

    self.assertIn('Search: 파이썬 (2)', main_area.text)
    self.assertNotIn(self.post_001.title, main_area.text)
    self.assertNotIn(self.post_002.title, main_area.text)
    self.assertIn(self.post_003.title, main_area.text)
    self.assertIn(post_about_python.title, main_area.text)
  • 아직 urls.pyviews.py를 수정하지 않았으므로 테스트를 진행하면 404에러가 발생한다.

urls.py 수정하여 PostSearch에 연결하기

  • CBV 스타일로 PostSearch View를 만들것이므로, 이에 맞게 urls.py를 작성한다.
  • URL에서 검색어에 해당하는 부분을 문자열로 받고, 이를 q라고 부르겠다는 의미로 <str:q>를 사용한다.
from django.contrib import admin
from django.urls import path
from . import views

urlpatterns = [
    path('search/<str:q>/', views.PostSearch.as_view()),
    path('delete_comment/<int:pk>/', views.delete_comment),
    path('update_comment/<int:pk>/', views.CommentUpdate.as_view()),
    path('update_post/<int:pk>/', views.PostUpdate.as_view()),
    path('create_post/', views.PostCreate.as_view()),
    path('tag/<str:slug>/', views.tag_page),
    path('category/<str:slug>/', views.category_page),
    path('<int:pk>/new_comment/', views.new_comment),
    path('<int:pk>/', views.PostDetail.as_view()),
    path('', views.PostList.as_view()),
]

views.py에 PostSearch 클래스 추가하기

  • PostList 클래스를 상속받아 PostSearch를 만든다.
def PostSearch(PostList):
    # PostSearch에서는 검색된 결과를 한 페이지에 모두 보여주기 위해 PostList에 지정된 Pagination 속성을 해제한다.
    paginate_by = None

    # PostList를 상속받아 만들어진 함수이므로, PostList의 get_queryset()은 지정된 모델의 모든 레코드를 가져오지만, 검색 기능은 검색된 결과만 가져와야하므로 오버라이딩한다.
    def get_queryset(self):
        # URL을 통해 넘어온 검색어를 받아 q에 저장한다.
        q = self.kwargs['q']
        # 여러 조건의 쿼리를 동시에 사용하는 경우 장고에서 제공하는 Q를 사용할 수 있다.
        # title__contains처럼 title.contains 대신 밑줄 2개를 대신 사용하는 것에 유의한다.
        # 제목과 태그 모두에 '파이썬'이 포함된 경우 중복을 막기 위해 distinct를 사용한다.
        post_list = Post.objects.filter(
            Q(title__contains=q) | Q(tags__name__contains=q)
        ).distinct()
        return post_list

    def get_context_data(self, **kwargs):
        context = super(PostSearch, self).get_context_data()
        q = self.kwargs['q']
        context['search_info'] = f'Search: {q} ({self.get_queryset().count()})'

        return context
  • 이제 테스트를 수행해보면 404 Error는 발생하지 않지만, main-area안에 Search: 파이썬 (2)가 없다고 한다.

템플릿 파일 수정하기

  • PostSearch는 PostList를 상송받아 만든 클래스이고, template_name을 따로 설정하지 않았으므로 post_list.html을 템플릿으로 사용한다.
  • search_info가 context로 넘어온 경우 표시되도록 다음과 같이 수정한다.
<h1>Blog
    {% if search_info %} <small class="text-muted">{{ search_info }}</small>{% endif %}
    {% if category %}<span class="badge bg-secondary float-none">{{ category }}</span>{% endif %}
    {% if tag %}<span class="badge bg-warning float-none"><i class="fas fa-tags"></i>{{ tag }} ({{ tag.post_set.count }})</span>{% endif %}
</h1>
  • 이제 다시 테스트를 수행해보면 OK가 출력된다.