URL의 파라미터를 이용해서 정상적으로 원하는 페이지로 이동하는 것을 확인했다면, 화면 밑에 페이지 번호를 표시하고 사용자가 페이지 번호를 클릭할 수 있기 처리한다. 페이지를 보여주는 작업은 다음과 같은 과정을 통해서 진행한다.
- 브라우저 주소창에서 페이지 번호를 전달해서 결과를 확인하는 단계
- JSP에서 페이지 번호를 출력하는 단계
- 각 페이지 번호에 클릭 이벤트 처리
- 전체 데이터 개수를 반영해서 페이지 번호 조절
페이지 처리는 단순히 링크의 연결이기 때문에 어렵지는 않지만, 목록페이지에서 조회 페이지, 수정 삭제 페이지 까지 페이지 번호가 계속해서 유지되어야만 하기 때문에 끝까지 신경 써야 하는 부분들이 많은 편이다.
1. 페이징 처리할 때 필요한 정보들
화면에 페이징 처리를 하기 위해서는 우선적으로 여러 가지 필요한 정보들이 존재한다. 화면에 페이지는 다음과 같은 정보들이 필요하다.
- 현재 페이지 번호
- 이전과 다음으로 이동 가능한 링크의 표시 여부 (prev, next)
- 화면에서 보여지는 페이지의 시작 번호와 끝 번호(startPage, endPage)
1-1. 끝 페이지 번호와 시작 페이지 번호
페이징 처리를 하기 위해서 우선적으로 필요한 정보는 현재 사용자가 보고 있는 페이지의 정보이다. 예를 들어, 사용자가 5페이지를 본다면 화면의 페이지 번호는 1부터 시작하지만, 사용자가 19페이지를 본다면 11부터 시작해야 하기 때문이다. (화면에 10개씩 페이지 번호를 출력한다고 가정한 상태)
흔히들 페이지를 계산할 때 시작 번호를 먼저 하려고 하지만, 오히려 끝 번호를 먼저 계산 해 두는 것이 수월하다. 끝 번호는 다음과 같은 공식으로 구할 수 있다.
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페이지로 끝나면 안되는 상황이 생길 수 도 있기 때문이다. 그럼에도 끝 번호를 먼저 계산하는 이유는 시작 번호를 계산하기 수월하기 때문이다.
만일 화면에 10개씩 보여준다면 시작번호는 무조건 끝 번호에서 9라는 값을 뺀 값이 된다.
this.startPage = this.endPage - 9;
끝 번호는 전체 데이터 수에 의해서 영향을 받는다. 예를 들어, 10개 씩 보여주는 경우 전체 데이터 수가 80개라고 가정하면 끝 번호는 10이 아닌 8이 되어야한다.
만일 끝 번호와 한 페이지당 출력되는 데이터 수의 곱이 전체 데이터 수보다 크다면 끝 번호는 다시 total을 이용해서 계산되어야 한다.
realEnd = (int) (Math.cil((total * 1.0) / amount));
if(readEnd < this.endPage){
this.endPage = readEnd;
}
먼저 전체 데이터수를 이용해서 진짜 끝 페이지가 몇 번까지 되는지를 계산한다. 만일 진짜 끝 페이지가 구해둔 끝 번호보다 작다면 끝 번호는 작은 값이 되야한다.
- 이전(prev)와 다음(next)
이전과 다음은 아주 간단히 구할 수 있다. 이전의 경우는 시작번호가 1보다 큰 경우라면 존재하게 된다.
this.prev = this.startPage > 1;
다음으로 가는 링크의 경우 위의 realEnd가 끝 번호보다 큰 경우에만 존재하게 된다.
this.next = this.endPage < readEnd;
2. 페이징 처리를 위한 클래스 설계
화면에 페이징 처리를 위해서 위와 같이 여러 정보가 필요하다면 클래스를 구성해서 처리하는 방식도 꽤 편한 방식이 될 수 있다. 클래스를 구성하면 Controller 계층에서 JSP 화면에 전달할 때에도 객체를 생성해서 Model에 담아 보내는 과정이 단순해지는 장점도 있다.
1) 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;
}
}
pageDTO는 생성자를 정의하고 Criteria와 전체 데이터 수를 파라미터로 지정한다. Criteria 안에는 페이지에서 보여주는 데이터 수와 현재 페이지 번호를 가지고 있기 때문에 이를 이용해서 필요한 모든 내용을 계산할 수 있다.
2) BoardConroller에서는 PageDTO를 사용할 수 있도록 Model에 담아서 화면에 전달해 줄 필요가 있다. 메서드를 다음과 같이 수정한다.
list()는 'pageMaker'라는 이름으로 PageDTO 클래스에서 객체를 만들어서 Model에 담아준다. PageDTO를 구성하기 위해서는 전체 데이터 수가 필요한데, 아직 그 처리가 이루어지지 않았으므로 임의의 값으로 123을 지정했다.
3. JSP에서 페이지 번호 출력
JSP에서 페이지 번호를 출력하는 부분은 JSTL을 이용해서 처리할 수 있다. SB Admin2는 부트 스트랩 기반으로 구성되어 있기 때문에 http://v4-alpha.getbootstrap.com/components/pagination/와 같이 부트 스트랩 관련 링크들에 필요한 예제들이 존재한다.!
1) 예제는 SB Admin2의 pages 폴더에 있는 tables.html 페이지의 페이지 처리를 이용해서 구성한다. 기존의 <table> 태그가 끝나는 직후에 페이지 처리를 추가한다.
Modal 창의 아래쪽에 별도의 <div class='row'>를 구성하고 페이지 번호들을 출력한다. pageMaker라는 이름으로 전달된 PageDTO를 이용해서 화면에 페이지 번호들을 출력한다.
예를 들어, 현재 total은 123이라는 숫자로 지정되어 있으므로 5페이지를 조회하면 next 값은 true가 되어야 한다. 반면에 amount 값이 20인 경우에는 7페이지까지만 출력되어야 한다.
3-1. 페이지 번호 이벤트 처리
화면에서 페이지 번호가 보이기는 하지만 아직 페이지 번호를 클릭했을 때 이벤트 처리가 남아있다. 일반적으로 <a> 태그의 href 속성을 이용하는 방법을 사용할 수도 있지만, 직접 링크를 처리하는 방식의 경우 검색 조건이 붙고 난 후에 처리가 복잡하게 되므로 JavaScript를 통해서 처리하는 방식을 이용한다.
우선 페이지와 관련된 <a> 태그의 href 속성값으로 페이지 번호를 가지도록 수정한다. (번호의 출력부분은 <c:out>을 이용해서 출력하는 것이 좋지만 예제에서는 가독성의 문제로 일반 EL을 이용했다.)
1) list.jsp의 일부에 다음과 같은 코드를 추가한다.
이제 화면에서는 <a> 태그는 href 속성값으로 단순히 번호만을 가지게 변경된다. 브라우저에서 만들어진 결과를 보면 아래와 같은 형태가 된다.
이 상태에서 페이지 번호를 클릭하게 되면 해당하는 URL이 존재하지 않기 때문에 문제가 생긴다!
2) <a> 태그가 원래의 동작을 못하도록 JavaScript 처리를 한다. 실제 페이지를 클릭하면 동작을 하는 부분은 별도의 <form> 태그를 이용해서 처리하도록 한다. (<c:out> 을 사용하는 것이 더 좋은 방법이지만 간단히 사용하기 위해 EL로 처리했다.)
(위치를 잘 모르겠어서 일단 아무데에나 넣었다...... )
3) 기존에 동작하던 JavaScript 부분은 아래와 같이 기존의 코드에 페이지 번호를 클릭하면 처리하는 부분이 추가된다.
list.jsp 에서는 <form> 태그를 추가해서 URL의 이동을 처리하도록 변경했다. JavaScript에서는 <a> 태그를 클릭해도 페이지 이동이 없도록 preventDefault() 처리를 하고, <form> 태그 내 pageNum 값은 href 속성값으로 변경한다. 이 처리를 하고나면 화면에서 페이지 번호를 클릭했을 때 <form> 태그 내의 페이지 번호가 바뀌는 것을 브라우저에서 개발자 도구를 통해 확인할 수 있다.
4) 마지막 처리는 actionForm 자체를 submit() 시켜야 한다.
정상적으로 이동되는 페이지를 확인할 수 있다!! ㅎㅎ ㅎ
4. 조회 페이지로 이동
목록 화면에서 페이지 번호를 클릭하면 정상적으로 원하는 페이지로 이동하는 것을 볼 수 있지만, 몇 가지 문제가 있다. 우선 사용자가 3페이지에 있는 게시글을 클릭한 후 다시 목록으로 이동해 보면 다음과 같이 무조건 1페이지 목록 페이지로 이동하는 증상이 일어난다.
페이징 처리를 하고 나면 특정 게시물의 조회 페이지로 이동한 후 다시 목록으로 돌아가는데 문제가 생긴다!
이를 해결하기 위해서는 조회 페이지로 갈 때 현재 목록 페이지의 pageNum과 amount를 같이 전달해야 한다. 이런 경우 페이지 이동에 사용했던 <form> 태그에 추가로 게시물의 번호를 같이 전송하고, action 값을 조정해서 처리할 수 있다.
원래 게시물의 제목에는 '/board/get?bno=xxx'로 이동할 수 있는 링크가 직접 처리되어 있었다.
페이지 번호는 조회 페이지에 전달되지 않기 때문에 조회 페이지에서 목록 페이지로 이동 할 때는 아무런 정보가 없이 다시 '/board/list'가 호출된다. 간단하게는 각 게시물의 링크에 추가로 '&pageNum=xx'와 같이 처리할 수도 있지만 나중에 여러 조건들이 추가되는 상황에서는 복잡한 링크를 생성해야만 한다.
<a> 태그로 복잡한 링크를 생성하는 방식이 나쁘다고는 말할 수 없다. 가장 대표적인 예가 검색엔진이다. 검색엔진에서는 출력된 정보와 링크를 저장해서 사용하기 때문에 <a> 태그 내의 링크가 완전한 URL인 경우가 노출에 유리하다. 만일 웹페이지가 검색엔진에 의해서 노출이 필요한 경우라면 직접 모든 문자열을 구성해 주는 방식이 더 좋다.
직접 링크로 연결된 경로를 페이지 이동과 마찬가지로 <form> 태그를 이용해서 처리할 것이므로 <a> 태그에는 이동하려는 게시물의 번호만을 가지게 수정한다.(이벤트 처리를 수월하게 하기 위해서 <a> 태그에 class 속성을 하나 부여했다.)
화면에서는 조회 페이지로 가는 링크 대신에 단순히 번호만이 출력된다. (마우스를 올려보면 아래쪽에서 확인 가능)
실제 클릭은 JavaScript를 통해서 게시물의 제목을 클릭했을 때 이동하도록 이벤트 처리를 새로 작성한다.
$('.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();
});
게시물의 제목을 클릭하면 <form> 태그에 추가로 bno 값을 전송하기 위해서 <input> 태그를 만들어 추가하고, <form> 태그의 action은 '/board/get'으로 변경한다. 위의 처리가 정상적으로 되었다면 게시물의 제목을 클릭했을 때 pageNum과 amount 파라미터가 추가로 전달되는 것을 볼 수 있다.
4-1. 조회 페이지에서 다시 목록 페이지로 이동 - 페이지 번호 유지
조회 페이지에 다시 목록 페이지로 이동하기 위한 파라미터들이 같이 전송되었다면 조회 페이지에서 목록으로 이동하기 위한 이벤트를 처리해야 한다. BoardController의 get() 메서드는 원래는 게시물의 번호만 받도록 처리되어 있었지만, 추가적인 파라미터가 붙으면서 Criteria를 파라미터로 추가해서 받고 전달한다.
@ModelAttribute는 자동으로 Model에 데이터를 지정한 이름으로 담아준다. @ModelAttribute를 사용하지 않아도 Controller에서 화면으로 파라미터가 된 객체는 전달이 되지만, 좀 더 명시적으로 이름을 지정하기 위해서 사용한다.
기존 get.jsp 에서는 버튼을 클릭하면 <form> 태그를 이용하는 방식이었으므로 필요한 데이터를 추가해서 이동하도록 수정한다.
get.jsp는 operForm이라는 id를 가진 <form> 태그를 이미 이용했기 때문에 cri라는 이름으로 전달된 Criteria 객체를 이용해서 pageNum 과 amount 값을 태그로 구성하고, 버튼을 클릭했을 때 정상적으로 목록 페이지로 이동하게 처리한다.
4-2. 조회 페이지에서 수정/삭제 페이지로 이동
조회 페이지에서는 'Modify'버튼을 통해서 수정/삭제 페이지로 이동하게 된다. 수정/삭제 페이지에서는 다시 목록으로 가는 버튼이 존재하므로 동일하게 목록 페이지에 필요한 파라미터 들을 처리해야 한다. BoardController에서는 get() 메서드에서 '/get'과 '/modify'를 같이 처리하므로 별도의 추가적인 처리 없이도 Criteria를 Model에 cri라는 이름으로 담아서 전달한다.
조회 페이지에서 <form> 태그는 목록 페이지로의 이동뿐 아니라 수정/삭제 페이지 이동에도 사용되기 때문에 파라미터들은 자동으로 같이 전송된다.
5. 수정과 삭제 처리
modify.jsp 에서는 <form> 태그를 이용해서 데이터를 처리한다. 거의 입력과 비슷한 방식으로 구현되는데, 이제 pageNum과 amount라는 값이 존재하므로 <form> 태그내에서 같이 전송할 수 있게 수정해야 한다.
modify.jsp 역시 Criteria를 Model 에서 사용하기 때문에 위와 같이 태그를 만들어서 <form> 태그 전송에 포함한다.
5-1. 수정/삭제 처리 후 이동
POST 방식으로 진행하는 수정과 삭제 처리는 BoardController 에서 각각의 메서드 형태로 구현되어 있으므로 페이지 관련 파라미터들을 처리하기 위해서는 변경해 줄 필요가 있다.
메서드의 파라미터에는 Criteria가 추가된 형태로 변경되고, RedirectAttributes 역시 URL 뒤에 원래의 페이지로 이동하기 위해서 pageNum과 amount 값을 가지고 이동하게 수정한다.
삭제 처리 역시 동일하게 Criteria를 받아들이는 방식으로 수정한다.
위와 같은 방식을 이용하면 수정/삭제 후 기존 사용자가 보던 페이지로 이동하는 것을 볼 수 있다.
수정과 달리 삭제는 처리 후 1페이지로 이동해도 무방하지만, 이왕이면 사용자들에게 자신이 보던 정보를 이어서 볼 수 있게 조치해 주는 방식 역시 어렵지않다.
5-2. 수정/삭제 페이지에서 목록 페이지로 이동
페이지 이동의 마지막은 수정/삭제를 취소하고 다시 목록 페이지로 이동하는 것이다. 목록 페이지는 오직 pageNum과 amount만을 사용하므로 <form> 태그의 다른 내용들은 삭제하고 필요한 내용만을 다시 추가하는 형태가 편리하다.
1) modify.jsp의 JavaScript 부분
만일 사용자가 'List' 버튼을 클릭한다면 <form> 태그에서 필요한 부분만 잠시 복사해서 보관해 두고, <form> 태그 내의 모든 내용은 지워버린다. 이후에 다시 필요한 태그들만 추가해서 '/board/list'를 호출하는 형태를 이용한다.
6. MyBatis에서 전체 데이터의 개수 처리
페이지의 이동이 모든 작업에서 정상적으로 이루어지는 것을 확인했다면 최종적으로는 데이터베이스에 있는 실제 모든 게시물의 수를 구해서 PageDTO를 구성할 때 전달해 주어야 한다. 전체의 개수를 구하는 SQL은 어렵거나 복잡하지 않기 때문에 어노테이션으로 처리해도 무방하지만
1) BoardMapper 인터페이스에 getTotalCount() 메서드를 정의하고 XML을 이용해서 SQL을 처리한다.
public int getTotalCount(Criteria cri);
getTotalCount()는 Criteria를 파라미터를 전달받도록 설계하지 않아도 문제가 생기지는 않지만, 게시물의 목록과 전체 데이터 수를 구하는 작업은 일관성 있게 Criteria를 받는 것이 좋다. (후에 검색에서 필요하다)
2) BoardMapper.xml에 아래와 같은 코드를 작성한다.
3) BoardService 와 BoardServiceImpl에서는 별도의 메서드를 작성해서 BoardMapper의 getTotalCount()를 호출한다.
BoardService의 getTotal()에는 굳이 Criteria는 파라미터로 전달될 필요가 없기는 하지만, 목록과 전체 데이터 개수는 항상 같이 동작하는 경우가 많기 때문에 파라미터로 지정한다.
4) BoardServiceImpl 클래스는 getTotal() 메서드를 구현한다 .
@Override
public int getTotal(Criteria cri) {
log.info("get total count");
return mapper.getTotalCount(cri);
}
5) BoardController 에서는 BoardService 인터페이스를 통해서 getTotal()을 호출하도록 변경한다.
@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));
}
이상으로 게시물의 등록, 수정, 삭제, 조회, 페이징 처리가 완료되었다! 뿌듯 ^_____________^
'Spring' 카테고리의 다른 글
[22] REST 방식과 Ajax를 이용하는 댓글처리 - REST 방식으로 전환 (0) | 2019.12.27 |
---|---|
[21] 기본적인 웹 게시물 관리 - 검색처리 (0) | 2019.12.26 |
[19] 기본적인 웹 게시물 관리 - MyBatis와 스프링에서 페이징 처리 (0) | 2019.12.23 |
[18] 기본적인 웹 게시물 관리 - 오라클 데이터베이스 페이징 처리 2 (0) | 2019.12.23 |
[17] 기본적인 웹 게시물 관리 - 오라클 데이터베이스 페이징 처리 (0) | 2019.12.23 |