-
[3-ch15 Spring 게시판] 검색 조건 처리Back-End/Spring Legacy 2022. 8. 29. 20:57
part3 마무리하며 소스 코드는 github에서 참고바랍니다.
https://github.com/ssowoni/Spring-Web-Board-Book
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가지이므로 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>
<조회 페이지에서 검색 처리 >
목록 페이지에서 조회 페이지로의 이동은 이미 <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>
반응형'Back-End > Spring Legacy' 카테고리의 다른 글
[4-ch17 댓글 처리 ① ] sql 댓글 테이블 생성과 영속 영역 설계 (MyBatis) (0) 2022.09.01 [4-ch16 REST 방식] @RestController, ResponseEntity, @RequestParam , 어노테이션과 JSON 알아보기 (0) 2022.08.31 [3-ch15 Spring ] MyBatis의 동적 태그 ( if, choose, trim)와 LIKE 사용 방법 (0) 2022.08.29 [3-ch14 Spring 게시판] view, 페이징의 화면 처리 (0) 2022.08.29 [Spring] RedirectAttributes 과 Model 알아보기 (0) 2022.08.29