63일차 - 스프링 (페이징)

Yohan·2024년 5월 22일
0

코딩기록

목록 보기
94/156
post-custom-banner

페이징

  • 게시물 목록이 화면에 뜰 때 아래에 1, 2, 3 .. 등의 넘기는 숫자와 한꺼번에 넘길 수 있거나 되돌릴 수 있는 next, prev 같은 기능을 추가해보려고한다.

BoardMapper.xml

    <select id="findAll" resultType="board">
        SELECT * FROM tbl_board
        ORDER BY board_no DESC
        LIMIT #{pageStart}, #{amount}
    </select>
  • LIMIT을 걸어서 페이징의 범위를 설정
    • pageStart는 그 페이지의 첫번 째 게시물번호, amount는 한 페이지당 게시물 목록 수를 나타낸다.

Page

package com.study.springstudy.springmvc.chap04.common;

import lombok.*;

@Getter
@ToString
@EqualsAndHashCode
@AllArgsConstructor
public class Page {

    private int pageNo; // 클라이언트가 요청한 페이지번호
    private int amount; // 클라이언트가 요청한 한 페이지당 게시물 목록 수

    public Page() {
        this.pageNo = 1;
        this.amount = 6;
    }

    // 페이지 범위 밖이면 1로 고정
    public void setPageNo(int pageNo) {
        if (pageNo < 1 || pageNo > Integer.MAX_VALUE) {
            this.pageNo = 1;
            return;
        }
        this.pageNo = pageNo;
    }

    public void setAmount(int amount) {
        if (amount < 6 || amount > 60) {
            this.amount = 6;
            return;
        }
        this.amount = amount;
    }

    /*
                만약에 한 페이지에 게시물을 10개씩 렌더링한다면

                1페이지 -> LIMIT 0, 10
                2페이지 -> LIMIT 10, 10
                3페이지 -> LIMIT 20, 10

                만약에 한 페이지에 게시물을 6개씩 렌더링한다면

                1페이지 -> LIMIT 0, 6
                2페이지 -> LIMIT 6, 6
                3페이지 -> LIMIT 12, 6

                만약에 한 페이지에 게시물을 N개씩 렌더링한다면

                1페이지 -> LIMIT 0 * N, N
                2페이지 -> LIMIT 1 * N, N
                3페이지 -> LIMIT 2 * N, N
                M페이지 -> LIMIT (M - 1) * N, N
             */
    public int getPageStart() {
        return (this.pageNo - 1) * this.amount;
    }

}
  • Page 클래스를 보면 PageNO, amount 필드가 보인다. 하지만 나는 sql에 pageStart를 넣었는데 어떻게 된일일까?
    -> getter를 만들면 sql에서는 필드를 생성한 줄 알기 때문에 필드로 사용가능

pageNo을 sql에 보내면되는거 아닌가 왜 굳이 pageStart를 사용해야할까?

  • 주석을 보면 M페이지 -> LIMIT (M - 1) * N, N 인 것을 알 수 있다.
    여기서 M = pageNo, N = amount이지만 (M - 1) * N 을 sql에 보내줘야 하는 것을 볼 수 있다. 즉, pageStart = (M - 1) * N 을 나타내고 이것을 sql에 보내주어야한다.

PageMaker

  • 페이지 화면 렌더링에 필요한 정보들을 계산하는 PageMaker 클래스를 만들어준다.
// 페이지 화면 렌더링에 필요한 정보들을 계산
@Getter @ToString
@EqualsAndHashCode
public class PageMaker {

    // 한 화면에 페이지를 몇개씩 배치할 것인지??
    private static final int PAGE_COUNT = 10;

    // 페이지 시작번호와 끝번호
    private int begin, end, finalPage;

    // 이전, 다음 버튼 활성화 여부
    private boolean prev, next;

    // 총 게시물 수
    private int totalCount;

    // 현재 페이지 정보
    private Page pageInfo;

