Spring Framework (14) 게시판 - 페이지네이션 (검색 X 경우) - Day 61

jaehoon ahn·2025년 12월 15일

Spring boot

목록 보기
12/20
post-thumbnail

페이지네이션

⇒ 페이지를 이동할 수 있는 기능

  • /board/1

  • /board/1?cp=2, cp=3 이런식으로 이동(페이지 이동시)

  • 각 네비게이션을 눌렀을 때는 쿼리스트링이 붙지 않지만, 페이지를 이동했을 때는 쿼리스트링 cp가 붙음

  • 공지, 정보, 자유 게시판 모두 동일

  • /board/1, 2, 3

    • 게시판 종류를 뭘 누르든 특정 게시판의 게시글 목록 조회를 해야하는 공통사항이 존재
    • 다 같은 요청으로 매핑해줘야 함
      • PathVariable로 처리

@PathVariable

/** 게시글 목록 조회
	 * 
	 * {boardCode}
	 * - /board/xxx : /board 이하 1레벨 자리에 어떤 주소값이 들어오든 모두
	 * 이 메서드에 매핑하겠다는 것
	 * 
	 *  /board/ 이하 1레벨 자리에 숫자로 된 요청 주소가 작성되어 있을 때만 동작
	 *  -> 정규표현식으로 제한해주면 된다.
	 *  
	 * {boardCode:[0-9]+}
	 * - [0-9] : 한 칸에 0~9사이 숫자 입력 가능
	 * - [0-9]+ : 모든 숫자 가능
	 * 
	 * @param boardCode : 게시판 종류 구분 번호 (1, 2, 3) => 공지, 정보, 자유게시판
	 * @return
	 */
	@GetMapping("{boardCode:[0-9]+}") 
	// /board/1, 2, 3 -> 뒤 숫자 받을 수 있도록
	// 쓰고싶은 변수명을 작성 
	// 위처럼 작성하면 /board/list 이렇게도 받을 수도 있다는 것
	// 숫자만 받아주겠다면 :[] 범위를 작성 -> 0-9는 0~9까지 숫자가 들어올 수 있음
	// 10부터는 [0-9]+, 즉 모든 숫자를 다 받아주겠다는 의미
	public String selectBoardList(@PathVariable("boardCode") int boardCode) {
		
		// src/main/resources/templates/board/boardList.html
		return "board/boardList";
	}
  • PathVariable

    • 요청 주소에 들어오는 값을 변수로 사용 가능
    • request scope로 셋팅해줌
  • 검색 유형 & 제목 검색

    • ex)
      • /board/2?key=t&query=폭탄
      • key는 제목, 제목 + 작성자 등 검색 유형
        • t는 제목
      • query → 검색한 내용
   * @param boardCode : 게시판 종류 구분 번호 (1, 2, 3) => 공지, 정보, 자유게시판
	 * @param cp : 현재 조회 요청한 페이지 번호(없으면 1)
	 * @param paramMap(검색 시 이용) : 제출된 파라미터가 모두 저장된 Map
	 * 					검색 시, key, query 담겨있음 
	 * ex) {key=t, query=폭탄}
public String selectBoardList(@PathVariable("boardCode") int boardCode,
			@RequestParam(value = "cp", required = false, defaultValue = "1") int cp, Model model,
			@RequestParam Map<String, Object> paramMap) {
		// paramMap => 검색시 {key=t, query=폭탄}
		// 검색 X => {} -> 빈 상태

		// src/main/resources/templates/board/boardList.html
		return "board/boardList";
	}

Pagination 객체

⇒ 게시글 목록 구성에 필요한 값을 저장한 객체

  • 게시판의 글을 가져오고(serviceImpl) 컨트롤러에게 넘기는 행위에 대한 순서
    // 특정 게시판의 지정된 페이지 목록 조회 서비스
    	@Override
    	public Map<String, Object> selectBoardList(int boardCode, int cp) {
    		// 1. 지정된 게시판에서(boardCode)에서
    		// 삭제되지 않은 게시글 수를 조회
    		
    		// 2. 1번의 결과 + cp를 이용해서 
    		// Pagination 객체를 생성
    		// * Pagination 객체 : 게시글 목록 구성에 필요한 값을 저장한 객체
    		
    		// List, Pagination 객체를 넘겨야 하므로, return 타입이 Map
    		
    		// 3. 특정 게시판의 지정된 페이지 목록 조회
    		// -> board가 담긴 리스트를 가져오겠다는 의미
    		
    		// 4. Pagination 객체 + 목록 조회 결과를 Map으로 묶음
    		
    		// 5. map 반환
    		return null;
    	}

Pagination class

package edu.kh.project.board.model.dto;

/* Pagination 뜻 : 목록을 일정 페이지로 분할해서 
 * 원하는 페이지를 볼 수 있게 하는 것 == 페이징 처리
 * 
 * Pagination 객체를 만드는 이유 : 페이징 처리에 필요한 값을 모아두고, 계산을 하는 객체의 역할로 만들기 위함
 * 
 * 
 */
