게시판 - 글 검색 기능 구현 (23.08.31)

·2023년 8월 31일
1

Spring

목록 보기
27/36
post-thumbnail

🌷 게시글 검색


이번 포스팅에서는 게시글 검색 기능을 구현해 보자.
검색 후 게시글 목록 조회는 이전에 만들어 놓았던 메소드를 활용하면 된다!

이전 포스팅 다시 보기
게시글 목록 조회 기능 (23.08.21)


👀 코드로 살펴보기

🌼 VS Code

현재 구현 중인 웹 사이트에서는 메인 페이지 상단 / 게시판 내부 하단 이렇게 두 개의 검색창이 있다.

메인 페이지 상단의 검색창에서는 boardCode는 1(공지사항 게시판), 검색 조건은 '제목'으로 간단하게 지정하여 구현해 볼 것이다!

두 번째로 게시판 내에서 게시글을 검색할 때는 select 태그로 '제목', '내용', '제목+내용', '작성자' 옵션을 각각 구분하여 좀 더 상세한 검색이 가능하게 구현해 보자.

🌱 header.jsp (메인 페이지 상단 검색창)

...
            <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>
...

🌱 boardList.jsp (게시판 내 검색창)

<%@ 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}">&lt;&lt;</a></li>

                    <!-- 이전 목록 마지막 번호로 이동 -->
                    <li><a href="/board/${boardCode}?cp=${pagination.prevPage}${sp}">&lt;</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}">&gt;</a></li>

                    <!-- 끝 페이지로 이동 -->
                    <li><a href="/board/${boardCode}?cp=${pagination.maxPage}${sp}">&gt;&gt;</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">&times;</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>

🌱 boardList.js

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 : 쿼리스트링을 제외한 실제 주소
    }
})

🌼 Spring

기존 '게시글 목록 조회' 메소드에서 검색어가 있을 때와 검색어가 없을 때로 조건을 나눈 뒤 상황에 알맞게 조금 수정해 보았다.

검색어가 있다는 것은 즉 유저가 검색을 한 경우를 의미한다.

🌱 BoardController.java

...
	// 게시글 목록 조회
	@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";
	}
...

🌱 BoardService.java

...
	/** 게시글 목록 조회 (검색)
	 * @param paramMap
	 * @param cp
	 * @return boardList
	 */
	Map<String, Object> selectBoardList(Map<String, Object> paramMap, int cp);

🌱 BoardServiceImpl.java

...
	// 게시글 목록 조회 (검색)
	@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;
	}

🌱 BoardDAO2.java

...
	/** 게시글 수 조회 (검색)
	 * @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);
		
	}

🌱 board-mapper.xml

동적 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을 활용하여 상황 별로 알맞게 조건절을 나누어 주는 것

이 두 가지를 유의하는 게 좋겠다! 👍

profile
풀스택 개발자 기록집 📁

0개의 댓글