검색 기능, 목록으로 기능

oYJo·2025년 3월 14일

Spring

목록 보기
26/29

검색 기능

1️⃣ Visual Studio Code

✔️webapp(WEB-INF(views(common))) → 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>
  ...

✔️webapp(WEB-INF(views(board))) → 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>

✔️webapp(resources(js(board))) → 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 : 쿼리스트링을 제외한 실제 주소
  }
})



location.pathname 을 보면 “/board/1”이 있다


2️⃣ Spring

✔️project(board(controller)) → 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";
	}
...

✔️project(board(model(service))) → BoardService.java

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

✔️project(board(model(service))) → 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;
	}

✔️project(board(model(dao))) → 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);
		
	}

✔️resources(mappers) → board-mapper.xml

...
<!-- 특정 게시판의 삭제되지 않고 검색 조건이 일치하는 게시글 수 조회 -->
<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️⃣ Visual Studio Code

✔️webapp(WEB-INF(views(board))) → boardDetail.jsp

...
<!-- 버튼 영역-->
<div class="board-btn-area">

  <!-- 로그인한 회원과 게시글 작성자 번호가 같은 경우-->
  <c:if test="${loginMember.memberNo == board.memberNo}">
    <button id="updateBtn">수정</button>
    <button id="deleteBtn">삭제</button>
  </c:if>

  <button id="goToListBtn">목록으로</button>
</div>
...

<script>

  const boardNo = "${board.boardNo}";

  const loginMemberNo = "${loginMember.memberNo}";

  // 게시판 코드 전역 변수로 선언
  const boardCode = "${boardCode}";
</script>

...

✔️webapp(WEB-INF(views(board))) → boardList.jsp

...
<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}${sp}">${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>
...

✔️webapp(resources(js(board))) → boardList.js

...
// 목록으로
const goToListBtn = document.getElementById("goToListBtn");

goToListBtn.addEventListener("click", ()=>{

  // 이동할 주소 저장
  let url = "/board/" + boardCode;

  const params = new URL(location.href).searchParams;

  let cp;
  if(params.get("cp") != ""){ // 쿼리스트링에 cp가 있을 경우
    cp = "?cp=" + params.get("cp");
  } else {
    cp = "?cp=1";
  }

  // 조립
  url += cp;

  // 검색 key, query가 존재하는 경우 url에 추가
  if(params.get("key") != null){
    const key = "&key=" + params.get("key");
    const query = "&query=" + params.get("query");

    url += key + query;
  }

  // location.href = "주소"; -> 해당 주소로 이동
  location.href = url;

})

profile
Hello! My Name is oYJo

0개의 댓글