ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [3-ch15 Spring 게시판] 검색 조건 처리
    Back-End/Spring Legacy 2022. 8. 29. 20:57

     

     

    part3 마무리하며 소스 코드는 github에서 참고바랍니다.

     

    https://github.com/ssowoni/Spring-Web-Board-Book

     

    GitHub - ssowoni/Spring-Web-Board-Book

    Contribute to ssowoni/Spring-Web-Board-Book development by creating an account on GitHub.

    github.com

     

     

     

    ch14의 페이징 처리에 사용했던 Criteria의 의도는 단순히 'pageNum'과 'amount'라는 파라미터를 수집하기 위해서이다. 페이징 처리에 검색 조건 처리가 들어가면 Criteria 역시 변화가 필요하다. ( Criteria는 '검색의 기준'을 의미한다. ) 

     

    검색 조건을 처리하기 위해서는 검색 조건(type)과 검색에 사용하는 키워드가 필요하므로 type과 keyword라는 변수를 추가한다. 

     

     

    <Criteria.java>

    package org.zerock.domain;
    
    import lombok.Getter;
    import lombok.Setter;
    import lombok.ToString;
    
    @Getter
    @Setter
    @ToString
    //Criteria는 '검색의 기준'을 의미한다.
    public class Criteria {
    	
    	private int pageNum; //페이지 번호
    	private int amount; // 한 페이지당 게시글 개수
    	
    	private String type;
    	private String keyword;
    	
    	
    	
    	public Criteria() {
    		this(1,10);
    	}
    
    	public Criteria(int pageNum, int amount) {
    		this.pageNum = pageNum;
    		this.amount = amount;
    	}
    	
    	//BoardMapper.xml에서 type 문자열을 쪼개기 위한 메서드
    	public String[] getTypeArr() {
    		return type == null? new String[] {} : type.split("");
    	}
    }

     

     

    getTypeArr은 검색 조건이 각 글자 (T,W,C)로 구성되어 있으므로 검색 조건을 배열로 만들어서 한 번에 처리하기 위함이다. getATypeArr()을 이용해서 MyBatis의 동적 태그를 활용할 수 있다. 

     

     

     

     

    BoardMapper.xml에서 Criteria 처리 ( MyBatis 동적 쿼리 )

     

     

    동적 쿼리 태그 관련 포스팅은 아래 클릭! 

     

    https://wonisdaily.tistory.com/67

     

    [3-ch15 Spring ] MyBatis의 동적 태그 ( if, choose, trim)와 LIKE 사용 방법

    검색 기능과 SQL 게시물의 검색 기능은 아래와 같이 분류가 가능하다. - 제목/내용/작성자와 같이 단일 항목 검색 - 제목 or 내용 , 제목 or 작성자 ,,,, 같은 다중 항목 검색 단일 항목 검색과, 복합

    wonisdaily.tistory.com

     


     

    검색 조건이 3가지이므로 6가지의 조합이 가능하지만, 각 문자열을 이용해 검색 조건을 결합하는 형태로 하면 3개의 동적 sql 구문만으로도 처리할 수 있다. <foreach> 를 이용해 검색 조건들을 처리하는데 typeArr이라는 속성을 이용한다. MyBatis는 원하는 속성을 찾알 때 getTypeArr()과 같이 이름에 기반을 두어 검색하기 때문에 Criteria에서 만들어둔 getTypeArr() 결과인 문자열의 배열이 <foreach>의 대상이 된다. 

     

     

    <!-- 페이징 처리 목록 조회 , 검색 기능 추가-->
    <select id="getListWithPaging" resultType="org.zerock.domain.BoardVO">
    <![CDATA[
    select bno, title, content, writer, regdate, updatedate
    from ( 
    select /*+ INDEX_DESC(tbl_board pk_board ) */ 
    rownum rn, bno, title,content, writer, regdate, updatedate 
    from tbl_board
    where 
    ]]>
    <trim prefix="(" suffix=") AND " prefixOverrides="OR">
    <foreach item="type" collection="typeArr">
    <trim prefix="OR">
    <choose>
    <when test="type == 'T'.toString()">
    title like '%'||#{keyword}||'%'
    </when>
    <when test="type == 'C'.toString()">
    content like '%'||#{keyword}||'%'
    </when>
    <when test="type == 'W'.toString()">
    writer like '%'||#{keyword}||'%'
    </when>
    </choose>
    </trim>
    </foreach>
    </trim>
    <![CDATA[
    
    rownum <= #{pageNum} * #{amount}
    )
    where rn> (#{pageNum} -1 ) * #{amount}
    ]]>
    </select>

     

     

     

    동적 sql을 이용해서 검색 조건을 처리하는 부분은 해당 데이터의 개수를 처리하는 부분에서도 동일하게 적용되어야 한다. ( 게시글의 개수 구할 때 count(*) ) 이 경우 간단한 방법은 동적 SQL을 처리하는 부분을 그대로 복사해서 넣어줄 수 있지만 만일 동적 SQL을 수정하는 경우 같이 수정해줘야 한다.

     

    MyBatis는 <sql>이라는 태그를 이용해 SQL의 일부를 별도로 보관하고 필요한 경우, include 시키는 형태로 사용할 수 있다. 

     

     

    <sql id="criteria">
    <trim prefix="(" suffix=") AND " prefixOverrides="OR">
        <foreach item="type" collection="typeArr">
            <trim prefix="OR">
                <choose>
                    <when test="type == 'T'.toString()">
                        title like '%'||#{keyword}||'%'
                    </when>
                    <when test="type == 'C'.toString()">
                        content like '%'||#{keyword}||'%'
                    </when>
                    <when test="type == 'W'.toString()">
                        writer like '%'||#{keyword}||'%'
                    </when>														
                </choose>
            </trim>
        </foreach>
    </trim>
    </sql>
    
    
    
    <!-- 페이징 처리 목록 조회 , 검색 기능 추가-->
    <select id="getListWithPaging" resultType="org.zerock.domain.BoardVO">
        <![CDATA[
            select bno, title, content, writer, regdate, updatedate
            from ( 
            select /*+ INDEX_DESC(tbl_board pk_board ) */ 
            rownum rn, bno, title,content, writer, regdate, updatedate 
            from tbl_board
            where 
        ]]>
    
        <include refid="criteria"></include>
    
        <![CDATA[	
    
            rownum <= #{pageNum} * #{amount}
            )
            where rn> (#{pageNum} -1 ) * #{amount}
        ]]>
    </select>
    
    <!-- 전체 페이지 수 조회 + 검색 조건 추가  -->	
    <select id="getTotalCount" resultType="int">
        select count(*) from tbl_board 
        where 
        <include refid="criteria"></include>
        bno >0
    </select>

     

     

    <trim> 태그의 prefix값은 시작하는 값을 의미하며 '('로 시작하고 suffix=") AND"는 끝난다는 것을 의미한다. 또한 prefixOverrides 처음 나온 OR 값을 삭제,대체한다. <choose> 안쪽의 동적 SQL은  'OR title, OR content, OR writer...'와 같은 구문을 만들어 주는데 trim에서 맨 앞에 생성되는 OR을 없애준다. 

     

     

    테스트 코드를 통해 테스트를 진행하며 생성되는 쿼리문을 살펴보자.

     

     

    <BoardMapperTests 의 일부>

    @Test
    public void testSearch() {
        Criteria criteria = new Criteria();
        criteria.setKeyword("수정");
        criteria.setType("T");
    
        List<BoardVO> list = mapper.getListWithPaging(criteria);
    
        list.forEach(board -> log.info(board));
    }

     

    setType에 값에 따라 쿼리문이 다르게 생성된다.

     

     

    1. 검색 조건이 없는 경우

    : where 다음에 바로 rownum이 오는 것을 확인할 수 있다. 이는 choose 반복문에서 일치하는 조건값이 없기 때문이다. 즉) 검색 조건을 아무것도 입력하지 않았을 때 실행되는 쿼리문이다.

     

     

     

     

    2. 단일 검색 (제목)

    : 위의 테스트 코드에서 setType으로 ("T")라는 제목 값을 하나 줬을 때 실행되는 쿼리문이다.

     

     

     

    3.다중 검색 (제목 or 내용)

    : setType으로 ("TC")라는 조건 값이 들어갔을 때 실행되는 쿼리문이다. 맨 앞 OR은 출력되지 않은 걸 확인할 수 있다. 

     

     

     

     


     

    화면에서 검색 조건 처리

     

    화면에서 검색은 아래와 같은 사항을 주의해서 개발해야한다.

    - 페이지 번호가 파라미터로 유지되었던 것처럼 검색 조건과 키워드 역시 항상 화면 이동 시 같이 전송되어야 한다. 
    - 화면에서 검색 버튼을 클릭하면 새로 검색을 한다는 의미이므로 1페이지로 이동한다.
    - 한글의 경우 GET 방식으로 이동하는 경우 문제가 생길 수 있으므로 주의해야 한다.

     

     

    <목록 화면에서 검색 처리>

     

     

    검색 버튼을 클릭하면 검색은 1페이지를 하도록 수정하고, 화면에 검색 조건과 키워드가 보이게 처리하는 작업을 우선으로 한다. 브라우저에서 검색 버튼을 클릭하면 <form> 태그의 전송은 막고, 페이지 번호는 1이 되게 처리한다. 화면에서 키워드가 없다면 검색을 하지 않도록 제어한다. 

     

    var searchForm = $("#searchForm");
    
    $("#searchForm button").on("click", function(e){
    
        if(!searchForm.find("option:selected").val()){
            alert("검색종류를 선택하세요");
            return false;
        }
    
        if(!searchForm.find("input[name='keyword']").val()){
            alert("키워드를 입력하세요");
            return false;
        }
    
        searchForm.find("input[name='pageNum']").val("1");
        e.preventDefault();
    
        searchForm.submit();
    
    });

     

     

    검색 후 주소창에 검색 조건과 키워드가 같이 GET 방식으로 처리되므로 이를 이용해서 list.jsp에 추가한다.

    <select> 태그의 내부는 삼항 연산자를 이용해 해당 조건으로 검색되었다면 selected라는 문자열을 출력해 화면에서 선택된 항목으로 보이도록 한다. 

     

     

    <!-- 검색 조건 처리 -->
    <div class='row'>
        <div class="col-lg-12">
    
            <form id='searchForm' action="/board/list" method='get'>
                <select name='type'>
                    <option value=""
                        <c:out value="${pageMaker.cri.type == null?'selected':''}"/>>--</option>
                    <option value="T"
                        <c:out value="${pageMaker.cri.type eq 'T'?'selected':''}"/>>제목</option>
                    <option value="C"
                        <c:out value="${pageMaker.cri.type eq 'C'?'selected':''}"/>>내용</option>
                    <option value="W"
                        <c:out value="${pageMaker.cri.type eq 'W'?'selected':''}"/>>작성자</option>
                    <option value="TC"
                        <c:out value="${pageMaker.cri.type eq 'TC'?'selected':''}"/>>제목
                        or 내용</option>
                    <option value="TW"
                        <c:out value="${pageMaker.cri.type eq 'TW'?'selected':''}"/>>제목
                        or 작성자</option>
                    <option value="TWC"
                        <c:out value="${pageMaker.cri.type eq 'TWC'?'selected':''}"/>>제목
                        or 내용 or 작성자</option>
                </select> <input type='text' name='keyword'
                    value='<c:out value="${pageMaker.cri.keyword}"/>' /> <input
                    type='hidden' name='pageNum'
                    value='<c:out value="${pageMaker.cri.pageNum}"/>' /> <input
                    type='hidden' name='amount'
                    value='<c:out value="${pageMaker.cri.amount}"/>' />
                <button class='btn btn-default'>Search</button>
            </form>
        </div>
    </div>

     

     

     

    type, keyword 미선택 시 경고창

     

    검색 후 1페이지로 이동

     


     

     

    <조회 페이지에서 검색 처리 >

     

    목록 페이지에서 조회 페이지로의 이동은 이미 <form> 태그로 처리했기 때문에 별도 처리할 건 없고 <input> 속성 값으로 type과 keyword도 함께 처리해준다. 

     

    get.jsp

    <button data-oper='modify' class="btn btn-default">Modify</button>
    <button data-oper='list' class="btn btn-info">List</button>
    <form id='operForm' action="/board/modify" method="get">
        <input type='hidden' id='bno' name='bno' value=${board.bno } />
        <input type='hidden' id='pageNum' name='pageNum' value=${cri.pageNum } />
        <input type='hidden' id='amount' name='amount' value=${cri.amount } />
        <input type='hidden' name='type' value='${cri.type}'>
        <input type='hidden' name='keyword' value='${cri.keyword}'>
    </form>

     

     

    <수정/삭제 페이지에서 검색 처리>

     

    modify.jsp

    <form role="form" action="/board/modify" method="post">
    
    <input type='hidden' id='pageNum' name='pageNum' value=${cri.pageNum } />
    <input type='hidden' id='amount' name='amount' value=${cri.amount } />
    <input type='hidden' name='type' value='${cri.type}'>
    <input type='hidden' name='keyword' value='${cri.keyword}'>

     

    수정/삭제 처리는 BoardController에서 redirect 방식으로 동작하므로 type 과 keyword 조건을 같이 리다이렉트 시에 포함시켜야 한다.  

     

    @PostMapping("/modify")
       public String modify(BoardVO board,RedirectAttributes rttr , @ModelAttribute("cri") Criteria cri) {
          log.info("modify.....");
          if(service.modify(board)) {
             rttr.addFlashAttribute("result", "success");
          }
          
          rttr.addAttribute("pageNum", cri.getPageNum());
          rttr.addAttribute("amount", cri.getAmount());
          rttr.addAttribute("type", cri.getType());
          rttr.addAttribute("keyword", cri.getKeyword());
          
          
          return "redirect:/board/list";
       }
       
       @PostMapping("/remove")
       public String remove(@RequestParam("bno") Long bno, RedirectAttributes rttr, @ModelAttribute("cri") Criteria cri) {
          log.info("remove ... " +bno);
          if(service.remove(bno)) {
             rttr.addFlashAttribute("result", "success");
          }
          rttr.addAttribute("pageNum", cri.getPageNum());
          rttr.addAttribute("amount", cri.getAmount());
          rttr.addAttribute("type", cri.getType());
          rttr.addAttribute("keyword", cri.getKeyword());
          
          return "redirect:/board/list";
       }

     

     

    또한, 리다이렉트는 get 방식으로 이루어지기 때문에 추가적인 파라미터를 처리해야한다. modify.jsp에서는 다시 목록으로 이동하는 경우 필요한 파라미터만 전송하기 위해서 <form>태그의 모든 내용을 지우고 다시 추가하는 방식을 이용했다. 따라서 keyword와 type 역시 추가하도록 아래와 같이 관련 JavaScript 코드를 수정해야 한다.

     

     

    <script type="text/javascript">
    $(document).ready(function(){
    
        var formObj = $("form");
    
        $('button').on("click",function(e){
            //기본동작을 막는다. 버튼을 누르면 해야하는 일 (ex. form 태그 안에서 버튼을 누르면 submit)을 막아준다.
            e.preventDefault();
            //$(this) = button을 의미한다.
            var operation =$(this).data("oper");
    
            console.log(operation);
    
            if(operation=='remove'){
                formObj.attr("action", "/board/remove");
            }else if(operation =='list'){
                /* self.location ="/board/list";
                return; */
                formObj.attr("action","/board/list").attr("method","get");
                var pageNumTag = $("input[name='pageNum']").clone(); //clone : 내용 복사 후 보관
                var amountTag = $("input[name='amount']").clone();
                var typeTag = $("input[name='type']").clone(); //clone : 내용 복사 후 보관
                var keywordTag = $("input[name='keyword']").clone();				
    
                formObj.empty();
                formObj.append(pageNumTag);
                formObj.append(amountTag);
                formObj.append(typeTag);
                formObj.append(keywordTag);
            }
            formObj.submit();
        });
    });
    </script>

     

    반응형

    댓글

Designed by Tistory.