9/28/2019

스프링 게시판 만들기 #12. 검색 구현 1

게시물 목록이 표시되고, 페이징 기능도 있으니... 이제 필요한건 검색 기능입니다.

기존 화면에 그대로 작업해도 되지만.. 지금까지의 흐름(?)에 맞게 별도의 페이지를 생성하겠습니다. 아예 새로만들 필요는 없고, 검색기능만 추가하면 되기 때문에 listPage를 그대로 사용하면 됩니다.

listPage.jsp 를 선택한 뒤, Ctrl(컨트롤) + C → Ctrl(컨트롤) + V 를 눌러줍니다.

listPage.jsp의 복사본 이름을 listPageSearch.jsp 로 변경해줍니다.

기능이 추가될수록 이름이 길어지는군요.

listPageSearch.jsp를 열어서 /board/listPage 로 되어있는 링크를 /board/listPageSearch 로 변경해줍니다.

이렇게 변경하지 않으면 페이징 링크가 /board/listPage 로 되기 때문입니다.

BoardController에서, 기존에 사용하던 listPage 메서드를 복사합니다.

listPageSearch.jsp는 listPage.jsp 를 복사한것이기 때문에 컨트롤러에서도 그대로 복사해서 사용할 수 있습니다.

복사한 뒤 @RequestMapping 의 값(value)를 /listPageSearch 로 변경해주고, 메서드이름인 getListPagegetLastPageSearch 로 변경해줍니다.

@RequestMapping만 맞으면 되고 메서드명은 달라도 상관없으나, 이것 역시 지나가는 개발자에게 물어봤을 때 바로 이해할 수 있도록 이름을 지어줍니다.

그리고 주석처리된 코드는 더이상 필요 없으니 지워주도록 합니다.

include/nav.jsp에 listPageSearch에 바로 접속할 수 있는 링크를 추가합니다.

프로젝트를 실행하면 추가했던 링크가 있는걸 확인할 수 있습니다. 클릭해서 listPageSearch에 접속합니다.

listPage와 동일합니다.

페이징 밑부분에 코드를 추가합니다.

<div>
  <select name="searchType">
      <option value="title">제목</option>
         <option value="content">내용</option>
      <option value="title_content">제목+내용</option>
      <option value="writer">작성자</option>
  </select>
  
  <input type="text" name="keyword" />
  
  <button type="button">검색</button>
 </div>

프로젝트를 실행해보면 검색할 수 있도록 엘리먼트가 추가되었습니다.

프론트쪽이 준비되었다면, 그 다음은 DB에서 데이터를 가져오도록 쿼리를 작성해야합니다.

위 스크린샷처럼 WHERE title = '테스트' 처럼 조건으로 사용하면 완벽하게 일치하는것만 가져올 수 있기 때문에

제목(title) 검색입니다.

이렇게 WHERE title LIKE '%테스트%' 처럼, LIKE를 이용한 조건을 사용합니다.

%(퍼센트 기호)가 앞에 있다면 앞에 다른 문자열이 있을 수 있다는것이고, 반대로 뒤에 있다면 뒤에 다른 문자열이 있을 수 있다는 의미입니다.

만약, '%bc'를 조건으로 사용한다면, 'abc'나 'bcc'처럼 '[다른 문자]bc'는 찾을 수 있으나, 'bcd'나 'bc1'처럼 bc[다른 문자]'는 찾을 수 없습니다.

내용(content) 검색입니다.

제목(title) + 내용(content) 검색입니다.

제목과 내용, 두가지 조건이 포함되어야하기 때문에 or 조건을 사용했습니다.

작성자(writer) 검색입니다.

boardMapper.xml을 열어서, listPage를 복사합니다. listPageSearch는 listPage를 복사한것이기 때문에, 같은 내용인 매퍼도 복사해서 사용할 수 있습니다.

매퍼의 id를 listPage 에서 listPageSearch 로 변경하고 쿼리를 추가합니다.
임시로 WHERE title LIKE '%${keyword}%' 를 추가했지만.. 여기엔 문제가 있습니다.

