[spring] 페이징 화면 처리

업데이트:



페이지를 보여주는 작업 단계

  1. 브라우저 주소창에서 페이지 번호를 전달해서 결과를 확인하는 단계
  2. JSP에서 페이지 번호를 출력하는 단계
  3. 각 페이지 번호에 클릭 이벤트 처리
  4. 전체 데이터 개수를 반영해서 페이지 번호 조절





페이징 처리할 때 필요한 정보들

  • 현재 페이지 번호(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에 담아 보내는 과정이 단순해지는 장점이 있다.

페이징1


//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페이지까지만 출력되어야 한다.

페이징2

페이징3





페이지 번호 이벤트 처리

우선 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>

페이징4


이 상태에서 페이지 번호를 클릭하면 해당하는 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 태그 내의 페이지 번호가 바뀌는 것을 개발자 도구를 통해 확인할 수 있다.

페이징5


마지막 처리는 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 파라미터가 추가로 전달되는 것을 볼 수 있다.

페이징6






조회 페이지에서 다시 목록 페이지로 이동 - 페이지 번호 유지

조회 페이지에서 다시 목록 페이지로 이동하기 위한 파라미터들이 같이 전송되었다면 조회 페이지에서 목록으로 이동하기 위한 이벤트를 처리해야 한다.

// 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));
}







  • 참고 : 코드로 배우는 스프링 웹 프로젝트

태그:

카테고리:

업데이트:

댓글남기기