4/14/2018

(구버전) 스프링 게시판 만들기 #7. 검색 기능 구현

'(구버전) 스프링 게시판 만들기'는 내용이 부족하다고 판단하여
스프링 게시판 만들기를 새로 작성하였습니다.

링크 및 참조용으로 현재 게시물은 남겨두겠지만,
가급적이면 새로운 스프링 게시판 만들기를 참조해주시기 바랍니다.

페이징 기능을 구현했으니, 이번엔 검색 기능을 구현하겠습니다. 게시판에서 검색을 할 때 필요한 조건은 사용자가 입력하는 키워드와 대상입니다. 여기서 대상은 글 제목, 글 내용, 작성자가 되겠습니다.

가장 먼저 해야할건 역시 쿼리 작성입니다.

 select bno, title, content, writer, regDate, viewCnt
    from (
        select bno, title, content, writer, regDate, viewCnt,
            row_number() over(order by bno desc) as rNum
        from myBoard
            where title like '%' || '더미' || '%'  -- 제목
            where content like '%' || '더미' || '%'  -- 내용
            where content write '%' || '더미' || '%'  -- 작성자
            where title like '%' || '더미' || '%' or (content like '%' || '2' || '%')  -- 제목과 내용
        ) mb
    where rNum between 1 and 10
        order by bno desc;

위 쿼리 그대로 사용하면 당연히 실행은 안되고, 조건 4가지(제목, 내용, 작성자, 제목과 내용)중 하나만 있는 상태에서 정상적으로 실행됩니다.

검색결과에서도 페이징 기능이 작동되야하므로, 페이징에서 사용한 쿼리의 내부에 조건문을 넣어서 사용합니다.

위 쿼리에서 mb는 MyBoard의 줄임문자로, 괄호 내부의 결과에 대한 별칭을 지정해둔 문자입니다.
mb 문자를 삭제하더라도 실행에 영향은 없습니다.

조건문을 이용하여 원하는 게시물만 검색할 수 있었으니, 같은 조건을 이용하여 해당 게시물의 갯수를 알 수 있습니다.

select count(bno)
  from myBoard
   where bno > 0
                and title like '%' || '더미' || '%';  -- 제목

검색 타입(제목, 내용, 작성자, 제목과 내용)과 검색어는 거의 항시, 고정적으로 사용되니 클래스로 작성합니다.

package com.kuzuro.domain;

public class SearchCriteria extends Criteria{

 private String searchType ="";
 private String keyword = "";
 
 public String getSearchType() {
  return searchType;
 }
 public void setSearchType(String searchType) {
  this.searchType = searchType;
 }
 public String getKeyword() {
  return keyword;
 }
 public void setKeyword(String keyword) {
  this.keyword = keyword;
 }
 
 @Override
 public String toString() {
  return super.toString() + " SearchCriteria [searchType=" + searchType + ", keyword=" + keyword + "]";
 }
}

새로만든 클래스 SearchCriteria는 기존에 작성한 Criteria를 상속(extend)받습니다. 즉, SearchCriteria는 클래스에 보이지 않아도 Criteria의 모든 요소를 사용할 수 있습니다.

이제 매퍼에 코드를 추가합니다. 추가된 코드는 기존의 코드와 조금 다르게 되어있습니다.

<!-- 검색 -->
<select id="listSearch" resultType="com.kuzuro.domain.BoardVO"
parameterType="com.kuzuro.domain.SearchCriteria">
select bno, title, content, writer, regDate, viewCnt
       from (
           select bno, title, content, writer, regDate, viewCnt,
               row_number() over(order by bno desc) as rNum
           from myBoard
      <include refid="search"></include>
     ) mb
    where rNum between #{rowStart} and #{rowEnd}
        order by bno desc
</select>

