Spring

스프링 - Part 3 - 기본적인 웹 게시물 관리 08 (페이징 화면 처리)

록's 2023. 3. 27. 18:43
728x90
반응형

페이징 화면 처리

URL의 파라미터를 이용해서 정상적으로 원하는 페이지로 이동하는 것을 확인했다면, 화면 밑에 페이지 번호를 표시하고 사용자가 페이지 번호를 클릭할 수 있게 처리한다.

 

페이지를 보여주는 작업

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

페이지 처리는 단순히 링크의 연결이기 때문에 어렵지는 않지만,  목록페이지에서 조회 페이지, 수정 삭제 페이지까지 페이지 번호가 계속해서 유지되어야만 하기때문에 끝까지 신경 써야 하는 부분들이 많은 편이다.

 

 

 

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

화면에 페이징 처리를 하기 위해서는 우선적으로 여러 가지 필요한 정보들이 존재한다.

 

  • 현재 페이지 번호(page)
  • 이전과 다음으로 이동 가능한 링크의 표시 여부 (prev, next)
  • 화면에서 보여지는 페이지의 시작 번호와 끝 번호(stardPage, endPage)

 

끝 페이지 번호와 시작 페이지 번호

 

페이징 처리를 하기 위해서 우선적으로 필요한 정보는 현재 사용자가 보고있는 페이지의 정보이다.

 

페이징의 끝번호 계산

this.endPage = (int)(Math.ceil(페이지번호 / 10.0)) * 10;

 

Math.ceil()은 소수점을 올림으로 처리하기 떄문에 다음과 같은 상황이 가능하다.

  • 1페이지의 경우 : Math.ceil(0.1) * 10 = 10
  • 10페이지의 경우 : Math.ceil(1) * 10 = 10
  • 111페이지의 경우 : Math.ceil(1.1) * 10 = 20

끝 번호(endPage)는 아직 개선의 여지가 있다. 만일 전체 데이터 수가 적다면 10페이지로 끝나면 안되는 상황이 생길수도 있기 때문이다. 그럼에도 끝 번호 (endPage)를 먼저 계산 하는 이유는 시작 번호(startPage)를 계산하기 수월하기 때문이다.

 

만일 화면에 10개씩 보여준다면 시작번호(startPage)는 무조건 끝 번호(endPage)에서 9라는 값을 뺀 값이 된다.

 

페이징의 시작번호(startPage)계산

this.startPage = this.endPage - 9;

 

 

total을 통한 endPage의 재계산

readEnd = (int) (Math.ceil((total * 1.0) / amount) );

if(realEnd < this.endPage) {
	this.endPage = realEnd;
}

 

이전(prev)과 다음(next)

이전(prev)과 다음은 아주 간단히 구할 수 있다. 이전(perv)의 경우는 시작 번호(startPage)가 1보다 큰 경우라면 존재하게 된다.

 

이전(prev)계산

this.prev = this.startPage > 1;

 

다음(next)으로 가는 링크의 경우 위의 realEnd가 끝번호(endPage)보다 큰 경우에만 존재하게 된다.

 

다음(next)계산

this.next = this.endPage < realEnd;

 

 

 

페이징 처리를 위한 클래스 설계

클래스를 구성하면 Controller 계층에서 JSP 화면에 전달할 때에도 객체를 생성해서 Model에 담아 보내는 과정이 단순해지는 장점이 있다.

 

PageDTO 클래스를 설계

 

// PageDTO.java

package org.codehows.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;
	}
}

 

PageDTO는 생성자를 정의하고 Criteria와 전체 데이터 수(total)를 파라미터로 지정한다.

 

 

BoardController에서는 PageDTO를 사용할 수 있도록 Model에 담아서 화면에 전달해 줄 필요가 있다.

 

// BoardController.java

... 생략 ...

   @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));
   }
   
... 생략 ...

 

 

 

JSP에서 페이지 번호 출력

 

JSP에서 페이지 번호를 출력하는 부분은 JSTL을 이용해서 처리할 수 있다. 

 

list.jsp

// list.jsp

... 생략 ...

</c:forEach>   
    </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>
                                
                                
   ... 생략 ...

 

 

 

페이지 번호 이벤트 처리

 

일반적으로 <a> 태그의 href 속성을 이용하는 방법을 사용할 수 도 있지만 직접 링크를 처리하는 방식의 경우 검색 조건이 붙고 난 후에 처리가 복잡하게 되므로 JavaScript를 통해서 처리하는 방식을 이용한다.

 

우선 페이지와 관련된 <a> 태그의 href 속성값으로 페이지 번호를 가지도록 수정한다.

 

 

list.jsp 일부 // <a> 태그 부분 수정

 

// list.jsp

... 생략 ...

 <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>
                                
... 생략 ...

 

위에 코드 밑에 연결해서 입력

// list.jsp

    <form id='actionForm' action="/board/kist" method='get'>
        <input type='hidden' name='pageNum' value='${pageMaker.cri.pageNum}'>
        <input type='hidden' name='amount' value='${pageMaker.cri.amount}'>
    </form>

기존에 동작하던 JavaScript 부분은 아래와 같이 코드에 페이지 번호를 클릭하면 처리하는 부분이 추가된다.

 

list.jsp 스크립트 부분 수정

// list.jsp

... 생략 ...

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

 

 

 

 

조회페이지로 이동

 

