[Spring] 게시판 검색 기능 추가하기

Jeini·2023년 5월 31일
0

🍃  Spring

목록 보기
27/33
post-thumbnail

💡 게시판 검색


✔️ 동적쿼리
: 검색할 대상을 뭘로 선택하느냐에 따라서 쿼리가 달라져야 한다.

✔️ 페이지 이동처리
: 검색할 결과에서 어떤 내용을 읽었는데 목록보기를 눌렀을 때 페이징 처리도 신경써야 한다.

  • 기본적으로 선택된 것이 selected
  • optionkeyword의 값을 받아야 함

💡 MyBatis의 동적 쿼리


📎 <sql> & <include>

✔️ 공통 부분을 <sql>로 정의하고 <include>로 포함시켜 재사용

  • <sql>태그를 이용해서 공통 부분을 따로 뺄 수 있다. id로 이름 지정
    ➡️ id값을 이용해서 include하면 공통부분이 안에 들어간다.

📎 <if>

  • 우리가 계속 쓰던 if문이 맞다.

  • 내용과 제목으로 검색할 때는 title하고 content에 OR로 연결해서 검색하게 한다.

  • 제목으로 검색할 때는 title

  • 작성자로 검색할 때는 writer

  • where 조건으로 true 쓴 것은 바로 AND가 오기 때문에 씀
    : true가 없으면 바로 AND가 나오기 때문이다.
    : 조건이 여러개 지정해야 되기 때문에 AND를 뺄 수가 없다. 여러개 연결할 때는 AND가 붙어야 한다. (OR도 마찬가지) 그래서 true를 붙이고 시작한다.

하지만 이건 적합하지 않다. if문은 이 3개 중에 여러개가 조건에 맞을 수 있다. 하지만 우리는 3개 중에 하나만 조건에 맞아야 한다. 그래서 if문 보다는 choose가 더 잘 어울린다.

📎 <choose> & <when> & <otherwise>

  • if-elseif문과 비슷하다.

  • test='option==T' 이면 밑에 test='option=='W' 는 검사하지 않는다.
    ➡️ 효율적임
    : 항상 그런 것은 아니고, 검색 기능에서 이 두개의 옵션이 동시에 들어올 수 없기 때문에 choose가 적합하다는 것

  • 위 두 옵션 둘다 false이면 <otherwise>가 동작된다.
    : <otherwise> = else 같은 느낌

📎 <foreach>

  • WHERE bno in(1,2,3) / WHERE bno = 1 이런식은 값을 알았을 때 쓸 수 있지만 값이 많을 때는 이렇게 쓰기 어렵다. 그럴 때 사용하는 것이 <foreach> 태그 이다.

  • for문 써서 배열로 주면 괄호 안에다가 콤마를 구분자로 해서 만들어 준다.
    ➡️ WHERE bno in(1,2,3) 이렇게 똑같이 만들어 준다.

  • 배열을 받아서 넘겨줘야 처리 가능

✏️ 구현


✔️ 검색 조건들이 모여있는 SerachCondition 객체 생성

package kr.ac.jipark09.domain;

import org.springframework.web.util.UriComponentsBuilder;

// 검색 조건
public class SearchCondition {
    // boardMapper.xml 검색 sql문 보고 들어갈 값 변수에 놓기
    private Integer page = 1; // Controller에서도 쓸 것이기 때문에 넣어준다.
    private Integer pageSize = 10;
//    private Integer offset = 0;
    private String keyword = "";
    private String option = "";

    public SearchCondition() {};

    public SearchCondition(Integer page, Integer pageSize, String keyword, String option) {
        this.page = page;
        this.pageSize = pageSize;
        this.keyword = keyword;
        this.option = option;
    }

    public Integer getPage() {
        return page;
    }

    public void setPage(Integer page) {
        this.page = page;
    }

    public Integer getPageSize() {
        return pageSize;
    }

    public void setPageSize(Integer pageSize) {
        this.pageSize = pageSize;
    }

    public Integer getOffset() {
        return (page - 1) * pageSize;
    }

    public String getKeyword() {
        return keyword;
    }

