요구사항
- 단순한 검색
- 제목, 작성자는 키워드를 이용해 검색
- 필터링을 이용한 복잡한 검색
- 완료 여부를 필터링
- 특정한 기간을 지정한 필터링
구현 순서
- 검색/필터링 조건 결정
- type에 따른 동적 쿼리 작성
- 검색 조건을 위한 화면 처리
검색/필터링 조건 정의
검색과 필터링에 필요한 데이터
- keyword: 제목, 작성자 검색에 사용하는 문자열
- finished : 완료 여부에 사용되는 boolean
- from, to : 특정 기간 검색을 위한 LocalDate
검색/필터링 조건 결정
검색 기능의 경우의 수를 구분
-'완료 여부', '제목', '작성자', '기간'의 조합으로 구성
검색의 실제값은 검색 종류에 따라 키워드를 이용하는데
-PageRequestDTO에 필요한 변수를 추가해서 구성한다.
PageRequestDTO에 추가할 데이터
- types : 검색 종류
- keyword : 검색 종류에 따라 사용할 키워드
- finished : 완료 여부
- from : 구간 시작값
- to : 구간 종료값
private String[] types;
private String keyword;
private boolean finished;
private LocalDate from;
private LocalDate to;
type에 따른 동적 쿼리 작성
요구사항
- 제목 ('t'), 작성자 ('w') 검색 처리
- 완료 여부('f'), 만료 기한('d')의 필터링 처리
- 동일한 SQL 쿼리는 SQL 조각으로 변경
MyBatis에서 지원하는 태그를 이용해 동적 쿼리를 작성한다.
- if : 조건문을 위한 태그
- trim(where, set) : prefix, suffix를 이용해 접두사 접미사를 붙여주는 태그
- choose(when, otherwise)
- foreach : 반복문을 위한 태그로 -Array, List, Map, Set에 이용
1. TodoMapperTests 클래스에 동적쿼리를 사용하는 테스트 코드 작성
@Test
public void testSelectSearch(){
PageRequestDTO pageRequestDTO=PageRequestDTO.builder()
.size(10)
.types(new String[]{"t", "w"})
.keyword("AAAA")
.build();
List<TodoVO> voList=todoMapper.selectList(pageRequestDTO);
voList.forEach(vo -> log.info(vo));
}
2. TodoMapper의 selectList 변경
변경 전, selectList
<select id="selectList" resultType="org.zerock.springex.domain.TodoVO">
select * from tbl_todo order by tno desc limit #{skip}, #{size};
</select>
(1) 검색 종류에 대한 반복문 추가 후, selectList
- MyBatis의 <forEach>태그는 EL표현식이 아닌, #{~}로 표현한다.
- collection : 사용할 컬렉션 명으로 여기서는 검색 종류(String[])를 사용한다.
- item : 컬렉션에서 사용할 변수명으로 여기서는 문자열 배열의 문자열을 사용한다.
<select id="selectList" resultType="org.zerock.springex.domain.TodoVO">
select * from tbl_todo
<foreach collection="types" item="type">
#{type}
</foreach>
order by tno desc limit #{skip}, #{size};
</select>
현재 실행되는 SQL 쿼리
select * from tbl_todo ? ? order by tno desc limit ?, ?
(2) {"t", "w"}에 유효한 문자열을 대입하기 위해 조건문 추가
- MyBatis의 <if>태그는 EL표현식이 아닌, #{~}로 표현한다.
- test="아이템=='비교대상'.toString()"조건문이 들어가는 자리로 문자열 비교
- %키워드%와 같은 형태로 구성해 여러개의 조건을 수행할 수 있도록 쿼리를 작성한다.
<select id="selectList" resultType="org.zerock.springex.domain.TodoVO">
select * from tbl_todo
<foreach collection="types" item="type">
<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>
현재 실행되는 SQL 쿼리
select * from tbl_todo
title like concat('%', ?, '%')
writer like concat('%', ?, '%')
order by tno desc limit ?, ?
(3) 반복문에 속성 추가
- <forEach> 태그의 속성
- open
- close
- separator
- WHERE 키워드
- OR 키워드
- '()' 처리
<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>
-> WHERE 키워드는 경우의 수를 따져야 한다.
- types가 없는 경우 : where 생성하지 않는다.
- types가 있는 경우 : 태그 안쪽에서 문자열이 생성되면 where 키워드를 추가한다.
-> type가 null이 아닌 경우
<select id="selectList" resultType="org.zerock.springex.domain.TodoVO">
select * from tbl_todo
<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>
order by tno desc limit #{skip}, #{size};
</select>
현재 실행되는 SQL 쿼리
select * from tbl_todo
(
title like concat('%', ?, '%')
OR
writer like concat('%', ?, '%')
)
order by tno desc limit ?, ?
(4) <trim>과 완료 여부/만료일 필터링
- 완료 여부 : PageRequestDTO의 finish 변수 값이 true 인 경우에, 'and finished=1' 문자열 생성
- 검색 조건이 존재하는 경우 : 'and finished=1'
- 검색 조건이 존재하지 않는 경우 : 'finished=1'
- 만료 기한 : PageRequestDTO의 from 변수와 to 변수가 null이 아닌 경우에, 'and dueDate between #{from} and #{to}' 문자열 생성
- 다른 조건도 존재하는 경우 : 'and dueDate between #{from} and #{to}'
- 구간 조건만 존재하는 경우 : 'dueDate between #{from} and #{to}'
완료 여부 처리
<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>
</where>
order by tno desc limit #{skip}, #{size};
</select>
완료 여부 테스트 코드 작성
@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));
}
검색 조건 있는 경우 SQL 쿼리문
select * from tbl_todo
where
(
title like concat('%', ?, '%')
OR
writer like concat('%', ?, '%')
)
and finished=1
order by tno desc limit ?, ?
검색 조건 없는 경우 SQL 쿼리문
select * from tbl_todo
where finished=1
order by tno desc limit ?, ?
만료 기한 처리
<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>
아무 조건 없이 from, to만 지정되는 경우 SQL 쿼리문
select * from tbl_todo
where finished=1
order by tno desc limit ?, ?
(5) 목록 데이터 반환과 전체 개수 반환하는 메서드에 동적 쿼리 적용
<sql>과 <include>
동일한 SQL 조각을 재사용하기 위해 MyBatis에서 제공하는 태그
- <sql> : 동적 쿼리 부분을 분리
- <include> : 동일하게 동적 쿼리 적용
<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>
테스트 코드 작성
@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()
select *
from tbl_todo
WHERE ( title like concat('%', ?, '%') OR writer like concat('%', ?, '%') )
and finished = 1
and dueDate between ? and ?
order by tno desc limit ?, ?
getCount()
select count(tno)
from tbl_todo
WHERE ( title like concat('%', ?, '%') OR writer like concat('%', ?, '%') )
and finished = 1
and dueDate between ? and ?
검색 조건을 위한 화면 처리
요구사항
- 목록 페이지에 카드 컴포넌트를 이용해 검색에 필요한 내용 담아 검색 화면 구현
- 화면에 검색 조건 표시
- 조회를 위한 링크 처리 (getLink())
- 페이지 이동 링크 처리
- 조회 화면에서 검색 / 필터링 유지
- 조회를 위한 링크 처리 (getLink())
- 수정 화면에서의 링크 처리
- List, Modify, Remove 버튼 링크 처리
목록 페이지에 카드 컴포넌트를 이용해 검색에 필요한 내용 담아 검색 화면 구현
카드 컴포넌트
https://getbootstrap.kr/docs/5.1/components/card/
카드 컴포넌트 기본 문법
<div class="card" style="width: 18rem;">
<img src="..." class="card-img-top" alt="...">
<div class="card-body">
<h5 class="card-title">Card title</h5>
<p class="card-text">Some quick example text to build on the card title and make up the bulk of the card's content.</p>
<a href="#" class="btn btn-primary">Go somewhere</a>
</div>
</div>
(1) 카드 컴포넌트를 이용해 검색 화면 기본 틀 구현
<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="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 clearBtn" type="reset">Clear</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
(2) <form> 태그를 이용해 TodoController에 검색 조건 전송 확인
PageRequestDTO(page=1, size=10, link=page=1&size=10,
types=[t, w], keyword=, finished=true, from=2022-03-01, to=2022-12-30)
selectList()
select * from tbl_todo
WHERE ( title like concat('%', ?, '%') OR writer like concat('%', ?, '%') )
and finished = 1
and dueDate between ? and ?
order by tno desc limit ?, ?
getCount()
select count(tno)
from tbl_todo WHERE ( title like concat('%', ?, '%') OR writer like concat('%', ?, '%') )
and finished = 1
and dueDate between ? and ?
검색 조건이 GET 방식의 쿼리스트링으로 생성
http://localhost:8080/
todo/list?size=10&finished=on&types=t&types=w&keyword=&from=2022-03-01&to=2022-12-30
1. 화면에 검색 조건 표시
PageRequestDTO 정보가 EL로 처리되지 않으므로 검색 후 검색 조건이 초기화
-> <div>의 검색조건을 배열로 처리하면 EL을 적용하기 어려우므로, 화면에서 처리를 위해 별도의 메서드 작성
(1) PageRequestDTO에 checkType 메서드 작성
public boolean checkType(String type){
if(types == null || types.length == 0){
return false;
}
return Arrays.stream(types).anyMatch(type::equals);
}
(2) 화면에서 EL 적용
EL 적용 전, list.jsp
<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="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 clearBtn" type="reset">Clear</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
EL 적용 후, list.jsp
<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="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 clearBtn" type="reset">Clear</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
2. 조회를 위한 링크 처리 (getLink())
검색 기능 추가시, 조회나 수정 화면에 있는 'List' 버튼도 검색 조건들을 그대로 유지해야한다.
PageRequest의 getLink()에 검색 조건을 추가하는 방식으로 구현
(1) PageRequest의 getLink() 메서드에 해당 검색 조건을 반영하도록 변경
-모든 검색 / 필터링 조건을 쿼리 스트링으로 구성
-한글이 가능한 keyword부분은 URLEncoder를 이용해 링크로 처리
변경 전, getLink()
public String getLink() {
if(link == null){
StringBuilder builder = new StringBuilder();
builder.append("page=" + this.page);
builder.append("&size=" + this.size);
link = builder.toString();
}
return link;
}
변경 후, getLink()
public String getLink() {
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]);
}
}
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();
}
(2) 화면에서 '/todo/read?tno=xxx'와 같은 링크가 올바른 검색조건을 반영하는 것을 확인
-검색 조건이 없으면 변경 전과 같이 페이지 관련 쿼리 스트링만 처리한다.
3. 페이지 이동 링크 처리
페이지 이동할 때에도 검색/필터링 조건을 유지하도록 스크립트 변경
즉, 검색/필터링 조건 유지하면서 페이지 번호만 변경하도록 한다.
변경 전, 스크립트
-직접 쿼리 스트링 추가
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")
self.location = `/todo/list?page=\${num}` //백틱(` `)을 이용해서 템플릿 처리
},false)
변경 후, 스크립트
-검색/필터링 부분에 name이 page인 부분만 추가해 <form> 태그를 submit 처리
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")
formObj.innerHTML += `<input type='hidden' name='page' value='\${num}'>`
formObj.submit();
},false)
조회 화면에서 검색 / 필터링 유지
getLink()를 이용해 링크 처리를 수행하므로 별도의 작업이 필요하지 않다.
수정 화면에서의 링크 처리
List, Modify, Remove 버튼 링크 처리
(1) List 버튼 링크 처리
document.querySelector(".btn-secondary").addEventListener("click",function(e) {
e.preventDefault()
e.stopPropagation()
self.location = "/todo/list?${pageRequestDTO.link}"
},false);
(2) Modify 버튼 링크 처리
-수정 후, 내용이 변경되어 검색/필터링 조건에 맞지 않게 되는 경우 발생
요구 사항
- 수정 후에는 조회 페이지로 이동하고, 검색/필터링 조건을 없앤다.
- 검색/필터링의 경우 수정한 후에 조회 페이지로 이동
- 검색/필터링 조건을 유지하지 않기 때문에, PageRequestDTO 정보를 전달하는 <input type='hidden'> 제거
변경 전, TodoController의 modify()메서드
@PostMapping("/modify")
public String modify(@Valid TodoDTO todoDTO,
PageRequestDTO pageRequestDTO,
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());
return "redirect:/todo/list";
}
변경 후, TodoController의 modify()메서드
@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("tno", todoDTO.getTno());
return "redirect:/todo/read";
}
(3) Remove 버튼 링크 처리
document.querySelector(".btn-danger").addEventListener("click",function(e) {
e.preventDefault()
e.stopPropagation()
formObj.action ="/todo/remove"
formObj.method ="post"
formObj.submit()
},false);
TodoController에서 삭제 처리 후, 리다이렉트 경로에 getLink() 결과 반영
변경 전, remove()
@PostMapping("/remove")
public String remove(Long tno, PageRequestDTO pageRequestDTO, RedirectAttributes redirectAttributes){
log.info("-------------remove------------------");
log.info("tno: " + tno);
todoService.remove(tno);
redirectAttributes.addAttribute("page", 1);
redirectAttributes.addAttribute("size", pageRequestDTO.getSize());
return "redirect:/todo/list";
}
변경 후, remove()
@PostMapping("/remove")
public String remove(Long tno, PageRequestDTO pageRequestDTO, RedirectAttributes redirectAttributes){
log.info("-------------remove------------------");
log.info("tno: " + tno);
todoService.remove(tno);
return "redirect:/todo/list?" + pageRequestDTO.getLink();
}
'Server Programming > Spring Boot Backend Programming' 카테고리의 다른 글
5장-2. Thymeleaf (0) | 2022.12.04 |
---|---|
5장-1. 스프링 부트의 시작 (+Thymeleaf, RESTful, JSON, API Server) (0) | 2022.12.04 |
4장-3. 스프링 Web MVC 구현 (2) 페이징 처리 (+ 백틱) (0) | 2022.12.01 |
4장-2. 스프링 Web MVC 구현 (1) CRUD (+@Configuration, @Bean, 브라우저 한글 처리) (0) | 2022.11.29 |
4장-1. 스프링과 스프링 Web MVC (1) | 2022.11.29 |