코드로 배우는 스프링 웹 프로젝트-14.페이징 화면 처리
코드로 배우는 스프링 웹 프로젝트-14.페이징 화면 처리
코드로 배우는 스프링 웹 프로젝트 - 개정판
2019년 7월 10일 인쇄판
Part3. 기본적인 웹 게시물 관리
Chapter14. 페이징 화면 처리
URL 파라미터로 정상적으로 원하는 페이지로 이동하는 것이 확인되었다면 화면 밑에 페이지 번호를 표시해 사용자가 페이지 번호를 클릭할 수 있게 처리한다.
- 브라우저 주소창에서 페이지 번호를 전달해 결과 확인하는 단계
- JSP에서 페이지 번호를 출력하는 단계
- 각 페이지 번호에 클릭 이벤트 처리
- 전체 데이터 개수를 반영해 페이지 번호 조절
14.1 페이징 처리할 때 필요한 정보들
- 현재 페이지 번호(page)
- 이전과 다음으로 이동 가능한 링크의 표시 여부(prev, next)
- 화면에서 보여지는 페이지의 시작 번호와 끝 번호(startPage, endPage)
14.1.1 끝 페이지 번호와 시작 페이지 번호
사용자가 19페이지를 본다면 페이지 번호가 화면에 11~20까지 떠야 한다.
페이징의 끝 번호 계산
this.endPage = (int)(Math.ceil(페이지번호 / 10.0)) * 10;
시작 번호 계산
this.startPage = this.endPage - 9;
끝 번호는 전체 데이터 수에 의해 영향을 받는다.
만일 10개씩 보여주는 경우 전체 데이터 수가 80이라고 가정하면 끝 번호는 10이 아닌 8이 되어야 한다.
만일 끝 번호와 한 페이지당 출력되는 데이터 수의 곱이 전체 데이터 수보다 크다면 끝 번호는 다시 total을 이용해 계산되면 된다.
total을 이용한 endPage의 재계산
realEnd = (int)(Math.ceil((total*1.0) / amount));
if(realEnd < this.endPage) {
this.endPage = realEnd;
}
이전과 다음
이전prev는 시작 번호가 1보다 큰 경우라면 존재한다.
this.prev = this.startPage > 1;
다음 링크는 realEnd가 끝 번호보다 큰 경우에만 존재한다.
this.next = this.endPage < realEnd;
14.2 페이징 처리를 위한 클래스 설계
클래스를 구성해 처리하면 편하다. Controller 계층에서 JSP 화면에 전달할 때도 객체를 생성해 Model에 담아 보내는 과정이 단순해진다.
org.zerock.domain 아래 PageDTO 클래스 설계
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를 다음과 같이 수정한다.
@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));
}
여기서 total 값은 임의의 값이다.
14.3 JSP에서 페이지 번호 출력
JSTL로 처리.
(SB Admin2는 부트 스트랩 기반으로 https://v4-alpha.getbootstrap.com/components/pagination과 과 같이 관련 링크들에 필요한 예제들이 있다)
list.jsp를 수정한다.
(pages - tables.html 페이지 처리를 이용해 구성한다)
기존의 table 태그가 끝나는 직후에 페이지 처리 추가.
</c:forEach>
</tbody>
</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 next"> <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 -->
Modal 창 아래족에 별도의 div class="row"를 구성하고 페이지 번호를 출력한다.
pageMaker
14.3.1 페이지 번호 이벤트 처리
<div class='pull-right'>
<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 ${pageMaker.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>
</div>
<!-- end Pagination -->
<a> 태그가 원래의 동작을 못하도록 Javascript 처리.
<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>
list.jsp에서 form 태그를 추가해 URL의 이동을 처리하도록 변경함.
<a> 태그를 클릭해도 페이지 이동이 없도록 preventDefault 처리를 하고 form 태그 내 pageNum 값은 href 속성값으로 변경.
pagination 부분과 javascript 부분에 수정이 있다.
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt" %>
<%@include file="../includes/header.jsp" %>
<div class="row">
<div class="col-lg-12">
<h1 class="page-header">Tables</h1>
</div>
<!-- /.col-lg-12 -->
</div>
<!-- /.row -->
<div class="row">
<div class="col-lg-12">
<div class="panel panel-default">
<div class="panel-heading">
Board List Page
<button id='regBtn' type="button" class="btn btn-xs pull-right" >
Register New Board
</button>
</div>
<!-- /.panel-heading -->
<div class="panel-body">
<table class="table table-striped table-bordered table-hover">
<thead>
<tr>
<th>번호</th>
<th>제목</th>
<th>작성자</th>
<th>작성일</th>
<th>수정일</th>
</tr>
</thead>
<tbody>
<c:forEach items="${list}" var="board">
<tr class="odd gradeX">
<td>
<c:out value="${board.bno}"/>
</td>
<td>
<a href='/board/get?bno=<c:out value="${board.bno}"/>'>
<c:out value="${board.title }"/>
</a>
</td>
<td><c:out value="${board.writer }"/></td>
<td><fmt:formatDate pattern="yyyy-MM-dd" value="${board.regdate }"/></td>
<td><fmt:formatDate pattern="yyyy-MM-dd" value="${board.updateDate }"/></td>
</tr>
</c:forEach>
</tbody>
</table>
<div class='pull-right'>
<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 ${pageMaker.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>
<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>
</div>
<!-- end Pagination -->
<!-- Modal -->
<div class="modal fade" id="myModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
<h4 class="modal-title" id="myModalLabel">Modal title</h4>
</div>
<div class="modal-body">
처리가 완료되었습니다.
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary">Save changes</button>
</div>
</div>
<!-- /.modal-content -->
</div>
<!-- /.modal-dialog -->
</div>
<!-- /.modal -->
</div>
<!-- /.panel-body -->
</div>
<!-- /.panel -->
</div>
<!-- /.col-lg-12 -->
</div>
<script type="text/javascript">
$(document).ready(function() {
var result='<c:out value="${result}"/>';
checkModal(result);
history.replaceState({}, null, null);
function checkModal(result) {
if(result==='' || history.state) {
return;
}
if(parseInt(result) > 0) {
$(".modal-body").html("게시글 " + parseInt(result) + "번이 등록되었습니다.");
}
$("#myModal").modal("show");
}
$("#regBtn").on("click", function() {
self.location="/board/register";
});
var actionForm = $("#actionForm");
$(".paginate_button a").on("click", function(e) {
e.preventDefault();
console.log('click');
actionForm.find("input[name='pageNum']").val($(this).attr("href"));
actionForm.submit();
});
});
</script>
<%@include file="../includes/footer.jsp" %>
14.4 조회 페이지로 이동
사용자가 3page 게시글을 클릭 후 목록으로 복귀하면 다시 1페이지로 이동된다.
해결하기 위해선 조회 페이지로 갈 때 현재 목록 페이지의 pageNum과 amount를 같이 전달해야 한다.
form 태그에 추가로 게시물의 번호를 같이 전송하고 action 값을 조정해 처리할 수 있다.
페이지 번호는 조회 페이지에 전달되지 않기 때문에, 조회 페이지에서 목록 페이지로 다시 이동할 때 /board/list를 다시 호출하는 것.
$(document).ready(function() {
var result='<c:out value="${result}"/>';
checkModal(result);
history.replaceState({}, null, null);
function checkModal(result) {
if(result==='' || history.state) {
return;
}
if(parseInt(result) > 0) {
$(".modal-body").html("게시글 " + parseInt(result) + "번이 등록되었습니다.");
}
$("#myModal").modal("show");
}
$("#regBtn").on("click", function() {
self.location="/board/register";
});
var actionForm = $("#actionForm");
$(".paginate_button a").on("click", function(e) {
e.preventDefault();
console.log('click');
actionForm.find("input[name='pageNum']").val($(this).attr("href"));
actionForm.submit();
});
$(".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();
});
});
<tbody>
<c:forEach items="${list}" var="board">
<tr class="odd gradeX">
<td>
<c:out value="${board.bno}"/>
</td>
<td>
<a class='move' href='<c:out value="${board.bno}"/>'>
<c:out value="${board.title }"/>
</a>
</td>
<td><c:out value="${board.writer }"/></td>
<td><fmt:formatDate pattern="yyyy-MM-dd" value="${board.regdate }"/></td>
<td><fmt:formatDate pattern="yyyy-MM-dd" value="${board.updateDate }"/></td>
</tr>
</c:forEach>
</tbody>
</table>
table의 제목 부분과 javascript 부분 이렇게 수정.
form 태그에 추가로 bno값을 전송하기 위해 input 태그를 추가하고 action을 /board/get으로 변경.
pageNum과 amount가 파라미터로 전달되는 것을 확인할 수 있다.
14.4.1 조회 페이지에서 다시 목록 페이지로 이동 - 페이지 번호 유지
조회 페이지에 다시 목록 페이지로 이동하기 위한 파라미터들이 같이 전송되었다면 조회 페이지에서 목록으로 이동하기 위한 이벤트를 처리해야 한다. BoardController의 get 메서드는 원래는 게시물의 번호만 받도록 처리되었지만 추가적인 파라미터가 붙으면서 Criteria도 추가해야 한다.
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에 데이터를 지정한 이름으로 담는다.
사용하지 않아도 Controller에 화면으로 파라미터가 된 객체가 전달되지만 좀 더 명시적으로 지정하기 위해 사용.
get.jsp 수정
</textarea>
</div>
<div class="form-group">
<label>Writer</label> <input class="form-control" name='writer'
value='<c:out value="${board.writer}"/>' readonly="readonly">
</div>
<button data-oper='modify' class="btn btn-default">Modify</button>
<button data-oper='list' class="btn btn-info">List</button>
<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>
</div>
<!-- /.panel-body -->
</div>
<!-- /.panel -->
</div>
14.4.2 조회 페이지에서 수정/삭제 페이지로 이동
Modify 버튼을 통해 수정/삭제 페이지로 이동한다.
BoardController get() 메서드에서 /get과 /modify를 함께 처리하므로 별도의 처리 없이도 Criteria를 Model에 담아 cri라는 이름으로 전달하게 된다.
14.5 수정과 삭제 처리
modify.jsp에서 form 태그를 이용해 데이터를 처리한다.
거의 입력과 비슷한 방식으로 구현되는데 이제 pageNum과 amount 값이 존재하므로 form 태그 내에서 같이 전송할 수 있게 수정해야 한다.
modify.jsp 일부
<div class="panel panel-default">
<div class="panel-heading">Board Register</div>
<!-- /.panel-heading -->
<div class="panel-body">
<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 }"/>'>
<div class="form-group">
<label>Bno</label> <input class="form-control" name='bno'
value='<c:out value="${board.bno}"/>' readonly="readonly">
</div>
14.5.1 수정/삭제 처리 후 이동
POST 방식으로 진행되는 수정과 삭제 처리는 BoardController에서 각각의 메서드 형태로 구현되었으므로 페이지 관련 파라미터 처리를 위해 변형해야 한다.
@PostMapping("/modify")
public String modify(BoardVO board, @ModelAttribute("cri") Criteria cri, RedirectAttributes rttr) {
log.info("modify: " + board);
if(service.modify(board)) {
rttr.addFlashAttribute("result", "sucess");
}
rttr.addAttribute("pageNum", cri.getPageNum());
rttr.addAttribute("amount", cri.getAmount());
return "redirect:/board/list";
}
@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";
}
14.5.2 수정/삭제 페이지에서 목록으로 이동
modify.jsp
$(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();
});
List 버튼 클릭 시 form 태그에서 필요한 부분만 잠시 복사 clone해서 보관해두고 form 태그 내의 모든 내용 empty.
14.6 MyBatis에서 전체 데이터 개수 처리
최종적으로는 데이터베이스에 있는 실제 모든 게시물의 수를 구해 PageDTO를 구성할 때 전달해 줘야 한다.
BoardMapper 인터페이스에 getTotalCount() 메서드 정의.
package org.zerock.mapper;
import java.util.List;
import org.apache.ibatis.annotations.Select;
import org.zerock.domain.BoardVO;
import org.zerock.domain.Criteria;
public interface BoardMapper {
//@Select("select * from tbl_board where bno > 0")
public List<BoardVO> getList();
public List<BoardVO> getListWithPaging(Criteria cri);
public void insert(BoardVO board);
public void insertSelectKey(BoardVO board);
public BoardVO read(Long bno);
public int delete(Long bno);
public int update(BoardVO board);
public int getTotalCount(Criteria cri);
}
<select id="getTotalCount" resultType="int">
select count(*) from tbl_board where bno > 0
</select>
</mapper>
BoardService에서도 별도의 메서드 작성.
package org.zerock.service;
import java.util.List;
import org.zerock.domain.BoardVO;
import org.zerock.domain.Criteria;
public interface BoardService {
public void register(BoardVO board);
public BoardVO get(Long bno);
public boolean modify(BoardVO board);
public boolean remove(Long bno);
//public List<BoardVO> getList();
public List<BoardVO> getList(Criteria cri);
public int getTotal(Criteria cri);
}
@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));
}