so woon!

페이징 구현 본문

Spring Boot/구현해보기

페이징 구현

xowoony 2022. 11. 30. 19:08

학습일 : 2022. 11. 30


페이징을 위해 base에 models라는 디렉토리를 하나 새로 생성한 뒤
PagingModel 이라는 이름의 클래스를 생성

경로는 다음과 같다




PagingModel.java

<36 페이지 요청시>
31------ 36 ------- 40 표시
s p e

이런식으로 나오게 구현을 해야 하기 때문에
앞서 선생님이랑 같이 풀어본 페이징 관련 공식들인

페이징(Paging) 혹은 페지네이션(Pagination)
[ c ] 페이지 당 표시할 게시글의 수 10  
[ p ] 요청한 페이지 번호 Assert >= 1  
[ t ] 전체 게시글의 개수   DB가
[ n ] 이동 가능한 최소 페이지 1  
[ x ] 이동 가능한 최대 페이지 t / c + ( t % c == 0 ? 0 : 1)  
(t -1) / c + 1 성능에 유리 ===> 이걸 사용하도록
[ s ] 표시 시작 페이지 ( ( p - 1 ) / 10 ) * 10 + 1 )  
[ e ] 표시 끝 페이지
(이동가능한 최대 페이지 이하여야 함)
Math.min( s + 9 , x ) ===> 이걸 사용하도록
s + 9 > x ? x : ( s + 9 )  

이 공식들을 클래스에 적어주도록 한다.

먼저, (alt+insert) 하여 constructor 를 추가 하는데,

하나는 totalCount, requestPage 를,
나머지 하나는 countPerPage, totalCount, requestpage 총 3개를 선택하고 constructor추가를 해준다.

그러고 나서 아래와 같이 공식을 적어준다.

package dev.xowoony.studymemberbbs.models;
//페이징
public class PagingModel {
    public final int countPerPage;  // 페이지당 표시할 게시글의 개수
    public final int totalCount;  // 전체 게시글 개수
    public final int requestPage;  // 요청한 페이지 번호
    public final int maxPage;  // 이동 가능한 최대 페이지
    public final int minPage;  // 이동 가능한 최소 페이지
    public final int startPage;  // 표시 시작 페이지
    public final int endPage;  // 표시 끝 페이지


    public PagingModel(int totalCount, int requestPage) {
        this(10, totalCount, requestPage);
    }

    public PagingModel(int countPerPage, int totalCount, int requestPage) {
        this.countPerPage = countPerPage;
        this.totalCount = totalCount;
        this.requestPage = requestPage;
        this.minPage = 1;
        this.maxPage = (totalCount - 1) / countPerPage + 1;
        this.startPage = ((requestPage - 1) / 10) * 10 + 1;
        this.endPage = Math.min(startPage + 9, maxPage);
    }
}

 

 

list.html

<!doctype html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title th:text="${'또치뱅크 - 게시판 목록 조회'}"></title>
    <th:block th:replace="~{fragments/head :: common}"></th:block>
    <link rel="stylesheet" th:href="@{/bbs/resources/stylesheets/list.css}">
    <script th:if="${board == null}">
        alert('존재하지 않는 게시판입니다.');
        if (window.history.length > 1) {
            window.history.back();
        } else {
            window.close();
        }
    </script>
    <script defer th:src="@{/resources/libraries/ckeditor/ckeditor.js}"></script>
    <script defer th:src="@{/bbs/resources/scripts/list.js}"></script>
