본문 바로가기
Spring

[웹프로젝트] 13. 게시판 검색

by moca7 2024. 10. 30.

 

 

ㅁ 검색 요청시 검색 결과가 화면에 뿌려진다. 그 검색 결과에도 페이징처리를 해본다.

- 검색 결과를 별도로 search.jsp 등의 페이지로 만들지 않고 /board/list.do를 재활용한다.

 

 

 

 

- 작성자, 제목, 내용으로 검색할 수 있다.

 

 

 

 

 

ㅁ list.jsp

 

 
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
   
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>    
<c:set var="contextPath" value="${pageContext.request.contextPath}" />
   
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>

    <style>
        #boardList th, #boardList td:not(:nth-child(2)){text-align: center;}
        #boardList>tbody>tr:hover{cursor:pointer;}
    </style>

</head>
<body>

    <div class="container p-3">

        <!-- Header, Nav start -->
        <jsp:include page="/WEB-INF/views/common/header.jsp"/>
        <!-- Header, Nav end -->
   
        <!-- Section start -->
        <section class="row m-3" style="min-height: 500px">
   
          <div class="container border p-5 m-4 rounded">
            <h2 class="m-4">게시글 목록</h2>
            <br>

            <!-- 로그인후 상태일 경우만 보여지는 글쓰기 버튼-->
            <a class="btn btn-secondary" style="float:right" href="">글쓰기</a>
            <br>
           
            <br>
            <table id="boardList" class="table table-hover" align="center">
                <thead>
                  <tr>
                    <th width="100px">번호</th>
                    <th width="400px">제목</th>
                    <th width="120px">작성자</th>
                    <th>조회수</th>
                    <th>작성일</th>
                    <th>첨부파일</th>
                  </tr>
                </thead>
                <tbody>
               
                    <c:choose>
                        <c:when test="${ empty list }">
                            <tr>
                                <td colspan="6">조회된 게시글이 없습니다.</td>
                            </tr>
                        </c:when>
                        <c:otherwise>
                            <c:forEach var="b" items="${ list }">
                            <tr>
                                <td>${ b.boardNo }</td>
                                <td>${ b.boardTitle }</td>
                                <td>${ b.boardWriter }</td>
                                <td>${ b.count }</td>
                                <td>${ b.registDt }</td>
                                <td>${ b.attachCount > 0 ? '*' : '' }</td>
                            </tr>
                        </c:forEach>
                       
                    </c:otherwise>

                                    </c:choose>
                </tbody>
            </table>
            <br>

            <ul id="paging_area" class="pagination d-flex justify-content-center">
               
                <li class="page-item ${ pi.currentPage == 1 ? 'disabled' : '' }">
                    <a class="page-link" href="${ contextPath }/board/list.do?page=${pi.currentPage-1}">Previous</a>
                </li>
               
                <c:forEach var="p" begin="${ pi.startPage }" end="${ pi.endPage }">
                    <li class="page-item ${ pi.currentPage == p ? 'active' : '' }">
                        <a class="page-link" href="${contextPath}/board/list.do?page=${ p }">${ p }</a>
                    </li>
                                </c:forEach>
               
                <li class="page-item ${ pi.currentPage == pi.maxPage ? 'disabled' : '' }">
                    <a class="page-link" href="${ contextPath }/board/list.do?page=${pi.currentPage+1}">Next</a>
                </li>
           
            </ul>
           
            <br clear="both"><br>
           
            <form id="search_form" action="${contextPath }/board/search.do" method="get" class="d-flex justify-content-center">
                    <input type="hidden" name="page" value="1">
                <div class="select" >
                    <select class="custom-select" name="condition">
                        <option value="user_id">작성자</option>
                        <option value="board_title">제목</option>
                        <option value="board_content">내용</option>
                    </select>
                </div>
                <div class="text">
                    <input type="text" class="form-control" name="keyword" value="${search.keyword}"> <!-- 검색후 search(map객체)에서 가져옴. el구문 특성상 search가 없어도 오류 안나고 빈값으로 보임. -->
                </div>
                <button type="submit" class="search_btn btn btn-secondary">검색</button>
            </form>
           
            <c:if test="${ not empty search }">                    
                <script>
                    $(document).ready(function(){
                        $("#search_form select").val('${search.condition}');
                       
                        // 검색 후의 페이징바 클릭시 search_form을 강제로 submit(단, 페이지 번호는 현재 클릭한 페이지 번호로 바꿔서)
                        $("#paging_area a").on("click", function(){
                           
                            let page = $(this).text(); // Previous | Next | 페이지번호
                            if(page == 'Previous'){
                                page = ${pi.currentPage - 1};
                            }else if(page == 'Next'){
                                page = ${pi.currentPage + 1};
                            }
                           
                            $("#search_form input[name=page]").val(page);
                            $("#search_form").submit();
                           
                            return false; // 기본이벤트(href='/board/list.do' url 요청)가 동작 안되도록 막는다. 이벤트가지고 pervent 어쩌구를 호출해서 막을 수도 있지만 이벤트 핸들러로  
                           
                        })
                       
                    })
                </script>
            </c:if>
 
          </div>
   
        </section>
        <!-- Section end -->
   
        <!-- Footer start -->
        <jsp:include page="/WEB-INF/views/common/footer.jsp"/>
        <!-- Footer end -->
   
    </div>
   
