1. 목록 데이터를 위한 DTO와 서비스 계층
🚀 TodoMapper에서 TodoVO의 목록과 전체 데이터의 수를 가져온다면 이를 서비스 계층에서 한 번에 담아서 처리하도록 DTO를 구성하는 것이 좋음
🚀 작성하려는 DTO는 PageResponseDTO라는 이름을 생성하고 다음과 같은 데이터와 가능을 가지도록 구성
- TodoDTO의 목록
- 전체 데이터의 수
- 페이지 번호 처리를 위한 데이터들 (시작 페이지 번호 / 끝 페이지 번호)
🚀 화면상에서 페이지 번호들을 출력하려면 현재 페이지 번호 page 와 페이지당 데이터의 수 size를 이용해서 계산할 필요가 있음
➡️ 이 때문에 작성하려는 PageResponseDTO는 생성자를 통해서 필요한 page나 size 등을 전달받도록 구성
PageResponseDTO가 가져야 하는 데이터를 정리해서 클래스를 구성
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는 여러 정보를 생성자를 이용해서 받아서 처리하는 것이 안전
예를 들어 PageRequestDTO 에 있는 page, size 값이 필요하고, TodoDTO 목록 데이터와 전체 데이터의 개수도 필요
PageResponseDTO의 생성자는 Lombok의 @Builder를 적용
public class PageResponseDTO<E> {
@Builder(builderMethodName = "withAll")
public PageResponseDTO(PageRequestDTO pageRequestDTO, List<E> dtoList, int total) {
log.info(pageRequestDTO);
this.page = pageRequestDTO.getPage();
this.size = pageRequestDTO.getSize();
this.total = total;
this.dtoList = dtoList;
}
1) 페이지 번호 계산
페이지 번호를 계산하려면 우선 현재 페이지의 번호 page 가 필요.
화면에 10개의 페이지 번호를 출력한다고 했을 때 다음과 같은 경우들이 생길수 있음.
- 현재 page가 1인 경우 : 시작 페이지 start는 1, 마지막 페이지 end는 10
- 현재 page가 10인 경우 : 시작 페이지 start는 1, 마지막 페이지 end는 10
- 현재 page가 11인 경우 : 시작 페이지 start는 11, 마지막 페이지 end는 20
A. 마지막 페이지 / 시작 페이지 번호의 계산
📍 end는 현재 페이지 번호를 기준으로 계산
this.end = (int)(Math.ceil(this.page/10.0)) * 10;
👾 page를 10으로 나눈 값을 올림처리 한 후 * 10
1 / 10 => 0.1 => 1 => 10
11 / 10.0 => 1.1 => 2 => 20
10 / 10 => 1.0 => 1 => 10
📍 시작 페이지 start의 경우 계산한 마지막 페이지에서 9를 빼면 됨
this.start = this.end - 9;
시작 페이지의 구성은 끝났지만 마지막 페이지의 경우 다시 전체 개수 total를 고려해야 함.
만일 10개씩 (size) 보여주는 경우 전체 개수 total가 75라면 마지막 페이지는 10이 아니라 8이 되어야 함.
int last = (int)(Math.ceil(total/(double)size));
123 / 10.0 => 12.3 => 13
100 / 10.0 => 10.0 => 10
75 / 10.0 => 7.5 => 8
📍 마지막 페이지 end는 앞에서 구한 last 값보다 큰 경우에 last 값이 end가 되어야만 함
this.end = end > last ? last : end;
B. 이전 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를 적용
2) TodoService / TodoServiceImpl
TodoService와 TodoServiceImpl에서는 작성된 PageResponseDTO를 반환타입으로 지정해서 getList()를 구성
public interface TodoService {
void register(TodoDTO todoDTO);
//List<TodoDTO> getAll();
PageResponseDTO<TodoDTO> getList(PageRequestDTO pageRequestDTO);
TodoDTO getOne(Long tno);
void remove(Long tno);
void modify(TodoDTO todoDTO);
}
@Override
public PageResponseDTO<TodoDTO> getList(PageRequestDTO pageRequestDTO) {
List<TodoVO> voList = todoMapper.selectList(pageRequestDTO);
List<TodoDTO> dtoList = new ArrayList<>();
for(TodoVO todoVO : voList) {
dtoList.add(modelMapper.map(todoVO, TodoDTO.class));
}
int total = todoMapper.getCount(pageRequestDTO);
PageResponseDTO<TodoDTO> pageResponseDTO = PageResponseDTO.<TodoDTO>withAll()
.dtoList(dtoList)
.total(total)
.pageRequestDTO(pageRequestDTO)
.build();
return pageResponseDTO;
}
TodoService의 getList()는 테스트를 통해서 결과를 확인
@Test
public void pageingTest() {
PageRequestDTO pageRequestDTO = PageRequestDTO.builder().page(1).size(10).build();
PageResponseDTO<TodoDTO> pageResponseDTO = todoService.getList(pageRequestDTO);
log.info(pageResponseDTO);
for (TodoDTO todoDTO : pageResponseDTO.getDtoList()) {
log.info(todoDTO);
}
}
3) TodoController와 JSP처리
TodoController의 list()에서는 PageRequestDTO를 파라미터로 처리하고,
Model에 PageResponseDTO의 데이터를 담을 수 있도록 변경
@RequestMapping("/list")
public void list(@Valid PageRequestDTO pageRequestDTO, BindingResult bindingResult, Model model) {
log.info(pageRequestDTO);
if(bindingResult.hasErrors()) {
pageRequestDTO = pageRequestDTO.builder().build();
}
model.addAttribute("responseDTO", todoService.getList(pageRequestDTO));
}
⚡️ list()는 @Valid를 이용해서 잘못된 파라미터 값들이 들어오는 경우 page는 1, size는 10으로 고정된 값을 처리하도록 구성
⚡️ 기존과 달리 Model에 responseDTO라는 이름으로 PageResponseDTO를 담아 주었기 때문에 list.jsp를 수정
<c:forEach var="dto" items="${responseDTO.dtoList}">
<tr>
<th scope="row">${dto.tno}</th>
<td><a href="/todo/read?tno=${dto.tno}" class="text-decoration-none">
<c:out value="${dto.title}"/>
</a></td>
<td>${dto.writer}</td>
<td>${dto.dueDate}</td>
<td>${dto.finished}</td>
</tr>
</c:forEach>
✓ 프로젝트를 실행하고 브라우저에서 "/todo/list"를 호출했을 때 1페이지에 해당하는 데이터들이 출력되는 것을 확인
4) 페이지 이동 확인
화면을 추가로 개발하기 전에 "/todo/list?page=xx&size=xx"를 호출해서 결과가 정상적으로 처리되는지 확인.
page의 경우는 음수가 될 수 없고, size는 100을 넘을 수 없음.
A. 화면에 페이지 이동을 위한 번호 출력
브라우저를 통해서 페이지의 이동에 문제가 없다는 것을 확인했다면 화면 아래쪽에 페이지 번호들을 출력하도록 구성.
페이지 번호는 부트스트랩의 pagination이라는 컴포넌트를 적용.
list.jsp에 <table>태그가 끝난 후에 <div>를 구성해서 화면에 적용
<div class="float-end">
<ul class="pagination flex-wrap">
<c:forEach var="num" begin="${responseDTO.start}" end="${responseDTO.end}">
<li class="page-item"><a class="page-link" href="#">${num}</a></li>
</c:forEach>
</ul>
</div>
✓ 브라우저에서 page를 변경시켜 페이지 번호들이 변경되는지 확인
B. 화면에서 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 var="num" begin="${responseDTO.start}" end="${responseDTO.end}">
<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>
⚡️ 11페이지 이상되었을 때 Previous 버튼이 보임
⚡️ 1 ~ 10 페이지의 경우 Previous 버튼이 보이지 않음
📍 현재 페이지의 번호는 class 속성에 "active"라는 속성값이 추가되어야 함. 삼항 연산자를 이용
<c:forEach var="num" begin="${responseDTO.start}" end="${responseDTO.end}">
<li class="page-item" ${responseDTO.page == num ? "active" : ""}>
<a class="page-link" href="#">${num}</a></li>
</c:forEach>
C. 페이지의 이벤트 처리
👾 화면에서 페이지 번호를 누르면 이동하는 처리는 자바스크립트를 이용해서 처리
👾 화면의 페이지 번호를 의미하는 <a> 태그에 직접 "onclick"을 적용할 수도 있지만, 한 번에 <ul> 태그에 이벤트를 이용해서 처리
👾 우선은 각 페이지 번호에 적절한 페이지 번호를 가지도록 구성. 이때는 "data-"속성을 이용해서 필요한 속성을 추가해주는 방식을 사용
"data-num"이라는 속성을 추가해서 페이지 번호를 보관하도록 구성
<c:if test="${responseDTO.prev}">
<li class="page-item">
<a class="page-link" data-num="${reponseDTO.start-1}">Previous</a>
</li>
</c:if>
<c:forEach var="num" begin="${responseDTO.start}" end="${responseDTO.end}">
<li class="page-item" ${responseDTO.page == num ? "active" : ""}>
<a class="page-link" data-num="${num}">${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> 태그가 끝난 부분에 이벤트 처리 추가
<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}`;
});
</script>
⚡️ 자바 스크립트의 이벤트 처리는 <ul> 태그에 이벤트를 등록하고 <a> 태그를 클릭했을 때만 data-num 속성값을 읽어와서 현재 주소(self.location)를 변경하는 방식으로 작성
⚡️ 자바 스크립트에서 백틱(`)을 이용하면 문자열 결합에 '+'를 이용해야 하는 불편함을 줄일 수 있음. 대신에 JSP의 EL이 아니라는 것을 표시하기 위하여 \${}로 처리
⚡️ 자바 스크립트 처리가 완료되면 화면상의 페이지 번호를 클릭해서 페이지 이동이 가능한지 확인
D. 조회 페이지로 이동
목록 페이지는 특정한 Todo의 제목 title을 눌러서 조회 페이지로 이동하는 기능이 존재
기존에는 단순히 tno만을 전달해서 '/todo/read?tno=33'과 같은 방식으로 이동했지만, 페이지 번호가 붙을 때는 page와 size 등을 같이 전달해 주어야만 조회 페이지에서 다시 목록으로 이동할 때 기존 페이지을 볼 수 있게 됨
이를 위해 list.jsp에는 각 Todo의 링크 처리 부분을 수정할 필요가 있음
페이지 이동 정보는 PageRequestDTO 안에 있으므로 PageRequestDTO 내부에 간단한 메서드를 작성해서 필요한 링크를 생성해서 사용. (파라미터로 전달되는 PageRequestDTO는 Model로 자동 전달되기 때문에 별도의 처리가 필요하지 않음)
PageRequestDTO에 link라는 속성을 추가하고 getLink()를 추가해서 GET 방식으로 페이지 이동에 필요한 링크들을 생성
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 수정
<c:forEach var="dto" items="${responseDTO.dtoList}">
<tr>
<th scope="row">${dto.tno}</th>
<td><a href="/todo/read?tno=${dto.tno}&${pageRequestDTO.link}"
class="text-decoration-none">
<c:out value="${dto.title}"/>
</a></td>
<td>${dto.writer}</td>
<td>${dto.dueDate}</td>
<td>${dto.finished}</td>
</tr>
</c:forEach>
⚡️ 기존의 tno=xx 뒤에 &를 추가하고 PageRequestDTO의 getLink()의 결과인 문자열을 생성하게 되면 다음과 같이 기존 링크에
page와 size가 추가된 형태가 됨
<a href="/todo/read?tno-3579&page=1&size=10" class="text-decoration-none" data-tno=357>
E. 조회에서 목록으로
조회 화면에서는 기존과 달리 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);
👾 브라우저는 특정한 페이지에서 조회 페이지로 이동해서 List 버튼을 눌렀을 때 정상적으로 이동하는지 확인
F. 조회에서 수정으로
조회 화면에서 수정 화면으로 이동할 때도 현재 페이지 정보를 유지해야 하기 때문에 read.jsp에서는 링크 처리 부분을 수정
document.querySelector('.btn-primary').addEventListener('click', function (e) {
self.location = `/todo/modify?tno=${dto.tno}&${pageRequestDTO.link}`;
}, false);
👾 화면에서 Modify 버튼을 누르면 아래와 같이 동작
G. 수정화면에서의 링크 처리
수정 화면에서도 다시 목록으로 가는 링크가 필요. TodoController의 read() 메서드는 GET 방식으로 동작하는 '/todo/modify'에도 동일하게 처리되므로 JSP에서 PageRequestDTO를 사용할 수 있음.
modify.jsp 의 List 버튼을 누르는 자바 스크립트의 이벤트 부분을 변경
document.querySelector('.btn-secondary').addEventListener('click', function (e) {
self.location = `/todo/list?${pageRequestDTO.link}`;
});
H. 수정 / 삭제 처리 후 페이지 이동
실제 수정 / 삭제 작업은 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를 조회 -> 수정 / 삭제 화면 -> 삭제 후 이동이 정상적으로 이루어지는지 확인
I. 수정 처리 후 이동
Todo를 수정한 후에 목록으로 이동할 때는 페이지 정보를 이용해야 하므로 TodoController의 modify() 에서는 PageRequestDTO를 받아서 처리하도록 변경.
@PostMapping("/modify")
public String midify(PageRequestDTO pageRequestDTO,
@Valid TodoDTO todoDTO,
BindingResult bindingResult,
RedirectAttributes redirectAttributes) {
if (bindingResult.hasErrors()) {
log.info("has error");
redirectAttributes.addFlashAttribute("errors", bindingResult.getAllErrors());
redirectAttributes.addAttribute("tno", todoDTO.getTno());
return "redirect:/todo/mofidy";
}
log.info(todoDTO);
todoService.modify(todoDTO);
redirectAttributes.addAttribute("page", pageRequestDTO.getPage());
redirectAttributes.addAttribute("size", pageRequestDTO.getSize());
return "redirect:/todo/list";
}
👾 정상적으로 수정된 후에는 '/todo/list'로 이동할 때 필요한 page나 size를 유지할 수 있도록 구성
[ 내용 참고 : IT 학원 강의 ]
'Spring & Spring Boot' 카테고리의 다른 글
[Spring] 검색 조건을 위한 화면 처리 (0) | 2024.04.30 |
---|---|
[Spring] 검색&필터링 (0) | 2024.04.29 |
[Spring] 페이징 처리를 위한 TodoMapper (0) | 2024.04.28 |
[Spring] Todo의 삭제 | 수정 기능 개발 (1) | 2024.04.28 |
[Spring] Todo 목록 | 조회 기능 개발 (0) | 2024.04.28 |