/*
*
* 스프링 프레임워크와 스프링MVC를 결합해서 이전에 웹MVC로 개발했던 Todo예제를 개발하며 전체적인 구조를 이해합니다.
*/
1. 페이징 처리
더미 데이터 추가하기
insert into tbl_todo (title, dueDate, writer) (select title, dueDate, writer from tbl_todo);
흔히 재귀복사라고 하는 방식으로 기존의 tbl_todo테이블의 내용을 다시 insert하는 방식입니다. 약 1000개 정도의 더미를 만들겠습니다.
페이지 처리를 위한 DTO
페이지 처리는 현재 페이지의 번호(page), 한 페이지당 보여주는 데이터의 수(size)가 기본적으로 필요합니다.
2개의 숫자를 매번 전달할 수도 있겠지만 확장성을 고려해서 별도의 DTO로 만들어 두는 것이 좋습니다.
dto패키지에 PageRequestDto클래스를 정의해 봅니다.
@Builder
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PageRequestDTO {
@Builder.Default
@Min(value = 1)
@Positive
private int page = 1;
@Builder.Default
@Min(value = 10)
@Max(value = 100)
@Positive
private int size = 10;
public int getSkip() {
return (page -1) * 10;
}
}
페이지번호(page)와 페이지당 개수(size)를 보관하는 용도외에도 limit에서 사용하는 건너뛰기(skip)의 수를 getSkip()으로 사용합니다.
page나 size는 기본값을 가지기위해서 @Builder.Default를 이용합니다.
@Min, @Max를 이용해서 외부에서 조작하는것에 대해서도 대비하도록 구성합니다.
TodoMapper의 목록 처리
TodoMapper에 PageRequestDTO를 파라미터로 처리하는 selectList()를 추가하고 TodoMapper.xml에는 SQL을 추가합니다.
List<TodoVO> selectList(PageRequestDTO pageRequestDTO);
<select id="selectLIst" resultType="org.zerock.springMVC.domain.TodoVO">
select * from tbl_todo order by tno desc limit #{skip}, #{size}
</select>
MyBatis는 기본적으로 getXXX, setXXX를 통해서 동작하기 때문에 #{skip}은 getSkip()을 호출하게 됩니다.
테스트코드를 통해 데이터가 잘 출력되는지 확인합니다.
@Test
public void testSelectList() {
PageRequestDTO pageRequestDTO = PageRequestDTO.builder()
.page(1)
.size(10)
.build();
List<TodoVO> volist = todoMapper.selectList(pageRequestDTO);
volist.forEach(vo -> log.info(vo));
}
TodoMapper의 count 처리
화면에 페이지번호를 구성하기 위해서는 전체 데이터의 수를 알아야만 가능합니다.
TodoMapper에 getCount()를 추가(나중에 검색을 대비해서 PageRequestDTO를 파라미터로 받도록 설계하겠습니다)하고,
TodoMapper.xml에 SQL을 추가합니다.
int getCount(PageRequestDTO pageRequestDTO);
<select id="getCount" resultType="int">
select count(tno) from tbl_todo
</select>
목록데이터를 위한 DTO와 서비스 계층
TodoMapper에서 TodoVO의 목록과 전체 데이터의 수를 가져온다면 이를 서비스계층에서 한번에 담아 처리하도록 DTO를 구성하는것이 좋습니다.
PageResponseDTO라는 이름으로 생성하고 다음과 같은 데이터와 기능을 가지도록 구성합니다.
■ TodoDTO의 목록
■ 전체 데이터 수
■ 페이지번호의 처리를 위한 데이터들(시작 페이지번호 / 끝 페이지번호)
화면상에서 페이지번호들을 출력하려면 현재 페이지번호(page)와 페이지당 데이터의수(size)를 이용해서 계산을 해야합니다.
이때문에 PageResponseDTO는 생성자를 통해서 필요한 page나 size등을 전달받도록 구성해야합니다.
public class PageResponseDTO<E> {
private int page;
private int size;
private int total;
// 시작 페이지 번호
private int start;
// 끝 페이지 번호
private int end;
// 이전 페이지의 존재 여부
private boolean prev;
// 다음 페이지의 존재 여부
private boolean next;
private List<E> dtoList;
}
PageResponseDTO는 제네릭을 이용해서 설계합니다.
그 이유는 나중에 다른 종류의 객체를 이용해서 PageResponseDTO를 구성할 수 있도록 하기 위해서입니다.
예를들어 게시판이나 회원정보등도 페이징처리가 필요할 수 있기 때문에 공통적인 처리를 위해서 제네릭으로 구성합니다.
PageResponseDTO는 여러정보를 생성자를 이용해서 받아서 처리하는 것이 안전합니다.
예를들어 PageRequestDTO에 있는 page, size값이 필요하고, TodoDTO목록데이터와 전체데이터수도 필요합니다.
PageResponseDTO의 생성자는 Lombok의ㅣ @Builder를 적용합니다.
@Builder(builderMethodName = "withAll")
public PageResponseDTO(PageRequestDTO pageRequestDTO, List<E> dtoList, int total){
this.page = pageRequestDTO.getPage();
this.size = pageRequestDTO.getSize();
this.total = total;
this.dtoList = dtoList;
}
페이지 번호의 계산
페이지 번호를 계산하려면 우선 현재 페이지의 번호(page)가 필요합니다.
화면에 10개의 페이지 번호를 출력한다고 했을때 다음과 같은 경우들이 생길 수 있습니다.
page가 1인 경우 : 시작 페이지(start)는 1, 마지막 페이지(end)는 10
page가 10인 경우 : 시작 페이지(start)는 1, 마지막 페이지(end)는 10
page가 11인 경우 : 시작 페이지(start)는 11, 마지막 페이지(end)는 20
마지막 페이지/시작 페이지 번호의 계산
흔히들 처음에 구해야하는것이 start라고 생각하지만, 마지막 페이지(end)를 구하는 계산이 더 편할 수 있습니다.
end는 현재의 페이지 번호를 기준으로 계산합니다.
this.end = (int)(Math.ceil(this.page / 10.0)) * 10; → page를 10으로 나눈값을 올림 처리한 후 * 10
마지막 페이지를 먼저 계산하는 진짜 이유는 시작 페이지(start)이 계산을 쉽게하기 위함입니다.
시작페이지의 경우 계산한 마지막 페이지에서 9를 빼면 되기 때문입니다.
this.start = this.end - 9;
시작페이지의 구성은 끝났지만 마지막페이지의 경우 다시 전체 개수(total)를 고려해야 합니다.
만일 10개씩(size)보여주는 경우 전체개수(total)가 75라면 마지막 페이지는 10이 아닌 8이 되어야 하기 때문입니다.
int last = (int)(Math.ceil((total/(double)size))); → 75 / 10.0 => 7.5 => 8
마지막 페이지(end)는 앞에서 구한 last값보다 작은 경우에 last값이 end가 되어야만 합니다.
int last = (int)(Math.ceil((total/(double)size)));
this. end = end > last ? last : end;
이전(prev)/다음(next)의 계산
이전(prev)페이지의 존재여부는 시작페이지(start)가 1이 아니라면 무조건 true가 되어야합니다.
다음(next)은 마지막 페이지(end)와 페이지당 개수(size)를 곱한 값보다 전체 개수(total)가 더 많은지를 보고 판단해야합니다.
this.prev = this.start > 1;
this.next = total > this.end * this.size;
PageResponseDTO는 최종적으로 Lombok의 @Getter를 적용해서 다음과 같은 형태가 됩니다.
@Getter
@ToString
public class PageResponseDTO<E> {
private int page;
private int size;
private int total;
// 시작 페이지 번호
private int start;
// 끝 페이지 번호
private int end;
// 이전 페이지의 존재 여부
private boolean prev;
// 다음 페이지의 존재 여부
private boolean next;
private List<E> dtoList;
@Builder(builderMethodName = "withAll")
public PageResponseDTO(PageRequestDTO pageRequestDTO, List<E> dtoList, int total){
this.page = pageRequestDTO.getPage();
this.size = pageRequestDTO.getSize();
this.total = total;
this.dtoList = dtoList;
this.end = (int) (Math.ceil(this.page / 10.0)) * 10;
this.start = end - 9;
int last = (int) (Math.ceil((total / (double)size)));
this.end = end > last ? last : end;
this.prev = this.start > 1;
this.next = total > this.end * this.size;
}
}
TodoService/TodoServiceImpl, test
TodoService와 TodoServiceImpl에 PageResponseDTO를 반환타입으로 지정해서 getList()를 구성합니다.(기존 getAll()을 대체)
// List<TodoDTO> getAll();
PageResponseDTO<TodoDTO> getList(PageRequestDTO pageRequestDTO);
/*@Override
public List<TodoDTO> getAll() {
List<TodoDTO> dtoList = todoMapper.selectAll().stream()
.map(vo -> modelMapper.map(vo, TodoDTO.class))
.collect(Collectors.toList());
return dtoList;
}*/
@Override
public PageResponseDTO<TodoDTO> getList(PageRequestDTO pageRequestDTO) {
List<TodoVO> voList = todoMapper.selectList(pageRequestDTO);
List<TodoDTO> dtoList = voList.stream()
.map(vo -> modelMapper.map(vo, TodoDTO.class))
.collect(Collectors.toList());
int total = todoMapper.getCount(pageRequestDTO);
PageResponseDTO<TodoDTO> pageResponseDTO = PageResponseDTO.<TodoDTO>withAll()
.dtoList(dtoList)
.total(total)
.pageRequestDTO(pageRequestDTO)
.build();
return pageResponseDTO;
}
@Test
public void testPaging() {
PageRequestDTO pageRequestDTO = PageRequestDTO.builder().page(1).size(10).build();
PageResponseDTO<TodoDTO> responseDTO = todoService.getList(pageRequestDTO);
log.info(responseDTO);
responseDTO.getDtoList().stream().forEach(todoDTO -> log.info(todoDTO));
}
TodoController와 JSP처리
@RequestMapping(value = "/list")
public void list(@Valid PageRequestDTO pageRequestDTO, BindingResult bindingResult, Model model) {
log.info("todo list...");
if (bindingResult.hasErrors()){
pageRequestDTO = PageRequestDTO.builder().build();
}
model.addAttribute("responseDTO", todoService.getList(pageRequestDTO));
}
<c:forEach items="${responseDTO.dtoList}" var="dto">
<tr>
<th scope="row"><c:out value="${dto.tno}"/></th>
<td><a href="/todo/read?tno=${dto.tno}" class="text-decoration-none"><c:out value="${dto.title}"/></a></td>
<td><c:out value="${dto.writer}"/></td>
<td><c:out value="${dto.dueDate}"/></td>
<td><c:out value="${dto.finished}"/></td>
</tr>
</c:forEach>
화면에 페이지이동을 위한 번호출력(Bootstrap이용)
화면아래쪽에 페이지번호들을 출력하도록 해줍니다.
페이지의 번호는 부트스트랩의 pagination이라는 컴포넌트를 적용하겠습니다.
https://getbootstrap.com/docs/5.1/components/pagination
list.jsp의 <table>뒤에 다음 코드를 추가합니다.
</table>
<div class="float-end">
<ul class="pagination" flex-wrap>
<c:forEach begin="${responseDTO.start}" end="${responseDTO.end}" var="num">
<li class="page-item">
<a class="page-link" href="#">${num}</a>
</li>
</c:forEach>
</ul>
</div>
화면에서 prev/next/현재페이지
페이지 번호들이 정상적으로 출력된다면 '이전/다음'을 다음과 같이 처리해 줍니다.
<div class="float-end">
<ul class="pagination" flex-wrap>
<c:if test="${responseDTO.prev}">
<li class="page-item">
<a class="page-link">Previous</a>
</li>
</c:if>
<c:forEach begin="${responseDTO.start}" end="${responseDTO.end}" var="num">
<li class="page-item">
<a class="page-link" href="#">${num}</a>
</li>
</c:forEach>
<c:if test="${responseDTO.next}">
<li class="page-item">
<a class="page-link">Next</a>
</li>
</c:if>
</ul>
</div>
현재페이지의 번호는 class속성에 active라는 속성값이 추가되어야 합니다. 삼항연산자를 이용해 다음과 같이 처리해줍니다.
<c:forEach begin="${responseDTO.start}" end="${responseDTO.end}" var="num">
<li class="page-item ${responseDTO.page == num? "active":""}">
<a class="page-link" href="#">${num}</a>
</li>
</c:forEach>
페이지의 이벤트처리
화면에서 페이지번호를 누르면 이동하는 처리는 자바스크립트를 이용해서 처리해야 합니다.
페이지 번호를 의미하는 <a>태그에 직접 'onClick'을 적용할 수 도 있지만, 한번에 <ul>태그에 이벤트를 이용해서 처리하도록 합니다.
우선 각 페이지번호에 적절한 페이지번호를 부여합니다. 이때는 'data-'속성을 이용해서 필요한 속성을 추가해주는 방식이 좋습니다.
예제에서는 'data-num'이라는 속성을 추가해서 페이지번호를 보관하도록 구성했습니다.
<ul class="pagination" flex-wrap>
<c:if test="${responseDTO.prev}">
<li class="page-item">
<a class="page-link" data-num="${responseDTO.start -1}">Previous</a>
</li>
</c:if>
<c:forEach begin="${responseDTO.start}" end="${responseDTO.end}" var="num">
<li class="page-item ${responseDTO.page == num? "active":""}">
<a class="page-link" data-num="${num}" href="#">${num}</a>
</li>
</c:forEach>
<c:if test="${responseDTO.next}">
<li class="page-item">
<a class="page-link" data-num="${responseDTO.end + 1}">Next</a>
</li>
</c:if>
</ul>
</ul>
</div>
<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")
self.location = `/todo/list?page=\${num}` //백틱(``)을 이용해서 템플릿처리
}, false)
</script>
자바스크립트에 백틱을 이용하면 문자열 결합에 +를 이용해야하는 불편함을 줄일 수 있습니다.
대신에 JSP의 EL이 아니라는것을 표시하기위해 '₩${}'로 처리해야만 합니다.
조회페이지로의 이동
목록페이지는 특정한 Todo의 제목(title)을 눌러서 조회페이지로 이동하는 기능이 존재합니다.
기존에는 단순히 tno만을 전달해서 'todo/read?tno=33'과 같은 방식으로 이동했지만,
페이지번호가 붙을 때는 page와 size등을 같이 전달해줘야 조회페이지에서 다시 목록으로 이동할때 기존 페이지를 볼 수 있게됩니다.
이를위해 list.jsp에는 각 Todo의 링크처리부분을 수정할 필요가 있습니다. 페이지이동정보는 PageRequestDTO안에 있으므로,
PageRequestDTO내부에 간단한 메소드를 작성해서 필요한 링크를 생성할때 사용합니다.
(파라미터로 전달되는 PageRequestDTO는 Model로 자동전달되기 때문에 별도의 처리가 필요하지 않습니다.)
public class PageRequestDTO {
... 생략 ...
private String link;
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;
}
}
이를 list.jsp에서는 다음과 같이 사용합니다.
<td><a href="/todo/read?tno=${dto.tno}&${pageRequestDTO.link}" class="text-decoration-none"><c:out value="${dto.title}"/></a></td>
조회에서 목록으로
조회화면에서는 기존과 달리 PageRequestDTO를 추가로 이용하도록 TodoController을 수정해야 합니다.
TodoController의 read()메소드는 PageRequestDTO파라미터를 추가해서 다음과 같이 수정합니다.
@GetMapping({"/read", "/modify"})
public void read(long tno, PageRequestDTO pageRequestDTO, Model model) {
TodoDTO todoDTO = todoService.getOne(tno);
log.info(todoDTO);
model.addAttribute("dto", todoDTO);
}
read.jsp에서는 'List'버튼의 링크를 다시 처리해주어야 합니다.
// 목록페이지로 이동하는 이벤트처리
document.querySelector(".btn-secondary").addEventListener("click", function (e){
self.location = "/todo/list?${pageRequestDTO.link}";
}, false)
조회에서 수정으로
조회화면에서 수정화면으로 이동할 때도 현재 페이지 정보를 유지해야하기 때문에 read.jsp를 다음과 같이 수정합니다.
document.querySelector(".btn-primary").addEventListener("click", function (e){
self.location = `/todo/modify?tno=${dto.tno}&${pageRequestDTO.link}`
}, false)
수정화면에서의 링크처리
TodoController의 read()메소드는 'todo/modify'에도 동일하게 처리되므로 JSP에서 PageRequestDTO를 사용할 수 있습니다.
modify.jsp의 'List'버튼을 다음과 같이 변경합니다.
document.querySelector(".btn-secondary").addEventListener("click", function (e) {
e.preventDefault()
e.stopPropagation()
selr.location = `/todo/list?${pageRequestDTO.link}`;
}, false)
수정/삭제 처리 후 페이지 이동
실제 수정/삭제 작업은 POST방식으로 처리되고 삭제 처리가 된 후에는 다시 목록으로 이동할 필요가 있습니다.
그렇기때문에 수정화면에서 <form>태그로 데이터를 전송할 때 페이지와 관련된 정보를 같이 추가해서 전달해야만 합니다.
이작업은 modify.jsp에 <input type='hidden'>을 이용해서 추가합니다.
<form action="/todo/modify" method="post">
<input type="hidden" name="page" value="${pageRequestDTO.page}">
<input type="hidden" name="size" value="${pageRequestDTO.size}">
TodoController에서 POST방식으로 이루어지는 삭제 처리에도 PageRequestDTO를 이용해서 <form>태그로 전송되는 태그들을 수집하고 수정 후에 목록 페이지로 이동할 때 page는 무조건 1페이지로 이동해서 size정보를 활용합니다.
@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";
}
수정 처리 후 이동
Todo를 수정한 후에 목록으로 이동할 때는 페이지정보를 이용해야 하므로 TodoController의 modify()를 다음과 같이 수정합니다.
@PostMapping("/modify")
public String modify(@Valid TodoDTO todoDTO, BindingResult bindingResult, PageRequestDTO pageRequestDTO, RedirectAttributes redirectAttributes) {
if (bindingResult.hasErrors()) {
log.info("has errors......");
redirectAttributes.addFlashAttribute("errors", bindingResult.getAllErrors());
redirectAttributes.addAttribute("tno", todoDTO.getTno());
return "redirect:/todo/modify";
}
todoService.modify(todoDTO);
redirectAttributes.addAttribute("page", pageRequestDTO.getPage());
redirectAttributes.addAttribute("size", pageRequestDTO.getSize());
return "redirect:/todo/list";
}
2. 검색/필터링 조건의 정의
대부분의 서비스에서는 검색기능을 제공합니다.
단순히 제목이나 내용 등을 검색하는 경우도 있고, 복잡한 검색조건을 필터링하는 경우도 있습니다. 둘의 차이는,
■ 검색(search): A 혹은 B 혹은 C와 같이 찾고자하는 경우입니다. ex) 제목 or 내용 or 작성자 / OR개념
■ 필터링(filtering): A인 동시에 B에도 해당한다는 개념입니다. ex) 완료된 일 중에서 특정 날짜까지 끝난 Todo / AND개념
■ 완료여부와 기간은 AND 필터링 / 제목,작성자는 OR 검색
■ 제목(title)과 작성자(writer)는 키워드(keyword)를 이용하는 검색처리
■ 완료여부를 필터링 처리
■ 특정한 기간을 지정(from, to)한 필터링 처리
■ 제목, 작성장 검색에 사용하는 문자열 - keyword
■ 완료여부에 사용되는 boolean타입 - finished
■ 특정기간 검색을 위한 LocalDate 변수 2개 - from, to
검색/필터링 조건의 결정
검색기능을 개발할 때는 우선 검색기능의 경우의 수를 구분하는 작업이 필요합니다.
검색은 목록기능에 사용하는 PageRequestDTO에 필요한 변수들을 추가해서 구성합니다.
PageRequestDTO에 types와 keyword, finished, from, to 변수들을 새로 추가해줍니다.
public class PageRequestDTO {
... 생략 ...
private String[] types;
private String keyword;
private boolean finished;
private LocalDate from;
private LocalDate to;
... 생략 ...
}
TodoMapper의 selectList()는 PageRequestDTO를 파라미터로 받고 있기 때문에 TodoMapper.xml만 수정해줍니다.
<select id="selectList" resultType="org.zerock.springMVC.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>
<if>를 이용해서 문자열을 비교할때는 't'.toString()과 같은 방식을 이용해야만 하는 점을 제외하면 어렵지 않습니다.
<trim>과 완료여부/만료일 필터링
'제목(t), 작성자(w)'에 대한 검색처리는 어느정도 완성됐지만 '완료여부(f)'와 '만료기간(d)'에 대한 처리가 남았습니다.
완료여부는 finished가 true인 경우에만 'finished = 1' 문자열이 만들어지도록 구성해야 합니다.
여기서 걸림돌은 앞에 검색조건이 있는 경우에는 'and finished = 1' 그렇지않으면 'finsished = 1'이 되어야한다는 점입니다.
MyBatis의 <trim>은 이런 경우에 유용합니다.
<if test="finished">
<trim prefix="and">
finished = 1
</trim>
</if>
<trim>을 적용해서 prefix를 하게되면 상황에 따라서 'and'가 추가됩니다. 같은방식으로 만료일도 처리해줍니다.
<if test="from != null and to != null">
<trim prefix="and">
dueDate between #{from} and #{to}
</trim>
</if>
<sql>과 <include>
MyBatis의 동적쿼리 적용은 단순히 목록데이터를 가져오는 부분과 전체 개수를 가져오는 부분에도 적용되어야만 합니다.
전체개수를 가져오는 getCount()에 파라미터로 PageRequestDTO타입을 지정한 이유도 그때문입니다.
MyBatis에는 <sql>태그를 이용해서 동일한 SQL조각을 재사용할 수 있습니다.
동적쿼리부분을 <sql>로 분리하고, <include>를 이용해서 적용시킵니다.
TodoMapper.xml에서 <sql id="search">로 동적쿼리부분을 분리합니다.
<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.springMVC.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>
검색 조건을 위한 화면처리
검색기능은 list.jsp에서 이루어지므로 검색관련된 화면을 작성하기위해 <div class='card'>를 추가하고 다음과 같이 만들어줍니다.
<!-- 기존 header -->
<!-- 추가하는 코드 -->
<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>
<butotn class="btn btn-info" type="reset">Clear</butotn>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- 기존 row-content -->
화면에 검색조건 표시하기
검색이 처리되긴 하지만 PageRequestDTO의 정보를 EL로 처리하지 않았기때문에 검색 후에는 검색부분이 초기화되는 문제가 있습니다.
작성된 <div>에 EL을 적용할때 가장 문제가 되는 부분은 제목, 작성자를 배열로 처리하고 있으므로 화면에서 처리할때 좀 더 편하게 사용하기위해서 PageRequestDTO에 별도의 메소드를 구성하도록 합니다.
public boolean checkType(String type) {
if (types == null || types.length == 0) {
return false;
}
return Arrays.stream(types).anyMatch(type::equals);
}
화면에서 EL을 적용하면 다음과 같이 됩니다.
<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="${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>
<butotn class="btn btn-info" type="reset">Clear</butotn>
</div>
</div>
</form>
검색조건 초기화 시키기
검색영역에 [Clear]버튼을 누르면 모든 검색조건이 초기화 되도록 수정합니다.
화면의 버튼에 'clearBtn'이라는 class속성을 추가합니다.
document.querySelector(".clearBtn").addEventListener("click", function (e){
e.preventDefault()
e.stopPropagation()
self.location = '/todo/list'
}, false)
조회를 위한 링크처리
검색기능이 추가되면 문제되는것은 조회나 수정화면에 있는 'List'버튼입니다.
기존과 달리 검색조건들을 그대로 유지해야하므로 상당히 복잡한 처리가 필요합니다.
다행히 PageRequestDTO의 getLink()는 이런 경우에 크게 도움이 됩니다.
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();
}
getLink()는 모든 검색/필터링 조건을 쿼리스트링으로 구성해야하기 때문에 조금 지저분하게 구성될 수 밖에 없습니다.
이렇게 하지 않는다면 화면에서 모든 링크를 수정해야 하기 때문에 더 복잡해지게 됩니다.
특히 눈여겨보아야할 부분은 한글이 가능한 keyword부분은 URLEncoder를 이용해서 링크로 처리할 수 있도록 한다는 점 입니다.
getLink()가 수정되면 화면에서 '/todo/read?tno=xx'와 같은 링크가 자동으로 검색조건을 반영하게 수정된 것을 확인할 수 있습니다.
페이지 이동 링크 처리
페이지 이동에도 검색/필터링 조건은 필요하므로 자바스크립트로 동작하는 부분을 수정해야 합니다.
기존에는 자바스크립트에서 직접 쿼리스트링을 추가해서 구성했지만, 검색/필터링 부분에 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)
조회화면에서 검색/필터링 유지
조회화면(read.jsp)에서 목록화면으로 이동하는 작업은 PageRequestDTO의 getLink()를 이용하므로 처리없이 정상작동 합니다.
[Modify]버튼클릭 역시 동일하게 동작하므로 추가 개발이 필요하지 않습니다.
수정화면에서의 링크처리
수정화면인 modify.jsp에는 [Remove], [Modify], [List]버튼이 존재하고 각 버튼에 대한 클릭이벤트가 처리되어 있습니다.
■ List버튼의 처리
document.querySelector(".btn-secondary").addEventListener("click", function (e) {
e.preventDefault()
e.stopPropagation()
selr.location = `/todo/list?${pageRequestDTO.link}`;
}, false)
■ Remove버튼의 처리
document.querySelector(".btn-danger").addEventListener("click", function (e) {
e.preventDefault()
e.stopPropagation()
formObj.action = `/todo/remove?${pageRequestDTO.link}`
formObj.method = "post"
formObj.submit()
}, false)
TodoController의 remove()는 이미 PageRequestDTO를 파라미터로 받고있기 때문에 삭제처리를 하고나서 리다이렉트경로에
getLink()의 결과를 반영하도록 수정하면 됩니다.
@PostMapping("/remove")
public String remove(long tno, PageRequestDTO pageRequestDTO) {
log.info("----------------remove---------------");
log.info("tno : " + tno);
todoService.remove(tno);
return "redirect:/todo/list?" + pageRequestDTO.getLink();
}
브라우저는 수정/삭제에서 유지된 검색/필터링 정보가 유지된채로 삭제되는것을 확인할 수 있습니다.
■ Modify버튼의 처리
Modify버튼의 처리는 기존과 많이 달라집니다. 검색/필터링 기능이 추가되면 Todo의 내용이 수정되면서 검색/필터링조건에 맞지 않게 될 수 있기 때문입니다.
따라서 안전하게 하려면 검색/필터링의 경우 수정한 후에 조회 페이지로 이동하게하고, 검색/필터링 조건은 없애는것이 안전합니다.
검색/필터링 조건을 유지하지 않는다면 modify.jsp에 선언된 <input type="hidden">태그의 내용은 필요하지 않으므로 삭제합니다.
TodoController에서는 'todo/list'가 아닌 'todo/read'로 이동하도록 수정합니다.
@PostMapping("/modify")
public String modify(@Valid TodoDTO todoDTO, BindingResult bindingResult, PageRequestDTO pageRequestDTO, RedirectAttributes redirectAttributes) {
if (bindingResult.hasErrors()) {
log.info("has errors......");
redirectAttributes.addFlashAttribute("errors", bindingResult.getAllErrors());
redirectAttributes.addAttribute("tno", todoDTO.getTno());
return "redirect:/todo/modify";
}
todoService.modify(todoDTO);
redirectAttributes.addAttribute("tno", todoDTO.getTno());
return "redirect:/todo/read";
}
수정 후 조회 시에는 단순 조회됩니다.
'개발 > JAVA' 카테고리의 다른 글
[자바웹개발워크북] 6. AJAX와 JSON (0) | 2023.02.03 |
---|---|
[자바웹개발워크북] 5. 스프링에서 스프링부트로 (0) | 2023.01.26 |
[자바웹개발워크북] 스프링 MVC 구현 - 환경설정, CRUD (0) | 2023.01.17 |
[자바웹개발워크북] 4-2. 스프링 Web MVC (0) | 2023.01.14 |
[자바웹개발워크북] 4-1.스프링과 MyBaits (0) | 2023.01.02 |