</body>
</html>
 

 

 

- key값 요청시 전달값 condition과 keyword를 줬다.

- 작성자, 제목, 내용의 key값(value 속성값)을 나중에 비교할 비교대상 컬럼명으로 줬다.

- form 요소 선택을 위해 id를 줬다.

- form 요소 action을 작성했다.

- 검색은 get방식이다.

 

- 검색 결과 중에 몇번 페이지를 요청할 건지 페이지 번호도 넘겨줘야 한다.

검색 submit을 했을 때는 당연히 1번 페이지 요청이다.

- 페이지 번호도 넘기기 위해 hidden 타입 input 요소를 뒀다.

 

 

 

 

ㅁ BoardController

 

package com.br.spring.controller;

import java.util.List;
import java.util.Map;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

import com.br.spring.dto.BoardDto;
import com.br.spring.dto.PageInfoDto;
import com.br.spring.service.BoardService;
import com.br.spring.util.PagingUtil;

import lombok.RequiredArgsConstructor;
 
@RequestMapping("/board")
@RequiredArgsConstructor
@Controller
public class BoardController {
 
  private final BoardService boardService;
  private final PagingUtil pagingUtil;
 
 
  // 메뉴 바에 있는 메뉴 클릭시    /board/list.do => 1번 페이지 요청
  // 페이징 바에 있는 페이지 클릭시 /board/list.do?page=xx
  @GetMapping("/list.do")
  public void list(@RequestParam(value="page", defaultValue="1") int currentPage, Model model) {
   
    int listCount = boardService.selectBoardListCount();
   
    PageInfoDto pi = pagingUtil.getPageInfoDto(listCount, currentPage, 5, 5);
    List<BoardDto> list = boardService.selectBoardList(pi);
   
    model.addAttribute("pi", pi);
    model.addAttribute("list", list);
   
    // return "board/list"; 생략해도 됨.
   
  }
 
   
 
  @GetMapping("/search.do")
  public String search(@RequestParam(value="page") int currentPage
             , @RequestParam Map<String, String> search
             , Model model) {
    // 요청시 전달값을 매개변수를 둬서 받기
    // 지금은 무조건 page라는 key값으로 1번이 올거라 defaultValue를 안썼다. 써도 된다.
    // 요청하는 페이지 번호는 currentPage에 담긴다.
   
    // 지금 condition과 keyword 값을 받아줄 dto 커맨드 객체가 없다. String 형 변수 2개를 둬서 받아도 된다. 근데 어차피 넘길때는 Map에 담아서 넘기도록 서비스 impl를 설계했었다.
    // ajax때 했던 @RequestBody로 Map으로 바로 받을 수 있었다.
    // 아니면 @RequestParam Map<String, String> search로 해도 된다. 이렇게 해본다. 알아서 key과 value값이 담긴다.
    // Map<String, String> search => {condition=user_id|board_Title|board_content, keyword=란}
   
    int listCount = boardService.selectSearchListCount(search);
    PageInfoDto pi = pagingUtil.getPageInfoDto(listCount, currentPage, 5, 5);
    List<BoardDto> list = boardService.selectSearchList(search, pi);
   
    model.addAttribute("pi", pi);
    model.addAttribute("list", list);
    model.addAttribute("search", search); // list.jsp가 로드되는 경우가 2가지 경우다. /search.do라는 요청으로 로드될 때는 search라는 key값으로 Map이 담겨있다.
   
    return "board/list"; // 이미 만든 list.jsp로 포워딩
  }
   
}

 

 

- list.jsp에서 pi와 list에서 뽑아내고 있다. Model 객체에 다음과 같은 key값으로 담ㄱ ㅗ포워딩 해줘야 한다.

- 요청시 전달값이 다수가 넘어올경우 보통 커맨득객체로 바인딩을 한다.

만약 이걸 담을만한 커맨드 객체가 없다면 Map같은걸로 받아낼 수 있다.

어차피 Map에 담아내야 되기 때문ㅇ ㅔMap을 세팅했다.

만약 String condition, String keyword 이렇게 각각의 변수로 요청시 전달값을 바인딩할 수 있다.

그리고 나중에 Map에 put으로 담기.

Map<String, String> search = new HashMap<>();