</head>
<body th:if="${board != null}">
<th:block th:replace="~{fragments/body :: header}"></th:block>
<th:block th:replace="~{fragments/body :: cover}"></th:block>
<main class="--main main">
    <div class="--content content">
        <h1 class="title">게시판</h1>
        <table class="table" id="table">
            <thead>
                <tr>
                    <th>번호</th>
                    <th>제목</th>
                    <th>작성자</th>
                    <th>조회수</th>
                    <th>일시</th>
                    <th>추천수</th>
                </tr>
            </thead>
            <tbody>
                <tr th:each="article : ${articles}">
                    <!--번호-->
                    <td th:text="${article.getIndex()}"></td>
                    <td>
                        <a class="title" th:href="@{/bbs/read (aid=${article.getIndex()})}">
                            <!--글번호-->
                            <span class="text" th:text="${article.getTitle()}"></span>
                            <!--글 타이틀-->
                            <span class="comment" th:text="${'[' + article.getCommentCount() + ']'}"></span>
                            <!--댓글 달린 갯수 표시-->
                        </a>
                    </td>
                    <td th:text="${article.getUserNickname()}"></td>
                    <!--      작성자          -->
                    <td th:text="${article.getView()}"></td>
                    <!--      조회수          -->
                    <td th:text="${#dates.format(article.getModifiedOn(), 'yyyy-MM-dd-HH:mm:ss')}"></td>
                    <!--       일시         -->
                    <td th:text="${article.getView()}"></td>
                    <!--       추천수         -->
                </tr>
            </tbody>
            <tfoot>
                <tr>
                    <td colspan="6">
                        <div class="page-container"
                             th:with="urlBuilder = ${T(org.springframework.web.servlet.support.ServletUriComponentsBuilder).fromCurrentRequest()}">
                            <!-- 페이징-->
                            <!--  <<  -->
                            <a class="page"
                               th:href="@{${urlBuilder.replaceQueryParam('page', paging.minPage).build().toUriString()}}"
                            th:if="${paging.requestPage > 1}">
                                <i class="fa-solid fa-angles-left"></i>
                            </a>
                            <!--  <  -->
                            <a class="page"
                               th:href="@{${urlBuilder.replaceQueryParam('page', paging.requestPage -1).build().toUriString()}}"
                               th:if="${paging.requestPage > 1}">
                                <i class="fa-solid fa-angle-left"></i>
                            </a>

                            <!-- 얘가 기준 - 요청한 페이지 -->
                            <a th:each="page : ${#numbers.sequence(paging.startPage, paging.endPage)}"
                               th:class="${'page ' + (page == paging.requestPage ? 'selected' : '')}"
                               th:text="${page}"
                               th:href="@{${urlBuilder.replaceQueryParam('page', page).build().toUriString()}}"></a>
                            <!-- 얘가 기준 - 요청한 페이지-->

                            <!--  >  -->
                            <a class="page"
                               th:href="@{${urlBuilder.replaceQueryParam('page', paging.requestPage + 1).build().toUriString()}}"
                            th:if="${paging.requestPage < paging.maxPage}">
                                <i class="fa-solid fa-angle-right"></i>
                            </a>

                            <!--  >>  -->
                            <a class="page"
                               th:href="@{${urlBuilder.replaceQueryParam('page', paging.maxPage).build().toUriString()}}"
                                th:if="${paging.requestPage < paging.maxPage}">
                                <i class="fa-solid fa-angles-right"></i>
                            </a>
                        </div>
                    </td>
                </tr>
            </tfoot>
        </table>
        <!--  글쓰기 버튼-->
        <div class="button-container" id="buttonContainer">
            <a class="--object-button" th:href="@{/bbs/write (bid=${board.getId()})}">글쓰기</a>
        </div>
        <form class="search-form" id="searchForm" method="get">
            <label class="label">
                <span hidden>검색 기준</span>
                <select class="--object-input" name="criterion">
                    <option value="all" th:selected="${criterion == null || criterion.equals('all')}">제목 + 내용</option>
                    <option value="title" th:selected="${criterion != null && criterion.equals('title')}">제목</option>
                    <option value="nickname" th:selected="${criterion != null && criterion.equals('nickname')}">작성자
                    </option>
                </select>
            </label>
            <label class="label">
                <span hidden>검색어</span>
                <input class="--object-input" maxlength="50" name="keyword" placeholder="검색어를 입력해 주세요." type="text"
                       th:value="${#request.getParameter('keyword')}">
            </label>
            <input name="bid" type="hidden" th:value="${board.getId()}">
            <input class="--object-button" type="submit" value="검색">
        </form>
    </div>

</main>
<input type="hidden" th:value="${bid}" name="bid">
<th:block th:replace="~{fragments/body :: footer}"></th:block>
</body>
</html>

















BbsMapper.xml
SELECT 쿼리를 이용한다.
resultType은 int 이다.

1. 만약 criterion이 null이 아니고 criterion이 nickname일 경우
`articles`.`user_email` 과 `user`.`email` 같을 경우에
`study_member`.`users` 테이블을 LEFT JOIN 한다.

2. 만약 criterion 이 null 이 아니고 criterion이 title일 경우
제목에 띄어쓰기가 있으면
띄어쓰기가 없는 것으로 바꾸어 준다.

3. 만약 criterion이 null 이 아니고 criterion이 all (제목+내용) 일 경우
제목이나 내용에 띄어쓰기가 포함되어 있으면
띄어쓰기가 없는 것으로 바꾸어 주도록 한다.