DAO에서 문자열 데이터를 받게되면 '%${keyword}%''%'keyword'%' 처럼, 받은 데이터의 앞뒤에 따옴표가 붙기 때문에 저대로 사용하면 에러가 발생할 수 있습니다.

concat 함수를 사용하면 문자열을 합칠 수 있습니다.

일단 모든 경우의 조건문을 다 집어넣어줍니다. 물론 저대로는 사용할 수 없습니다.

이렇게 매퍼에서 조건문을 사용할 수 있습니다. searchType의 값에 따라 제목, 내용, 제목+내용, 작성자를 구분할 수 있습니다.

<!-- 게시물 목록 + 페이징 + 검색 -->
<select id="listPageSearch" parameterType="hashMap" resultType="com.board.domain.BoardVO">
 select
  bno, title, writer, regDate, viewCnt
 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>
 
 order by bno desc
  limit #{displayPost}, #{postNum}
</select> 

여기에서 드는 의문점은 searchType과 keyword를 어디서 가져오냐? 입니다. listPage와 다르므로, 이부분은 DAO부터 컨트롤러까지 작업해줘야합니다.

BoardDAO에 코드를 추가합니다.

// 게시물 목록 + 페이징 + 검색
 public List<BoardVO> listPageSearch(
   int displayPost, int postNum, String searchType, String keyword) throws Exception;

BoardDAO 구현부인 BoardDAOImpl에 코드를 추가합니다.

// 게시물 목록 + 페이징 + 검색
 @Override
 public List<BoardVO> listPageSearch(
   int displayPost, int postNum, String searchType, String keyword) throws Exception {

  HashMap<String, Object> data = new HashMap<String, Object>();
  
  data.put("displayPost", displayPost);
  data.put("postNum", postNum);
  
  data.put("searchType", searchType);
  data.put("keyword", keyword);
  
  return sql.selectList(namespace + ".listPageSearch", data);
 }

메서드의 매개변수로 searchType과 keyword를 받을 수 있도록 했으며, 해시맵에도 searchType과 keyword를 추가했습니다.

BoardService에 코드를 추가합니다.

// 게시물 목록 + 페이징 + 검색
public List<BoardVO> listPageSearch(
  int displayPost, int postNum, String searchType, String keyword) throws Exception;

BoardService에 구현부인 BoardServiceImpl에 코드를 추가합니다.

// 게시물 목록 + 페이징 + 검색
@Override
public List<BoardVO> listPageSearch(
  int displayPost, int postNum, String searchType, String keyword) throws Exception {
 return  dao.listPageSearch(displayPost, postNum, searchType, keyword);
}

BoardController의 listPageSearc 메서드를 수정 및 코드 추가합니다.

// 게시물 목록 + 페이징 추가 + 검색
@RequestMapping(value = "/listPageSearch", method = RequestMethod.GET)
public void getListPageSearch(Model model, @RequestParam("num") int num, 
  @RequestParam("searchType") String searchType, @RequestParam("keyword") String keyword
  ) throws Exception {

 
 Page page = new Page();
 
 page.setNum(num);
 page.setCount(service.count());  
 
 List<BoardVO> list = null; 
 //list = service.listPage(page.getDisplayPost(), page.getPostNum());
 list = service.listPageSearch(page.getDisplayPost(), page.getPostNum(), searchType, keyword);
 
 model.addAttribute("list", list);
 model.addAttribute("page", page);
 model.addAttribute("select", num);
 
}

매개변수부에 @RequestParam("searchType") String searchType, @RequestParam("keyword") String keyword 를 추가하여, URL을 통해 searchType과 keyword를 받아낼 수 있도록 했습니다.

기존 list를 주석 처리했고, 그 대신 list = service.listPageSearch(page.getDisplayPost(), page.getPostNum(), searchType, keyword); 를 입력했습니다.

위에서 작업한 service와 dao 및 mapper에 사용될 데이터인 searchType과 keyword가 들어가있습니다.

프로젝트를 실행하고 listPageSearch 페이지에 접속하면 에러가 발생합니다. 에러 내용을 보니, 문자열(String) 데이터인 searchType가 없다고 합니다.

