IT/development

[springBoot] 페이지네이션 처리(feat. Oracle)

알 수 없는 사용자 2023. 5. 7. 15:13
반응형

xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://www.mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="study.example.thboard.mapper.BoardMapper">
    <!--게시글 목록 조회-->
    <select id="selectBoards" parameterType="Criteria" resultType="BoardVo">
        /* 게시글 목록 조회 */
        <![CDATA[
        SELECT T1.no
	         , T1.boardNo
	         , T1.title
	         , T1.content
	         , T1.writer
	         , T1.createDate
	         , T1.updateDate
	      FROM
              (SELECT /*+ INDEX_DESC(BOARD BOARD_PK)*/
                       rownum AS no
                     , BOARD_NO AS boardNo
                     , TITLE AS title
                     , CONTENT AS content
                     , WRITER AS writer
                     , CREATE_DATE AS createDate
                     , UPDATE_DATE AS updateDate
                     , USE_YN
                  FROM BOARD
                 WHERE 1=1
                   AND USE_YN = 'Y'
                   AND rownum <= #{pageNum} * #{amount}
              )T1
         WHERE no > (#{pageNum} -1) * #{amount}
         ]]>
    </select>

    <!--게시글 전체 카운트-->
    <select id="selectTotBoardCnt" resultType="int">
        SELECT COUNT(*) FROM BOARD
    </select>
</mapper>

오라클 페이지네이션 쿼리 핵심

더보기
/* 샘플 SQL */
SELECT T2.rn
      , T2.id
      , T2.name
      , T2.email
      , TO_CHAR(T2.create_date, 'YYYY-MM-DD') AS createDate /* 쿼리의 가장 바깥쪽에서 날짜 -> 문자열 변환을 해주는게 성능에 좋다고 한다. */
   FROM  (
            SELECT ROWNUM as rn
                 , T1.id
                 , T1.name
                 , T1.email
                 , T1.create_date
              FROM   
                ( /* 1차 서브쿼리에서 등록일 내림차순으로 정렬 */
                  * 혹은 ROW_NUMBER() OVER(ORDER BY create_date DESC) AS rn 이런식으로.. 
                  SELECT id
                       , name
                       , email
                       , create_date
                    FROM user_info
                ORDER BY create_date DESC) T1   
            WHERE ROWNUM <= 30) T2 /* 2차 서브쿼리에서 정렬된 데이터를 1부터 30까지만 30건만 필터링(현재 페이지 번호 * 가져올 게시글 목록 수) */
    WHERE T2.rn > 20 // 혹은 >= 21 /* 30건 중 21번부터 30번까지 10건만 최종적으로 필터링(현재 페이지 * 페이지 목록 수 중 시작값은 현재 페이지 - 1 * 가져올 게시글 목록 수) */

정렬을 한 데이터를 서브쿼리로 감싸서 ROWNUM으로 마지막 인덱스까지만 1차 필터링(rownum <= 마지막인덱스)

1차 필터링 된 데이터에서 ROWNUM으로 시작 인덱스로 2차 필터링( rownum >= 시작 인덱스) 

페이징 처리 정보 출처: https://okky.kr/articles/282926

 

OKKY - 페이징(Paging)에 대한 이해 - (2) ROW NUMBER 을 이용한 게시물 가져오기.

흔히 웹 개발을 처음 하는 개발자가 처음 겪게 되는 난관은 바로 게시판의 페이징(Paging)이 아닐까 합니다. 특히 DBMS 와 연동해서 개발하는 경우가 많은데, 각 DBMS 마다 페이징 방법도 많고 성능

okky.kr

페이지 정보 객체

package study.example.thboard.vo;

import lombok.Data;

@Data
//페이지 정보
public class Criteria {

    private int pageNum;     // 페이지 번호(현재 페이지가 몇 페이지인지)
    private int amount;      // 한 화면에 출력한 페이지 개수
	
    //기본값은 1페이지의 페이지수 10으로 고정
    public Criteria() {
        this(1,10);
    }

    public Criteria(int pageNum, int amount) {
        this.pageNum = pageNum;
        this.amount = amount;
    }
}

페이지네이션 계산(화면에서 사용하기 위함)

package study.example.thboard.vo;

import lombok.Data;

@Data
//페이지네이션 처리
public class PageMaker {
    private int startPage;  //페이징 화면 하단의 시작 번호(5페이지라고 하면 [1][2][3][4][5] 여기서 제일 첫 번 째 시작번호)
    private int endPage;    //페이징 화면 하단의 끝 번호(5페이지라고 하면 [1][2][3][4][5] 여기서 제일 끝 번호)
    private boolean prev, next; // 이전, 다음 존재 여부

    private int totalCount; // 전체 게시글 수
    private Criteria cri;      // 프론트에서 전달하는 pageNum(현재 페이지), amount(출력 페이지) 전달 역할

    public PageMaker(int totalCount, Criteria cri) {
        this.totalCount = totalCount;
        this.cri = cri;

        this.endPage = (int)(Math.ceil(cri.getPageNum()/10.0)) * 10;
        this.startPage = this.endPage - 9;

        int realEnd = (int)(Math.ceil((totalCount * 1.0) / cri.getAmount()));

        if(realEnd < this.endPage) {
            this.endPage = realEnd;
        }
        this.prev = this.startPage > 1;
        this.next = this.endPage < realEnd;
    }
}

mapper(interface)

package study.example.thboard.mapper;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
import study.example.thboard.vo.BoardVo;
import study.example.thboard.vo.Criteria;

import java.util.List;

@Repository @Mapper
public interface BoardMapper {
    /* 게시글 목록 조회 */
    List<BoardVo> selectBoards(Criteria cri);

    /* 전체 게시글 카운트 */
    int selectTotBoardCnt();
}

service

package study.example.thboard.service;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import study.example.thboard.mapper.BoardMapper;
import study.example.thboard.vo.BoardVo;
import study.example.thboard.vo.Criteria;

import java.util.List;

@Service
@Slf4j
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class BoardService {

    private final BoardMapper boardMapper;

    /**
     * 게시글 목록 조회
     * @param cri
     * @throws Exception
     * @return
     */
    public List<BoardVo> getBoards(Criteria cri) throws Exception{
        return boardMapper.selectBoards(cri);
    }

    /**
     * 게시글 전체 카운트
     * @return
     */
    public int getTotBoardCnt() {
        return boardMapper.selectTotBoardCnt();
    }
}

controller

package study.example.thboard.controller;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.ModelAndView;
import study.example.thboard.service.BoardService;
import study.example.thboard.service.FileService;
import study.example.thboard.vo.BoardVo;
import study.example.thboard.vo.Criteria;
import study.example.thboard.vo.PageMaker;
import study.example.thboard.vo.FileVo;

import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.List;

@Controller
@RequiredArgsConstructor
@Slf4j
@RequestMapping(value = "/")
public class MainController {

    private final BoardService boardService;
    private final FileService fileService;

    /**
     * 게시글 목록 조회
     * @param cri
     * @return
     */
    @GetMapping(value = "")
    public ModelAndView main(@ModelAttribute Criteria cri) {

        ModelAndView mv = new ModelAndView("pages/index");
        List<BoardVo> boardList = null;

        try {
            //전체 게시글 카운트
            int totBoardCnt = boardService.getTotBoardCnt();
            //게시글 목록 조회(페이징)
            boardList = boardService.getBoards(cri);
            //화면 페이징 처리 객체
            PageMaker pageMaker = new PageMaker(totBoardCnt, cri);
            mv.addObject("list", boardList);
            mv.addObject("pageMaker", pageMaker);

        } catch (Exception e) {
            e.printStackTrace();
        }
        return mv;
    }
}

 

html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorate="~{layouts/default_layout}">

    <script layout:fragment="script" th:inline="javascript" type="text/javascript">
        $(document).ready(function () {
            //글쓰기
            $("#btnSave").on("click", function () {
                $("#frm").attr("action", "/regForm").submit();
            });

            //검색
            $("#btnSearch").on("click", function () {
                $("#frm").submit();
            });
        });

        //삭제
        function del(no) {
            let boardNo = $("<input>").attr("type", "hidden").attr("name", "boardNo").val(no);
            $("#frm").attr("method", "post")
                     .attr("action", "del")
                     .append(boardNo)
                     .submit();
        }
    </script>

<!-- Content -->
<div layout:fragment="content">
      <form id="frm" th:object="${boardVo}" action="" method="get">
      <main>
        <div class="container-fluid px-4">
          <h1>게시글 관리</h1>
            <table class="table table-striped table-sm">
                                    <thead>
                                        <tr>
                                            <th>순번</th>
                                            <th>제목</th>
                                            <th>내용</th>
                                            <th>작성자</th>
                                            <th>등록일</th>
                                            <th>수정일</th>
                                            <th></th>
                                        </tr>
                                    </thead>
                                    <tbody>
                                        <tr th:if="${#lists.size(list) >0}" th:each="list : ${list}">
                                            <td th:text="${list.no}"></td>
                                            <td><a th:href="@{/detail(boardNo=${list.boardNo})}" th:text="${list.title}"></a></td>
                                            <td th:text="${list.content}"></td>
                                            <td th:text="${list.writer}"></td>
                                            <td th:text="${list.createDate}"></td>
                                            <td th:text="${list.updateDate}"></td>
                                            <td><a href="#" th:href="'javascript:del('+${list.boardNo}+')'" class="btn btn-danger">삭제</a></td>
                                        </tr>
                                    </tbody>
                                </table>
        </div>

        <!-- 게시판 하단 페이지네이션 영역 start -->
        <nav aria-label="Page navigation">
            <ul class="pagination justify-content-center">
                <!-- prev -->
                <li class="page-item" th:if="${pageMaker.prev} == true">
                    <a class="page-link" th:href="@{/(pageNum=${pageMaker.startPage}-1)}">Prev</a>
                </li>
                <!-- pageMaker의 startPage부터 endPage까지 루프, a태그의 href에 idx를 링크(get방식으로 pageNum을 붙여서) -->
                <li class="page-item" id="paginate_btn" th:each="idx: ${#numbers.sequence(pageMaker.startPage, pageMaker.endPage)}" th:classappend="${pageMaker.cri.pageNum} == ${idx} ? active : null">
                    <a class="page-link" th:href="@{/(pageNum=${idx})}" th:text="${idx}"></a>
                </li>
                <!-- next -->
                <li class="page-item" th:if="${pageMaker.next} == true and ${pageMaker.endPage > 0}">
                    <a class="page-link" th:href="@{/(pageNum=${pageMaker.endPage}+1)}">Next</a>
                </li>
            </ul>
        </nav>
        <!-- // 게시판 하단의 페이지네이션 영역 end -->

          <button type="button" id="btnSave" class="btn btn-info">글쓰기</button>

      </main>
      </form>
</div>
</html>

코드 참조: 코드로 배우는 스프링 웹 프로젝트(도서)

반응형