[spring] 페이징 화면 처리
업데이트:
페이지를 보여주는 작업 단계
- 브라우저 주소창에서 페이지 번호를 전달해서 결과를 확인하는 단계
- JSP에서 페이지 번호를 출력하는 단계
- 각 페이지 번호에 클릭 이벤트 처리
- 전체 데이터 개수를 반영해서 페이지 번호 조절
페이징 처리할 때 필요한 정보들
- 현재 페이지 번호(page)
- 이전과 다음으로 이동 가능한 링크의 표시 여부(prev, next)
- 화면에서 보여지는 페이지의 시작 번호와 끝 번호(startPage, endPage)
끝 페이지 번호와 시작 페이지 번호
페이지를 계산할 때 끝 번호를 먼저 계산하는 것이 수월하다.
// 페이징의 끝 번호(endPage) 계산 공식
this.endPage = (int)(Math.ceil(현재 페이지번호 / 10.0)) * 10;
Math.ceil()은 소수점을 올림으로 처리하기 때문에 다음과 같은 계산이 가능하다.
- 1페이지의 경우 : Math.ceil(0.1) * 10 = 10
- 10페이지의 경우 : Math.ceil(1) * 10 = 10
- 11페이지의 경우 : Math.ceil(1.1) * 10 = 20
만일 화면에 페이지 번호를 10개씩 보여준다면 시작 번호(startPage)는 무조건 끝 번호(endPage)에서 9라는 값을 뺀 값이 된다.
// 페이징 시작 번호(startPage) 계산
this.startPage = this.endPage - 9;
끝 번호(endPage)는 전체 데이터 수(total)에 의해서 영향을 받는다. 예를 들어 페이지 번호를 10개씩 보여주는 경우 전체 데이터 수(total)가 80개라 가정하면 끝 번호는 10이 아닌 8이 되어야한다.
만일 끝 번호와 한 페이지당 출력되는 데이터 수(amount)의 곱이 전체 데이터 수보다 크다면 끝 번호는 다시 total을 이용해서 다시 계산되어야 한다.
// total을 통한 endPage의 재계산
realEnd = (int)(Math.ceil((total * 1.0) / amount));
if(realEnd < this.endPage){
this.endPage = realEnd;
}
먼저 전체 데이터 수(total)를 이용해서 진짜 끝 페이지가 몇 번까지 되는지를 계산한다. 만일 진짜 끝 페이지가 구해둔 끝 번호보다 작다면 끝 번호는 작은 값이 되어야 한다.
이전(prev)과 다음(next)
- 이전(prev)
this.prev = this.startPage > 1;
- 다음(next)
//다음(next)으로 가는 링크의 경우 위의 realEnd가 끝 번호보다 큰 경우에만 존재한다. this.next = this.endPage < realEnd;
페이징 처리를 위한 클래스 설계
클래스를 구성하면 Controller 계층에서 JSP 화면에 전달할 때에도 객체를 생성해서 Model에 담아 보내는 과정이 단순해지는 장점이 있다.
//PageDTO.java
package org.zerock.domain;
import lombok.Getter;
import lombok.ToString;
@Getter
@ToString
public class PageDTO {
private int startPage;
private int endPage;
private boolean prev, next;
private int total;
private Criteria cri;
public PageDTO(Criteria cri, int total) {
this.cri = cri;
this.total = total;
this.endPage = (int)(Math.ceil(cri.getPageNum() / 10.0)) * 10;
this.startPage = this.endPage - 9;
int realEnd = (int)(Math.ceil((total * 1.0) / cri.getAmount()));
if(realEnd < this.endPage) {
this.endPage = realEnd;
}
this.prev = this.startPage > 1;
this.next = this.endPage < realEnd;
}
}
BoardController에서는 PageDTO를 사용할 수 있도록 Model에 담아서 화면에 전달해야하기 때문에 메서드를 수정한다.
@GetMapping("/list")
public void list(Criteria cri, Model model) {
log.info("list : " + cri);
model.addAttribute("list", service.getList(cri));
model.addAttribute("pageMaker", new PageDTO(cri, 123));
}
//PageDTO 두 번째 값은 전체 데이터 수인데 아직 그 처리가 이루어지지 않아서 임의의 값을 넣었다.
JSP에서 페이지 번호 출력
JSP에서 페이지 번호를 출력하는 부분은 JSTL을 이용해 처리한다. SB Admin2의 pages 폴더에 있는 tables.html 페이지의 페이지 처리를 이용해서 구성한다.(나는 왜 안보이지..ㅎ..ㅎㅎ) total 태그가 끝나는 직후에 페이지 처리를 추가한다.
<!-- list.jsp -->
</table><!-- end table -->
<div class="pull-right">
<ul class="pagination">
<c:if test="${pageMaker.prev}">
<li class="paginate_button previous"><a href="#">Previous</a></li>
</c:if>
<c:forEach var="num" begin="${pageMaker.startPage}"
end="${pageMaker.endPage}">
<li class="paginate_button"><a href="#">${num}</a></li>
</c:forEach>
<c:if test="${pageMaker.next}">
<li class="paginate_button next"><a href="#">Next</a></li>
</c:if>
</ul>
</div>
<!-- end Pagination -->
현재 total은 123으로 지정되어 있으므로 5페이지를 조회하면 next 버튼이 보여야하고, amount 값이 20인 경우에는 7페이지까지만 출력되어야 한다.
페이지 번호 이벤트 처리
우선 a 태그의 href 속성값으로 페이지 번호를 가지도록 수정한다.
<ul class="pagination">
<c:if test="${pageMaker.prev}">
<li class="paginate_button previous"><a href="${pageMaker.startPage -1}">Previous</a></li>
</c:if>
<c:forEach var="num" begin="${pageMaker.startPage}"
end="${pageMaker.endPage}">
<li class="paginate_button" ${pageMaket.cri.pageNum == num ? "active":""}>
<a href="${num}">${num}</a></li>
</c:forEach>
<c:if test="${pageMaker.next}">
<li class="paginate_button next"><a href="${pageMaker.endPage + 1}">Next</a></li>
</c:if>
</ul>
이 상태에서 페이지 번호를 클릭하면 해당하는 URL이 존재하지 않기 때문에 문제(404)가 발생한다.
a 태그가 원래의 동작을 하지 못하도록 자바스크립트 처리를한다. 그리고 페이지 이동 부분은 별도의 form
태그를 이용해서 처리한다.
<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}'>
</form>
... 생략
<!-- 스크립트 부분 -->
var actionForm = $("#actionForm");
$(".paginate_button a").on("click", function(e){
e.preventDefault();
console.log('click');
actionForm.find("input[name='pageNum']").val($(this).attr("href"));
});
위의 처리를 하고나면 화면에서 페이지 번호를 클릭했을 때 form 태그 내의 페이지 번호가 바뀌는 것을 개발자 도구를 통해 확인할 수 있다.
마지막 처리는 actionForm 자체를 submit()시켜야 한다.
<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}'>
</form>
... 생략
<!-- 스크립트 부분 -->
var actionForm = $("#actionForm");
$(".paginate_button a").on("click", function(e){
e.preventDefault();
console.log('click');
actionForm.find("input[name='pageNum']").val($(this).attr("href"));
});
조회 페이지로 이동
사용자가 1페이지 외 다른 페이지로 이동 후 게시글을 클릭하고 다시 목록으로 이동하면 무조건 1페이지로 이동하는 문제가 있다.
이 문제를 해결하려면 조회 페이지로 갈 때 현재 목록 페이지의 pageNum과 amount를 같이 전달해야한다.
<!-- list.jsp 일부-->
<td>
<a class="move" href='<c:out value="${board.bno}" />'>
<c:out value="${board.title}" /></a>
</td>
실제 클릭은 자바스크립트를 통해 게시물의 제목을 클릭했을 때 이동하도록 이벤트 처리를 새로 작성한다.
<!-- list.jsp 일부-->
$(".move").on("click", function(e){
e.preventDefault();
actionForm.append("<input type='hidden' name='bno' value='"+
$(this).attr("href")+"'>");
actionForm.attr("action", "/board/get");
actionForm.submit();
});
게시물의 제목을 클릭했을 때 pageNum과 amount 파라미터가 추가로 전달되는 것을 볼 수 있다.
조회 페이지에서 다시 목록 페이지로 이동 - 페이지 번호 유지
조회 페이지에서 다시 목록 페이지로 이동하기 위한 파라미터들이 같이 전송되었다면 조회 페이지에서 목록으로 이동하기 위한 이벤트를 처리해야 한다.
// BoardController 수정
@GetMapping({"/get", "/modify"})
public void get(@RequestParam("bno") Long bno,
@ModelAttribute("cri") Criteria cri, Model model) {
log.info("/get or modify");
model.addAttribute("board", service.get(bno));
}
@ModelAttribute는 자동으로 Model에 데이터를 지정한 이름으로 담아준다. @ModelAttribute를 사용하지 않아도 Controller에서 화면으로 파라미터가 된 객체는 전달이 되지만, 좀 더 명시적으로 이름을 지정하기 위해 사용한다.
기존 get.jsp에서는 버튼을 클릭하면 form 태그를 이용하는 방식이었으므로 필요한 데이터를 추가해서 이동하도록 수정한다.
<form id='operForm' action="/board/modify" method="get">
<input type='hidden' id='bno' name='bno'
value='<c:out value="${board.bno}"/>'>
<input type='hidden' name='pageNum'
value='<c:out value="${cri.pageNum}"/>'>
<input type='hidden' name='amount'
value='<c:out value="${cri.amount}"/>'>
</form>
3페이지 게시글 중 게시글 조회 페이지에서 List 버튼을 눌렀을 시 3페이지 목록 페이지로 이동되면 된다.
조회 페이지에서 수정/삭제 페이지로 이동
BoardController에서는 get() 메서드에서 ‘/get’과 ‘/modify’를 같이 처리하므로 별도의 추가적인 처리 없이도 Criteria를 Model에 cri라는 이름으로 담아서 전달한다.
조회 페이지에서 form 태그는 목록 페이지로의 이동뿐 아니라 수정/삭제 페이지 이동에도 사용되기 때문에 파라미터들은 자동으로 같이 전송된다.
수정과 삭제처리
modify.jsp에서는 form 태그를 이용해서 데이터를 처리한다. pageNum과 amount라는 값을 form 태그 내에서 같이 전송할 수 있게 수정한다.
<form role="form" action="/board/modify" method="post">
<!-- 추가 -->
<input type="hidden" name="pageNum" value='<c:out value="${cri.pageNum}" />'>
<input type="hidden" name="amount" value='<c:out value="${cri.amount}" />'>
수정/삭제 처리 후 이동
BoardController에서 각각의 메서드 형태로 구현되어 있으므로 페이지 관련 파라미터들을 처리하기 위해서는 변경해 줄 필요가 있다.
// BoardController.java
//modify()
@PostMapping("/modify")
public String modify(BoardVO board,
@ModelAttribute("cri") Criteria cri, RedirectAttributes rttr) {
log.info("modify: "+board);
if(service.modify(board)) {
rttr.addFlashAttribute("result", "success");
}
rttr.addAttribute("pageNum", cri.getPageNum());
rttr.addAttribute("amount", cri.getAmount());
return "redirect:/board/list";
}
// remove()
@PostMapping("/remove")
public String remove(@RequestParam("bno") Long bno,
@ModelAttribute("cri") Criteria cri, RedirectAttributes rttr) {
log.info("remove....."+bno);
if (service.remove(bno)) {
rttr.addFlashAttribute("result", "success");
}
rttr.addAttribute("pageNum", cri.getPageNum());
rttr.addAttribute("amount", cri.getAmount());
return "redirect:/board/list";
}
수정/삭제 페이지에서 목록 페이지로 이동
<!-- modify.jsp 스크립트 부분 -->
<script type="text/javascript">
$(document).ready(function(){
var formObj = $("form");
$('button').on("click", function(e){
e.preventDefault();
var operation = $(this).data("oper");
console.log(operation);
if(operation === 'remove'){
formObj.attr("action", "/board/remove");
}else if(operation === 'list'){
//move to list
formObj.attr("action", "/board/list").attr("method", "get");
var pageNumTag = $("input[name='pageNum']").clone();
var amountTag = $("input[name='amount']").clone();
formObj.empty();
formObj.append(pageNumTag);
formObj.append(amountTag);
}
formObj.submit();
});
});
</script>
만일 사용자가 ‘List’ 버튼을 클릭한다면 form 태그에서 필요한 부분만 잠시 복사(clone)해서 보관하고, form 태그 내의 모든 내용을 지운다. 이후에 다시 필요한 태그들만 추가해 ‘/board/list’를 호출하는 형태다.
MyBatis에서 전체 데이터의 개수 처리
- org.zerock.mapper.BoardMapper 인터페이스에 메서드 추가
public int getTotalCount(Criteria cri);
- src/main/resources/…/BoardMapper.xml 추가
<select id="getTotalCount" resultType="int">
SELECT COUNT(*) FROM tbl_board WHERE bno > 0
</select>
- BoardService 인터페이스 추가
public int getTotal(Criteria cri);
- BoardServiceImpl 클래스 추가
// 굳이 Criteria 파라미터로 전달될 필요가 없지만,
// 목록과 전체 데이터 개수는 항상 같이 동작하는 경우가 많기 때문에 파라미터로 지정한다.
@Override
public int getTotal(Criteria cri) {
log.info("get total count");
return mapper.getTotalCount(cri);
}
- BoardController 클래스 수정
@GetMapping("/list")
public void list(Criteria cri, Model model) {
log.info("list : " + cri);
model.addAttribute("list", service.getList(cri));
//model.addAttribute("pageMaker", new PageDTO(cri, 123));
int total = service.getTotal(cri);
log.info("total : " + total);
model.addAttribute("pageMaker", new PageDTO(cri, total));
}
- 참고 : 코드로 배우는 스프링 웹 프로젝트
댓글남기기