컨트롤러에서는 searchType과 keyword를 받도록 되어있는데, URL에는 searchType과 keyword가 없기 때문에 에러가 발생한겁니다.

그러므로 searchType과 keyword를 넣어주면 잘 동작됩니다.

searchType는 셀렉트박스에서 가져오는 데이터고, keyword는 사용자가 입력한 검색어입니다.

listPageSearch 메서드의 매개변수 부분을 수정해줍니다.

@RequestParam(value = "searchType",required = false, defaultValue = "title") String searchType,
   @RequestParam(value = "keyword",required = false, defaultValue = "") String keyword

value 는 받고자할 데이터의 키, required 는 해당 데이터의 필수여부, defaultValue 는 만약 데이터가 들어오지 않았을 경우 대신할 기본값입니다.

defaultValue 가 있으면 URL에 해당 값이 없더라도 기본값을 지정할 수 있으므로 에러가 발생하지 않을겁니다.

프로젝트를 실행하고 listPageSearch 페이지에 접속하면 예상대로 에러가 나오지 않습니다.

URL에 &searchType=title&keyword=테스트 를 입력하면 여전히 결과는 잘 나옵니다.

하지만 매번 URL에 searchType과 keyword를 입력해서 검색하는건 싫습니다.

아직 코딩이 덜 되었기 때문에, searchType과 keyword를 입력하고 검색 버튼을 눌러도 아무런 반응이 없습니다.

button 태그에 id="searchBtn 을 추가하고, 스크립트를 추가합니다.

<script>

 document.getElementById("searchBtn").onclick = function () {
    
  let searchType = document.getElementsByName("searchType")[0].value;
  let keyword =  document.getElementsByName("keyword")[0].value;
  
  console.log(searchType)
  console.log(keyword)
 };
</script>

id가 searchBtn인 html 엘리먼트에 클릭 이벤트가 발생하면, function() {} 내부의 코드가 실행됩니다.

name이 searchType인 html 엘리먼트중 첫번째([0])의 값을 변수(let) serarchType에 저장하고, name이 keyword html 엘리먼트중 첫번째([0])의 값을 변수(let) keyword에 저장합니다.

여기서 왜 첫번째([0])가 나오냐면, html 엘리먼트에 사용되는 name 속성은 2개 이상 복수로 사용할 수 있기 때문에 document.getElementsByName() 로 데이터를 가져오려고하면 배열로 가져오기 때문에 가장 첫번째인 0번째 데이터를 가져오는겁니다.

여기서 왜 첫번째가 1이 아닌 0이냐면.. 프로그래밍에서는 시작번호는 0입니다.

searchType를 선택하고, keyword를 입력하고 검색 버튼을 클릭하면 콘솔창에 입력했던 결과가 나옵니다. 그렇다는건 제대로 코딩이 되었다는 의미입니다.

저렇게 콘솔창을 보기 위해서는, 크롬 기준으로 F12를 눌러서 개발자 도구를 실행하면 됩니다.

코딩이 잘된걸 확인했으니, 실제로 동작할 코드를 작성합니다.

location.href = "/board/listPageSearch?num=1" + "&searchType=" + searchType + "&keyword=" + keyword;

location.href = [ URL ] 는 해당 URL로 이동하는 기능입니다. searchType은 선택한 검색 타입, keyword는 검색어가 들어가므로, '작성자'를 선택하고 '123'을 입력했다면 이동될 실제 URL은 /board/listPageSearch?num=1&searchType=writer&keyword=123 이 됩니다.

프로젝트를 실행한 뒤, searchType를 선택하고 keyword를 입력한 뒤 검색을 눌러봅니다.

선택한 searchType와 입력한 keyword로 이동되었으며, 게시물 목록 또한 잘 표시되는걸 확인할 수 있습니다.

하지만 지금까지의 과정에서 작업하지 않은 사항이 있어서 다음과 같은 문제가 남아있습니다.

첫번째는 페이지를 이동하면 searchType과 keyword가 없어집니다. 페이징에 있는 링크 URL에 searchType과 keyword가 없기 때문에 생기는 문제입니다.