search.put("condition", condition);

- 스프링에서는 기본적으로 @RequestParam이다. 보이지 않을 뿐이지 다 붙어있다고 보면 된다.

- ajax때는 @RequestBody를 썼다. 그때도 비동기식을 ㅗ전달된 데이터를 Map으로 어쩍 ㅜ할 때 

근데 지금은 동기식방식이기 때문에 저걸 쓰면 안된다.

내부적을 ㅗjson형식으로 온다. 그래서 그걸 풀어서 담기게 해야함. 그걸 잭슨이 하는거고.

그걸 대체시ㅣㅋ는게 @RequestBody다.

- 결론. 비동기식은 @RequestBody를 붙인다. 동기식은 @RequestParam을 붙인다.

 

- Map이랑 String 변수랑 다 두면 Map에도 담기고 String 변수들에도 다 담김?

제일 좋은건 컨트롤러 메소드에 로그를 찍는다. @Slf4j 추가하고.

log.debug("map: {}", search);

log.debug("condition: {}, keyword: {}", condition, keyword);

- 다 담긴다. 

 

 

 

 

 

ㅁ BoardServiceImpl

 

package com.br.spring.service;

import java.util.List;
import java.util.Map;

import org.springframework.stereotype.Service;

import com.br.spring.dao.BoardDao;
import com.br.spring.dto.BoardDto;
import com.br.spring.dto.PageInfoDto;
import com.br.spring.dto.ReplyDto;

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Service
public class BoardServiceImpl implements BoardService {

  private final BoardDao boardDao;
 

  @Override
  public int selectBoardListCount() {
    return boardDao.selectBoardListCount();
  }

  @Override
  public List<BoardDto> selectBoardList(PageInfoDto pi) {
    return boardDao.selectBoardList(pi);
  }

  @Override
  public int selectSearchListCount(Map<String, String> search) {
    return boardDao.selectSearchListCount(search);
  }

  @Override
  public List<BoardDto> selectSearchList(Map<String, String> search, PageInfoDto pi) {
    return  boardDao.selectSearchList(search, pi);
  }

  @Override
  public int insertBoard(BoardDto b) {
    return 0;
  }

  @Override
  public int updateIncreaseCount(int boardNo) {
    return 0;
  }

  @Override
  public BoardDto selectBoard(int boardNo) {
    return null;
  }

  @Override
  public int deleteBoard(int boardNo) {
    return 0;
  }

  @Override
  public List<ReplyDto> selectReplyList(int boardNo) {
    return null;
  }

  @Override
  public int insertReply(ReplyDto r) {
    return 0;
  }
 
}

 

 

 

ㅁ BoardDao

 

package com.br.spring.dao;

import java.util.List;
import java.util.Map;

import org.apache.ibatis.session.RowBounds;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.stereotype.Repository;

import com.br.spring.dto.BoardDto;
import com.br.spring.dto.PageInfoDto;

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
@Repository
public class BoardDao {

  private final SqlSessionTemplate sqlSession;

  public int selectBoardListCount() {
    return sqlSession.selectOne("boardMapper.selectBoardListCount");
  }

  public List<BoardDto> selectBoardList(PageInfoDto pi) {
   
    RowBounds rowBounds = new RowBounds((pi.getCurrentPage() - 1) * pi.getBoardLimit(), pi.getBoardLimit());
   
    return sqlSession.selectList("boardMapper.selectBoardList", null, rowBounds);
  }

  public int selectSearchListCount(Map<String, String> search) {
    return sqlSession.selectOne("boardMapper.selectSearchListCount", search);
  }

  public List<BoardDto> selectSearchList(Map<String, String> search, PageInfoDto pi) {
   
    RowBounds rowBounds = new RowBounds((pi.getCurrentPage() - 1) * pi.getBoardLimit(), pi.getBoardLimit());
   
    return sqlSession.selectList("boardMapper.selectSearchList", search, rowBounds); // 쿼리를 완성시키기 위해 2번째 인자값으로 map객체를 넘김.
  }
   
}

 

- 검색결과에 만족하는 게시글의 갯수가 반환된다.

 

 

 

 

 