    public void setKeyword(String keyword) {
        this.keyword = keyword;
    }

    public String getOption() {
        return option;
    }

    public void setOption(String option) {
        this.option = option;
    }

    @Override
    public String toString() {
        return "SearchCondition{" +
                "page=" + page +
                ", pageSize=" + pageSize +
                ", offset=" + getOffset() +
                ", keyword='" + keyword + '\'' +
                ", option='" + option + '\'' +
                '}';
    }
}
  • offset을 getter에 계산식으로 바꿈
    ➡️ mapper의 #{offset} 은 SearchCondition의 getter를 사용한다.

✔️ mapper에 검색에 쓸 sql문 작성

	<!-- 검색에 쓸 sql-->
    <select id="searchSelectPage" parameterType="SearchCondition" resultType="BoardDto">
        SELECT bno, title, content, writer, view_cnt, comment_cnt, reg_date
        FROM board
        WHERE true
        AND title LIKE concat('%', #{keyword}, '%')
        ORDER BY reg_date DESC , bno DESC
            LIMIT #{offset}, #{pageSize}

    </select>

    <!--검색 결과가 몇개가 나왔는지 알아야 페이징을 할 수 있음-->
    <select id="searchResultCnt" parameterType="SearchCondition" resultType="BoardDto">
        SELECT count(*)
        FROM board
        WHERE true
          AND title LIKE concat('%', #{keyword}, '%')
        ORDER BY reg_date DESC , bno DESC
            LIMIT #{offset}, #{pageSize}

    </select>
  • SearchCondition 은 mybatis.config.xml에 alias 등록해 줘야 패키지명 다 안쓰게 할 수 있다.

✔️ BoardDaoImpl에다가 sql문을 호출하는 메서드를 작성

	@Override
    public int searchResultCnt(SearchCondition sc) throws Exception {
        return session.selectOne(namespace + "searchResultCnt", sc);
    }

    @Override
    public List<BoardDto> searchSelectPage(SearchCondition sc) throws Exception {
        return session.selectList(namespace + "searchSelectPage", sc);
    }

✔️ test 하기

 	@Test
    public void searchSelectPageTest() throws Exception {
        boardDao.deleteAll();
        for (int i = 1; i <= 20; i++) {
            BoardDto boardDto = new BoardDto("title" + i, "asfadf", "asdf");
            boardDao.insert(boardDto);
        }

        SearchCondition sc = new SearchCondition(1, 10, "title2", "T");
        List<BoardDto> list = boardDao.searchSelectPage(sc);
        System.out.println("list=" + list);

        assertTrue(list.size() == 2);

    }

    @Test
    public void searchResultCntTest() throws Exception {
        boardDao.deleteAll();
        for (int i = 1; i <= 20; i++) {
            BoardDto boardDto = new BoardDto("title" + i, "asfadf", "asdf");
            boardDao.insert(boardDto);
        }

        SearchCondition sc = new SearchCondition(1, 10, "title2", "T");
        int result = boardDao.searchResultCnt(sc);
        assertTrue(result == 2);

    }

✔️ BoardServiceImpl 추가

	@Override
    public List<BoardDto> getSearchSelectPage(SearchCondition sc) throws Exception {
        return boardDao.searchSelectPage(sc);
    }

    @Override
    public int getSearchResultCnt(SearchCondition sc) throws Exception {
        return boardDao.searchResultCnt(sc);
    }

✔️ BoardController 수정:

✏️ before

	@GetMapping("/list")
    public String list(@RequestParam(defaultValue = "1") Integer page,
                       @RequestParam(defaultValue = "10") Integer pageSize,
                       String option, String keyword,  Model model, HttpServletRequest request) {
        if(!loginCheck(request))
            return "redirect:/login/login?toURL=" + request.getRequestURL();  // 로그인을 안했으면 로그인 화면으로 이동

        try {
            // pageHandler 활용
            int totalCnt = boardService.getCount();
            PageHandler pageHandler = new PageHandler(totalCnt, page, pageSize);

            if(page < 0 || page > pageHandler.getTotalPage()) {
                page = 1;

            } if(pageSize < 0 || pageSize > 20) {
                pageSize = 10;
            }

            // page, pageSize를 파라미터로 받아서 offset과 pageSize를 map에 저장
            Map map = new HashMap();
            map.put("offset", (page - 1) * pageSize);
            map.put("pageSize", pageSize);

            List<BoardDto> list = boardService.getPage(map);
            model.addAttribute("list", list); // jsp로 보냄
            model.addAttribute("ph", pageHandler); // jsp에서 pageHandler를 가지고 페이지를 나타냄
            model.addAttribute("page", page);
            model.addAttribute("pageSize", pageSize);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return "boardList"; // 로그인을 한 상태이면, 게시판 화면으로 이동
    }

  • 검색 조건들에서 파라미터로 받을 게 너무 많아진다.
    ➡️ SerachCondition 으로 묶은 큰 이유

✏️ after

 	@GetMapping("/list")
    public String list(SearchCondition sc, Model model, HttpServletRequest request) {
        if(!loginCheck(request))
            return "redirect:/login/login?toURL=" + request.getRequestURL();  // 로그인을 안했으면 로그인 화면으로 이동

        try {
            // pageHandler 활용
            int totalCnt = boardService.getSearchResultCnt(sc);
            model.addAttribute("totalCnt", totalCnt);

            PageHandler pageHandler = new PageHandler(totalCnt, sc);

            List<BoardDto> list = boardService.getSearchSelectPage(sc);
            model.addAttribute("list", list); // jsp로 보냄
            model.addAttribute("ph", pageHandler); // jsp에서 pageHandler를 가지고 페이지를 나타냄
        } catch (Exception e) {
            e.printStackTrace();
        }
        return "boardList"; // 로그인을 한 상태이면, 게시판 화면으로 이동
    }

  • 간단해진 파라미터
  • SearchCondition 앞에는 @ModelAttribute 가 생략되어 있다.

✔️ PageHandler 수정: SerachCondition으로 묶었으니 바꿔줘야 함

✏️ before

public class PageHandler {

    private int totalCnt; // 총 게시물 갯수
    private int pageSize; // 한 페이지의 크기
    private int naviSize = 10; // 페이지 내비게이션의 크기
    private int totalPage; // 전체 페이지의 갯수
    private int page; // 현재 페이지
    private int beginPage; // 내비게이션의 첫 번째 페이지
    private int endPage; // 내비게이션의 마지막 페이지
    private boolean showPrev; // 이전 페이지로 이동하는 링크를 보여줄 것인지의 여부 (1페이지만 있을 때는 있으면 안되니까)
    private boolean showNext; // 다음 페이지로 이동하는 링크를 보여줄 것인지의 여부 (10의 배수로 채워지지 않았을 때 있으면 안되니까)

    // totalCnt와 page만 받아울 경우 pageSize는 기본 10으로 설정
    public PageHandler(int totalCnt, int page) {
        this(totalCnt, page, 10);
    }

    // 계산을 해서 페이지에 보여줄 화면을 구성
    public PageHandler(int totalCnt, int page, int pageSize) {
        this.totalCnt = totalCnt;
        this.page = page;
        this.pageSize = pageSize;

        // totalPage = 전체게시물 / 10 (올림) => pageSize 정수 주의!!! 정수랑 정수랑 나눠서 소수점 x
        totalPage = (int)Math.ceil(totalCnt / (double)pageSize);
        // 25 / 10 * 10 + 1 = 11
        // (page - 1) => 10 / 10 * 10 + 1 = 11되버림. 10의 자리는 그 따음페이지로 넘어가면 안됨. 10의 자리때문에 -1 해줘야함
        beginPage = (page - 1) / naviSize * 10 + 1;
        endPage = Math.min(beginPage + naviSize - 1, totalPage);
        showPrev = beginPage != 1;
        showNext = endPage != totalPage;
    }
    ...

✏️ after

package kr.ac.jipark09.domain;

public class PageHandler {
    // 검색
    private SearchCondition sc;
//    private int page; // 현재 페이지
//    private int pageSize; // 한 페이지의 크기
//    private String option;
//    private String keyword;

    private int totalCnt; // 총 게시물 갯수
    private int naviSize = 10; // 페이지 내비게이션의 크기
    private int totalPage; // 전체 페이지의 갯수
    private int beginPage; // 내비게이션의 첫 번째 페이지
    private int endPage; // 내비게이션의 마지막 페이지
    private boolean showPrev; // 이전 페이지로 이동하는 링크를 보여줄 것인지의 여부 (1페이지만 있을 때는 있으면 안되니까)
    private boolean showNext; // 다음 페이지로 이동하는 링크를 보여줄 것인지의 여부 (10의 배수로 채워지지 않았을 때 있으면 안되니까)

    public PageHandler(int totalCnt, SearchCondition sc) {
        setTotalCnt(totalCnt);
        this.sc = sc;

        doPaging(totalCnt, sc);
    }

    // 계산을 해서 페이지에 보여줄 화면을 구성
    public void doPaging(int totalCnt, SearchCondition sc) {
        this.totalCnt = totalCnt;

        totalPage = (int)Math.ceil(totalCnt / (double)sc.getPageSize());
        beginPage = (sc.getPage() - 1) / naviSize * 10 + 1;
        endPage = Math.min(beginPage + naviSize - 1, totalPage);
        showPrev = beginPage != 1;
        showNext = endPage != totalPage;
    }

    public int getTotalCnt() {
        return totalCnt;
    }

    public void setTotalCnt(int totalCnt) {
        this.totalCnt = totalCnt;
    }

    public int getNaviSize() {
        return naviSize;
    }

    public void setNaviSize(int naviSize) {
        this.naviSize = naviSize;
    }

    public int getTotalPage() {
        return totalPage;
    }

    public void setTotalPage(int totalPage) {
        this.totalPage = totalPage;
    }

    public int getBeginPage() {
        return beginPage;
    }

    public void setBeginPage(int beginPage) {
        this.beginPage = beginPage;
    }

    public int getEndPage() {
        return endPage;
    }

    public void setEndPage(int endPage) {
        this.endPage = endPage;
    }

    public boolean isShowPrev() {
        return showPrev;
    }

    public void setShowPrev(boolean showPrev) {
        this.showPrev = showPrev;
    }

    public boolean isShowNext() {
        return showNext;
    }

    public void setShowNext(boolean showNext) {
        this.showNext = showNext;
    }

    // 페이지 네비게이션을 프린트하는 메서드
    public void print() {
        System.out.println("page = " + sc.getPage());
        System.out.print(showPrev ? "[PREV] " : "");
        for(int i = beginPage; i <= endPage; i++) {
            System.out.print(i + " ");
        }
        System.out.println(showNext ? "[NEXT]" : "");
    }

    @Override
    public String toString() {
        return "PageHandler{" +
                "sc=" + sc +
                ", totalCnt=" + totalCnt +
                ", naviSize=" + naviSize +
                ", totalPage=" + totalPage +
                ", beginPage=" + beginPage +
                ", endPage=" + endPage +
                ", showPrev=" + showPrev +
                ", showNext=" + showNext +
                '}';
    }
}
  • 생성자 바뀜
  • 페이지 계산하는 것을 생성자에서 doPaging() 메서드로 바꿈

✔️ SearchCondition에 쿼리스트링 자동완성 메서드를 만듬

	// 페이지를 줘서 해당 페이지에 대한 네비게이션도 해줘야 함
    public String getQueryString(Integer page) {
        // ?page=1&pageSize=10&option=T&keyword="title" -> 일일이 치기 귀찮음 이렇게 만들어주는 메서드를 만들어줌
        return UriComponentsBuilder.newInstance()
                .queryParam("page", page)
                .queryParam("pageSize", pageSize)
                .queryParam("option", option)
                .queryParam("keyword", keyword)
                .build().toString();

    }

    // 쿼리스트링 처리 부분
    public String getQueryString() {
        return getQueryString(page);
    }
  • getQueryString()
    : BoardController가 매개변수로 SearchCondition에서 멤버변수를 다 받는다. 검색 내용 결과를 봤다가 목록으로 돌아갈 때 쿼리스트링으로 이 값들이 유지를 해야 한다. 이 값들을 하나하나 처리하는 것이 번거롭기 때문에 메서드를 만들어 준다.

✔️ BoardMapper의 쿼리 수정 ➡️ 동적 쿼리로 바꾸기

 <!-- 검색에 쓸 sql-->
    <select id="searchSelectPage" parameterType="SearchCondition" resultType="BoardDto">
        SELECT bno, title, content, writer, view_cnt, comment_cnt, reg_date
        FROM board
        WHERE true
          <choose>
              <when test='option == "T"'>
                  AND title LIKE concat('%', #{keyword}, '%')
              </when>
              <when test='option == "W"'>
                  AND writer LIKE concat('%', #{keyword}, '%')
              </when>
              <otherwise>
                  AND (title LIKE concat('%', #{keyword}, '%')
                        OR content LIKE concat('%', #{keyword}, '%'))
              </otherwise>
          </choose>
        AND title LIKE concat('%', #{keyword}, '%')
        ORDER BY reg_date DESC , bno DESC
            LIMIT #{offset}, #{pageSize}

    </select>

    <!--검색 결과가 몇개가 나왔는지 알아야 페이징을 할 수 있음-->
    <select id="searchResultCnt" parameterType="SearchCondition" resultType="int">
        SELECT count(*)
        FROM board
        WHERE true
        <choose>
            <when test='option == "T"'>
                AND title LIKE concat('%', #{keyword}, '%')
            </when>
            <when test='option == "W"'>
                AND writer LIKE concat('%', #{keyword}, '%')
            </when>
            <otherwise>
                AND (title LIKE concat('%', #{keyword}, '%')
                OR content LIKE concat('%', #{keyword}, '%'))
            </otherwise>
        </choose>
    </select>
  • T가 문자열이니까 쌍따옴표로 써준다.

✔️ 중복 sql문 수정: <sql><input>

	<!--중복제거 -->
    <sql id ="searchCondition">
        <choose>
            <when test='option == "T"'>
                AND title LIKE concat('%', #{keyword}, '%')
            </when>
            <when test='option == "W"'>
                AND writer LIKE concat('%', #{keyword}, '%')
            </when>
            <otherwise>
                AND (title LIKE concat('%', #{keyword}, '%')
                OR content LIKE concat('%', #{keyword}, '%'))
            </otherwise>
        </choose>
    </sql>

    <!-- 검색에 쓸 sql-->
    <select id="searchSelectPage" parameterType="SearchCondition" resultType="BoardDto">
        SELECT bno, title, content, writer, view_cnt, comment_cnt, reg_date
        FROM board
        WHERE true
        <include refid="searchCondition"/>
            ORDER BY reg_date DESC , bno DESC
            LIMIT #{offset}, #{pageSize}

    </select>

    <!--검색 결과가 몇개가 나왔는지 알아야 페이징을 할 수 있음-->
    <select id="searchResultCnt" parameterType="SearchCondition" resultType="int">
        SELECT count(*)
        FROM board
        WHERE true
        <include refid="searchCondition"/>
    </select>

💡 out 태그


게시판 글쓰기에서 자바스크립트 코드를 넣으면 실행이 된다. 잘못해서는 보안에 우려가 생긴다. 이를 방지하기 위해 out태그를 사용한다.

  • 여기에 코드를 넣게 하면 안되며 보여주기 할 때도 저렇게 보여주면 안된다.
  • core 태그에 있는 라이브러리를 사용해야 한다.
<c:out value=' ${boardDto.title}'/>
<c:out value=" ${boardDto.content}"/>
  • 안에 있는 내용을 자동으로 변환시켜줌

  • 코드가 아닌 텍스트 형식으로 바꼈다. 안전쓰~
  • 보이는 것은 잘 보이는데 실행이 되질 않는다.
  • 해커들의 악의적인 공격으로부터 해결할 수 있다.

Reference
: https://fastcampus.co.kr/dev_academy_nks

profile
Fill in my own colorful colors🎨

0개의 댓글