7. 검색 / 필터링 조건의 정의
- 검색 / 필터링 조건
- 제목(title)과 작성자(writer)는 키워드(keyword)를 이용하는 검색 처리
- 완료 여부를 필터링 처리
- 특정 기간을 지정(from, to)한 필터링 처리
- 검색 / 필터링에 필요한 데이터
- 제목, 작성자 검색에 사용하는 문자열 - keyword
- 완료 여부에 사용되는 boolean 타입 - finished
- 특정 기간 검색을 위한 LocalDate 변수 2개 - from, to
1) 검색 / 필터링 조건의 결정
- 검색 기능의 경우의 수를 구분하는 작업 필요
- 검색 종류를 types라고 지정, 제목(t)와 작성자(w)로 구분해서 검색의 실제값은 검색 종류에 따라 키워드(keyword)를 이용
- PageRequestDTO에 필요한 변수들을 추가해서 구성
// PageRequestDTO
package org.zerock.springex.dto;
import ...
public class PageRequestDTO {
...
private String[] types;
private String keyword;
private boolean finished;
private LocalDate from;
private LocalDate to;
...
}
2) types에 따른 동적 쿼리
- Mybatis에는 실행 시에 쿼리를 만들 수 있는 여러 태그들 제공
- if
- trim(where, set)
- choose
- foreach: 반복 처리를 위해 제공됨, List, Map, Set과 같은 컬렉션 계열이나 배열을 이용할 수 있음
- TodoMapperTests 클래스에 새로운 테스트 메서드 추가
// TodoMapperTests
@Test
public void testSelectSearch() {
PageRequestDTO pageRequestDTO = PageRequestDTO.builder()
.page(1)
.size(10)
.types(new String[]{"t", "w"})
.keyword("AAAA")
.build();
List<TodoVO> voList = todoMapper.selectList(pageRequestDTO);
voList.forEach(vo -> log.info(vo));
}
- TodoMapper의 selectList()는 PageRequestDTO를 파라미터로 받고 있으므로 변경없이 바로 사용 가능하므로 TodoMapper.xml만 수정
<!-- TodoMapper.xml -->
<select id = "selectList" resultType = "org.zerock.springex.domain.TodoVO">
select * from tbl_todo
<!-- <select id = "selectList">태그에 Mybatis의 <foreach>를 적용 -->
<foreach collection = "types" item = "type">
#type
</foreach>
order by tno desc limit #{skip}, #{size}
</select>
- 테스트 코드 실행 시, 쿼리문이 정상적이지 않아 에러가 나지만 출력된 쿼리문을 확인하면 다음과 같이 출력됨
select * from tbl_todo ? ? order by tno desc limit ?, ?
- if 적용 시 더 현실적인 쿼리를 만들어낼 수 있음
<!-- TodoMapper.xml -->
<select id = "selectList" resultType = "org.zerock.springex.domain.TodoVO">
select * from tbl_todo
<foreach collection = "types" item = "type">
<!-- 검색 타입이 t(제목)일 때, 제목에 keyword가 포함된 데이터 검색 -->
<if test = "type == 't'.toString()">
title like concat('%', #{keyword}, '%')
</if>
<!-- 검색 타입이 w(작성자)일 때, 작성자에 keyword가 포함된 데이터 검색 -->
<if test = "type == 'w'.toString()">
writer like concat('%', #{keyword}, '%')
</if>
</foreach>
order by tno desc limit #{skip}, #{size}
</select>
- 테스트 코드 실행시 다음과 같은 쿼리문 출력
select * from tbl_todo
title like concat('%', ?, '%')
title like concat('%', ?, '%')
order by tno desc limit ?, ?
- <foreach>에 open, close, separator 속성을 적용해서 쿼리문에 ()와 OR 처리
<!-- TodoMapper.xml -->
<select id = "selectList" resultType = "org.zerock.springex.domain.TodoVO">
select * from tbl_todo
<foreach collection = "types" item = "type" open = "(" close = ")" separator = " OR ">
<if test = "type == 't'.toString()">
title like concat('%', #{keyword}, '%')
</if>
<if test = "type == 'w'.toString()">
writer like concat('%', #{keyword}, '%')
</if>
</foreach>
order by tno desc limit #{skip}, #{size}
</select>
- 테스트 코드 실행시 다음과 같은 쿼리문 출력
select * from tbl_todo
(
title like concat('%', ?, '%')
OR
title like concat('%', ?, '%')
)
order by tno desc limit ?, ?
- <where>
- types가 null이 아닌 경우에만 where 키워드 추가
<!-- TodoMapper.xml -->
<select id = "selectList" resultType = "org.zerock.springex.domain.TodoVO">
select * from tbl_todo
<where>
<if test = "types != null and types.length > 0">
<foreach collection = "types" item = "type" open = "(" close = ")" separator = " OR ">
<if test = "type == 't'.toString()">
title like concat('%', #{keyword}, '%')
</if>
<if test = "type == 'w'.toString()">
writer like concat('%', #{keyword}, '%')
</if>
</foreach>
</if>
</where>
order by tno desc limit #{skip}, #{size}
</select>
- 테스트 코드 실행시 다음과 같은 쿼리문 출력
- types가 null인 경우
- where 절 출력 안됨
- select * from tbl_todo order by tno desc limit ?, ? - types가 't' 혹은 'w'인 경우('t'인 경우)
- select * from tbl_todo WHERE (title like concat('%', ?, '%')) order by tno desc limit ?, ?
- <trim>과 완료 여부 / 만료일 필터링
- 완료 여부는 PageRequestDTO의 finished 변수 값이 true인 경우에만 'finished = 1'과 같은 문자열이 쿼리문에 추가되도록 구성
- 앞에 다른 조건이 있는 경우 'and finished = 1'로, 다른 조건이 없는 경우 그냥 'finished = 1'로 추가되어야 함
- 이런 경우 Mybatis에서 <trim>을 사용
<!-- TodoMapper.xml -->
<select id = "selectList" resultType = "org.zerock.springex.domain.TodoVO">
select * from tbl_todo
<where>
<if test = "types != null and types.length > 0">
<foreach collection = "types" item = "type" open = "(" close = ")" separator = " OR ">
<if test = "type == 't'.toString()">
title like concat('%', #{keyword}, '%')
</if>
<if test = "type == 'w'.toString()">
writer like concat('%', #{keyword}, '%')
</if>
</foreach>
</if>
<!-- trim을 적용하여 prefix를 하게 되면 상황에 따라서 'and'가 추가됨 -->
<if test = 'finished'>
<trim prefix = "and">
finished = 1
</trim>
</if>
</where>
order by tno desc limit #{skip}, #{size}
</select>
- 테스트 코드에서 finished 조건 추가 후 실행시 다음과 같은 쿼리문 출력
package org.zerock.springex.mapper;
import ...
public class TodoMapperTests {
...
@Test
public void testSelectSearch() {
PageRequestDTO pageRequestDTO = PageRequestDTO.builder()
.page(1)
.size(10)
.types(new String[]{"t", "w"})
.keyword("스프링")
.finished(true)
.build();
List<TodoVO> voList = todoMapper.selectList(pageRequestDTO);
voList.forEach(vo -> log.info(vo));
}
}
- types 조건을 제외하여 finished만이 조건으로 설정된 경우 다음과 같이 'and'가 없는 쿼리문 출력
- 같은 방식으로 만료일 처리
<!-- TodoMapper.xml -->
<select id = "selectList" resultType = "org.zerock.springex.domain.TodoVO">
select * from tbl_todo
<where>
<if test = "types != null and types.length > 0">
<foreach collection = "types" item = "type" open = "(" close = ")" separator = " OR ">
<if test = "type == 't'.toString()">
title like concat('%', #{keyword}, '%')
</if>
<if test = "type == 'w'.toString()">
writer like concat('%', #{keyword}, '%')
</if>
</foreach>
</if>
<if test = 'finished'>
<trim prefix = "and">
finished = 1
</trim>
</if>
<if test = "from != null and to !== null">
<trim prefix = "and">
dueDate between #{from} and #{to}
</trim>
</if>
</where>
order by tno desc limit #{skip}, #{size}
</select>
- <sql>과 <include>
- Mybatis의 동적 쿼리 적용은 목록 데이터를 가져오는 부분에도 적용되지만 전체 개수를 가져오는 부분에도 적용되어야 함
- 전체 개수를 가져오는 TodoMapper의 getCount()에 파라미터로 PageRequestDTO 타입을 지정한 이유는 동적 쿼리를 적용하기 위함
- Mybatis에 <sql> 태그를 이용해서 동일한 SQL 조각을 재사용 가능
- 동적 쿼이 부분을 <sql>로 분리, 동적 쿼리가 적용될 부분은 <include>를 이용
<!-- TodoMapper.xml -->
<sql id = "search">
<where>
<if test = "types != null and types.length > 0">
<foreach collection = "types" item = "type" open = "(" close = ")" separator = " OR ">
<if test = "type == 't'.toString()">
title like concat('%', #{keyword}, '%')
</if>
<if test = "type == 'w'.toString()">
writer like concat('%', #{keyword}, '%')
</if>
</foreach>
</if>
<if test = 'finished'>
<trim prefix = "and">
finished = 1
</trim>
</if>
<if test = "from != null and to != null">
<trim prefix = 'and'>
dueDate between #{from} and #{to}
</trim>
</if>
</where>
</sql>
<select id = "selectList" resultType = "org.zerock.springex.domain.TodoVO">
select * from tbl_todo
<include refid = "search"></include>
order by tno desc limit #{skip}, #{size}
</select>
<select id = "getCount" resultType = "int">
select count(tno) from tbl_todo
<include refid = "search"></include>
</select>
- 테스트 코드에서 from, to 조건 추가 후 실행시 다음과 같은 쿼리문 출력
// TodoMapperTests
@Test
public void testSelectSearch() {
PageRequestDTO pageRequestDTO = PageRequestDTO.builder()
.page(1)
.size(10)
.types(new String[]{"t", "w"})
.keyword("스프링")
// .finished(true)
.from(LocalDate.of(2021,12,01))
.to(LocalDate.of(2022,12,31))
.build();
List<TodoVO> voList = todoMapper.selectList(pageRequestDTO);
voList.forEach(vo -> log.info(vo));
log.info(todoMapper.getCount(pageRequestDTO));
}
- selectList() 쿼리
- getCount() 쿼리
8. 검색 조건을 위한 화면 처리
- 검색 기능은 /WEB-INF/views/todo/list.jsp에서 이루어지므로 list.jsp에 검색 관련 화면을 작성하기 위해 <div class = 'card'>를 하나 추가하고 검색에 필요한 내용들을 담을 수 있도록 구성
<!-- list.jsp -->
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix ="c" uri = "http://java.sun.com/jsp/jstl/core" %>
<!doctype html>
<html lang="en">
<head>
...
</head>
<body>
<div class = "row content">
<div class = "col">
<div class = "card">
<div class = "card-body">
<h5 class = "card-title">Search</h5>
<form action = "/todo/list" method = "get">
<input type = "hidden" name = "size" value = "${pageRequestDTO.size}">
<div class = "mb-3">
<input type = "checkbox" name = "types" value = "finished">완료여부
</div>
<div class = "mb-3">
<input type = "checkbox" name = "types" value = "t">제목
<input type = "checkbox" name = "types" value = "w">작성자
<input type = "text" name = "keyword" class = "form-control">
</div>
<div class = "input-group mb-3 dueDateDiv">
<input type = "date" name = "from" class = "form-control">
<input type = "date" name = "to" class = "form-control">
</div>
<div class = "input-group mb-3">
<div class = "float-end">
<button class = "btn btn-primary" type = "submit">Search</button>
<button class = "btn btn-info" type = "reset">Clear</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
...
</body>
</html>
- 화면 구성 결과
- 검색 결과
1) 화면에 검색 조건 표시하기
- 검색이 처리되기는 하지만 PageRequestDTO의 정보를 EL로 처리하지 않아 검색 후 검색부분이 초기화되는 문제
- 작성된 <div>에 EL을 적용할 때 제목(title)과 작성자(writer)를 배열로 처리하고 있어 문제가 됨
- PageRequestDTO에 별도 메서드를 구성하여 더 편하게 사용할 수 있음
// PageRequestDTO
package org.zerock.springex.dto;
import ...
public class PageRequestDTO {
...
public boolean checkType(String type) {
if(types == null || types.length == 0) {
return false;
}
return Arrays.stream(types).anyMatch(type::equals);
}
}
- 화면에서 EL 적용
<!-- list.jsp -->
<form action = "/todo/list" method = "get">
<input type = "hidden" name = "size" value = "${pageRequestDTO.size}">
<div class = "mb-3">
<input type = "checkbox" name = "finished" ${pageRequestDTO.finished?"checked":""}>완료여부
</div>
<div class = "mb-3">
<input type = "checkbox" name = "types" value = "t" ${pageRequestDTO.checkType("t")?"checked":""}>제목
<input type = "checkbox" name = "types" value = "w" ${pageRequestDTO.checkType("w")?"checked":""}>작성자
<input type = "text" name = "keyword" class = "form-control" value = '<c:out value = "${pageRequestDTO.keyword}"/>'>
</div>
<div class = "input-group mb-3 dueDateDiv">
<input type = "date" name = "from" class = "form-control" value = "${pageRequestDTO.from}">
<input type = "date" name = "to" class = "form-control" value = "${pageRequestDTO.to}">
</div>
<div class = "input-group mb-3">
<div class = "float-end">
<button class = "btn btn-primary" type = "submit">Search</button>
<button class = "btn btn-info" type = "reset">Clear</button>
</div>
</div>
</form>
- 검색 조건 초기화 시키기
- 검색 영역에서 Clear 버튼을 누르면 모든 검색조건 무효화시켜 '/todo/list' 호출하도록 수정
- 화면에 clearBtn이라는 class 속성 추가
<!-- list.jsp -->
<div class = "input-group mb-3">
<div class = "float-end">
<button class = "btn btn-primary" type = "submit">Search</button>
<button class = "btn btn-info clearBtn" type = "reset">Clear</button>
</div>
</div>
<script>
document.querySelector(".clearBtn").addEventListener("click", function(e) {
e.preventDefault()
e.stopPropagation()
self.location = '/todo/list'
})
</script>
2) 조회를 위한 링크 처리
- 조회나 수정 화면에서 'List' 버튼을 클릭할 때 검색 조건들을 유지하도록 처리
- PageRequestDTO의 getLink()를 사용, getLink()를 통해 생성되는 링크에서 검색 조건 등을 반영해 주도록 수정
// PageRequestDTO
package org.zerock.springex.dto;
import ...
public class PageRequestDTO {
...
public String getLin() {
StringBuilder builder = new StringBuilder();
builder.append("page=" + this.page);
builder.append("&size=" + this.size);
if(finished) {
builder.append("&finished=on");
}
if(types != null && types.length > 0){
for (int i = 0 ; i < types.length ; i++) {
builder.append("&types=" + types[i]);
}
}
// keyword 부분은 URLEncoder를 이용해서 링크로 처리할 수 있도록 처리해야 함
if(keyword != null) {
try {
builder.append("&keyword=" + URLEncoder.encode(keyword, "UTF-8"));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
if (from != null) {
builder.append("&from=" + from.toString());
}
if (to != null) {
builder.append("&to=" + to.toString());
}
return builder.toString();
}
}
3) 페이지 이동 링크 처리
- 페이지 이동에서 검색 / 필터링 조건 필요하므로 자바스크립트로 동작하는 부분을 수정
<!-- list.jsp -->
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix ="c" uri = "http://java.sun.com/jsp/jstl/core" %>
<!doctype html>
<html lang="en">
<head>
...
</head>
<body>
<div class = "row content">
...
</div>
<div class = "row content">
<div class = "col">
<div class="card">
...
<script>
document.querySelector(".pagination").addEventListener("click", function(e) {
e.preventDefault()
e.stopPropagation()
const target = e.target
if(target.tagName !== 'A') {
return
}
const num = target.getAttribute("data-num")
const formObj = document.querySelector("form")
// 검색 / 필터링 부분에 name이 page인 부분만 추가
formObj.innerHTML += `<input type = 'hidden' name = 'page' value = '\${num}'>`
// <form> 태그를 submit으로 처리해주면 검색 / 필터링 조건을 유지하면서 페이지 번호만 변경 가능
formObj.submit();
}, false)
</script>
...
</div>
</div>
</div>
...
</body>
</html>
4) 조회 화면에서 검색 / 필터링 유지
- 조회화면(read.jsp)에서 목록 화면으로 이동하는 작업은 PageRequestDTO의 getLink()를 이용하므로 아무런 처리가 없어도 정상적으로 동작함
- 수정(Modify) 버튼도 동일하게 동작하므로 추가 개발이 필요하지 않음
5) 수정 화면에서의 링크 처리
- 수정 화면인 modify.jsp에는 [Remove], [Modify], [List] 버튼이 존재하고 각 버튼에 대한 클릭 이벤트 처리가 되어있음
- List 버튼 처리
- List 버튼은 PageRequestDTO의 GetLink()를 이용해 처리
- Remove 버튼 처리
- Remove는 삭제된 후에 1페이지로 이동
- 삭제 후 기존 페이지와 검색 / 필터링 조건을 유지하고 싶다면 PageRequestDTO를 이용
<!-- modify.jsp -->
<script>
document.querySelector(".btn-danger").addEventListener("click", function(e) {
e.preventDefault()
e.stopPropagation()
<!-- TodoController의 remove() 메서드가 이미 PageRequestDTO를 파라미터로 받고 있음 -->
<!-- 따라서 리다이렉트 하는 경로에 getLink()의 결과를 반영하도록 수정 -->
formObj.action = "/todo/remove?${pageRequestDTO.link}"
formObj.method = "post"
formObj.submit()
}, false);
</script>
- Modify 버튼 처리
- 검색 / 필터링 조건에 따라 검색했는데 수정하면서 조건에 맞지 않게 될 수 있음
- 따라서 안전하게 하려면 검색 / 필터링의 경우 수정한 후에 조회 페이지로 이동하게 하고, 검색 / 필터링 조건은 없애는 것이 안전
<form action = "/todo/modify" method = "post">
<%-- 검색 / 필터링 조건을 유지하지 않는다면 modify.jsp에 선언된 <input type = "hidden"> 태그의 내용은 필요하지 않으므로 삭제 --%>
<%-- <input type = "hidden" name = "page" value = "${pageRequestDTO.page}"> --%>
<%-- <input type = "hidden" name = "size" value = "${pageRequestDTO.size}"> --%>
...
</form>
- TodoController에서는 '/todo/list'가 아닌 '/todo/read'로 이동하도록 수정
// TodoController
package org.zerock.springex.controller;
import ...
public class TodoController {
...
@PostMapping("/modify")
public String modify(PageRequestDTO pageRequestDTO,
@Valid TodoDTO todoDTO,
BindingResult bindingResult,
RedirectAttributes redirectAttributes) {
if(bindingResult.hasErrors()) {
log.info("has errors.......");
redirectAttributes.addFlashAttribute("errors", bindingResult.getAllErrors());
redirectAttributes.addAttribute("tno", todoDTO.getTno());
return "redirect:/todo/modify";
}
log.info(todoDTO);
todoService.modify(todoDTO);
redirectAttributes.addAttribute("page", pageRequestDTO.getPage());
redirectAttributes.addAttribute("size", pageRequestDTO.getSize());
// 리다이렉트 경로를 '/todo/read'로 변경
return "redirect:/todo/read";
}
}
- 조회에서 수정 / 삭제 이동 시에는 검색 / 필터링 조건 유지
- 수정 후 조회 시에는 단순 조회
'back-end > Java' 카테고리의 다른 글
[자바 웹 개발 워크북] 5.2 - Thymeleaf (0) | 2023.03.14 |
---|---|
[자바 웹 개발 워크북] 5.1 - 스프링 부트 (0) | 2023.03.14 |
[자바 웹 개발 워크북] 4.4 - 스프링 Web MVC 구현하기(4) (0) | 2023.02.21 |
[자바 웹 개발 워크북] 4.4 - 스프링 Web MVC 구현하기(3) (0) | 2023.02.15 |
[자바 웹 개발 워크북] 4.4 - 스프링 Web MVC 구현하기(2) (0) | 2023.02.14 |