10/03/2019

스프링 게시판 만들기 #13. 검색 구현 2

지난번 게시물에서 언급된 문제는 다음과 같습니다.

1. 페이지 번호를 클릭하여 페이지를 이동하면, 검색조건과 검색어가 없어짐
2. 검색결과와 무관하게 페이징이 생성됨

1번 문제는 검색조건과 검색어를 유지할 수 있는 코드 자체가 없기 때문에 생긴 문제고, 2번은 페이징 기능에서 전체 게시물 갯수를 가져올 때 조건을 두지 않아서 생긴 문제입니다.

먼저 페이징 부분부터 작업하겠습니다.

BoardController에서 page.setCount(service.count()); 가 페이징을 만들때 게시물의 갯수를 구하는 메서드입니다.

매퍼를 보시면 이처럼, 아무런 조건이 없이 게시물 전체 갯수를 가져오도록 되어있습니다.

이렇게 조건을 넣어주면, 조건에 맞는 게시물만 가져오므로 갯수가 달라지게 됩니다.

그렇다면, 매퍼에서 게시물 총 갯수를 구하는 쿼리문에 게시물 검색과 동일한 조건문을 넣으면 될 것 같습니다.

<!-- 게시물 총 갯수 + 검색 적용 -->
<select id="searchCount" parameterType="hashMap" resultType="int">
 select count(bno) from tbl_board  
  
 <if test='searchType.equals("title")'>
  WHERE title LIKE concat('%', #{keyword}, '%')
 </if>
 
 <if test='searchType.equals("content")'>
  WHERE content LIKE concat('%', #{keyword}, '%')
 </if>
 
 <if test='searchType.equals("title_content")'>
  WHERE title LIKE concat('%', #{keyword}, '%') 
   or content LIKE concat('%', #{keyword}, '%')
 </if>
 
 <if test='searchType.equals("writer")'>
  WHERE writer LIKE concat('%', #{keyword}, '%')
 </if>
 
</select>

조건을 추가하면서, searchType과 keyword를 받아야하므로 파라미터 타입(parameterType)를 추가했습니다.

BoardDAO에 코드를 추가합니다.

// 게시물 총 갯수 + 검색 적용
public int searchCount(String searchType, String keyword) throws Exception;

BoardDAOImpl에 코드를 추가합니다.

// 게시물 총 갯수 + 검색 적용
@Override
public int searchCount(String searchType, String keyword) throws Exception {
 
 HashMap data = new HashMap();
 
 data.put("searchType", searchType);
 data.put("keyword", keyword);
 
 return sql.selectOne(namespace + ".searchCount", data); 
}

검색에 필요한 searchType과 keyword를 받고 있습니다.

BoardService에 코드를 추가합니다.

// 게시물 총 갯수 + 검색 적용
public int searchCount(String searchType, String keyword) throws Exception;

BoardServiceImpl에 코드를 추가합니다.

// 게시물 총 갯수
@Override
public int searchCount(String searchType, String keyword) throws Exception {
 return dao.searchCount(searchType, keyword);
}

page.setCount(page.count()); 는 이제 사용하지 않으니 주석처리하고 방금 추가한 코드를 추가합니다.

page.setCount(service.searchCount(searchType, keyword));

이제 검색을 하면, 검색조건과 검색어에 맞도록 페이지 번호가 생성됩니다.

하지만 여전히 검색조건과 검색어는 텅텅 비어있습니다. 지금 상황에서 자기가 검색한 내용을 확인하려면 url을 확인해야하네요.

만약, 페이징에 대해서 잘 이해가 안되시는분은 스프링 게시판 만들기 #9. 페이징 구현 1 또는 다른 블로그의 글을 참조하시면 되겠습니다.

다음은 검색조건과 검색어를 입력한 그대로 유지하고 페이지 번호에 검색조건과 검색어를 추가하여, 다른 페이지로 이동하더라도 검색 상태가 유지되도록 작업하겠습니다.

model.addAttribute("searchType", searchType);
model.addAttribute("keyword", keyword);

검색한 후, 검색조건과 검색어를 사용자가 입력한대로 유지하는 방법은 간단합니다.
모델을 이용하여 입력받은 검색조건과 검색어를 그대로 뷰(jsp)에 되돌려 줍니다.

jstl을 이용하여 셀렉트에는 selected 를 추가할 수 있도록, 검색어 입력란에는 입력한 검색어가 그대로 입력되도록 합니다.

<div>
 <select name="searchType">
     <option value="title" <c:if test="${searchType eq 'title'}">selected</c:if>>제목</option>
        <option value="content" <c:if test="${searchType eq 'content'}">selected</c:if>>내용</option>
     <option value="title_content" <c:if test="${searchType eq 'title_content'}">selected</c:if>>제목+내용</option>
     <option value="writer" <c:if test="${searchType eq 'writer'}">selected</c:if>>작성자</option>
 </select>
 
 <input type="text" name="keyword" value="${page.keyword}"/>
 
 <button type="button" id="searchBtn">검색</button>
</div>

jstl문법중 if문이 나온김에 간단하게 설명하겠습니다.

<c:if test="${page.searchType eq 'title'}">selected</c:if> test 내부에는 조건이 들어가고, 이 조건이 참인 경우 <c:if> ~ </c:if> 사이에 있는 문자인 selected를 출력, 거짓인 경우 아무것도 출력하지 않습니다.

지금은 조건이 ${page.searchType eq 'title'} 인데, 이는 page.searchType와 문자열 title가 같은지(equals) 확인하는것 입니다. jstl에서는 .equals(); 대신 eq 로 짧게 사용합니다.

즉, 검색 조건을 제목으로 한 경우, 컨트롤러로 전송되는 searchType의 값은 title이며, 이 값이 다시 뷰(jsp)로 전송되면, searchType의 값과 같은 값을 가진 option태그에 selected가 생겨서 선택된 상태가 됩니다.

검색어인 ${keyword}는 그냥 입력한 그대로 출력하니 따로 설명할 필요가 없겠습니다.

실제로 잘 동작되는지 확인하기 위해, 검색조건은 '제목+내용' 검색어는 '제목1'로 입력하고 검색 버튼을 클릭하면

검색조건과 검색어가 모두 유지되는걸 확인할 수 있습니다.

이렇게 사용자가 입력한 데이터를 그대로 되돌려주는것으로 어렵지않게 검색조건과 검색어를 유지할 수 있습니다.

하지만, 페이지 번호에는 여전히 검색조건과 검색어가 들어가있지 않습니다.

첫번째 페이지만 검색되는 검색이라니... 쓸모가 없습니다.

다른 페이지 번호에도 검색이 유지되려면 어떻게해야할지 확인할 필요가 있습니다.

검색결과가 나온 페이지에서는 url 뒤쪽에 &searchType=title&keyword=제목2 가 입력되었는데, 페이지 번호의 링크는 검색조건과 검색어가 없습니다.

검색조건과 검색어는 사용자가 입력하는걸 그대로 사용하니, 저 뒤에붙는 문자열만 추가하면 될 것 같습니다.

Page.java 파일에 변수와 메서드를 추가합니다.

private String searchTypeKeyword; 

public void setSearchTypeKeyword(String searchType, String keyword) {
 
 if(searchType.equals("") || keyword.equals("")) {
  searchTypeKeyword = ""; 
 } else {
  searchTypeKeyword = "&searchType=" + searchType + "&keyword=" + keyword; 
 }  
}

public String getSearchTypeKeyword() {
 return searchTypeKeyword;
}

이름짓는 기술 좀 생겼으면 좋겠네요..

setSearchTypeKeyword(); 메서드를 이용해, 검색조건(searchType)과 검색어(keyword)가 공란("")이 아니라면, url 뒤에 들어갈 &searchType=[검색조건]&keyword=[검색어] 문자열을 만듭니다.

// 검색 타입과 검색어
page.setSearchTypeKeyword(searchType, keyword);

이제 컨트롤러에 setSearchTypeKeyword(); 메서드를 호출하며, 필요한 매개변수인 searchType과 keyword를 넣어줍니다.

setSearchTypeKeyword(); 를 통해 만들어진 문자열은 page내부에 있으므로, 만들어진 문자열이 각 페이지 번호 url의 뒤에 들어갈 수 있도록 입력해줍니다.

<c:if test="${page.prev}">
 <span>[ <a href="/board/listPageSearch?num=${page.startPageNum - 1}${page.searchTypeKeyword}">이전</a> ]</span>
</c:if>

<c:forEach begin="${page.startPageNum}" end="${page.endPageNum}" var="num">
 <span>
 
  <c:if test="${select != num}">
   <a href="/board/listPageSearch?num=${num}${page.searchTypeKeyword}">${num}</a>
  </c:if>    
  
  <c:if test="${select == num}">
   <b>${num}</b>
  </c:if>
    
 </span>
</c:forEach>

<c:if test="${page.next}">
 <span>[ <a href="/board/listPageSearch?num=${page.endPageNum + 1}${page.searchTypeKeyword}">다음</a> ]</span>
</c:if>

이제 프로젝트를 재시작하고 검색을 한 뒤, 페이지 번호가 어떻게 되었는지 확인해봅니다.

검색조건과 검색어가 모두 포함된걸 확인할 수 있습니다.

페이지를 넘겨도 검색조건과 검색어가 유지됩니다.

그런데 말입니다...

.setSearchTypeKeyword() 메서드에 검색조건(searchType)과 검색어(keyword)가 사용되는데, 굳이 모델에다가 또 검색조건(searchType)과 검색어(keyword)를 넣을 필요가 있을까요?

저대로 사용해도 문제될건 없지만, 어차피 Page.java에 검색조건(searchType)과 검색어(keyword)가 들어가니, 굳이 모델에 넣어줄 필요는 없어보입니다.

Page.java에 searchType과 keyword를 추가하고, getsset와 setter를 추가합니다.
그리고 기존에 사용한 searchTypeKeyword.setSearchTypeKeyword() 는 주석처리해주고, getSearchTypeKeyword() 를 수정합니다.

public String getSearchTypeKeyword() {
 
 if(searchType.equals("") || keyword.equals("")) {
  return ""; 
 } else {
  return "&searchType=" + searchType + "&keyword=" + keyword; 
 }
}

private String searchType;
private String keyword; 

public void setSearchType(String searchType) {
 this.searchType = searchType;  
}

public String getSearchType() {
 return searchType;
} 

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

public String getKeyword() {
 return keyword;
}

searchType과 keyword는 평범하게 입력받는것이고, .setSearchTypeKeyword()는 입력받은 searchType과 keyword가 공란("")이라면 공란을 그대로 반환(return)하고, 공란("")이 아닌 경우 url에 추가될 문자열을 반환(return)합니다.

BoardController에서, 이제 사용하지 않는 page.setSearchTypeKeyword(searchType, keyword);, model.addAttribute("searchType", searchType);, model.addAttribute("keyword", keyword); 는 주석처리하고

page에 searchType과 keyword를 넣어주는 코드를 작성해줍니다.

page.setSearchType(searchType);
page.setKeyword(keyword);

<div>
 <select name="searchType">
     <option value="title" <c:if test="${page.searchType eq 'title'}">selected</c:if>>제목</option>
        <option value="content" <c:if test="${page.searchType eq 'content'}">selected</c:if>>내용</option>
     <option value="title_content" <c:if test="${page.searchType eq 'title_content'}">selected</c:if>>제목+내용</option>
     <option value="writer" <c:if test="${page.searchType eq 'writer'}">selected</c:if>>작성자</option>
 </select>
 
 <input type="text" name="keyword" value="${page.keyword}"/>
 
 <button type="button" id="searchBtn">검색</button>
</div>

이제 searchType과 keyword는 page에서 불러올 수 있으므로, 앞에 page. 만 추가해주면 됩니다.

검색조건과 검색어를 입력하고 검색 후 페이지를 이동하면, 검색조건과 검색어는 그대로 유지되어있으며 각 페이지 번호의 url에도 검색조건과 검색어가 잘 입력된걸 확인할 수 있습니다.

게시물 수정
  1. 20.07.09 15:00 게시판 수료 완료하였습니다.

    좋은 게시물 남겨주셔서 감사드립니다.
    아무것도 모르는 상태에서 진행해도 참 따라하기 쉽게 설명해주셔서 많은 도움이 된 것 같습니다.

    감사합니다.

    답글삭제
    답글
    1. 안녕하세요? 방문해주셔서 감사합니다.

      저야말로 감사하고, 되도록이면 빨리-_- 다음 글을 작성하겠..습니다..ㅠㅠ

      삭제
  2. 익명8/04/2020

    감사합니다!! 정말 잘 보고 잘 따라했습니다. 바쁜일 끝내시고 글 올려주시면 감사히 공부하겠습니다!

    답글삭제
    답글
    1. 안녕하세요? 방문해주셔서 감사합니다.

      넵.. 조만간..올리겠습니더..ㅠㅠ

      삭제
  3. input type="text" name="keyword" value="${page.keyword}"

    이 부분에 page.을 빼고 실행하니 에러가 사라지네요 빼고 수행 하는 게 맞겠죠??

    답글삭제
    답글
    1. 저도 같은문제입니다. input 에 value 를 ${page.keyword} 로 작성했을 경우
      javax.el.PropertyNotFoundException: 타입 [com.board.domain.Page]에서 프로퍼티 [keyword]을(를) 찾을 수 없습니다.
      오류가 발생해서 page. 제거후 프로젝트 재시작 해보면 아무 문제없이 페이징이나 searchType 값, keyword 값유지 되더라구요!

      삭제
    2. 익명8/22/2023

      와 ................. 주인장님 말고도 은인이십니다.. 정말 감사합니다.. 덕분에 해결했어요!!

      삭제
  4. 익명5/03/2021

    공부하는데 정말 큰 도움이 되고 있습니다!
    그런데 한가지 궁금한게 있는데요~ 마지막 쯤 Page.java에서
    SearchTypeKeyword와.setSearchTypeKeyword을 주석처리하고
    .getSearchTypeKeyword에서 조건에 따라 리턴값을 설정해두셨는데

    여기서 SearchTypeKeyword를 주석처리했는데 listPageSearch.jsp에서
    ${page.searchTypeKeyword } 값이 어떻게해서 출력되는지 알고 싶습니다.

    page.java에서 private String searchTypeKeyword 를 주석처리하면
    jsp에서 호출이 안되는 줄 알았는데 되는게 신기하더라구요 !

    답글삭제
    답글
    1. 안녕하세요? 방문해주셔서 감사합니다.

      본문 후반부쯤에 언급되는 내용인데, page 라는 객체에 필요한 데이터가 모두 담겨져있기 때문에 가능합니다.

      기존에는 검색 타입과 검색어를 하나하나 정성스럽게(...) 모델에 넣어줬지만
      바뀐 코드는 page라는 객체 내부에 페이징에 필요한 모든 데이터를 넣어서 한번에 보내주었기 때문에 사용할 수 있습니다.

      삭제
    2. 익명5/10/2021

      오랜만에 보는 잘 설명된 코딩 블로그네요.

      저도 따라 해보다가 질문자 분과 같은 의문이 생겨서 살펴봤는데

      page.java에서 private String searchTypeKeyword는 주석 처리가 되었지만

      public String getSearchTypeKeyword로 정의가 되어있어서 searchTypeKeyword로 return값만 받아오도록

      처리가 되어있기 때문에 결국 String searchTypeKeyword = "if문 처리 후 리턴 값"

      이라고 정의 되었다고 보시면 됩니다.

      삭제
  5. 글에 하트 기능이 없어서 아쉽네요.

    답글삭제
    답글
    1. 안녕하세요? 방문해주셔서 감사합니다.

      추천 기능같은거라면..
      로그인 구현 이후로 계획은 하고-_-; 있습니다.
      언제가 될지 모르겠지만요-_ㅠ...

      삭제
  6. 익명8/22/2023

    감사합니다~~~~~~~~~~~~!!!!!!!

    답글삭제
  7. 익명9/21/2023

    안녕하세요 덕분에 검색 기능 잘 따라하고 있습니다!

    그런데 저는 검색할 때
    db에 데이터를 추가 후 검색하면 서치타입에서 검색이 되는 키워드가 있고 되지 않는 키워드가 있더라구요

    또한 위 상황에서 똑같은 데이터를 새로 db에 추가하면 검색이 잘 됩니다 ㅠㅠ
    왜 이럴까요??

    답글삭제