원래 게시물의 제목에는 /board/get?bno=xxx로 이동할 수 있는 링크가 직접 처리되어 있었다.

 

list.jsp

// list.jsp

... 생략 ...

 <tr>
    <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>   
</table>

... 생략 ...

 

화면에서는 조회페이지로 가능 링크 대신에 단순히 번호만이 출력된다

 

실제 클릭은 JavaScript를 통해서 게시물의 제목을 클릭했을 때 이동하도록 이벤트 처리를 새로 작성한다.

 

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

 

 

 

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

 

조회 페이지에서 다시 목록 페이지로 이동하기 위한 파라미터들이 같이 전송되었다면 조회 페이지에서 목록으로 이동하기 위한 이벤트를 처리해야한다. BoardController의 get() 메서드는 원래는 게시물의 번호만 받도록 처리되어 있었지만, 추가적인 파라미터가 붙으면서 Criteria를 파라미터로 추가해서 받고 전달한다.

 

 

BoardController 클래스

// BoardController.java

... 생략 ...

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

... 생략 ...

 

 

 

기존 get.jsp에서는 버튼을 클릭하면 <form> 태그를 이용하는 방식이었으므로 필요한 데이터를 추가해서 이동하도록 수정한다.

 

get.jsp

// get.jsp

... 생략 ...

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

... 생략 ...

 

 

 

수정과 삭제 처리

modify.jsp에서는 <form> 태그를 이용해서 데이터를 처리한다. 거의 입력과 비슷한 방식으로 구현되는데, 이제 pageNum과 amount라는 값이 존재하므로 <form> 태그 내에서 같이 전송할 수 있게 수정해야한다.

 

modify.jsp

 

// modify.jsp

... 생략 ...

<div class="panel-heading">Board Modify Page</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}"/>'>

... 생략 ...

 

modify.jsp 역시 Criteria를 Model에서 사용하기 때문에 위와 같이 태그를 만들어서 <form> 태그 전송에 포함한다.

 

 

수정/삭제 처리후 이동

 

POST 방식으로 진행하는 수정과 삭제 처리는 BoardController에서 각각의 메서드 형태로 구현되어 있으므로 페이지 관련 파라미터들을 처리하기 위해서는 변경해 줄 필요가 있다.

 

BoardController의 modify() 부분 추가수정

// BoardController.java

@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";
   }

 

삭제 처리 역시 동일하게 Criteria를 받아들이는 방식으로 수정한다.

 

BoardController의 remove() 부분 추가수정

// BoardController.java

   @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";
   }

 

 

 

수정/삭제 페이지에서 목록  페이지로 이동

 

페이지 이동의 마지막은 수정/삭제를 취소하고 다시 목록 페이지로 이동하는 것이다.

목록 페이지는 오직 pageNum과 amount만으로 사용하므로 <form> 태그의 다른 내용들은 삭제하고 필요한 내용만을 다시 추가하는 형태가 편하다.

 

 

modify.jsp의 JavaScript 부분

// 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>

 

 

 

 

MyBatis에서 전체 데이터의 개수 처리

페이지의 이동이 모든 작업에서 정상적으로 이루어지는 것을 확인 했다면 최종적으로는 데이터베이스에서 있는 실제 모든 게시물의 수(total)를 구해서 PageDTO를 구성할 때 전달해 주어야 한다. 전체의 개수를 구하는 SQL은 어렵거나 복잡하지 않기 때문에 어노테이션으로 처리해도 무방하지만 BoardMapper 인터페이스에 getTotalCount() 메서드를 정의하고 XML을 이용해서 SQL을 처리한다.

 

 

 

BoardMapper 인터페이스

// BoardMapper.java

package org.codehows.mapper;

import java.util.List;

import org.codehows.domain.BoardVO;
import org.codehows.domain.Criteria;

public interface BoardMapper {

... 생략 ...

  public int getTotalCount(Criteria cri);		<< 추가
}

 

 

getTotalCount()는 Criteria를 파라미터를 전달받도록 설계하지 않아도 문제가 생기지는 않지만, 게시물의 목록고 전체 데이터 수를 구하는 작업은 일관성 있게 Criteria를 받는 것이 좋다.

 

 

// BoardMapper.xml

... 생략 ...
</select>

<select id="getTotalCount" resultType="int">
	select count(*) from tbl_board where bno > 0
</select>

... 생략 ...

 

 

BoardService와 BoardServiceImpl에서는 별도의 메서드를 작성해서 BoardMapper의 getTotalCount() 를 호출한다.

 

 

BoardService 인터페이스 일부

// BoardService.java

... 생략 ..

	// 추가
	public int getTotal(Criteria cri);
}

 

 

BoardService의 getTotal()에 굳이 Criteria는 파라미터로 전달될 필요가 없기는 하지만, 목록과 전체 데이터 개수는 항상 같이 동작하는 경우가 많기 때문에 파라미터로 지정한다.

 

BoardServiceImpl 클래스는 getTotal() 메서드를 구현한다.

 

// BoardServiceImpl.java

... 생략 ...

	@Override
	public int getTotal(Criteria cri) {
		log.info("get total count");
		return mapper.getTotalCount(cri);
	}
}

 

 

BoardController에서는 BoardService 인터페이스를 통해서 getTotal()을 호출하도록 변경한다.

// BoardController.java

... 생략 ...

@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));
   }
   
   ... 생략 ...

 

728x90
반응형