ㅁ board-mapper.xml

 

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="boardMapper">

  <resultMap id="boardResultMap" type="BoardDto">
    <result column="board_no" property="boardNo" />
    <result column="board_title" property="boardTitle" />
    <result column="user_id" property="boardWriter" />
    <result column="count" property="count" />
    <result column="regist_date" property="registDt" />
    <result column="attach_count" property="attachCount" />
  </resultMap>


  <select id="selectBoardListCount" resultType="_int">
    select
           count(*)
      from board
     where status = 'Y'      
  </select>


  <select id="selectBoardList" resultMap="boardResultMap">
    select
           b.board_no
         , board_title
         , user_id
         , count
         , to_char(regist_date, 'YYYY-MM-DD') as "regist_date"
         , (
             select count(*)
               from attachment
              where ref_type = 'B'
                and ref_no = b.board_no
            ) as "attach_count"
      from board b
      join member on (user_no = board_writer)
     where b.status = 'Y'
     order by board_no desc
  </select>
 
 
  <select id="selectSearchListCount" resultType="_int">
    select
           count(*)
      from board b
      join member on (user_no = board_writer)
     where b.status = 'Y'  
       and ${condition} like '%' || #{keyword} || '%'
  </select>
 
  <select id="selectSearchList" resultMap="boardResultMap">
    select
           b.board_no
         , board_title
         , user_id
         , count
         , to_char(regist_date, 'YYYY-MM-DD') as "regist_date"
         , (
             select count(*)
               from attachment
              where ref_type = 'B'
                and ref_no = b.board_no
            ) as "attach_count"
      from board b
      join member on (user_no = board_writer)
     where b.status = 'Y'
       and ${condition} like '%' || #{keyword} || '%'
     order by board_no desc
  </select>
 
</mapper>

 

 

 

- 마이바티스 때는 동적쿼리를 사용했었다. 

넘어오는 condition이 3개 중 뭐냐에 따라서 if else처리를 했다. 그렇게 해도 된다.
- 그런데 이번에는 동적쿼리를 사용하지 않으려고 요청시 전달값(검색조건)을 아예 테이블에 실재 존재하는 컬럼명으로 보냈다.  

- board_title, board_content는 board 테이블의 컬럼이다.
그런데 user_id는 board 테이블이 아니라서 조인을 해줘야한다.

 

- #{ }는 그 타입에 맞춰서 값이 채워진다. 숫자면 따옴표 없이, 문자면 따옴표로 감싸서 값이 채워진다.

- 컬럼명에는 #{ }이 아니라 ${ }를 써야 한다. 실제 테이블 내에 존재하는 컬럼명을 지칭하려면 따옴표가 감싸지면 안된다.

오라클에서의 키워드, 테이블명, 컬럼명 등의 메타데이터로 값을 대체시키고자 한다면 ${ }로 값을 채운다.

그럼 어떤 타입이든 따옴표가 감싸지지 않은 채로 채워진다.

 

 

- 첫쿼리: 조인, 조건한줄추가, where 조건절의 status가 중복되어서 board에 b 별칭 부여.

- 두번째: 조건한줄추가

- 쿼리 자체는 검색 결과가 싹 다 조회된다. 

이 중 사용자가 원하는 몇개 데이터만 조회해야한다. RowBounds를 쓰면 된다.

 

 

 

=========================================================

 

 

 

- 그런데 검색을 하면 페이징처리가 잘 되긴 하는데 2번, 3번 등의 페이지를 누르면 검색한게 풀린다.

- 현재 페이징 바는 무조건 list.do를 요청하고 있기 때문이다. 

- 검색 후 list.jsp왔을 대는 search_form을 강제로 submit 시키면 된다. 단 내가 요청한 번호를 넘긴다.

 

- 검색후 결과에서 다른 페이지를 눌러도 search.do로 요청된다. 

 

 

 

 

- 여기서 기본 이벤트는 <a> 태그가 클릭될 때 발생하는 기본 동작을 의미합니다. 보통 <a> 태그는 클릭 시 해당 링크의 href 속성에 지정된 URL로 페이지를 이동합니다. 이 동작을 막기 위해 return false를 사용하거나 event.preventDefault()를 호출할 수 있습니다.

즉, return false는 <a> 태그 클릭 시 발생하는 기본 페이지 이동 동작을 중단하고, 대신 자바스크립트로 정의된 기능(페이징 동작에 맞춰 search_form을 submit)만 실행되도록 합니다.

- 기본 이벤트 중단과 이벤트 전파 중단은 다릅니다. 

기본 이벤트 중단은 요소 자체의 동작을 막는 것이고, 이벤트 전파 중단은 이벤트가 상위 요소로 전달되는 것을 막는 것입니다.

기본 이벤트 중단: 요소가 가지고 있는 고유의 기본 동작을 막는 것입니다. 예를 들어, <a> 태그를 클릭하면 링크로 이동하는 것이 기본 동작인데, 이를 막기 위해 event.preventDefault()나 return false를 사용합니다. 

이벤트 전파 중단: 이벤트가 부모 요소로 전파되는 것을 막는 것입니다. 이벤트는 일반적으로 요소에서 발생하면 상위 부모 요소들로 계속 전파됩니다(버블링). 이 전파를 막기 위해 event.stopPropagation()을 사용합니다. 

 

 

 

- 페이지 깜박거림없이 하고싶으면 지금까지 한 걸 ajax로 바꾸면된다.