    public PageMaker(Page page, int totalCount) {
        this.pageInfo = page;
        this.totalCount = totalCount;
        makePageInfo();
    }


    // 페이지 생성에 필요한 데이터를 만드는 알고리즘
    private void makePageInfo() {

        // 1. end값을 계산
        /*
            지금 사용자가 7페이지를 보고 있다면
            페이지 구간: 1 ~ 10 구간

            지금 사용자가 24페이지를 보고 있다면
            페이지 구간: 21 ~ 30 구간

            // 5개씩 페이지를 배치하는 경우는
            7page :  6 ~ 10
            24page :  21 ~ 25

            // 공식: (올림 (현재 사용자가 위치한 페이지넘버 / 한 화면에 보여줄 페이지 수)) * 한 화면에 보여줄 페이지 수
         */

        this.end = (int) (Math.ceil((double) pageInfo.getPageNo() / PAGE_COUNT) * PAGE_COUNT);

        // 2. begin
        this.begin = this.end - PAGE_COUNT + 1;

        // 3. 마지막 페이지 구간에서 end값 보정
        /*
        * 총 게시물이 237개이고 한 화면에 게시물을 10개씩 배치하고 있다면
        * 1 ~ 10페이지 구간 : 게시물이 총 100개
        * 11 ~ 20페이지 구간 : 게시물이 총 100개
        * 21 ~ 30페이지 구간 : 게시물이 총 37개
        *
        * -> 과연 마지막 구간에서 end값이 30이 맞는가?
        * -> 실제로는 24로 보정되어야 함.

        // 마지막 페이지 번호를 구하는 공식
        * 게시물이 351개 한 페이지당 게시물 10개씩 배치
        * 끝페이지 번호? 36페이지
        *
        * 올림 (총 게시물 수 / 한 페이지당 배치할 게시물 수)
        */

        this.finalPage = (int) Math.ceil((double) totalCount / pageInfo.getAmount());

        // 마지막 구간에서 end값을 finalPage로 보정
        if (finalPage < this.end) {
            this.end = finalPage;
        }

        // 4. 이전 버튼 활성화 여부 (1페이지 구간일 때)
        this.prev = begin != 1;

        // 5. 다음 버튼 활성화 여부 (마지막페이지 구간일 때)
        this.next = this.end < finalPage;
    }

}
  • PageMaker 객체를 생성하면 page정보, 총 게시물 수, 페이지 생성 알고리즘을 모두 가져올 수 있다.
    -> totalCount는 DB에서 가져온다.
  1. Controller에서 Service로 요청
  2. Service에서 mapper로 반환
  3. Mapper에서 sql 구현
  • end값 공식 적용
  • begin값은 end값 - 한 화면에 보여줄 페이지 수 + 1
  • 검색했을 때에도 페이징이 적용되어야하므로 Page를 상속한다.
package com.study.springstudy.springmvc.chap04.common;

import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

@Getter @Setter @ToString
@EqualsAndHashCode
// 검색 + 페이징
public class Search extends Page {

    // 검색어, 검색조건
    private String keyword, type;

    // 검색 기본값은 빈문자 = 전체 출력
    public Search() {
        this.keyword = "";
    }
}

page 파라미터 추가

  • BoareMapper(interface), BoardService, BoardController의 목록을 조회하는 부분에 파라미터로 page를 추가해주어 페이징을 적용시킨다.

Controller

  • Search page 로 파라미터를 주어 Search클래스에 있는 필드들까지 총 4개의 필드를 사용할 수 있게했다.

Service

업로드중..