public class Pagination {
	private int currentPage; // 현재 페이지 번호
	private int listCount; // 전체 게시글 수, selectBoardList에서 listCount로 가져옴

	private int limit = 10; // 한 페이지 목록에 보여지는 게시글 수
	private int pageSize = 10; // 보여질 페이지 번호 개수, 즉 1~10페이지 나타내는 전체 페이지 크기
	// 만약 pageSize가 5라면 1~5, 6~10 이렇게 보임

	// 아래 5가지는 값을 직접 바꿔줘야하는 필드
	// 가장 첫페이지는 당연히 1페이지 -> 그래서 minPage는 따로없음
	// minPage는 가장 첫번째 페이지를 의미
	private int maxPage; // 마지막 페이지 번호
	private int startPage; // 보여지는 맨 앞 페이지 번호
	private int endPage; // 보여지는 맨 뒤 페이지 번호
	// startPage, endPage : < or > 버튼 클릭 시, 시작과 끝을 의미 
	// 1~10이면 startPage = 1, endPage = 10
	
	private int prevPage; // 이전 페이지 모음의 마지막 번호
	private int nextPage; // 다음 페이지 모음의 시작 번호
	// < or >는 다음페이지의 첫번째 페이지로 가야함
	// 만약 1~10페이지에서 >를 누르면 -> 11~20 페이지 중 11페이지 값이 보여야 함
	// 만약 현재 21~30이면
	// < 눌렀을 때  -> 20번 페이지
	// > 눌렀을 때 -> 31번 페이지
}
  • 기본 생성자 X
  • 2개, 4개 생성자
    public Pagination(int currentPage, int listCount) {
    		this.currentPage = currentPage;
    		this.listCount = listCount;
    
    		calculate();
    	}
    
    	// 4개짜리 생성자(currentPage, listCount, limit, pageSize)
    	public Pagination(int currentPage, int listCount, int limit, int pageSize) {
    		this.currentPage = currentPage;
    		this.listCount = listCount;
    		this.limit = limit;
    		this.pageSize = pageSize;
    
    		calculate();
    	}
  • getter는 전부 만들기
  • setter는 currentPage, listCount, limit, pageSize

calculate()

페이징 처리에 필요한 값을 계산해서 필드에 대입하는 메서드

⇒ (startPage, endPage, maxPage, prevPage, nextPage)

  • 각 2개, 4개의 생성자 및 setter에 calculate() 메서드 호출
    • 값이 바뀔 때마다 다시 계산해줘야 하기 때문
/**
	 * 페이징 처리에 필요한 값을 계산해서 필드에 대입하는 메서드 (startPage, endPage, maxPage, prevPage,
	 * nextPage)
	 */
	private void calculate() {
		// 필드 중 limit을 10이라고 가정
		// 만약 게시글이 240개면 maxPage는 24
		// 만약 게시글이 241개면 maxPage는 25

		// 1. maxPage : 최대 페이지 == 즉, 마지막 페이지를 의미하는 것 == 총 페이지 수를 나타냄

		// 한 페이지에 게시글이 10개(limit)씩 보여질 경우
		// 게시글 수 = 95개라면 -> 10페이지가 나와야 함
		// 게시글 수 = 100개라면 -> 10페이지가 나와야 함
		// 게시글 수 = 101개라면 -> 11페이지
		maxPage = (int) Math.ceil((double) listCount / limit);
		// 만약 95 / 10 -> 9
		// (double) 95 / 10 -> 9.5
		// ceil이면 10.0 -> int로 바꾸면 10
		// maxPage는 10

		// 2. startPage : 페이지 번호 목록의 시작 번호
		// 페이지 번호 목록이 10개(pageSize)씩 보여지는 경우
		// 21~30페이지가 있다면 21, 시작 페이지를 구하겠다는 의미

		// 현재 페이지가 1 ~ 10 라면 : 1페이지
		// 현재 페이지가 11 ~ 20 라면 : 11페이지

		startPage = (currentPage - 1) / pageSize * pageSize + 1;
		// 만약 현재 5페이지에 있다면
		// (5-1) / (pageSize)10 * 10 + 1 => 1
		// 시작 페이지 1

		// 만약 10페이지라면
		// 0 * 0 + 1 => 1

		// 만약 11페이지라면
		// 1 * 10 + 1 => 11

		// 3. endPage : 페이지 번호 목록의 마지막 번호
		endPage = pageSize - 1 + startPage;
		// 만약 15페이지에 있다면
		// 9 + 11 = 20

		// 페이지 끝 번호가 최대 페이지 수를 초과한 경우

		if (endPage > maxPage) {
			// 만약 endPage가 70이고, maxPage가 68이라면
			// endPage는 유효한 게시판 값들이 존재하는 maxPage로 대체해줘야 한다.
			endPage = maxPage;
		}

		// 4. prevPage
		// prevPage : "<" 클릭 시 이동할 페이지 번호
		// (이전 레벨 번호 목록 중 마지막 번호)

		if (currentPage <= pageSize) {
			// 더 이상 이전으로 갈 페이지가 없을 경우
			prevPage = 1;
		} else {
			prevPage = startPage - 1;
		}

		// 5. nextPage
		// nextPage : ">" 클릭 시 이동할 페이지 번호
		// (다음 레벨 번호 목록 중 시작 번호)
		// 레벨은 10페이지씩 넘어가는 것을 의미

		if (endPage == maxPage) {
			// 더 이상 다음으로 넘어갈 페이지가 없을 경우
			nextPage = maxPage;
		} else {
			nextPage = endPage + 1;
		}
	}

