해당 프로젝트는 코드로 배우는 스프링 웹 프로젝트(개정판)을 기반으로 진행됩니다.
게시물 관리의 마지막은 "검색 처리"이다.
select 태그를 이용해서 검색 기능과 화면을 처리해보자.
15.1 검색 기능과 SQL
게시물의 검색 기능은 아래와 같이 분류가 가능하다.
1) 제목 / 내용 / 작성자와 같이 단일 항목 검색
2) 제목 or 내용 / 제목 or 작성자 / 내용 or 작성자 / 전체 와 같은 다중 항목 검색
15.2 MyBatis의 동적 SQL
: MyBatis는 동적(Dynamic) 태그 기능을 통해서 SQL을 파라미터들의 조건에 맞게 조정할 수 있는 기능을 제공한다.
15.2.1 MyBatis의 동적 태그들
- if : 특정한 조건이 True가 되었을 때 포함된 SQL을 사용하고자 할 때 작성. (True가 된 조건들에 대해 모두 동작)
- choose (when, otherwise) : if와 달리 여러 상황들 중 하나의 상황에서만 동작함
- trim (where, set) : 태그의 내용을 앞의 내용과 관련되어 원하는 접두/접미를 처리 가능
- prefix : <trim>문에 의해 생성되는 SQL 구문 앞에 추가
- suffix : <trim>문의 의해 생성되는 SQL 구문 뒤에 추가
- prefixOverrides : <trim>문에 의해 생성되는 SQL의 가장 앞에 해당 문자가 있으면 지워버림.
- suffixOverrides : <trim>문에 의해 생성되는 SQL의 가장 뒤에 해당 문자가 있으면 지워버림.
- foreach : List, 배열, 맵 등을 이용해서 루프를 처리
15.3 검색 조건 처리를 위한 Criteria의 변화
: 앞서 페이징 처리에 사용되었던 Critera의 의도는 단순히 'pageNum'과 'amount'라는 파라미터를 수집하기 위해서였다. 이제 검색조건을 처리하기 위해서는 검색조건(type)과 검색에 사용되는 키워드가 필요하므로 해당 클래스를 확장해본다.
Criteria.java 클래스 수정
package org.zerock.domain;
import org.springframework.web.util.UriComponentsBuilder;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@Getter
@Setter
@ToString
public class Criteria {
private int pageNum;
private int amount; //한 페이지에 보일 게시글의 개수
private String type;
private String keyword;
public Criteria() {
this(1, 20);
}
public Criteria(int pageNum, int amount) {
this.pageNum = pageNum;
this.amount = amount;
}
public String[] getTypeArr() {
//검색조건이 T(Title), W(Writer), C(Content)로 구성되어 있으므로 검색 조건을 배열로 만들어서 한 번에 처리하기 위함
return type == null ? new String[] {}: type.split("");
}
}
15.3.1 BoardMapper.xml에서 Criteria 처리
: getListWithPaging()을 수정해서 동적 SQL을 처리해본다.
BoardMapper.xml
<!-- 게시글의 페이지 번호와 읽어올 개수에 따라 게시글을 조회하는 함수 -->
<!-- CDATA 섹션은 XML에서 사용할 수 없는 부등호를 사용하기 위함. XML의 경우 '<,>'는 태그로 인식하는데, 이를 막기 위함이다. -->
<select id ="getListWithPaging" resultType="org.zerock.domain.BoardVO">
<![CDATA[
select *
from
(select /*+ INDEX_DESC(tbl_board pk_board) */
rownum rn, bno, title, content, writer, cdate, udate
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>
검색 조건이 3가지 이므로 총 6가지의 조합이 가능하지만, 각 문자열을 이용해서 검색 조건을 결합하는 형태로 하면 3개의 동적 SQL구문만으로도 처리할 수 있다.
동적 SQL이 잘 동작하는지 테스트 코드를 만들어 테스트 해보자.
BoardMapperTests.java의 일부
테스트1
@Test
public void testSearch() {
Criteria cri = new Criteria();
cri.setKeyword("새로");
cri.setType("TCW");
List<BoardVO> list = mapper.getListWithPaging(cri);
list.forEach(board -> log.info(board));
}
실행 결과1
: 컨텐츠, 작성자, 제목 모두에서 검색될 수 있도록 쿼리가 생성됨.
테스트2
@Test
public void testSearch() {
Criteria cri = new Criteria();
cri.setKeyword("안녕");
cri.setType("T");
List<BoardVO> list = mapper.getListWithPaging(cri);
list.forEach(board -> log.info(board));
}
실행결과2
: 제목에서만 검색됨을 확인
MyBatis에서는 이라는 태그를 이용해서 SQL일부를 별도로 보관하고, 필요할 때 시키는 형태로 사용가능하다.
BoardMapper.xml <sql>, <include> 적용하기
..생략
<!-- sql 태그는 id라는 속성을 이용해서 필요한 경우에 동일한 SQL의 일부를 재사용할 수 있다. -->
<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>
<!-- 게시글의 페이지 번호와 읽어올 개수에 따라 게시글을 조회하는 함수 -->
<!-- CDATA 섹션은 XML에서 사용할 수 없는 부등호를 사용하기 위함. XML의 경우 '<,>'는 태그로 인식하는데, 이를 막기 위함이다. -->
<select id ="getListWithPaging" resultType="org.zerock.domain.BoardVO">
<![CDATA[
select *
from
(select /*+ INDEX_DESC(tbl_board pk_board) */
rownum rn, bno, title, content, writer, cdate, udate
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>
15.4 화면에서 검색 조건 처리
: 화면에서 검색은 아래와 같은 사항들을 주의해서 개발해야 한다.
- 페이지 번호가 파라미터로 유지되었던 것처럼 검색 조건과 키워드 역시 항상 화면 이동 시 같이 전송되어야 함.
- 화면에서 검색 버튼을 클릭하면 새로 검색을 한다는 의미이므로 1페이지로 이동해야 함.
- 한글의 경우 GET 방식으로 이동하는 경우 문제가 생길 수 있으므로 주의가 필요함.
15.4.1 목록 화면에서의 검색 처리
: 목록 화면인 list.jsp에서 검색 조건과 키워드가 들어갈 수 있게 HTML 수정이 필요하다.
list.jsp
..생략
<!-- 수정 후 : 검색 조건 처리 부분 시작 -->
<div class='row'>
<div class="col-lg-12">
<form id='searchForm' action="/board/list" method="get">
<select name='type'>
<option value="">---</option>
<option value="T">제목</option>
<option value="C">내용</option>
<option value="W">작성자</option>
<option value="TC">제목 or 내용</option>
<option value="TW">제목 or 작성자</option>
<option value="WC">내용 or 작성자</option>
<option value="TCW">제목 or 내용 or 작성자</option>
</select>
<input type='text' name='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>
<!-- 수정 후 : 검색 조건 처리 부분 끝 -->
<!-- start Pagination -->
<div class='pull-right'>
..생략
실행 화면
: 현재는 아래와 같은 문제가 발생한다.
- 예를 들어, 3페이지를 보다가 검색을 하면 3페이지에서 결과가 나온는 문제
- 검색 후 페이지를 이동하면 검색 조건이 사라지는 문제
- 검색 후 화면에서는 어떤 검색 조건과 키워드를 이용했는지 알 수 없는 문제
list.jsp의 검색 버튼의 이벤트 처리
: 검색 버튼을 클릭하면 검색을 1페이지를 향하도록 수정하고, 화면에 검색 조건과 키워드가 보이게 처리 해보자.
<script>
..생략
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"); //검색 후 페이지 번호는 1이 되도록 세팅
e.preventDefault();
searchForm.submit();
});
});
</script>
브라우저에서 검색 버튼을 클릭하면 <form>태그의 전송은 막고, 페이지의 번호는 1이 되도록 처리한다.
검색 후에는 주소창에 검색 조건과 키워드가 같이 GET 방식으로 처리되므로 이를 이용해서 <select> 태그나 <input> 태그의 내용 수정이 필요하다.
list.jsp에서 검색 조건과 키워드를 보여주는 부분
<!-- 수정 후 : 검색 조건 처리 부분 시작 -->
<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="WC" <c:out value="${pageMaker.cri.type eq 'WC' ? 'selected':'' }"/>>내용 or 작성자</option>
<option value="TCW" <c:out value="${pageMaker.cri.type eq 'TCW' ? '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> 태그를 아래와 같이 수정한다.
list.jsp의 일부
: 검색 조건과 키워드에 대한 처리가 되면 검색 후 페이지를 이동해서 동일한 검색 사항들이 계속 유지되는 것을 볼 수 있다.
<form id='actionForm' action="/board/list" method='get'>
<input type='hidden' name='pageNum' value = '${pageMaker.cri.pageNum}'>
<input type='hidden' name='amount' value = '${pageMaker.cri.amount}'>
<input type='hidden' name='type' value = '${pageMaker.cri.type}'>
<input type='hidden' name='keyword' value = '${pageMaker.cri.keyword}'>
</form>
실행 결과
: 어느 페이지에서도 제목에 "새로"가 들어간 게시글만 보임을 확인
15.4.2 조회 페이지에서 검색 처리
목록 페이지에서 조회 페이지로의 이동은 이미 <form> 태그를 이용해서 처리 했기 때문에 Criteria의 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='<c:out value="${board.bno }"/>'>
<input type="hidden" id='pageNum' name='pageNum' value='<c:out value="${cri.pageNum }"/>'>
<input type="hidden" id='amount' name='amount' value='<c:out value="${cri.amount }"/>'>
<input type="hidden" id='type' name='type' value='<c:out value="${cri.type }"/>'>
<input type="hidden" id='keyword' name='keyword' value='<c:out value="${cri.keyword }"/>'>
</form>
<!-- /.table-responsive -->
..생략
15.4.3 수정/삭제 페이지에서 검색 처리
조회 페이지에서 수정/삭제 페이지로의 이동은 GET 방식을 통해서 이동하고, 이동방식 또한 <form> 태그를 이용하는 방식이므로, Criteria의 type과 keyword 값만 추가로 전달해주면 된다.
modify.jsp
..생략
<div class="panel-heading"> Board Register </div>
<!-- /.panel-heading -->
<div class="panel-body">
<form role="form" action="/board/modify" method="post" >
<input type="hidden" id='pageNum' name='pageNum' value='<c:out value="${cri.pageNum }"/>'>
<input type="hidden" id='amount' name='amount' value='<c:out value="${cri.amount }"/>'>
<input type="hidden" id='pageNum' name='type' value='<c:out value="${cri.type }"/>'>
<input type="hidden" id='amount' name='keyword' value='<c:out value="${cri.keyword }"/>'>
..생략
수정/삭제 처리는 BoradController에서 redirect 방식으로 동작하므로 type과 keyword 조건을 같이 리다이렉트 시에 포함시켜야 한다.
BoardController.java 일부
..생략
//글 수정 메소드
@PostMapping("/modify")
public String modify(BoardVO board, RedirectAttributes rttr, @ModelAttribute("cri") Criteria cri) {
log.info("modify.." + board);
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, Model model, RedirectAttributes rttr, @ModelAttribute("cri") Criteria cri) {
log.info("/remove..");
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";
}
modify.jsp에서는 다시 목록으로 이동하는 경우에 필요한 파라미터만 전송하기 위해서 <form> 태그의 모든 내용을 지우고 다시 추가하는 방식을 이용하는데 이 때, type과 keyword도 전달할 수 있도록 수정이 필요하다.
modify.jsp
<script type="text/javascript">
$(document).ready(function() {
var formObj = $("form");
$('button').on("click", function(e) {
e.preventDefault(); // 기본 동작인 submit을 막고, 'data-oper' 속성을 이용해서 원하는 기능을 동작할 수 있도록 함. (마지막에 submit 처리)
var operation = $(this).data("oper");
console.log(operation);
//변수의 타입까지 모두 값을 때
if(operation === 'remove'){
formObj.attr("action", "/board/remove");
} else if (operation === 'list'){
//move to list
//self.location = "/board/list"; // self.location : 현재 페이지를 다른 페이지(URL)로 이동
formObj.attr("action", "/board/list").attr("method", "get");
//list 페이지로 이동할 때는 모든 값이 필요없고, pageNum, amount, type, keyword값만 필요함
var pageNumTag = $("input[name='pageNum']").clone(); //pageNum값을 복사하여 pageNumTag 변수에 저장
var amountTag = $("input[name='amount']").clone(); //amount값을 복사하여 pageNumTag 변수에 저장
var typeTag = $("input[name='type']").clone(); //type값을 복사하여 typeTag 변수에 저장
var keywordTag = $("input[name='keyword']").clone(); //keyword값을 복사하여 keywordTag 변수에 저장
//form 태그 내 모든 내용 삭제 후, 필요 태그(pageNum, amount, type, keyword)만 다시 추가
formObj.empty();
formObj.append(pageNumTag);
formObj.append(amountTag);
formObj.append(typeTag);
formObj.append(keywordTag);
}
formObj.submit();
});
});
</script>
실행 화면
UriComponentsBuilder를 이용하는 링크 생성
웹 페이지에서 매번 파라미터를 유지하는 일이 번거로울 때 사용을 추천한다.
web.util.UriComponentsBuilder는 여러 개의 파라미터들을 연결해서 URL 형태로 만들어 주는 기능을 한다.
URL을 만들어주면 리다이렉트 혹은 <form>태그를 사용하는 상황을 많이 줄 일 수 있다.
Criteria.java 일부
: queryParam()이라는 메소드를 이용해서 필요한 파라미터들을 쉽게 추가 가능하다. (GET 방식에 적합한 URL 인코딩된 결과로 만들어지므로 한글 처리에 신경쓰지 않아도 된다는 장점이 있음)
//여러 개의 파라미터들을 연결해서 URL의 형태로 만들어주는 기능을 가짐
//URL을 만들어주면, 리다리렉트를 하거나, <form> 태그를 사용하는 상황을 많이 줄일 수 있는 장점이 있음
public String getListLink() {
UriComponentsBuilder builder = UriComponentsBuilder.fromPath("")
.queryParam("pageNum", this.getPageNum())
.queryParam("amount", this.getAmount())
.queryParam("type", this.getType())
.queryParam("keyword", this.getKeyword());
return builder.toUriString();
}
BoardController.java 수정
//글 수정 메소드
@PostMapping("/modify")
public String modify(BoardVO board, RedirectAttributes rttr, @ModelAttribute("cri") Criteria cri) {
log.info("modify.." + board);
if(service.modify(board)) {
rttr.addFlashAttribute("result", "success");
}
return "redirect:/board/list" + cri.getListLink();
}
//글 삭제 메소드
@PostMapping("/remove")
public String remove(@RequestParam("bno") Long bno, Model model, RedirectAttributes rttr, @ModelAttribute("cri") Criteria cri) {
log.info("/remove..");
if( service.remove(bno)) {
rttr.addFlashAttribute("result", "success");
}
return "redirect:/board/list" + cri.getListLink();
}
UriComponentsBuilder로 생성된 URL은 화면에서도 유용하게 사용될 수 있는데,
주로 JavaScript를 사용할 수 없는 상황에서 링크를 처리해야하는 상황에서 사용된다.
다음 시간에는 REST 방식과 Ajax를 이용하는 댓글 처리 방법에 대해 알아본다.
'개발 > [Spring] 블로그 만들기' 카테고리의 다른 글
[코드로 배우는 스프링 웹 프로젝트] 17-1강. Ajax 댓글 처리 (게시판 댓글 페이징/CRUD 처리하기...) (0) | 2022.04.28 |
---|---|
[코드로 배우는 스프링 웹 프로젝트] 16강. REST 방식으로 전환 (REST/ @RestController/Restlet Client 프로그램) (0) | 2022.03.06 |
[코드로 배우는 스프링 웹 프로젝트] 14강. 페이징 화면 처리 (페이지네이션, 페이징처리, JQuery) (0) | 2022.02.13 |
[코드로 배우는 스프링 웹 프로젝트] 13강. MyBatis와 스프링에서 페이징 처리 (0) | 2022.02.03 |
[코드로 배우는 스프링 웹 프로젝트] 12강. 오라클 데이터베이스 페이징 처리 (Oracle/SQL/페이징처리) (0) | 2022.01.30 |