BoardMapper.xml 수정 (Search클래스 추가, 검색에 대한 조건을 다뤄서 sql나눔)

  • tc는 title or content를 나타냄
  • LIKE 뒤에 '%내용%' 형태로 써야하지만 %% 사이에는 #{}가 올 수 없기 때문에 CONCAT으로 씀
    <select id="findAll" resultType="board">
        SELECT * FROM tbl_board
        <if test="type == 'title'">
            WHERE title LIKE CONCAT('%', #{keyword}, '%')
        </if>
        <if test="type == 'content'">
            WHERE content LIKE CONCAT('%', #{keyword}, '%')
        </if>
        <if test="type == 'writer'">
            WHERE writer LIKE CONCAT('%', #{keyword}, '%')
        </if>
        <if test="type == 'tc'">
            WHERE title LIKE CONCAT('%', #{keyword}, '%')
                OR content LIKE CONCAT('%', #{keyword}, '%')
        </if>
        ORDER BY board_no DESC
        LIMIT #{pageStart}, #{amount}
    </select>

list.jsp

<div class="bottom-section">
        <!-- 페이지 버튼 영역 -->
        <nav aria-label="Page navigation example">
          <ul class="pagination pagination-lg pagination-custom">
            
            <!-- 첫번 째 페이지로 -->
            <c:if test="${maker.pageInfo.pageNo != 1}">
              <li class="page-item">
                <a class="page-link" href="/board/list?pageNo=1"><<</a>
              </li>
            </c:if>

            <!-- 이전 페이지 -->
            <c:if test="${maker.prev}">
              <li class="page-item">
                <a class="page-link" href="/board/list?pageNo=${maker.begin -1}"
                  >prev</a>
              </li>
            </c:if>

            <!-- 페이지 -->
            <c:forEach var="i" begin="${maker.begin}" end="${maker.end}">
              <li data-page-num="${i}" class="page-item">
                <a class="page-link" href="/board/list?pageNo=${i}">${i}</a>
              </li>
            </c:forEach>

            <!-- 다음 페이지 -->
            <c:if test="${maker.next}">
              <li class="page-item">
                <a class="page-link" href="/board/list?pageNo=${maker.end + 1}"
                  >next</a
                >
              </li>
            </c:if>

            <!-- 마지막 페이지로 -->
            <c:if test="${maker.pageInfo.pageNo != maker.finalPage}">
              <li class="page-item">
                <a class="page-link" href="/board/list?pageNo=${maker.finalPage}">>></a>
              </li>
            </c:if>


          </ul>
        </nav>
      </div>
  • 클릭한 페이지만 다른색으로 활성화되는 함수도 추가
      // 클릭한 페이지만 다른색으로 활성화 (어떤 페이지를 눌렀는지 알 수 있게 하기위함)
      function appendActivePage() {
        // 1. 현재 위치한 페이지 번호를 알아낸다.
        //  -> 주소창에 묻어있는 페이지 파라미터 숫자를 읽거나
        //     서버에서 내려준 페이지번호를 읽는다.
        const currentPage = "${maker.pageInfo.pageNo}";
        // console.log("현재페이지: " + currentPage);

        // 2. 해당 페이지번호와 일치하는 li태그를 탐색한다.
        const $li = document.querySelector(
          `.pagination li[data-page-num="\${currentPage}"]`
        );

        // 3. 해당 li태그에 class = active를 추가한다.
        $li.classList.add("active");
      }

      appendActivePage();
  • .pagination li[data-page-num="\${currentPage}"]의 경우
    -> 예를 들어 현재 페이지가 3인 경우 [data-page-num="3"] 이다.
      <div class="top-section">
        <!-- 검색창 영역 -->
        <div class="search">
          <form action="/board/list" method="get">

            <select class="form-select" name="type" id="search-type">
              <option value="title" selected>제목</option>
              <option value="content">내용</option>
              <option value="writer">작성자</option>
              <option value="tc">제목+내용</option>
            </select>

            <input type="text" class="form-control" name="keyword">

            <button class="btn btn-primary" type="submit">
              <i class="fas fa-search"></i>
            </button>

          </form>
        </div>
      </div>
  • form 태그안에 있는 sql문의 name은 필드명과 일치해야함
    -> Search 클래스의 type, keyword와 일치!
profile
백엔드 개발자
post-custom-banner

0개의 댓글