MyBatis가 제공하는 ROWBOUNDS 객체

⇒ 지정된 크기만큼 건너 뛰고(offset) 제한된 크기만큼(limit)의 행을 조회하는 객체

RowBounds

int limit = pagination.getLimit(); // 10개
		int offset = (cp - 1) * limit;
		// offset만큼 건너 뛰고 가져오겠다는 의미
		// 만약 2페이지라면 -> (2 - 1) * 10 -> 10
		// -> 10개 건너 뛰고 가져오겠다는 의미
	RowBounds rowBounds = new RowBounds(offset, limit);
		

게시판 종류에 맞는 게시글 가져오기

  • 작성일
    • 초 ( ~초 전)
    • 분 (~분 전)
    • 시간 (~시간 전)
    • 24시간 지나면 일자 출력 (2025-12-15)
-- 번호 / 제목[댓글개수] / 작성자닉네임 / 작성일 / 조회수 / 좋아요갯수
SELECT BOARD_NO, BOARD_TITLE, MEMBER_NICKNAME, READ_COUNT, 
(SELECT COUNT(*)
FROM "COMMENT" C 
WHERE C.BOARD_NO = B.BOARD_NO) COMMENT_COUNT, -- 상관쿼리(댓글개수), 
(SELECT COUNT(*)
FROM "BOARD_LIKE" L
WHERE L.BOARD_NO = B.BOARD_NO) LIKE_COUNT,
CASE 
WHEN SYSDATE - BOARD_WRITE_DATE < 1 / 24 / 60
THEN FLOOR((SYSDATE - BOARD_WRITE_DATE) * 24 * 60 * 60) || '초 전'	
WHEN SYSDATE - BOARD_WRITE_DATE < 1 / 24
THEN FLOOR((SYSDATE - BOARD_WRITE_DATE) * 24 * 60) || '분 전'	
WHEN SYSDATE - BOARD_WRITE_DATE < 1
THEN FLOOR((SYSDATE - BOARD_WRITE_DATE) * 24) || '시간 전'	
ELSE TO_CHAR(BOARD_WRITE_DATE, 'YYYY-MM-DD')
END  -- 작성일을 조건에 따라 쓰기
FROM "BOARD" B
JOIN "MEMBER" USING(MEMBER_NO)
WHERE BOARD_DEL_FL = 'N'
ORDER BY BOARD_NO DESC;

CDATA (XML에서 쿼리를 짤 때, CASE문에서 < 에러 발생시 해결하는 것)

<!-- 
    	<![CDATA["문자열"]]>
    	해당 태그 내부에 작성된 문자열은 태그와 같은 특수기호로 해석하지 말고
    	문자 그대로 인식하라는 태그/명령 (순수 문자 데이터임을 지정해주는 태그)
     -->
	<select id="selectBoardList">
		SELECT BOARD_NO, BOARD_TITLE, MEMBER_NICKNAME,
		READ_COUNT,
		(SELECT COUNT(*)
		FROM "COMMENT" C
		WHERE C.BOARD_NO = B.BOARD_NO) COMMENT_COUNT, -- 상관쿼리(댓글개수),
		(SELECT COUNT(*)
		FROM "BOARD_LIKE" L
		WHERE L.BOARD_NO = B.BOARD_NO) LIKE_COUNT,
		<![CDATA[
		CASE
		WHEN SYSDATE - BOARD_WRITE_DATE < 1 / 24 / 60
		THEN FLOOR((SYSDATE - BOARD_WRITE_DATE) * 24 * 60 * 60) || '초 전'
		WHEN SYSDATE - BOARD_WRITE_DATE < 1 / 24
		THEN FLOOR((SYSDATE - BOARD_WRITE_DATE) * 24 * 60) || '분 전'
		WHEN SYSDATE - BOARD_WRITE_DATE < 1
		THEN FLOOR((SYSDATE - BOARD_WRITE_DATE) * 24) || '시간 전'
		ELSE TO_CHAR(BOARD_WRITE_DATE, 'YYYY-MM-DD')
		END BOARD_WRITE_DATE
		]]>
		FROM "BOARD" B
		JOIN "MEMBER" USING(MEMBER_NO)
		WHERE BOARD_DEL_FL = 'N' AND BOARD_CODE = #{boardCode}
		ORDER BY BOARD_NO DESC
	</select>

타임리프 - #lists

<!-- #lists : 타임리프에서 list 관련 기능을 제공하는 객체 -->
<!-- ${#lists.size(boardList) == 0} -->
<th:block th:if="${#lists.isEmpty(boardList)}">
		<tr>
			<th colspan="6">게시글이 존재하지 않습니다.</th>
		</tr>
</th:block>

0개의 댓글