두번째는 검색결과와 무관하게 페이징이 생성됩니다. BoardController에서 모든 게시물의 갯수를 이용하여 페이징을 생성하기 때문에 생기는 문제입니다.

이 두가지 문제는 다음 글에서 해결하겠습니다.

게시물 수정
  1. 익명5/12/2020

    너무 유용한 정보 감사합니다.
    정말 잘 보고있습니다.
    혹시 다음편도 곧 올라오나요???

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

      제가 5월초부터 프로젝트 진행중이라... 5월말이나 6월초에 작성할 예정입니다.

      게시물 날짜가 작년으로 되어있는데, 이건 그냥... 다른 글이랑 순서랑 날짜 맞추려다보니 이렇게 되었네요;

      삭제
  2. 매퍼 쿼리는 구게시물이 더 짧아보이는데 장단점이 있나요?

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

      이 게시물에서 사용한 매퍼의 쿼리문은 쿼리문 하나에서 조건문을 사용하고
      구 게시물에서는 조건문을 미리 만들어두었다가 호출하듯이 사용하는 방식입니다.

      이쪽 업계에서는 재활용할수록 좋은거니 당연히 후자가 더 좋은데...
      잘 모르는 사람의 경우 어디에 뭐가 만들어져있는지 확인해야하는 불편함이 있어서, 이번 게시물에서는 한번에 작성했습니다.

      삭제
  3. 익명5/26/2020

    검색과 연동한 페이징 처리가 간단할 줄 알았는데 어렵네요... 제가 지식이 짧아 어디가 틀린지 도통 모르겠습니다 ㅠㅠ 다음 편 기다립니다 흑흑

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

      대부분의 입문자분들이 게시판 만들기에서 첫번째로 겪는 고통(?)이 페이징일거라 예상합니다.

      두번째 고통은 파일 첨부고 (특히 경로부분)
      세번째는 이지윅 에디터같은거겠지요...?

      삭제
  4. 처음으로 이글 보면서 페이징처리가 이해가됬네요 ㅠㅠ
    정말 감사합니다
    검색기능도 얼른 올려주세요!!!!! 지금도 기다리고있습니다

    답글삭제
  5. 와 드디어 다시 하시는군요. 혹시 2단 카테고리 검색도 구현해주실수있나요? 1단카테고리를 db에서 불러와서 셀렉하면 그에따라 2단카테고리의 값이 변경되어서 글등록같이 한번쓰고 말것은 상관없는데 검색처럼 조건이 유지되야 하는 것은 2단카테고리가 힘들더라구요 ㅠㅠ

    답글삭제
  6. 스프링 입문하려고 강의 찾다가 좋은 강의 보고 갑니다 ^~^!!

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

      작성 날짜가 저래보이지만, 사실은 비교적 최근에(6월초) 쓴 글이랍니다..ㅠㅠ
      날짜 맞추니뭐니하면서 저렇게되버렸지만.. 조만간 추가적으로 게시물 작성하겠습니다(__)

      삭제
  7. 1. 궁금한게 조건이 더생기고 카테고리 형식이 더생기면 컨트롤러에 파라미터로 받는 개수가 계속 늘어나잖아요. 구버전에서는 searchcriteria로 모델 통으로 받는데 이거랑 장단점이 무엇이죠?
    2. 현재 게시물 서치 개발할때는 이방법을 더 많이쓰나요 구버전 방법을 더 많이쓰나요?

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

      1. 페이징 기본 기능만 클래스화 한것이 현재 게시물에서 작성하는 코드이고, 페이징 기본 기능 + 검색기능 + @ 한것이 구버전 게시물에서 작성한 코드입니다.

      검색 조건이 추가되더라도 searchcriteria 클래스에 코드만 추가하면 얼마든지 재활용이 가능하기 때문에, searchcriteria쪽이 장점이 많습니다. (기존 코드의 재활용으로 인한 실제 코드의 간소화)

      그럼에도 이 게시물에서는 기본 기능만 클래스화했고, 이후의 검색 기능은 추가하지 않았는데
      따로 명시되진 않았으나, 이 게시물의 대상자는 입문자이기 때문에.. 클래스화되어 코드가 줄어들수록 이해하기가 어려워집니다.

      그렇기 때문에 기본 기능의 클래스화 또한 코드를 줄이지 않고, 그대로 옮기는 방식으로 처리했었습니다.


      2. 실무에서는 이미 개발된 경우가 많아서 이미 searchcriteria처럼 클래스화된걸 사용하는 경우가 많고
      프론트 프레임워크/라이브러리에 따라서 검색한 데이터를 프론트에서 페이징 처리하는 경우도 있습니다.

      실제로 직접 처음부터 개발하는 경우는 별로없으나, 일반적으로 개발할때는 재활용할 수 있도록 개발하는것이 이후 처리에 유리하기 때문에, 결과적으로는 구버전의 searchcriteria 처럼 통으로 클래스화 하는 방식이 더 많이 사용되라라 생각합니다.

      삭제
    2. 친절한 설명 감사드립니다.
      님같은 분이 사수라면 참좋을듯..

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

      저는 나태해서 안됩니다..허허;

      삭제
  8. 작성자가 댓글을 삭제했습니다.

    답글삭제
    답글
    1. 안녕하세요. 좋은 글 보면서 공부중입니다. 한가지 질문이 있어서 문의드립니다...

      현재 listPageSearch 부분에서 이상한 오류가 생기는 중인데요...

      listPage에서는 글 목록이 잘 나오는 반면, listPageSearch 부분에서는 글 목록만 안나오는 상황입니다.

      구글 개발자도구로 열어보면 foreach 부분이 제대로 돌아가지 않은것 같습니다.

      제가 궁금한건 listPage는 정상적으로 foreach문이 구동해서 글 목록이 보이는 반면,

      listPageSearch 에서는 foreach 문이 정상적으로 돌지 않는게 궁금합니다.

      일단 게시글대로 따라하면서 왠만한 건 스스로 고쳤었는데...여기는 도저히 감이 안잡혀서 질문드립니다...

      혹시 시간 괜찮으시다면 답변 부탁드리겠습니다. 감사합니다!

      삭제
    2. 윽...죄송합니다... 제가 BoardDAOImpl 에서 오버라이드 만들어놓고 리턴값을 null로 방치했었네요;; 어쩐지 디버그에서 계속 list에 리턴값이 null로 되어있던...ㅠㅠ

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

      원가 제대로 된것 같은데 의도대로 안된다면
      디버그 모드를 이용하여 코드를 따라가는게 답인거같습니다...ㅠㅠ

      삭제
  9. 안녕하세요 스프링 공부하는데 너무 큰 도움을 얻고 있습니다. 양질의 글 정말 감사합니다!
    다름이 아니라 검색 기능 script로 하신 부분 때문에 댓글 남기는데요
    어차피 HomeController에서 get으로 받고 있으니까 form 태그로 두른 다음에 submit 버튼으로해서 구현하면 스크립트 설정 안해도 알아서 파라미터 값으로 searchType과 keyword가 넘어가지 않나요?
    이제 막 공부중이라 정확하지 않지만 제 방식으로 해도 되는지 그리고 굳이 저렇게 하신 이유가 있다면 듣고 싶습니당

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

      먼저 결론을 말하자면, 말씀하신 방법으로도 가능합니다.

      다만 주의해야할 사항으로는, form + get 방식으로 데이터를 전송할 경우, form의 action에는 쿼리스트링.. 즉 주소?키=값&키=값 처럼, 데이터를 넣은 url을 사용할 수 없습니다. 물론 본문의 코드로는 이런 문제가 발생하진 않지만요.

      본문의 url을 직접 만드는 방식으로 한 이유는... 제가 판단했을때 페이징 기능은 데이터를 전달하는게 아니라 페이지 이동이라고 판단했으며, 데이터 전달이 아니므로 form을 사용하지 않았습니다.

      삭제
  10. 익명8/22/2023

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

    답글삭제