<sql id="search">
 <if test="searchType != null">
  <if test="searchType == 't'.toString()">where title like '%' || #{keyword} || '%'</if>
  <if test="searchType == 'c'.toString()">where content like '%' || #{keyword} || '%'</if>
  <if test="searchType == 'w'.toString()">where writer like '%' || #{keyword} || '%'</if>
  <if test="searchType == 'tc'.toString()">where (title like '%' || #{keyword} || '%') or (content like '%' || #{keyword} || '%')</if>
 </if>
</sql>

검색할 때 사용되는 두가지의 조건인 검색 타입과 검색어는 고정된 값이 아닌 유동적인 값입니다. 특히나 검색어는 사용자가 입력하는 그 값이 검색어가 되기 때문에 추측할 수 없으나, 검색 타입은 제목, 내용, 작성자, 제목과 내용 이렇게 4가지로 구분되기 때문에 조건문을 이용할 수 있습니다.

조건문은 <sql> 태그로 별도로 작성하되 아이디(id)를 설정하고, 사용될 쿼리에 <include refid="search"></include> 태그를 이용하여 추가합니다.

검색어에 맞는 게시물 갯수를 구하는 쿼리도 작성합니다.

<select id="countSearch" resultType="int">
 select count(bno)
  from myBoard
   <include refid="search"></include>
<![CDATA[
   and bno > 0
]]>
</select>

여기서 집고 넘어갈 부분은 글 번호(bno)의 조건문이 where에서 and로 바뀌었다는겁니다. 바뀐 이유는 검색타입+검색어 조건이 앞으로 왔기 때문입니다.

2개의 조건이 모두 만족되는 조건을 두번째 이후부터 사용하려면 where가 아닌 and로 작성해야하는데, 글번호 조건을 먼저 where로 걸어두고 이후에 검색타입+검색어 조건을 and로 걸어두게된다면, <sql> 태그를 하나 더 만들어야합니다.

지금은 쿼리의 양이 많이 않기에 용량이나 속도 차이는 거의 없으나, 기존에 만들어둔걸 이용하지 않을 필요는 없기에 순서를 바꿨습니다.

DAO와 Service에 새로운 메서드 listSearch, countSearch를 작성합니다. 이 메서드는 매개변수의 타입이 Criteria가 아닌 SearchCriteria입니다.

다음으로, 컨트롤러에 코드를 작성합니다.

// 글 목록 + 페이징 + 검색
@RequestMapping(value = "/listSearch", method = RequestMethod.GET)
public void listPage(@ModelAttribute("scri") SearchCriteria scri, Model model) throws Exception {
 logger.info("get list search");
 
 List<BoardVO> list = service.listSearch(scri);
 model.addAttribute("list", list);
 
 PageMaker pageMaker = new PageMaker();
 pageMaker.setCri(scri);
 //pageMaker.setTotalCount(service.listCount());
 pageMaker.setTotalCount(service.countSearch(scri));
 model.addAttribute("pageMaker", pageMaker);
}

이전에 작성했던 listPage.jsp와 차이점은 매핑된 경로와 CriteriaSearchCriteria로 바꾼것과, 게시물 전체 갯수를 파악하는 pageMaker.setTotalCount(service.listCount()); 대신, 검색된 게시물의 전체 갯수를 파악하는 pageMaker.setTotalCount(service.countSearch(scri)); 입니다.

클래스 PageMaker에서 URI를 만드는 메서드 makeQuery는 page와 perPageNum을 조합했는데, 검색 기능이 생기며 추가된 searchType과 keyword를 추가되었으므로, 메서드 makeSearch를 추가로 작성합니다.

keyword는 인코딩에 따라 의도한것과 다른 결과가 나올 수 있기 때문에 인코딩 기능을 추가합니다.

public String makeSearch(int page)
{
  
 UriComponents uriComponents =
            UriComponentsBuilder.newInstance()
            .queryParam("page", page)
            .queryParam("perPageNum", cri.getPerPageNum())
            .queryParam("searchType", ((SearchCriteria)cri).getSearchType())
            .queryParam("keyword", encoding(((SearchCriteria)cri).getKeyword()))
            .build(); 
 return uriComponents.toUriString();  
}

private String encoding(String keyword) {
 if(keyword == null || keyword.trim().length() == 0)
 { return ""; }
 
 try {
  return URLEncoder.encode(keyword, "UTF-8");
 } catch(UnsupportedEncodingException e)
 { return ""; }
}

listPage.jsp를 복사(Ctrl+C)후 붙여넣기(Ctrl+V)하여 이름을 listSearch.jsp로 변경합니다.

makeQuery의 역할이 makeSearch로 대처되었으므로, makeQuery를 사용하던 코드를 수정합니다.

그리고 home.jsp에서 검색 페이지로 바로 접속할 수 있게 링크도 하나 추가합니다.

페이지 추가했을 때랑 별 차이가 없습니다.

하지만 브라우저에 직접 주소를 입력하면(http://localhost:8080/board/listSearch?page=2&perPageNum=10&searchType=t&keyword=테스트) 검색 기능이 잘 작동되는걸 확인할 수 있습니다.

listSearch.jsp에서 테이블과 페이징 사이에 검색기를 추가합니다.

<div class="search">
 <select name="searchType">
  <option value="n"<c:out value="${scri.searchType == null ? 'selected' : ''}"/>>-----</option>
  <option value="t"<c:out value="${scri.searchType eq 't' ? 'selected' : ''}"/>>제목</option>
  <option value="c"<c:out value="${scri.searchType eq 'c' ? 'selected' : ''}"/>>내용</option>
  <option value="w"<c:out value="${scri.searchType eq 'w' ? 'selected' : ''}"/>>작성자</option>
  <option value="tc"<c:out value="${scri.searchType eq 'tc' ? 'selected' : ''}"/>>제목+내용</option>
 </select>
 
 <input type="text" name="keyword" id="keywordInput" value="${scri.keyword}"/>

 <button id="searchBtn">검색</button>
 
 <script>
 $(function(){
  $('#searchBtn').click(function() {
   self.location = "listSearch"
     + '${pageMaker.makeQuery(1)}'
     + "&searchType="
     + $("select option:selected").val()
     + "&keyword="
     + encodeURIComponent($('#keywordInput').val());
    });
 });   
 </script>
</div>

검색기의 스트립트 부분은 제이쿼리를 사용하고 있으므로, 제이쿼리 CDN이 없다면 <head> 태그에 추가합니다.

<!-- 제이쿼리 -->
<script src='https://code.jquery.com/jquery-3.3.1.min.js'></script>

검색 타입과 검색어를 입력하고, 검색 버튼을 누르면 검색된 결과가 표시됩니다.

브라우저의 주소창이 변경되었고, 검색어에 맞는 결과만 목록에 표시되었으며, 검색기의 검색타입과 검색어도 입력한대로 유지되는걸 확인할 수 있습니다.

게시물 수정
  1. 혹시 따라하시다가 500error가 나시는 분들을 위해
    mybatis 버전이 3.3.0 미만이신 분들은 boardMappers.xml에서 sql property부분을 '%#{~~~}%' 부분에서 #이아닌 $로 사용하셔야됩니다

    답글삭제
  2. select bno, title, content, writer, regDate, viewCnt
    from (
    //내용생략// ) mb
    where rNum between 1 and 10
    order by bno desc;

    쿼리문안에 mb라고 적혀있는건 어떤이유에서 들어있는건가요?

    답글삭제
    답글
    1. 안녕하세요.
      말씀하신 mb는 MyBoard의 약자로서 별칭으로 지정해둔 문자입니다.
      여기에선 사실상 아무 의미없는 텍스트이며, 실제로 mb를 삭제하고 실행하여도 작동하는데 이상이 없습니다.

      그럼에도 불구하고 사용한 이유는, 언젠가 사용할 것 같아서 넣어둔건데... 뭔가 은그슬쩍 넘어가버렸네요.

      저부분은 from ( ..//생략//.. ) as [별칭] 이렇게 사용하는 방식이며, as 명령어 특성상 생략할 수 있으므로, from ( ..//생략//.. ) [별칭] 와 같이 작성되었습니다.

      해당 부분에 대해서 내용을 추가해두겠습니다.

      설 잘 쇠세요.

      삭제
    2. 예상은 했지만 맞았군요..ㅎㅎㅎ 감사합니다..ㅎㅎ
      설날은 잘 보내셨나요~!
      한해 복 많이 받으시길 바라겠습니다

      삭제
  3. 안녕하세요
    궁금한점이 하나있습니다

    listSearch.jsp 부분에서요

    select option value 안에 ${scri.searchType}
    이건 컨트롤러에서 model에 넣지않고
    어떻게 사용하는 방식인가요??

    답글삭제
    답글
    1. 안녕하세요?

      검색기의 셀렉트 박스는 컨트롤러의 get에서 값을 가져오는게 아니라, 입력된(혹은 선택된) URL을 기준으로 합니다.

      즉, 컨트롤러에서 값을 보내주는것이 아니라컨트롤러에 요청된 URL에 따라 값이 달라집니다.

      여기서 이 URL은 searchCroteria의 헝태를 하고 있으며, 컨트롤러에서도 searchCroteria를 매개변수로 사용하고 있으므로 바로 접근이 가능합니다.

      삭제
  4. 안녕하세요
    option value="w">작성자 /option

    여기부분에서 option value=" w" 이부분은 url에서 파라미터인 부분이고
    ${scri.searchType eq ' w' 이부분은 쿼리에서 <if test="searchType=='w'.toString() > 여기서의 w를말하는 부분인가요?? 엄청헷갈리네요 ,,..

    답글삭제
    답글
    1. option value 다음 이부분이 자꾸 짤리네요
      c:out value="${scri.searchType eq 'w' ? 'selected' : ''}"/>

      삭제
  5. input type="text" name="keyword" id="keywordInput" value="${scri.keyword}"

    이부분에서 value가 있어야하는 이유를 잘 모르겠습니다 .
    인풋박스에 keyword name에 친 값이 만들어진 scri 객체의 keyword변수를 통해 쿼리로 들어가는것 아닌가요??
    지금 scri의 keyword에서 가지고있는값이 없는 상태 아닌가요??

    답글삭제
    답글
    1. 안녕하세요?

      먼저 option value="w"는 jsp에있는 셀렉트 박스의 값입니다. 이 역할은 검색 버튼을 클릭하였을 때, 자신이 가지고있는 검색 조건(w)을 넘겨주는 역할을 하고 있습니다.

      <c:out value="${scri.searchType eq 'w' ? 'selected' : ''}"/>는 jstl로 URL 또는 세션에 있는 scri.searchType값을 가져와서, 이 값이 'w'라면 selected를 출력하고, 아니면 공백('')을 출력한다는 조건문입니다.

      만약 아무런 검색을 하지 않은 상태라면, 이 조건문에 의해 '-----'가 선택되어있을겁니다. 하지만 다른 타입(제목, 내용, 작성자, 제목+내용)으로 검색한 뒤라면, 예를들어 '내용'으로 검색한 경우, 내용의 option값에 해당되는 'c'가 전달되게 됩니다.

      이 검색타입과 검색어로 검색한 결과를 새로운 화면에 출력하는데, 이때 URI에 검색타입과 검색어가 같이 표시됩니다. 이때 위에서 언급한 조건문이 이를 감지하여, 조건이 맞으면 selected를 출력하고 조건이 안맞으면 공백('')을 출력합니다.

      그러므로 5개의 option중 내용에 해당되는 조건문만 selected를 출력하고, 나머진 공백을 출력하겠죠. option태그에서 selected란 말그대로 '선택됨'이라는 의미이므로, 사용자가 다시 선택하지 않더라도 이미 '내용'이 선택되어져있는 상태가 됩니다.

      검색어도 마찬가지입니다.
      아무런 검색을 하지 않은 상황이라면 ${scri.keyword}의 값은 공백('')인 상태입니다. 여기서 어떤 내용이든 검색을 하게되면, 그 검색어가 url이나 세션으로 넘어오게되는데, 그 값을 value에 넣어주는것 입니다.

      -

      이렇게 사용자가 입력했던 값을 다시 넣어주는 이유는.. 만약 검색 결과가 많아서 2페이지로 넘어갔다고 가정해봅시다. 검색 타입과 검색어를 본문 처럼 작업하지 않고 텅 비어있는 상태로 둔다면, 2페이지로 넘어갈 때 '2페이지'라는 값만 넘어가고 검색타입과 검색어 값은 넘어가지 않게되므로 아무것도 검색하지 않은 상태의 2페이지가 출력되버립니다. (물론 이부분은 해결할 수 있는 부분이지만, 불필요한 작업이지요)
      또한, 사용자 본인이 어느 검색타입과 어느 검색어로 했는지 확인시키기 위함도 있지요.

      -

      구글이나 네이버, 다음 등에서 검색을 해보시면...

      예를들어 네이버에서 검색 타입은 '블로그', 검색어는 '커피'로 검색해보면
      url에 검색타입 where=post, 검색어 query=커피가 계속 유지되어있는걸 알 수있으며

      마찬가지로 검색결과에 '블로그'부분만 초록색으로 표시되어있으며
      검색창에는 입력했던 '커피'가 계속 남아있는걸 알 수 있습니다.

      삭제
    2. 추가적으로 댓글에 태그에 사용되는 꺽쇠(홑화살괄호. <, >)는 그대로 입력하면 태그로 인식하여 자동으로 삭제되므로, 댓글창 위에 있는 HTML 태그 특수문자 변환을 이용하셔 작성하시면 됩니다. :)

      삭제
  6. // 글 목록 + 페이징 + 검색
    @RequestMapping(value = "/listSearch", method = RequestMethod.GET)
    public void listPage(@ModelAttribute("scri") SearchCriteria scri, Model model) throws Exception {
    logger.info("get list search");

    List<BoardVO> list = service.listSearch(scri);
    model.addAttribute("list", list);

    PageMaker pageMaker = new PageMaker();
    pageMaker.setCri(scri);
    pageMaker.setTotalCount(service.listCount());
    model.addAttribute("pageMaker", pageMaker);
    }

    pageMaker.setTotalCount(service.listCount());
    이부분이

    위에 만들어놓은
    scri 기준으로 나온 총결과
    service .countSearch(scri) 함수가 쓰여야 되는것 아닌가요?

    답글삭제
    답글
    1. 앗.. 깃허브를 보니 작업은 해놨는데 게시물에는 안되었네요;

      감사합니다. 본문에도 수정해놓겠습니다.

      삭제
  7. 안녕하세요

    <select name="searchType">
    <option value="n"<c:out value="${scri.searchType == null ? 'selected' : ''}"/>>-----</option>
    <option value="t"<c:out value="${scri.searchType eq 't' ? 'selected' : ''}"/>>제목</option>
    <option value="c"<c:out value="${scri.searchType eq 'c' ? 'selected' : ''}"/>>내용</option>
    <option value="w"<c:out value="${scri.searchType eq 'w' ? 'selected' : ''}"/>>작성자</option>
    <option value="tc"<c:out value="${scri.searchType eq 'tc' ? 'selected' : ''}"/>>제목+내용</option>
    </select>

    <input type="text" name="keyword" id="keywordInput" value="${scri.keyword}"/>

    <button id="searchBtn">검색</button>

    이 부분에


    scri.searchType 값을 저장할수는 없겠지만

    혹시 <option value="c" > 내용</option> 해도 작동이 되는건지 궁금합니다.



    이클립스 오류로 인해 실행이 안되어서 간단한 질문드려 죄송합니다

    답글삭제
    답글
    1. <option value="c"<c:out value="${scri.searchType eq 'c' ? 'selected' : ''}"/>>내용</option><option value="c">내용</option>로 사용해도 기능적인 문제는 없습니다.

      다만 검색하고나서 검색 셀렉트 박스가 기본상태(-----)로 되어있기 때문에, 다음 페이지를 넘어갈 때 searchType가 전용되지 않을 경우 결과가 의도한대로 나오지 않을 가능성이 높습니다.

      삭제
  8. 안녕 하세요
    Select
    Option 의 value가 n일때 쿼리문중
    Searchtype조건문에 searchtype=='n'tostring 같은 건필요가 없는건가요? 널일때에도 키워드를 넣어서 전체검색이 되어야한다고생각해서요..

    답글삭제
    답글
    1. 본문에 있는 코드가 말씀하시는 역할을 합니다. searchType를 n으로 했을 경우, 최종적으로 실행되는 매퍼의 동적 쿼리에서 구분됩니다.

      <sql id="search">
      <if test="searchType != null">
      <if test="searchType == 't'.toString()">where title like '%' || #{keyword} || '%'</if>
      [ 생략 ]
      </if>
      </sql>


      값이 n이라면 조건문에 따라 아무것도 표시하지 않기 때문에

      select count(bno) from myBoard <include refid="search"></include>and bno > 0 쿼리는 select count(bno) from myBoard and bno > 0가 됩니다.

      아, 이대로 있으면 조건문이 where가 아니라 바로 and가 나와서 에러가 발생되겠네요. 또 예전의 흐지부지 끝낸 문제가 또 드러나는군요;

      이 문제를 해결하기 위해선 and조건을 where 변경하고, 동적 쿼리의 조건을 and로 변경한다음 메인 쿼리의 가장 마지막에 위치하도록 수정하면 되겠습니다.

      삭제
  9. 익명5/16/2019

    검색을 한번하고 다른걸 검색하려고하면 아무것도 나타나질않네요 ㅠㅠ 한번만 검색되는데 왜이런거져 ㅠ

    답글삭제
    답글
    1. 안녕하세요?

      한번만 검색된 후 다시 검색되지 않는다는 건
      1. 페이지를 넘어갈 경우 검색 상태가 유지되지 않는것인지
      2. 한번 검색한 이후 더이상 검색 버튼이 작동하지 않는다는 것인지
      3. 한번 검색한 이후, 다른 검색을 할 경우 결과가 나오지 않는다는 것인지
      이 중에 하나일것 같은데 아마도 3번인것 같네요.

      가장 먼저 확인해야할 건, 크롬을 기준으로 F12를 눌러 관리자 모드를 실행한 뒤 Console 탭에서 스크립트 관련 에러가 있는지 확인해봐야합니다.

      에러가 없다면, 검색타입과 검색어를 입력하고 검색 버튼을 눌렸을 때 컨트롤러로 데이터가 전송되는지 확인해봐야 할 것 같습니다.

      삭제
  10. 익명5/20/2019

    안녕하세요 궁금한 점이 있어 질문합니다!

    컨트롤러 검색 부분 작성에서
    @RequestMapping(value = "/listSearch", method = RequestMethod.GET)
    public void listPage(@ModelAttribute("scri") SearchCriteria scri, Model model) throws Exception {
    logger.info("get list search");

    List list = service.listSearch(scri);
    model.addAttribute("list", list);

    PageMaker pageMaker = new PageMaker();
    pageMaker.setCri(scri);
    //pageMaker.setTotalCount(service.listCount());
    pageMaker.setTotalCount(service.countSearch(scri));
    model.addAttribute("pageMaker", pageMaker);
    }

    이 부분에서
    컨트롤러에 pageMaker.setTotalCount(service.countSearch(scri)); 이걸 쓰면 검색 페이지 들어갈 때 매퍼 부분에서 오류가 뜹니다 처음 화면에 들어갈 때 where이 아니라 and를 써서 오류가 뜨네요 그래서 and를 where로 바꾸니 첫 화면은 잘 들어가는데 검색을 하니 where이 두 번 들어가서 오류가 뜨네요 그래서 이 부분을 그냥 listCount()로 하니 첫 화면이나 검색 부분에서 오류가 안 뜨지만 검색을 했을 때 검색된 게시글 개수가 아닌 전체 게시글 개수가 나오네요 그래서 매퍼 부분에서 sql를 두 개 만들었습니다 countSearch에서 and bno > 0 에서 and를 where로 그리고 새로 만든 sql문을 뒤로 빼고 where을 and로 바꾸니 잘 돌아가네요 블로그에 작성된 글을 보고 pageMaker.setTotalCount(service.countSearch(scri)); 이걸 넣었는데 오류가 떠서 깃헙에 들어가서 봤더니 listCount()라고 되어있길래 고쳤는데 게시글 개수가 안 맞네요 블로거님이 쓰신 sql문을 하나 더 만들어야 한다는 얘기가 이건거 같은데 저는 기존 sql문 하나로는 구동이 완벽히 안돼서 질문글 남겨봅니다

    답글삭제
    답글
    1. 안녕하세요?

      무슨 말씀인가해서 깃허브를 봤는데, 제가 사용했던 코드랑 다르더군요;
      아마도 깃허브에 커밋한 이후에 문제를 발견해서 수정한다음 커밋을 안해둔것 같네요-_-;

      보니깐 검색 매퍼에서 결과의 총 갯수를 구하는 커리의 조건문이,
      본문엔 동적 SQL뒤에 and 로 이어지는데 실제 코드에선 where로 조건을 먼저 걸고 뒤에 동적 SQL문으로 이어집니다.
      그도 그럴게, cri의 값이 없다면 동적 SQL문은 아무것도 적용되지 않으니까요.

      다만 이렇게 될 경우, cri의 값이 있을 때 미리 걸어둔 where 조건문 뒤에 동적 SQL문이 이어지므로, 동적 SQL문의 조건은 모두 and조건이 되겠지요.

      <select id="countSearch" resultType="int">
      select count(bno)
      from myBoard

      <![CDATA[
      where bno > 0
      ]]>
      <include refid="search"></include>

      </select>

      <sql id="search">
      <if test="searchType != null">
      <if test="searchType == 't'.toString()">and title like '%' || #{keyword} || '%'</if>
      <if test="searchType == 'c'.toString()">and content like '%' || #{keyword} || '%'</if>
      <if test="searchType == 'w'.toString()">and writer like '%' || #{keyword} || '%'</if>
      <if test="searchType == 'tc'.toString()">and (title like '%' || #{keyword} || '%') or (content like '%' || #{keyword} || '%')</if>
      </if>
      </sql>

      이렇게 되어있다면 생각하신대로 동작할겁니다. 단, 동적 SQL문을 사용하는 다른 쿼리의 조건부를 확인하시면 되겠습니다.

      삭제
    2. 익명5/22/2019

      네 저도 저렇게 바꾸고 listSearch에도 and로 시작하니 from myBoard 뒤에 where bno > 0 를 새로 적어줬습니다!!

      삭제
    3. 죄송한데 boardMapper.xml 부분이 명령어가 오류가 나는데 혹시 boardMapper.xml 부분만 보여주실수 있나요??
      boardMapper.xml 부분만 이메일로 아니면 공유 가능할까요 ??

      삭제
    4. 안녕하세요?

      이 게시물에서 사용한 코드는 깃허브에서 확인하실 수 있습니다. 다만 진행이 끝난 상태의 코드이므로 게시물의 내용에 맞춰서 보실 필요가 있습니다.

      어느 명령어를 사용했고 어느 오류가 발생했는지 안다면 해결하는건 어렵지않습니다. 스프링에서 에러가 발생한 경우, 에러 창에 에러 메시지가 나오기 때문에 그것을 찾아보시면 쉽게 해결하실 수 있을겁니다.

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

    답글삭제
  12. 안녕하세요

    게시판 공부하는데 많은 도움을 받고 있습니다.

    글을 보면서 전 리스트와 검색을 통합하고 싶어서 조금 수정을 하다가
    $(function(){
    $('#searchBtn').click(function() {
    self.location = "listSearch"
    + '${pageMaker.makeQuery(1)}'
    + "&searchType="
    + $("select option:selected").val()
    + "&keyword="
    + encodeURIComponent($('#keywordInput').val());
    });
    });

    + '${pageMaker.makeQuery(1)}'부분에서 + '${pageMaker.makeSearch(1)}'를 넣었더니 지속적인 오류가 발생하여 해당부분을 수정했더니 원활하게 작동하고 있습니다. 다만 해당 부분이 어떤 차이가 있는지와 + '${pageMaker.makeSearch(1)}'를 작성했더니 쉼표가 자동적으로 검색어에 추가 되었고 해당 검색어와 쉼표가 2배씩 늘어나면서 오류가 발생했습니다 그 부분또한 알려주신다면 정말 감사하겠습니다.

    답글삭제
    답글
    1. 안녕하세요? 방문해주셔서 감사합니다.
      makeQuery와 makeSearch는 pageMaker를 상속받는 VO입니다.
      makeSearch의 경우 검색 기능에 대응하기 위한 vo이므로, 관련된 데이터인 검색어/타입의 정보가 들어가게됩니다.

      makeSearch를 사용하지 않을 경우, 페이징 기능에 대한 문제는 없겠으나 검색어/타입의 정보가 유지되지 않는 문제가 발생하게 됩니다.

      makeSearch를 사용할 때 콤마(,)가 추가되는 경우, 데이터가 2번 이상 전송되는 경우입니다. 예를 들어 keyword=,,&searchType=n 이런식으로 keyword에 콤마가 생기는 경우, jsp파일 내에 keyword라는 이름을 가진 html요소가 있어서 이를 같이 전송하기 때문에 생기는 문제입니다.

      삭제
    2. 친절한 답변 알기쉬운 설명에 감사합니다.
      오늘도 좋은 하루 보내세요.

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

    답글삭제
  14. 안녕하세요 질문이 있어서 올립니다
    검색기능을 만드는데 문제가 좀 있어서요
    첫번째는 mariaDB 에서
    select count(bno)
    FROM tbl_board
    WHERE bno > 0
    AND title LIKE '%' || '더미' || '%'; 를 입력했을때 카운트는 되지만 경고가 뜹니다
    warning: truncated incorrect DOUBLE value: '더미'
    warning: truncated incorrect DOUBLE value: '%' 이렇게 10개의 경고가 떴습니다.
    2번째는 위 글대로 했을때
    하지만 브라우저에 직접 주소를 입력하면(http://localhost:8080/board/listSearch?page=2&perPageNum=10&searchType=t&keyword=테스트) 검색 기능이 잘 작동되는걸 확인할 수 있습니다.
    이부분도 홈페이지에 변화는 일어나지 않았다는 거구요 (검색이 되지않고 전체목록이 떴습니다.)
    3번째는 결국 마지막까지 만들어서 서버를 다시시작하고 돌려봤는데요
    검색 카테고리를 제목으로 두고 테스트 를 검색하면 HTTP 상태 500 이 뜨구요
    Error querying database. Cause: java.sql.SQLSyntaxErrorException: (conn=350) You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'where bno > 0' at line 10 이런 내용이 뜹니다.
    boardMapping.xml 안에 해당되는 내용인데
    <![CDATA 를 없애도 보고 and 랑 where 랑 바꿔가면서 이것저것 다 해봤는데 어떻게 해야할지 모르겠습니다. 도와주세요ㅠㅠ
    그리고 and 를 where 로 바꾼건 and로 하고 돌리면 검색하는 링크만 들어가도 에러가 떠서 그렇습니다ㅠ

    답글삭제
    답글
    1. 안녕하세요? 늦게 확인해서 죄송합니다.

      쿼리문에 문법적인 에러가 있어서 생기는 에러인데, 말그대로 문법이 잘못되었거나 세미콜론이 들어가 있는 경우 발생합니다.

      매퍼에 있는 쿼리문을 DBMS에서 실행해보시고, 정상적으로 실행되는지 확인해보셔야할 것 같습니다.

      <![CDATA[ ]]>는 조건에 사용된 부등호 where bno > 0를 인식할 수 있도록하는 코드이므로 올바른 위치에 있어야합니다.

      삭제
    2. 답변 감사합니다..근데 전혀 다른 문제가 생겼습니다.
      답변을 남겨주셔서 스프링을 켰더니 에러가나서 에러내용을 검색해서 고치는 방법대로 했다가 프로그램이 엉망이 되서 전부 삭제하고 다시 스프링을 깔고 깃허브에 저장하라고 하신대로 한걸 임포트해서 가져왔는데요
      임포트를 하니 board 라는 이름으로 파일이 생겼고, 톰캣서버를 다시 추가해서 board 프로젝트를 추가해줬습니다.
      추가후 프로젝트를 돌려봤는데 nav.jsp의 글 목록, 글 작성, 글 목록 페이징 링크들은 잘 뜨는데 셋다 클릭해도
      http 상태 404 에러가 뜹니다..
      내용은 'Origin 서버가 대상 리소스를 위한 현재의 representation을 찾지 못했거나, 그것이 존재하는지를 밝히려 하지 않습니다.' 이구요 세가지 링크 모두 동일하게 뜹니다.
      톰캣이 문제라고 생각하고 삭제후 다시깔아보고 경로도 바꿔보고 여러가지 해봤는데 톰캣을 깔았을때는 localhost:8080이 되는데 서버만 돌렸다 하면 404에러가 뜹니다..
      깃허브에서 임포트하는 경우에는 재설정 해줘야하는 뭔가가 있는건가요?ㅠ

      삭제
  15. 안녕하세요 덕분에 많은 도움 받고 있습니다.
    boardMapper.xml에서 오류가 뜨는것 같은데, "혹시 따라하시다가 500error가 나시는 분들을 위해
    mybatis 버전이 3.3.0 미만이신 분들은 boardMappers.xml에서 sql property부분을 '%#{~~~}%' 부분에서 #이아닌 $로 사용하셔야됩니다"
    이 게시물 보고 바꾼 부분 빼면은 코딩은 그대로 했습니다.
    Error querying database. Cause: java.sql.SQLSyntaxErrorException: ORA-00933: SQL 명령어가 올바르게 종료되지 않았습니다
    이런 에러가 나와서요 ㅠㅠ 도움주시면 감사하겠습니다!!

    답글삭제
    답글
    1. SQL: select count(bno) from myBoard and bno > 0
      이렇게뜨네요

      삭제
    2. 저도 같은 증상인데 혹시 찾으셨나요??

      삭제
    3. 안녕하세요? 방문해주셔서 감사합니다. 늦게 확인해서 죄송합니다.

      상단의 ORA-00933 에러같은 경우 쿼리문의 조립에 문제가 생긴 경우이므로 매퍼의 조건식을 바꿔주셔야합니다.

      삭제
  16. 게시글 검색이 잘되는데 검색을 한후에 검색조건이 풀려버립니다. 예를들어 2번째 셀렉에 있는 내용으로 검색하면 내용으로 검색잘되지만 검색후 그 셀렉창이 처음인 제목으로 바뀌던데 이거 고정 어찌할까요?

    답글삭제
    답글
    1. 아 잘되네요 ㅎㅎ제가 잘못봤네요

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

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

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

      삭제
  19. 문득 궁금증이생기는데, url에 검색내용 페이지번호가 다 나오게 되면 보안적인문제도 있을수있는데 url을 가리는 방법은 없나요?post방식 말구요

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

      주소표시줄..그러니까 URL쪽에 현재 검색과 관련된 데이터를 가리고싶으시다면
      세션값에다가 값을 넣어서 사용하거나 input hidden 등을 이용하시면 되겠습니다.
      다만 input hidden은 일반적으론 보이지 않으나 소스보기를 통하여 확인할 수 있습니다.

      삭제
    2. Input hidden 을 어디에 넣으면되는거죠?input쓴다면 post로 하면되는거같긴한데 생각보다 잘 안되네요ㅠ

      삭제
    3. Post방법으로 키워드를 보내서 검색까진 잘되는데 검색후 2페이지나 3페이지로 클릭시 키워드가 풀려서 검색안한 전체글 페이지 2 나 3으로 넘어가네요ㅠ

      삭제
  20. 안녕하세요. 문득 보다가 습관적으로 @ModelAttribute("scri") SearchCriteria scri를 그냥 지나쳤는데요
    정확히 여기서 modelattribute가 하는일이 무엇인가요?
    list jsp보면 jsp쪽에 데이터를 넘겨주는것도아니고,
    반대로 jsp쪽에서 컨트롤러로 데이터를 넘겨주는것도 아닌거같아서요

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

      확인이 늦었네요ㅠㅠ;
      scri는 프론트에서 검색 조건(검색어, 검색타입)을 백으로 전달해주는 역할입니다.

      삭제
  21. 쿼리부분에서 질문이 있는데
    xml에서


    where title like '%' || #{keyword} || '%'
    와같이 if test라는것을 많이쓰잖아요
    1. 여기서 if test안에 들어가는 searchType은 자바의 변수인가요 ? 아니면 jsp 페이지에서의 searchType을 이야기하는것인가요?
    2. jsp페이지에서의 변수라고 하면 자바쪽 VO에서 선언을 안해도 되는것인가요?

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

      말씀하신대로 searchType는 Criteria를 상속받는 SearchCriteria에서 사용되는 변수입니다.
      또한 jsp페이지에서 사용하는 searchType이기도합니다

      명확하게 말하자면 이름만 같은 다른 데이터지만, 데이터 흐름에서 항상 동일한 값을 가지고 있기 때문에 [ 뷰 - 백 - db ] 모두 이어진다고 보시면 됩니다.

      해당 데이터는 root-context.xml 의 설정값을 불러오며
      개발자가 신경 안써도 같은 변수명은 스프링이 알아서 매칭해줍니다. (프레임워크의 장점)

      삭제
    2. 한가지 더 질문이있습니다.
      저는 그게 파라미터처럼 jsp에서 컨트롤러로 던저주는 변수로 알고있는데,
      자바에서 뵨슈선언을 안해도 정상적으로 돌아가더라구요.
      그럼 말씀하신거는 자바에서 같은이름으로 선언을 한다면 동일 값을 searchType에 넣어주어
      같은 데이터를 담고있다는 뜻인거죠?

      삭제
    3. 이전 답글에 작성한 명확하게 말하자면 이름만 같은 다른 데이터지만, 데이터 흐름에서 항상 동일한 값을 가지고 있기 때문에 [ 뷰 - 백 - db ] 모두 이어진다고 보시면 됩니다. 대로...

      명확하게 말하자면 [ 프론트 ↔ 백 ]에서 오가는 searchType은 이름만 같은것입니다. 이렇게 이름이 같을때 프론트에서 백으로, 즉 컨트롤러나 매퍼에서 받을때 자동으로 매칭해주는 기능이 프레임워크에 포함되어있습니다.

      그러므로, 첫줄에서 '이름만 같다'라고 했으나 실제로 프론트의 searchType와 백의 searchType는 우리가 의도한 데이터만 가지고 있으므로, 사용하는데 있어서 사실상 같은 데이터라고 보시면 됩니다.


      '다르지만 같은'이라는 모호한 표현해서 햇갈릴 수 있는데...
      예를들어 프론트에서 searchType이 아닌 user_name 라고 만들어서(실제로는 searchType의 데이터가 들어가있음) 백으로 보낼 경우, 백단에서는 user_name를 다시 searchType로 바꿔서 사용하면 됩니다. 이렇게하면 나중에 유지보수할때 귀찮아질테지만... 이런 귀찮은 작업을 하지 않기 위해서 프론트와 백에서 공통적으로 사용하는 변수명은 같은 이름으로 다룹니다.

      삭제
  22. listSearch 주소 들어가려면 404 뜨고 .

    PageNotFound - No mapping found for HTTP request with URI [/board/listSearch] in DispatcherServlet with name 'appServlet'

    이렇게 떠요 어디에 매핑해줘야 하는건가요? listSearch.jsp 가 잘못된건가요?

    답글삭제
    답글
    1. 는 controller 에 매핑해주니까 되네요 근데 이번엔 500 에러 뜨면서 ora-00933 이 뜨는중 ㅠㅠ

      삭제
    2. 매퍼의 조건식을 어떻게 바꿔야할지 감이안잡혀요 ㅠㅠ

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

      작업하신대로 ULR과 매퍼의 매핑URL은 같아야하고
      ora-00933에러의 경우 sql문이 잘못 작성된걸로 추측됩니다.

      삭제
    4. 그 깃허브 board master 에 있는 boardMapper.xml 파일 그대로 가져다 쓰는데 이렇게떠용 ㅠ

      ### SQL: select count(bno) from myBoard and bno > 0
      ### Cause: java.sql.SQLSyntaxErrorException: ORA-00933: SQL command not properly ended

      ; bad SQL grammar []; nested exception is java.sql.SQLSyntaxErrorException: ORA-00933: SQL command not properly ended
      ]을(를) 발생시켰습니다.
      java.sql.SQLSyntaxErrorException: ORA-00933: SQL command not properly ended

      삭제
    5. 에러 메시지에 있는 쿼리문을 보니, boardMapper.xml에서 countSearch에 해당되는 쿼리문입니다.
      search 함수가 정상적으로 동작되려면 searchType에 값이 있어야하는데, searchType에 값이 없어서 정상적인 쿼리문이 되지 않았네요.

      <sql id="search">
      <if test="searchType != null">
      <if test="searchType == 't'.toString()">where title like '%' || #{keyword} || '%'</if>
      <if test="searchType == 'c'.toString()">where content like '%' || #{keyword} || '%'</if>
      <if test="searchType == 'w'.toString()">where writer like '%' || #{keyword} || '%'</if>
      <if test="searchType == 'tc'.toString()">where (title like '%' || #{keyword} || '%')
      or (content like '%' || #{keyword} || '%')</if>
      </if>
      </sql>



      <select id="countSearch" resultType="int">
      select count(bno)
      from myBoard
      <include refid="search"></include>
      <![CDATA[
      and bno > 0
      ]]>
      </select>

      삭제
  23. select bno, title, content, writer, regDate, viewCnt
    from (
    select bno, title, content, writer, regDate, viewCnt,
    row_number() over(order by bno desc) as rNum
    from myBoard
    where title like '%' || '' || '%' -- 제목
    ) mb
    where rNum between 1 and 10
    order by bno desc;

    젤첨에 이거 4개다 따로 작성하고 커밋해줘야 하는건가요?
    그리고 제가 '더미' 가 아니라 내용물이 뒤죽박죽이라 더미로 하면 안나오는데
    그냥 || '' || 이런식으로 해도 되는건가요?

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

      || 은 문자열을 더하는 역할입니다.
      '%' || '구글' || '%''%구글%'

      본문에서는 '더미'라는 데이터를 넣어서 테스트했고, 실제로는 #{keyword}으로 변경하여 사용했습니다.

      삭제
  24. 새해 복 많이 받으십시오 궁금한게 있어서 여쭙니다
    지금 그대로 따라해봤는데 keyword 부분이랑 rowStart rowEnd 부분이 ?로 들어가서 아예 실행이 안되네요 왜그럴까요

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

      로그에 ? 라고 나왔다면 정상입니다. (? 부분이 입력되는 값으로 변경되는 구조입니다)
      실제로 봐야할건, 정말로 제대로된 데이터가 들어갔는지의 여부입니다.

      로그상에도 그 데이터가 표시될텐데, 확인이 어렵다면 출력코드(System.out.println)등을 이용하여 프론트에서 전달된 값이 컨트롤러에 제대로 전송되었는지 확인해야합니다.

      삭제
    2. 오 빠르게 답변해주셨네요 그 github에서 게시판 코드 올려주신거 가져와서 써봤는데 num=1페이지에선 잘뜨는데 num=2페이제에선 값이 없어서 글목록이 하나도 안뜨네요 이건 왜그럴까요

      삭제
    3. 해결했습니다 postNum이 10으로 고정되어 있어서 그랬던거네요 displayPost부분에서 (num-1)*postNum을 (num-1)*10으로 바꿔주고 postNum을 display + 10으로 해주니 잘뜨네요

      삭제
  25. 안녕하세요. 궁금한게있는데요.
    지금 이게 먹히지 않아서
    java.sql.SQLSyntaxErrorException: ORA-00933: SQL 명령어가 올바르게 종료되지 않았습니다. 이런에러가뜨네여...

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

      말씀하신 ORA-00933 에러는 2개 이상의 명령어를 세미콜론으로 구분하지 않고 사용했거나, 명령어에 오타가 있을 경우 발생하는 에러입니다.

      삭제
  26. 익명5/10/2023

    여기서 막히시는 분들이 꽤 많으실꺼 같습니다.. 정말 막혔다면 이 방법을 써보세요
    일단 SELECT bon,title,content,writer,regdate,viewCnt 부분 From myBoard밑에
    WHERE 1=1을 붙여 주시면 일단은 오류가 나지 않습니다 무조건 참을 만들어 주기 때문이죠검색 조회 쿼리를 만들때는 사용해도 되지만 업데이트가 DELECT 문에서는 사용하면 안됩니다

    답글삭제
    답글
    1. 익명5/10/2023

      그래서 이렇게 할시 제목에 없는걸 검색해도 받아는 와집니다 물론 결과는 안나옵니다
      대신 맞는 제목을 검색하면 제목에 맞는 검색어는 잘 가져옵니다
      많은 커뮤니티 사이트를 보면 막 쳐도 일단 주소 키워드 옆에 사이트로는 이동이 되는데
      아마 WHERE 1=1 때문이지 않을까 조심스럽게 생각해봅니다

      삭제
    2. 익명5/10/2023

      피곤해서 좀 빼먹었는데요
      사이트로는 이동은 되지만 쿼리에 검색 결과가 없어 검색 결과가 따로 나오지는 않습니다

      삭제
    3. 익명5/10/2023

      어찌보면 불확실성을 배제한 잘못된 방법일 수도 있지만
      너무 막히면.. 이렇게 하는것도 전 나쁘지 않다고 생각합니다

      삭제
  27. 익명5/20/2023

    안녕하세요 검색구현하는 페이지 까지 다 완료 하였는데 검색할 내용 적고 검색 버튼을 누르면 검색한 내용만 뜨는게 아니라 현재창 그대로 반응이 없는데 이유좀 알수있을까요 ?? url에는 keyword='내가적은내용' 으로 바껴있긴합니다.

    답글삭제
  28. 익명5/26/2023

    안녕하세요 한페이지 내에서 윗쪽에 검색창과 왼쪽에 검색창 두개 구현할려고 진행중인데.. 왼쪽 메뉴쪽에 검색창은 잘 작동되는데 윗쪽에 검색창을 검색하면 자꾸 왼쪽메뉴에 키워드를 받아와서 왼쪽메뉴창 검색으로 작동이 되네요. 어딜 손 봐야할지 알수있을까요 ??

    답글삭제
  29. 안녕하세요..
    구글링하다가 여기까지왔는데요
    검색을하면 영어,숫자는 정상적으로 되는데
    한글만 넣으면 ã……êµ 이런식으로 한글이 깨져버리더라고요
    UTF-8 설정 다했고 produces = "text/html;charset=UTF-8"
    까지 다 지정했는데
    다른페이지 다른기능에서는 정상적으로 한글 잘되는데
    꼭 검색기능에서만 한글이 깨져서 들어가더라고요.. 아시는분 댓글좀 남겨주세요

    답글삭제
    답글
    1. 저같은사람있을까봐 해결방법 남깁니다.
      server.xml에서




      포트번호는 설정한거에따라 다르고
      각각 설정 뒤에 URIEncoding="UTF-8" 붙여주면정상적으로 넘어옵니다.
      감사합니다

      삭제