<!--  목록  -->
<select id="selectArticlesByBoardId"
        resultType="dev.xowoony.studymemberbbs.vos.bbs.ArticleReadVo">
    SELECT `article`.`index` AS `index`,
    `article`.`user_email` AS `userEmail`,
    `article`.`board_id` AS `boardId`,
    `article`.`title` AS `title`,
    `article`.`view` AS `view`,
    `article`.`written_on` AS `writtenOn`,
    `article`.`modified_on` AS `modifiedOn`,
    `user`.`nickname` AS `userNickname`,
    COUNT(`comment`.`index`) AS `commentCount`

    FROM `study_bbs`.`articles` AS `article`
         LEFT JOIN `study_member`.`users` AS `user` ON `article`.`user_email` = `user`.`email`
        LEFT JOIN `study_bbs`.`comments` AS `comment` ON `article`.`index` = `comment`.`article_index`
    WHERE `article`.`board_id` = #{boardId}
    <if test="criterion != null and criterion.equals('title')">
        AND REPLACE(`article`.`title`, ' ', '') LIKE CONCAT('%', REPLACE(#{keyword}, ' ', ''), '%')
    </if>
    <if test="criterion != null and criterion.equals('all')">
        AND (REPLACE(`article`.`title`, ' ', '') LIKE CONCAT('%', REPLACE(#{keyword}, ' ', ''), '%')
        OR REPLACE(`article`.`content`, ' ', '') LIKE CONCAT('%', REPLACE(#{keyword}, ' ', ''), '%'))
    </if>
    <if test="criterion != null and criterion.equals('nickname')">
        AND BINARY `user`.`nickname` = #{keyword}
    </if>
    GROUP BY `article`.`index`
    ORDER BY `article`.`index` DESC
    LIMIT #{limit} OFFSET #{offset}
</select>
<!-- 페이징 -->
<select id="selectArticleCountByBoardId"
        resultType="int">
    SELECT COUNT(0)
    FROM `study_bbs`.`articles`
    <if test="criterion != null and criterion.equals('nickname')">
        LEFT JOIN `study_member`.`users` AS `user` ON `articles`.`user_email` = `user`.`email`
    </if>
    WHERE `board_id` = #{boardId}
    <if test="criterion != null and criterion.equals('title')">
        AND REPLACE(`title`, ' ', '') LIKE CONCAT('%', REPLACE(#{keyword}, ' ', ''), '%')
    </if>
    <if test="criterion != null and criterion.equals('all')">
        AND (REPLACE(`title`,' ', '') LIKE CONCAT('%', REPLACE(#{keyword}, ' ', ''), '%')
        OR REPLACE(`content`, ' ', '') LIKE CONCAT('%', REPLACE(#{keyword}, ' ', ''), '%'))
    </if>
    <if test="criterion != null and criterion.equals('nickname')">
        AND BINARY `user`.`nickname` = #{keyword}
    </if>
</select>




IBbsMapper.java (인터페이스)

    // 목록 만들기 + 페이징
    ArticleReadVo[] selectArticlesByBoardId(@Param(value = "boardId") String boardId,
                                            @Param(value = "criterion") String criterion,
                                            @Param(value = "keyword") String keyword,
                                            @Param(value = "limit") int limit,
                                            @Param(value = "offset") int offset);
    int selectArticleCountByBoardId(@Param(value = "boardId") String boardId,
                                    @Param(value = "criterion") String criterion,
                                    @Param(value = "keyword") String keyword);




BbsService.java

    // 목록 만들기 + 페이징
    public ArticleReadVo[] getArticles(BoardEntity board, PagingModel paging, String criterion, String keyword) {
        return this.bbsMapper.selectArticlesByBoardId(board.getId(), criterion, keyword, paging.countPerPage,
                (paging.requestPage -1) * paging.countPerPage);
        }






BbsController.java
표시할 글이 없을 경우 페이지 기본값을 1로 지정해주기 위해
처음에는 아래와 같이 작성했으나

        if (page <1) {
            page = 1;
        }

좀 더 있어보이고 보기 좋게

	page = Math.max(1, page);

이렇게 수정하였다.

표시할 글이 없을 경우 1과 page 중 더 큰 값을 내놓게 되기 때문에
글이 없을 경우에도 1페이지가 기본 값이 된다.

	System.out.println(page);

했을 때
원래는 null 이 찍히는데
위에

        page = Math.max(1, page);

을 적어주기 때문에 1페이지가 찍힐 수 있게 된다.

 //목록 만들기
    @RequestMapping(value = "list",
            method = RequestMethod.GET,
            produces = MediaType.TEXT_HTML_VALUE)
    public ModelAndView getList(@RequestParam(value = "bid", required = false) String bid,
                                @RequestParam(value = "page", required = false, defaultValue = "1") Integer page,
                                @RequestParam(value = "criterion", required = false) String criterion,
                                @RequestParam(value = "keyword", required = false) String keyword) {
        // 페이징
        page = Math.max(1, page);
        ModelAndView modelAndView = new ModelAndView("bbs/list");
        BoardEntity board = this.bbsService.getBoard(bid);
        modelAndView.addObject("board", board);
        if (board != null) {
            // 검색을 했을 경우
            int totalCount = this.bbsService.getArticleCount(board, criterion, keyword); // 페이지네이션 때문에 가지고 옴 (100개라면 10페이지 까지 표시해주기 위해서)

            PagingModel paging = new PagingModel(totalCount, page);
            modelAndView.addObject("paging", paging);

            ArticleReadVo[] articles = this.bbsService.getArticles(board, paging, criterion, keyword); // 게시글
            modelAndView.addObject("articles", articles);
        }
        return modelAndView;
    }


실행결과



울고싶어라

'Spring Boot > 구현해보기' 카테고리의 다른 글

회원탈퇴 구현  (0) 2022.12.28
디렉토리 경로 인식  (0) 2022.12.15
댓글 삭제하기 구현  (0) 2022.11.27
댓글 수정하기 구현  (0) 2022.11.27
게시글에 댓글 달기 구현  (5) 2022.11.27
Comments