[Spring] #5. 게시판 만들기 _ 페이징처리
페이징 처리란 select를 했을 때 전체 목록을 가져오되 정렬이 필요하다.
보통 최신순으로 목록이 보여지게 되고 내림차순으로 보여지게 된다.
여기서 내림차순을 기준으로 정할 수 있는 것이 boardNumber 가 되는데
사실상 기준을 boardNumber로 잡았다 하더라도
시퀀스의 숫자는 추가 삭제 수정 모든 적용이 되기 문에 규칙성이 없이 만들어 진다.
(1번 게시글을 작성했다가 삭제하면 2번 게시글이 가장 최신글이 된다.)
결론은 ROWNUM을 통해 행에 번호을 달아주어 쿼리를 짜서 나온 테이블에 행 번호에 값으로 붙게 되는 규칙성을 만들어 줘야 한다.
SELECT ROWNUM,B.*FROM TBL_BOARD B;
위와 같이 BOARD_NUMBER에는 규칙성이 없지만 ROWNUM을 통해 규칙성을 만들어 줄 수 있다.
우리가 사용해야 할 목록에서는 내림차순으로 조회해야하기 때문에 아래와 같이 작성한다.
SELECT ROWNUM,B.*FROM TBL_BOARD B
ORDER BY BOARD_NUMBER DESC;
mapper 변경
<select id="selectAll" resultType="boardVO">
SELECT BOARD_NUMBER, BOARD_TITLE, BOARD_WRITER, BOARD_CONTENT, BOARD_REGISTER_DATE, BOARD_UPDATE_DATE
FROM
(
SELECT ROWNUM R, BOARD_NUMBER, BOARD_TITLE, BOARD_WRITER, BOARD_CONTENT, BOARD_REGISTER_DATE, BOARD_UPDATE_DATE
FROM
(
SELECT BOARD_NUMBER, BOARD_TITLE, BOARD_WRITER, BOARD_CONTENT, BOARD_REGISTER_DATE, BOARD_UPDATE_DATE
FROM TBL_BOARD B
ORDER BY BOARD_NUMBER DESC
) D2
<![CDATA[
WHERE ROWNUM <= #{page} * #{amount}
) WHERE R > (#{page} - 1) * #{amount}
]]>
</select>
외부에서 Criteria의 필드인 #{page} #{amount} 받아서 연산을 하였다.
현재 페이지가 어떤 값인지에 따라서 시작값과 끝값을 구한 것이다.
여기서 <![CDATA[ ]]> 는 MyBatis에서 태그안의 쿼리는 연산자로 인식 할 수 있게 하는 괄호이다.
Criteria 주입
Criteria 생성
package com.example.app.domain.vo;
import lombok.Data;
import org.springframework.stereotype.Component;
@Component
@Data
public class Criteria {
private int page;
private int amount;
public Criteria create(int page, int amount) {
this.page = page;
this.amount = amount;
return this;
}
}
BoardController
@GetMapping("/list")
public void list(Criteria criteria, Model model){
model.addAttribute("boards", boardService.showAll(criteria));
}
BoardService
public List<BoardVO> showAll(Criteria criteria);
BoardService를 구현받는 Service
public List<BoardVO> showAll(Criteria criteria) {
return boardDAO.findAll(criteria);
}
BoardDAO
public List<BoardVO> findAll(Criteria criteria){
return boardMapper.selectAll(criteria);
}
BoardMapper
public List<BoardVO> selectAll(Criteria criteria);
Test
BoardServiceTest
@Test
public void showAllTest(){
boardService.showAll(new Criteria().create(1,10)).stream().map(BoardVO::getBoardTitle).forEach(log::info);
}
BoardControllerTest
@Test
void list() throws Exception{
log.info("boards: " + mockMvc.perform(MockMvcRequestBuilders.get("/board/list")
.param("page","1")
.param("amount","10")
).andReturn().getModelAndView().getModelMap());
}
현재페이지를 기준으로 StartPage, EndPage를 정해햐 한다.
또 이전과 다음버튼 설정, 예를 들어 첫번째 페이지에서는 이전 페이지가 없어야 하고 마지막페이지에서는 다음버튼이 없어야 한다. 이런 페이징 처리를 해결하기 위해서는 DTO를 만들어 사용해야한다.
DTO는 화면에서만 쓰기 때문에, DB조회없이 화면에서만 사용하기 때문이다.
package com.example.app.domain.vo;
import lombok.Data;
import org.springframework.stereotype.Component;
@Component
@Data
public class PageDTO {
// 페이지 단위 수(페이지가 1~5까지 나올 것인지 1~10까지 나올 것인지)
private int pageCount;
// 현재 페이지를 기준으로 시작 페이지
private int startPage;
// 현재 페이지를 기준으로 마지막 페이지
private int endPage;
// 가장 마지막 페이지
private int realEnd;
private boolean prev, next;
// 전체 게시글 개수
private int total;
// 화면에서 받아온 page, amount를 필드로 구성한 객체
private Criteria criteria;
public PageDTO createPageDTO(Criteria criteria, int total){
return createPageDTO(criteria, total, 10);
}
public PageDTO createPageDTO(Criteria criteria, int total, int pageCount){
this.criteria = criteria;
this.total = total;
this.pageCount = pageCount;
// 현재 페이지를 기준으로 페이지 단위에 맞춰서 마지막 페이지 계산
this.endPage = (int)(Math.ceil(criteria.getPage() / (double)pageCount)) * pageCount;
this.startPage = endPage - pageCount + 1;
// 게시글 전체 개수를 통해 가장 마지막 페이지 계산
this.realEnd = (int)(Math.ceil((double)total / criteria.getAmount()));
// 만약 가장 마지막 페이지보다 마지막 페이지가 더 클 경우(endPage는 배수로 증가하기 떄문)
if(realEnd < endPage){
// 게시글이 한 개도 없다면, realEnd는 0이 되고, endPage도 0이 된다.
// 따라서 realEnd가 0이라면 endPage를 1로 변경해주어야 한다.
endPage = realEnd == 0 ? 1 : realEnd;
}
this.prev = startPage > 1;
this.next = endPage < realEnd;
return this;
}
}
컨트롤러에서 DTO total를 가져오기 위해서는 데이터베이스에서 count를 사용한 갯수를 가져와야 한다.
BoardMapper.xml
<select id="getTotal" resultType="_int">
SELECT COUNT(BOARD_NUMBER) FROM TBL_BOARD
</select>
BoardMapper
// 전체 개수
public int getTotal();
BoardDAO
// 전체 개수
public int findCountAll(){
return boardMapper.getTotal();
}
BoardService
// 전체 개수
public int getTotal();
BoardService 구현체
@Override
public int getTotal() {
return boardDAO.findCountAll();
}
BoardController
// 게시글 목록
@GetMapping("/list")
public void list(Criteria criteria, Model model){
if(criteria.getPage() ==0){
criteria.create(1,10);
}
model.addAttribute("boards", boardService.showAll(criteria));
model.addAttribute("pagination",new PageDTO().createPageDTO(criteria,boardService.getTotal()));
}
화면 외부에서 Criteria를 받아오는데 이것을 화면의 Form태그로 전송을 하게 되며
위에서 사용한 "pagination"은 html에서 타임리프로 사용할 수 있게 된다.
if(criteria.getPage() ==0){
criteria.create(1,10);
}
이 부분은 처음에 전달된게 없을 때, 즉 처음 화면을 불러올 때 전달 된게 없으므로 처음 보여지는 값은
1페이지 안에 10개의 항목이 보여지게 한 것이다. 이 값을 pageDTO로 전달하여 DTO 필드를 받아서 list 에서 pagenation으로 사용할 수 있게 되는 것이다.
list.html
<div th:object="${pagination}">
<div style="text-align: center">
<a class="changePage" th:href="*{startPage - 1}" th:if="*{prev}"><code><</code></a>
<th:block th:each="page:${#numbers.sequence(pagination.startPage, pagination.endPage)}">
<a class="changePage" th:href="${page}" th:if="${pagination.criteria.page != page}"><code th:text="${page}"></code></a>
<code th:unless="${pagination.criteria.page != page}" th:text="${page}"></code>
</th:block>
<a class="changePage" th:href="*{endPage + 1}" th:if="*{next}"><code>></code></a>
</div>
<form th:action="@{/board/list}" th:object="${criteria}" name="pageForm">
<input type="hidden" th:field="*{page}">
<input type="hidden" th:field="*{amount}">
</form>
</div>
list.html 페이징처리 버튼 구현
<script th:inline="javascript">
let $pages = $("a.changePage");
$pages.on("click", function (e) {
e.preventDefault();
$(document.pageForm.page).val($(this).attr("href"));
document.pageForm.submit();
});
</script>