<백엔드>
- 검색 기능
- 프로젝트를 기획할 때는 생각이 안 났었는데 나는 검색 기능을 구현하고 싶었다
- 그래서 은연 중에 와이어프레임 그릴 때 검색창을 넣었었나보다 ㅋㅋㅋ
# articles/urls.py
from django.urls import path
from . import views
urlpatterns = [
...
path("search/<str:query>/", views.SearchView.as_view(), name="search_view"),
]
- 우선 url을 설정해준다
- 프론트엔드에서 검색어를 받아서 백엔드로 보내면 백엔드에서 그 검색어로 필터링을 하기 위해 url에 문자열을 받기로 했다
from rest_framework import status
from rest_framework.views import APIView
from rest_framework.response import Response
from .serializers import ArticleListSerializer
from .models import Article
from django.db.models.query_utils import Q
import urllib
...
class SearchView(APIView):
def get(self, request, query):
decoded_query = urllib.parse.unquote(query)
articles = Article.objects.filter(Q(title__contains=decoded_query) | Q(content__contains=decoded_query))
serializer = ArticleListSerializer(articles, many=True)
if articles:
return Response(serializer.data, status=status.HTTP_200_OK)
else:
return Response("검색 결과가 없습니다", status=status.HTTP_204_NO_CONTENT)
- 일단 검색을 위해 새로 import 한 건 Q랑 urllib다
- Q는 검색 조건을 더 깔끔하게 쓰기 위해 사용했다
- 보통 복잡한 조건문을 써야 할 때 쓴다고 하는데 내가 구현한 건 조건이 복잡하진 않다
- 근데 뭔가 Q안에 조건이 하나씩 들어간 게 깔끔하고 좋아서 사용했다
- 그리고 사실 Q를 안 쓰고 or 조건을 어떻게 넣는지 모른다,,ㅎㅎ
- urllib는 문자열이 url에 들어가면 이상한 영어와 특수문자 형식으로 바뀐다
- 이렇게 url에 한글을 써서 보내면
- 이렇게 특수문자와 영어로 암호처럼 나온다
- 검색함수의 query라는 매개변수에는 이 복잡한 문자열이 담겨있다
- 그래서 이렇게 바뀌어버린 문자열을 다시 원래의 검색어인 '게시글'로 바꿀 필요가 있다
- 그걸 하기 위한 게 urllib고 위에 def get 바로 아래줄과 같이 사용할 수 있다
- 한글에서 외계어로 바뀐 걸 인코딩이라고 하고, 외계어에서 한글로 바꾸는 걸 디코딩이라고 한다
- 저 외계어를 부르는 명칭이 있었는데 까먹었다
- 여튼 저렇게 함수를 짜고 포스트맨에서 실행해보면 제목에 "게시글"이 포함되어 있거나 내용에 "게시글"이 포함되어 있는 게시글 리스트가 쭉 나온다
- 검색 함수를 실행하면 ArticleListSerializer로 직렬화되어서 내용은 없지만 19번의 경우 내용에 "게시글"을 포함시켜서 작성했다
<프론트엔드>
- 게시글 목록 & 검색 기능
- 기능 구현에 있어서 나에게 챌린지는 항상 html과 css다
- 자바스트립트도 어렵지만 뭔가 성공해내면 희열감이 있는데 css는 진짜 너무 어렵고 구현에 성공해도 마냥 기쁘지만은 않다 의도한 대로 나오지 않으면 넘 짜증난다 ㅋㅋㅋ
- 그래서 부트스트랩은 필수템이고.. 게시글 목록 UI도 부트스트랩을 이용했다
- 성공한 모습을 먼저 보여주겠다
- 원래 게시글 목록은 내 파트가 아닌데 검색 기능을 구현하려면 게시글 목록이 필요해서 그냥 같이 만들었다
- 일단 html
<!--article_list.html-->
...
<div class="container text-center mt-3">
<div class="card mb-4">
<div class="card-body">
<table class="table table-hover table-striped">
<colgroup>
<col width=10%>
<col width=50%>
<col width=15%>
<col width=25%>
</colgroup>
<thead>
<tr>
<th>No.</th>
<th>제목</th>
<th>작성자</th>
<th>작성일</th>
</tr>
</thead>
<tbody id="article-list-kmj">
</tbody>
</table>
</div>
</div>
</div>
...
- tbody 태그 아래에 th(글번호), td(제목), td(작성자), td(작성일) 태그를 js로 붙일 예정이다
// loader.js
// 게시글 목록 UI
function articleList(articles) {
articles.forEach(async article => {
const tBody = document.getElementById("article-list-kmj")
const newTr = document.createElement("tr")
newTr.setAttribute("onclick", `articleDetail(${article.pk})`)
tBody.appendChild(newTr)
const newThNo = document.createElement("th")
newThNo.innerHTML = article.pk
newTr.appendChild(newThNo)
const newTdTitle = document.createElement("td")
newTdTitle.innerHTML = article.title
newTr.appendChild(newTdTitle)
const newTdOwner = document.createElement("td")
newTdOwner.innerHTML = article.nickname
newTr.appendChild(newTdOwner)
const newTdTime = document.createElement("td")
newTdTime.innerHTML = article.created_at
newTr.appendChild(newTdTime)
})
}
- 위에서 방금 얘기한 걸 js로 그대로 구현한 것이다
- 이제 백엔드와 통신하는 검색 함수를 js로 작성해보자
// api.js
// 검색 결과물 백엔드에서 가져오기
async function getQueryArticles(query) {
const response = await fetch(`${backend_base_url}/api/articles/search/${query}/`, {
method: "GET"
});
return response
}
- get요청이라 코드가 간단하다
- query에는 검색창에 입력된 글자가 담길 것이다
- response에는 백엔드에서 보낸 http status가 포함될 것이다
<!--index.html-->
...
<form>
<div class="mx-auto input-group mt-5">
<div class="mx-auto d-flex">
<input id="query" type="text" class="form-control" style="width: 40vw;" placeholder="검색어 입력"
aria-label="search" aria-describedby="button-addon2">
<button class="btn btn-primary" type="button" id="button-addon2" onclick="handleSearch()">검색</button>
</div>
</div>
</form>
...
- 일단 검색창 html이다 화면으로 보면
- 빨간 네모 친 부분이 검색창이다
- 검색어를 입력 후 검색 버튼을 누르면 아래 js가 실행된다
// index.js
async function handleSearch() {
const query = document.getElementById("query").value
if (query) {
window.location.href = `${frontend_base_url}/articles/articles_list.html?query=${query}`;
} else {
alert("검색어를 입력해 주세요!")
}
}
- 검색 버튼이 눌리면 input 박스에서 사용자가 검색한 단어를 가져오고 검색된 단어가 있으면 위 url로 이동하고 아무것도 작성하지 않은 채로 검색 버튼을 누른 경우 경고창이 뜬다
- 위 url로 이동하면 아래 js가 실행된다
// articles_list.js
// 글 목록 가져오기
window.onload = async function () {
const urlParams = new URLSearchParams(window.location.search);
const query = urlParams.get("query");
if (query) {
q_articles = await getQueryArticles(query)
if (q_articles.status == 200) {
articles = await q_articles.json();
} else if (q_articles.status == 204) {
alert("해당 검색어가 포함된 게시글을 찾을 수 없습니다!")
articles = await getArticles()
}
} else {
articles = await getArticles()
}
articles.sort((x, y) => y.pk - x.pk)
articleList(articles)
}
- 검색을 해서 이동한 경우랑 그냥 글목록 버튼을 타고 들어온 경우를 if문으로 분리했다
- 검색을 한 경우에는 if문을 통과한다
- 위에서 작성한 백엔드와 통신하는 검색함수를 실행해서 그 반환값을 q_articles에 담는다
- q_articles.status로 접근하면 백엔드에서 반환한 status 값이 들어있다
- 200이면 검색어에 해당하는 게시글이 있는 것이고, 204면 해당하는 게시글이 없는 것이다
- 게시글이 검색되면 json으로 변환시켜 articles에 담고 없으면 경고창이 뜬 후에 모든 게시글 목록을 담는다
- else문은 검색이 아닌 글목록 버튼을 눌러 들어온 경우에 해당한다
- 3가지 경우로 articles에 게시글 목록들이 담기면 pk값을 기준으로 내림차순한다(최신순)
- 생성시간 기준으로 해도 되긴 하는데 내가 가진 데이터 중에 article에 생성시간 필드가 붙기 전에 작성한 게시글들이 있어서 pk 역순으로 정렬이 안 되길래 그냥 pk를 기준으로 정렬했다
- 그 다음 맨 처음에 썼던 게시글 목록 UI 함수에 articles를 매개변수로 해서 실행한다
- 그럼 아래와 같이 나온다
- 메인페이지에 댓글, 게시글 목록 카드형식으로 보이게 하기
- 어제 이걸 해결하지 못한 채 자서 마음이 불편했다
- 오늘 처음으로 한 일이 이거였다
- 일단 html은 이런 구성으로 돼있다
- 이렇게 되기까지의 여정을 처음부터 설명하려면 너무 복잡하고 많은 고난을 겪어서 당연히 여기에 공유를 해야 하지만 나는 너무 피곤한 상태다...
- 그냥 css만 공유한다..
/* index.css */
.card-box {
display: flex;
/* 부모요소에 플랙스 적용, 가로정렬 됨 */
flex-direction: column;
/* 방향을 세로로 바꾸고 */
margin-bottom: 1rem;
}
@media(min-width: 768px) {
#most-like-comment {
display: flex;
flex-wrap: wrap;
/* 넘치는 카드 아래로 넘기기 */
margin: 0 -1rem;
/* 카드 바깥 쪽 공간 없애기 */
}
#recently-article {
display: flex;
flex-wrap: wrap;
/* 넘치는 카드 아래로 넘기기 */
margin: 0 -1rem;
/* 카드 바깥 쪽 공간 없애기 */
}
.card-box {
width: 50%;
/* 카드 2단 배치 */
padding: 0 1rem;
/* 카드 사이 간격 */
}
}
@media(min-width: 1200px) {
.card-box {
width: 33.33333%;
/* 카드 3단 배치 */
}
}
.card-img-top {
height: 8rem;
object-fit: cover;
}
- 이렇게 했더니 카드가 3개로 잘 나눠졌고 반응형도 잘 반응한다 Good~