이번 포스팅에서는 게시글 검색 기능을 구현해 보자.
검색 후 게시글 목록 조회는 이전에 만들어 놓았던 메소드를 활용하면 된다!
⏬ 이전 포스팅 다시 보기 ⏬
게시글 목록 조회 기능 (23.08.21)
현재 구현 중인 웹 사이트에서는 메인 페이지 상단 / 게시판 내부 하단 이렇게 두 개의 검색창이 있다.
메인 페이지 상단의 검색창에서는 boardCode는 1(공지사항 게시판), 검색 조건은 '제목'으로 간단하게 지정하여 구현해 볼 것이다!
두 번째로 게시판 내에서 게시글을 검색할 때는 select 태그로 '제목', '내용', '제목+내용', '작성자' 옵션을 각각 구분하여 좀 더 상세한 검색이 가능하게 구현해 보자.
...
<form action="/board/1" method="GET">
<fieldset> <!-- form태그 내 영역 구분 -->
<!--
input의 name 속성 == 제출 시 key
input에 입력된 내용 == 제출 시 value
autocomplete="off" : 브라우저 제공 자동완성 off
-->
<input type="search" name="query" id="query"
placeholder="검색어를 입력해주세요."
autocomplete="off" value="${param.query}">
<%-- 제목 검색 --%>
<input type="hidden" name="key" value="t">
<!-- 검색 버튼 -->
<!-- button type="submit" 이 기본값 -->
<button id="searchBtn" class="fa-solid fa-magnifying-glass"></button>
</fieldset>
</form>
...
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%-- map에 저장된 값들을 각각 변수에 저장 --%>
<c:set var="pagination" value="${map.pagination}"/>
<c:set var="boardList" value="${map.boardList}"/>
<%-- <c:set var="boardName" value="${boardTypeList[boardCode - 1].BOARD_NAME}"/> --%>
<c:forEach items="${boardTypeList}" var="boardType">
<c:if test="${boardType.BOARD_CODE == boardCode}" >
<c:set var="boardName" value="${boardType.BOARD_NAME}"/>
</c:if>
</c:forEach>
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>게시판 이름</title>
<link rel="stylesheet" href="/resources/css/board/boardList-style.css">
</head>
<body>
<main>
<jsp:include page="/WEB-INF/views/common/header.jsp"/>
<%-- 검색을 진행한 경우 파라미터(key, query)를
쿼리스트링 형태로 저장한 변수를 선언 --%>
<c:if test="${!empty param.key}" >
<c:set var="sp" value="&key=${param.key}&query=${param.query}"/>
</c:if>
<section class="board-list">
<h1 class="board-name">${boardName}</h1>
<c:if test="${!empty param.key}" >
<h3 style="margin:30px">"${param.query}" 검색 결과</h3>
</c:if>
<div class="list-wrapper">
<table class="list-table">
<thead>
<tr>
<th>글번호</th>
<th>제목</th>
<th>작성자</th>
<th>작성일</th>
<th>조회수</th>
<th>좋아요</th>
</tr>
</thead>
<tbody>
<c:choose>
<c:when test="${empty boardList}">
<%-- 조회된 게시글 목록이 비어 있거나 null인 경우 --%>
<!-- 게시글 목록 조회 결과가 비어있다면 -->
<tr>
<th colspan="6">게시글이 존재하지 않습니다.</th>
</tr>
</c:when>
<c:otherwise>
<!-- 게시글 목록 조회 결과가 있다면 -->
<c:forEach items="${boardList}" var="board">
<tr>
<td>${board.boardNo}</td>
<td>
<%-- 썸네일이 있을 경우 --%>
<c:if test="${!empty board.thumbnail}" >
<img class="list-thumbnail" src="${board.thumbnail}">
</c:if>
<%-- ${boardCode} : @Pathvariable로 request scope에 추가된 값 --%>
<a href="/board/${boardCode}/${board.boardNo}?cp=${pagination.currentPage}">${board.boardTitle}</a>
[${board.commentCount}]
</td>
<td>${board.memberNickname}</td>
<td>${board.boardCreateDate}</td>
<td>${board.readCount}</td>
<td>${board.likeCount}</td>
</tr>
</c:forEach>
</c:otherwise>
</c:choose>
</tbody>
</table>
</div>
<div class="btn-area">
<!-- 로그인 상태일 경우 글쓰기 버튼 노출 -->
<c:if test="${!empty loginMember}" >
<button id="insertBtn">글쓰기</button>
</c:if>
</div>
<div class="pagination-area">
<ul class="pagination">
<!-- 첫 페이지로 이동 -->
<li><a href="/board/${boardCode}?cp=1${sp}"><<</a></li>
<!-- 이전 목록 마지막 번호로 이동 -->
<li><a href="/board/${boardCode}?cp=${pagination.prevPage}${sp}"><</a></li>
<!-- 특정 페이지로 이동 -->
<c:forEach var="i" begin="${pagination.startPage}"
end="${pagination.endPage}" step="1">
<c:choose>
<c:when test="${i == pagination.currentPage}">
<!-- 현재 보고있는 페이지 -->
<li><a class="current">${i}</a></li>
</c:when>
<c:otherwise>
<!-- 현재 페이지를 제외한 나머지 -->
<li><a href="/board/${boardCode}?cp=${i}${sp}">${i}</a></li>
</c:otherwise>
</c:choose>
</c:forEach>
<!-- 다음 목록 시작 번호로 이동 -->
<li><a href="/board/${boardCode}?cp=${pagination.nextPage}${sp}">></a></li>
<!-- 끝 페이지로 이동 -->
<li><a href="/board/${boardCode}?cp=${pagination.maxPage}${sp}">>></a></li>
</ul>
</div>
<!-- 검색창 -->
<form action="${boardCode}" method="get" id="boardSearch">
<select name="key" id="searchKey">
<option value="t">제목</option>
<option value="c">내용</option>
<option value="tc">제목+내용</tion>
<option value="w">작성자</option>
</select>
<input type="text" name="query" id="searchQuery" placeholder="검색어를 입력해주세요.">
<button>검색</button>
</form>
</section>
</main>
<!-- 썸네일 클릭 시 모달창 출력 -->
<div class="modal">
<span id="modalClose">×</span>
<img id="modalImage" src="/resources/images/user.png">
</div>
<jsp:include page="/WEB-INF/views/common/footer.jsp"/>
<%-- boardList.js 연결 --%>
<script src="/resources/js/board/boardList.js"></script>
</body>
</html>
const insertBtn = document.getElementById("insertBtn");
// 글쓰기 버튼 클릭 시
if(insertBtn != null){
insertBtn.addEventListener("click", ()=>{
// JS BOM 객체 중 location
// location.href = "주소"
// 해당 주소 요청 (GET 방식)
location.href = `/board2/${location.pathname.split("/")[2]}/insert`
// "/board2/" + location.pathname.split("/")[2];
// /board2/1/insert
})
}
// 검색창 이전 검색 기록을 남겨 놓기
const boardSearch = document.querySelector("#boardSearch");
const searchKey = document.querySelector("#searchKey");
const searchQuery = document.querySelector("#searchQuery");
const options = document.querySelectorAll("#searchKey > option");
(()=>{
const params = new URL(location.href).searchParams;
const key = params.get("key"); // t, c, tc, w 중 하나
const query = params.get("query"); // 검색어
if(key != null){ // 검색을 했을 때
searchQuery.value = query; // 검색어를 화면에 출력
// option 태그를 하나씩 순차 접근해서 value가 key랑 같으면
// selected 속성 추가
for(let op of options){
if(op.value == key){
op.selected = true;
}
}
}
})();
// 검색어 입력 없이 제출된 경우
boardSearch.addEventListener("submit", e=>{
if(searchQuery.value.trim().length == 0){ // 검색어 미입력 시
e.preventDefault(); // form 기본 이벤트 제거
location.href = location.pathname; // 해당 게시판 1페이지로 이동
// location.pathname : 쿼리스트링을 제외한 실제 주소
}
})
기존 '게시글 목록 조회' 메소드에서 검색어가 있을 때와 검색어가 없을 때로 조건을 나눈 뒤 상황에 알맞게 조금 수정해 보았다.
검색어가 있다는 것은 즉 유저가 검색을 한 경우를 의미한다.
...
// 게시글 목록 조회
@GetMapping("/{boardCode:[0-9]+}") // boardCode는 1자리 이상의 숫자
public String selectBoardList(@PathVariable("boardCode") int boardCode
, @RequestParam(value = "cp", required = false, defaultValue = "1") int cp
, Model model
, @RequestParam Map<String, Object> paramMap // 파라미터 전부 다 담겨 있음
) {
// boardCode 확인
// System.out.println("boardCode : " + boardCode);
if(paramMap.get("key") == null) { // 검색어가 없을 때(검색 X)
// 게시글 목록 조회 서비스 호출
Map<String, Object> map = service.selectBoardList(boardCode, cp);
// 조회 결과를 request scope에 세팅 후 forward
model.addAttribute("map", map);
} else { // 검색어가 있을 때(검색 O)
paramMap.put("boardCode", boardCode);
Map<String, Object> map = service.selectBoardList(paramMap, cp);
model.addAttribute("map", map);
}
return "board/boardList";
}
...
...
/** 게시글 목록 조회 (검색)
* @param paramMap
* @param cp
* @return boardList
*/
Map<String, Object> selectBoardList(Map<String, Object> paramMap, int cp);
...
// 게시글 목록 조회 (검색)
@Override
public Map<String, Object> selectBoardList(Map<String, Object> paramMap, int cp) {
// 1. 특정 게시판의 삭제되지 않고 검색 조건이 일치하는 게시글 수 조회
int listCount = dao.getListCount(paramMap);
// 2. 1번 조회 결과 + cp를 이용해서 Pagination 객체 생성
// -> 내부 필드가 모두 계산되어 초기화
Pagination pagination = new Pagination(cp, listCount);
// 3. 특정 게시판에서
// 현재 페이지에 해당하는 부분에 대한 게시글 목록 조회
// + 단, 검색 조건이 일치하는 글만
List<Board> boardList = dao.selectBoardList(pagination, paramMap);
// 4. pagination, boardList를 Map에 담아서 반환
Map<String, Object> map = new HashMap<>();
map.put("pagination", pagination);
map.put("boardList", boardList);
return map;
}
...
/** 게시글 수 조회 (검색)
* @param paramMap
* @return listCount
*/
public int getListCount(Map<String, Object> paramMap) {
return sqlSession.selectOne("boardMapper.getListCount_search", paramMap);
}
/** 게시글 목록 조회
* @param pagination
* @param paramMap
* @return boardList
*/
public List<Board> selectBoardList(Pagination pagination, Map<String, Object> paramMap) {
// 1) offset 계산
int offset
= (pagination.getCurrentPage() - 1) * pagination.getLimit();
// 2) RowBounds 객체 생성
RowBounds rowBounds = new RowBounds(offset, pagination.getLimit());
// 3) selectList("namespace.id", 파라미터, RowBounds)
return sqlSession.selectList("boardMapper.selectBoardList_search", paramMap, rowBounds);
}
동적 SQL을 활용하여 검색 조건에 따라 달라지는 코드를 작성한다.
JOIN 구문은 if 태그
로, WHERE절의 조건 구문은 경우가 여러 개이므로 choose 태그
로 작성해 준다. 😉
...
<!-- 특정 게시판의 삭제되지 않고 검색 조건이 일치하는 게시글 수 조회 -->
<select id="getListCount_search" resultType="_int">
SELECT COUNT(*)
FROM BOARD
<!-- 작성자 검색 -->
<if test='key == "w"'>
JOIN MEMBER USING(MEMBER_NO)
</if>
WHERE BOARD_DEL_FL = 'N'
AND BOARD_CODE = #{boardCode}
<choose>
<when test='key == "t"'>
<!-- 제목 -->
AND BOARD_TITLE LIKE '%${query}%'
</when>
<when test='key == "c"'>
<!-- 내용 -->
AND BOARD_CONTENT LIKE '%${query}%'
</when>
<when test='key == "tc"'>
<!-- 제목 + 내용 -->
AND (BOARD_TITLE LIKE '%${query}%' OR BOARD_CONTENT LIKE '%${query}%')
</when>
<when test='key == "w"'>
<!-- 작성자(닉네임) -->
AND MEMBER_NICKNAME LIKE '%${query}'
</when>
</choose>
</select>
<!-- 게시글 목록 조회(검색)-->
<!-- CDATA 태그 : 해당 태그 내부에 작성된 것은 모두 문자로 취급 -->
<select id="selectBoardList_search" resultMap="board_rm">
SELECT BOARD_NO, BOARD_TITLE, MEMBER_NICKNAME, READ_COUNT,
<![CDATA[
CASE
WHEN SYSDATE - B_CREATE_DATE < 1/24/60
THEN FLOOR( (SYSDATE - B_CREATE_DATE) * 24 * 60 * 60 ) || '초 전'
WHEN SYSDATE - B_CREATE_DATE < 1/24
THEN FLOOR( (SYSDATE - B_CREATE_DATE) * 24 * 60) || '분 전'
WHEN SYSDATE - B_CREATE_DATE < 1
THEN FLOOR( (SYSDATE - B_CREATE_DATE) * 24) || '시간 전'
ELSE TO_CHAR(B_CREATE_DATE, 'YYYY-MM-DD')
END B_CREATE_DATE,
]]>
(SELECT COUNT(*) FROM "COMMENT" C
WHERE C.BOARD_NO = B.BOARD_NO) COMMENT_COUNT,
(SELECT COUNT(*) FROM BOARD_LIKE L
WHERE L.BOARD_NO = B.BOARD_NO) LIKE_COUNT,
(SELECT IMG_PATH || IMG_RENAME FROM BOARD_IMG I
WHERE I.BOARD_NO = B.BOARD_NO
AND IMG_ORDER = 0) THUMBNAIL
FROM "BOARD" B
JOIN "MEMBER" USING(MEMBER_NO)
WHERE BOARD_DEL_FL = 'N'
AND BOARD_CODE = #{boardCode}
<choose>
<when test='key == "t"'>
<!-- 제목 -->
AND BOARD_TITLE LIKE '%${query}%'
</when>
<when test='key == "c"'>
<!-- 내용 -->
AND BOARD_CONTENT LIKE '%${query}%'
</when>
<when test='key == "tc"'>
<!-- 제목 + 내용 -->
AND (BOARD_TITLE LIKE '%${query}%' OR BOARD_CONTENT LIKE '%${query}%')
</when>
<when test='key == "w"'>
<!-- 작성자(닉네임) -->
AND MEMBER_NICKNAME LIKE '%${query}'
</when>
</choose>
ORDER BY BOARD_NO DESC
</select>
게시판 내 검색창에서 원하는 검색 조건을 선택한 뒤 검색어를 입력해 보자!
나는 '테스트'를 검색해 보았다.
검색 버튼을 클릭하자 검색어가 제목에 포함된 게시글 목록이 조회되는 모습이다.
게시글 검색 기능은
1. paramMap에 담긴 K:V 값을 적재적소에 꺼내 오는 것
2. board-mapper.xml에서 SELECT절을 작성할 때 동적 SQL을 활용하여 상황 별로 알맞게 조건절을 나누어 주는 것
이 두 가지를 유의하는 게 좋겠다! 👍