Spring Boot Board Project_18 게시판 테이블 및 목록 조회

송지윤·2024년 4월 22일
0

Spring Framework

목록 보기
51/65
/* 게시판 테이블 생성 */
CREATE TABLE "BOARD" (
	"BOARD_NO"	NUMBER		NOT NULL,
	"BOARD_TITLE"	NVARCHAR2(100)		NOT NULL,
	"BOARD_CONTENT"	VARCHAR2(4000)		NOT NULL,
	"BOARD_WRITE_DATE"	DATE	DEFAULT SYSDATE	NOT NULL,
	"BOARD_UPDATE_DATE"	DATE		NULL,
	"READ_COUNT"	NUMBER	DEFAULT 0	NOT NULL,
	"BOARD_DEL_FL"	CHAR(1)	DEFAULT 'N'	NOT NULL,
	"BOARD_CODE"	NUMBER		NOT NULL,
	"MEMBER_NO"	NUMBER		NOT NULL
);

COMMENT ON COLUMN "BOARD"."BOARD_NO" IS '게시글 번호(PK)';

COMMENT ON COLUMN "BOARD"."BOARD_TITLE" IS '게시글 제목';

COMMENT ON COLUMN "BOARD"."BOARD_CONTENT" IS '게시글 내용';

COMMENT ON COLUMN "BOARD"."BOARD_WRITE_DATE" IS '게시글 작성일';

COMMENT ON COLUMN "BOARD"."BOARD_UPDATE_DATE" IS '게시글 마지막 수정일';

COMMENT ON COLUMN "BOARD"."READ_COUNT" IS '조회수';

COMMENT ON COLUMN "BOARD"."BOARD_DEL_FL" IS '게시글 삭제 여부(Y/N)';

COMMENT ON COLUMN "BOARD"."BOARD_CODE" IS '게시판 종류 코드 번호';

COMMENT ON COLUMN "BOARD"."MEMBER_NO" IS '작성한 회원 번호(FK)';


/* 게시판 종류 테이블 생성 */
CREATE TABLE "BOARD_TYPE" (
	"BOARD_CODE"	NUMBER		NOT NULL,
	"BOARD_NAME"	NVARCHAR2(20)		NOT NULL
);

COMMENT ON COLUMN "BOARD_TYPE"."BOARD_CODE" IS '게시판 종류 코드 번호';

COMMENT ON COLUMN "BOARD_TYPE"."BOARD_NAME" IS '게시판명';


/* 게시글 좋아요 테이블 생성 */
CREATE TABLE "BOARD_LIKE" (
	"MEMBER_NO"	NUMBER		NOT NULL,
	"BOARD_NO"	NUMBER		NOT NULL
);

COMMENT ON COLUMN "BOARD_LIKE"."MEMBER_NO" IS '회원 번호(PK)';

COMMENT ON COLUMN "BOARD_LIKE"."BOARD_NO" IS '게시글 번호(PK)';


/* 게시글 이미지 테이블 생성 */
CREATE TABLE "BOARD_IMG" (
	"IMG_NO"	NUMBER		NOT NULL,
	"IMG_PATH"	VARCHAR2(200)		NOT NULL,
	"IMG_ORIGINAL_NAME"	NVARCHAR2(50)		NOT NULL,
	"IMG_RENAME"	NVARCHAR2(50)		NOT NULL,
	"IMG_ORDER"	NUMBER		NULL,
	"BOARD_NO"	NUMBER		NOT NULL
);


COMMENT ON COLUMN "BOARD_IMG"."IMG_NO" IS '이미지 번호(PK)';

COMMENT ON COLUMN "BOARD_IMG"."IMG_PATH" IS '이미지 요청 경로';

COMMENT ON COLUMN "BOARD_IMG"."IMG_ORIGINAL_NAME" IS '이미지 원본명';

COMMENT ON COLUMN "BOARD_IMG"."IMG_RENAME" IS '이미지 변경명';

COMMENT ON COLUMN "BOARD_IMG"."IMG_ORDER" IS '이미지 순서';

COMMENT ON COLUMN "BOARD_IMG"."BOARD_NO" IS '게시글 번호(PK)';

/* 댓글 테이블 생성 */
CREATE TABLE "COMMENT" (
	"COMMENT_NO"	NUMBER		NOT NULL,
	"COMMENT_CONTENT"	VARCHAR2(4000)		NOT NULL,
	"COMMENT_WRITE_DATE"	DATE	DEFAULT SYSDATE	NOT NULL,
	"COMMENT_DEL_FL"	CHAR(1)	DEFAULT 'N'	NOT NULL,
	"BOARD_NO"	NUMBER		NOT NULL,
	"MEMBER_NO"	NUMBER		NOT NULL,
	"PARENT_COMMENT_NO"	NUMBER		NOT NULL
);

COMMENT ON COLUMN "COMMENT"."COMMENT_NO" IS '댓글 번호(PK)';

COMMENT ON COLUMN "COMMENT"."COMMENT_CONTENT" IS '댓글 내용';

COMMENT ON COLUMN "COMMENT"."COMMENT_WRITE_DATE" IS '댓글 작성일';

COMMENT ON COLUMN "COMMENT"."COMMENT_DEL_FL" IS '댓글 삭제 여부(Y/N)';

COMMENT ON COLUMN "COMMENT"."BOARD_NO" IS '게시글 번호(PK)';

COMMENT ON COLUMN "COMMENT"."MEMBER_NO" IS '회원 번호(PK)';

COMMENT ON COLUMN "COMMENT"."PARENT_COMMENT_NO" IS '부모 댓글 번호';


--------------------- PK -----------------------

ALTER TABLE "BOARD" ADD CONSTRAINT "PK_BOARD" PRIMARY KEY (
	"BOARD_NO"
);

ALTER TABLE "BOARD_TYPE" ADD CONSTRAINT "PK_BOARD_TYPE" PRIMARY KEY (
	"BOARD_CODE"
);

ALTER TABLE "BOARD_LIKE" ADD CONSTRAINT "PK_BOARD_LIKE" PRIMARY KEY (
	"MEMBER_NO",
	"BOARD_NO"
);

ALTER TABLE "BOARD_IMG" ADD CONSTRAINT "PK_BOARD_IMG" PRIMARY KEY (
	"IMG_NO"
);

ALTER TABLE "COMMENT" ADD CONSTRAINT "PK_COMMENT" PRIMARY KEY (
	"COMMENT_NO"
);

-------------------- FK -------------------------

ALTER TABLE "BOARD" ADD CONSTRAINT "FK_BOARD_TYPE_TO_BOARD_1" FOREIGN KEY (
	"BOARD_CODE"
)
REFERENCES "BOARD_TYPE" (
	"BOARD_CODE"
);



ALTER TABLE "BOARD" ADD CONSTRAINT "FK_MEMBER_TO_BOARD_1" FOREIGN KEY (
	"MEMBER_NO"
)
REFERENCES "MEMBER" (
	"MEMBER_NO"
);



ALTER TABLE "BOARD_LIKE" ADD CONSTRAINT "FK_MEMBER_TO_BOARD_LIKE_1" FOREIGN KEY (
	"MEMBER_NO"
)
REFERENCES "MEMBER" (
	"MEMBER_NO"
);



ALTER TABLE "BOARD_LIKE" ADD CONSTRAINT "FK_BOARD_TO_BOARD_LIKE_1" FOREIGN KEY (
	"BOARD_NO"
)
REFERENCES "BOARD" (
	"BOARD_NO"
);


ALTER TABLE "BOARD_IMG" ADD CONSTRAINT "FK_BOARD_TO_BOARD_IMG_1" FOREIGN KEY (
	"BOARD_NO"
)
REFERENCES "BOARD" (
	"BOARD_NO"
);


ALTER TABLE "COMMENT" ADD CONSTRAINT "FK_BOARD_TO_COMMENT_1" FOREIGN KEY (
	"BOARD_NO"
)
REFERENCES "BOARD" (
	"BOARD_NO"
);


ALTER TABLE "COMMENT" ADD CONSTRAINT "FK_MEMBER_TO_COMMENT_1" FOREIGN KEY (
	"MEMBER_NO"
)
REFERENCES "MEMBER" (
	"MEMBER_NO"
);


ALTER TABLE "COMMENT" ADD CONSTRAINT "FK_COMMENT_TO_COMMENT_1" FOREIGN KEY (
	"PARENT_COMMENT_NO"
)
REFERENCES "COMMENT" (
	"COMMENT_NO"
);


---------------------- CHECK -----------------------

-- 게시글 삭제 여부
ALTER TABLE "BOARD" ADD
CONSTRAINT "BOARD_DEL_CHECK"
CHECK("BOARD_DEL_FL" IN ('Y', 'N') );

-- 댓글 삭제 여부
ALTER TABLE "COMMENT" ADD
CONSTRAINT "COMMENT_DEL_CHECK"
CHECK("COMMENT_DEL_FL" IN ('Y', 'N') );


------------------------------------------------------

/* 게시판 종류(BOARD_TYPE) 추가 */
CREATE SEQUENCE SEQ_BOARD_CODE NOCACHE;

INSERT INTO "BOARD_TYPE" VALUES(SEQ_BOARD_CODE.NEXTVAL, '공지 게시판');
INSERT INTO "BOARD_TYPE" VALUES(SEQ_BOARD_CODE.NEXTVAL, '정보 게시판');
INSERT INTO "BOARD_TYPE" VALUES(SEQ_BOARD_CODE.NEXTVAL, '자유 게시판');

COMMIT;

SELECT * FROM BOARD_TYPE;

-------------------------------------------------------
/* 게시글 번호 시퀀스 생성 */
CREATE SEQUENCE SEQ_BOARD_NO NOCACHE;


/* 게시판(BOARD) 테이블 샘플 데이터 삽입(PL/SQL)*/


SELECT * FROM "MEMBER";

-- DBMS_RANDOM.VALUE(0,3) : 0.0 이상, 3.0 미만의 난수
-- CEIL( DBMS_RANDOM.VALUE(0,3) ) : 1,2,3 중 하나

-- ALT + X 로 실행
BEGIN
	FOR I IN 1..2000 LOOP
		
		INSERT INTO "BOARD"
		VALUES(SEQ_BOARD_NO.NEXTVAL,
					 SEQ_BOARD_NO.CURRVAL || '번째 게시글',
					 SEQ_BOARD_NO.CURRVAL || '번째 게시글 내용 입니다',
					 DEFAULT, DEFAULT, DEFAULT, DEFAULT,
					 CEIL( DBMS_RANDOM.VALUE(0,3) ), -- BOARD_CODE(게시판종류)
					 1 -- MEMBER_NO(작성회원번호)
		);
		
	END LOOP;
END;

COMMIT;

-- 게시판 종류별 샘플 데이터 삽입 확인
SELECT BOARD_CODE, COUNT(*)
FROM "BOARD"
GROUP BY BOARD_CODE
ORDER BY BOARD_CODE;

---------------------------------------------------
-- 부모 댓글 번호 NULL 허용
ALTER TABLE "COMMENT" 
MODIFY PARENT_COMMENT_NO NUMBER NULL;


/* 댓글 번호 시퀀스 생성 */
CREATE SEQUENCE SEQ_COMMENT_NO NOCACHE;



/* 댓글 ("COMMNET") 테이블에 샘플 데이터 추가*/

BEGIN
	FOR I IN 1..2000 LOOP
	
		INSERT INTO "COMMENT"	
		VALUES(
			SEQ_COMMENT_NO.NEXTVAL,
			SEQ_COMMENT_NO.CURRVAL || '번째 댓글 입니다',
			DEFAULT, DEFAULT,
			CEIL( DBMS_RANDOM.VALUE(0, 2000) ), -- 게시글번호
			2, -- 댓글작성회원번호
			NULL -- 부모댓글번호
		);
	END LOOP;
END;


COMMIT;

-- 게시글 번호 최소값, 최대값
SELECT MIN(BOARD_NO), MAX(BOARD_NO) FROM "BOARD";

-- 댓글 삽입 확인
SELECT BOARD_NO, COUNT(*) 
FROM "COMMENT"
GROUP BY BOARD_NO
ORDER BY BOARD_NO;

-- 댓글 총 개수 확인
SELECT COUNT(*) FROM "COMMENT";

ERDCloud 로 테이블 생성

Interceptor

Filter 와 작동되는 부분이 다름

Handler Mapping == GetMapping, PostMapping 등등
Dispatcher Servlet 이 Handler Mapping 한테 맞는 거 찾아달라고 요청하면 Handler Mapping이 찾아서 어떤 Controller 가 처리할지 결정해서 넘겨줌

응답을 Controller 가 가지고 돌아와서 Dispatcher Servlet 이 forward
View Resolver 가 View 보여줌 그 흐름을 catch 하는 애 Interceptor

Client <-> Filter -> Dispatcher Servlet <-> Interceptor <-> Controller

게시판 목록도 DB 에 저장해뒀다가 보여줌
늘어날 때마다 작성하는 게 아님
interceptor 이용해서 보여줄거임

header.html

  <nav>
    <ul>
      <li>
        <a href="#">공지사항</a>
      </li>
      <li>
        <a href="#">자유 게시판</a>
      </li>
      <li>
        <a href="#">질문 게시판</a>
      </li>
    </ul>
  </nav>

application scope 에 올려둘거임 URL 에 /board/1 이렇게 작성해도 어디에서든 접근 가능하게

1. Interceptor 사용하기 위해 BoardTypeInterceptor 클래스 생성

HandlerInterceptor 인터페이스 상속 받아서 사용
alt + shift + s -> Override

주석만 지우고 사용
나머지는 지우면 작동 안됨

Interceptor : 요청/응답 가로채는 객체 (Spring 지원)

HandlerInterceptor 인터페이스를 상속 받아서 구현해야한다.

  • preHandle (전처리) : Dispatcher Servlet -> Controller 사이 수행
  • postHandle (후처리) : Controller -> Dispatcher Servlet 사이 수행
  • afterCompletion (뷰 완성(forward 코드 해석) 후) : View Resolver -> Dispatcher Servlet 사이 수행

BoardTypeInterceptor 클래스

@Slf4j
public class BoardTypeInterceptor implements HandlerInterceptor {

	// BoardService 의존성 주입
	@Autowired
	private BoardService service;

필드명에 @Autowirde 붙여서 의존성 주입하는 이유

config 파일에서 BoardTypeInterceptor 를 new 연산자로 생성하여 Bean 등록 해줘야해서

@RequiredArgsContstructor 이거 생성하면 매개변수 생성자밖에 못 만듦

-> @RequiredArgsContstructor 안 쓸거임

	// 전처리
	@Override
	public boolean preHandle(HttpServletRequest request,
							HttpServletResponse response,
							Object handler)
			throws Exception {
		
		// application scope : 
		// - 서버 종료 시까지 유지되는 Servlet 내장 객체
		// - 서버 내에 딱 한개만 존재
		// -> 모든 클라이언트가 공용으로 사용
		
		// application scope == ServletContext 객체 얻어오기
		ServletContext application = request.getServletContext();
		
		// application scope 에 "boardTypeList" 가 없을 경우
		if(application.getAttribute("boardTypeList") == null) {
			
			log.info("BoardTypeInterceptor - preHandle(전처리) 동작 실행");

print 구문 같은 거
log.info("BoardTypeInterceptor - preHandle(전처리) 동작 실행");


			// boardTypeList 조회 서비스 호출
			List<Map<String, Object>> boardTypeList = service.selectBoardTypeList();
			
			// 조회 결과를 application scope 에 추가
			application.setAttribute("boardTypeList", boardTypeList);
		}
		
		return HandlerInterceptor.super.preHandle(request, response, handler);
	}

interceptor 해서 Controller 거치는 게 아니라 Service 로 바로 가서 sql 조회
Controller 거치지 않고 감
바로 Service 생성해서 호출할 거임

board-mapper.xml

resultType 미작성 할 수 있는 조건

  • mapper 메서드의 반환형이 별칭으로 등록되어 있을 때
    java 에서 기본적으로 사용하는 Type 들은 별칭으로 등록되어 있음
    DTO 는 DBConfig 에 별칭 등록 해뒀음 edu.kh.project


	<!-- 게시판 종류 조회 -->
	<select id="selectBoardTypeList">
		SELECT BOARD_CODE "boardCode", BOARD_NAME "boardName"
		FROM BOARD_TYPE
		ORDER BY BOARD_CODE
	</select>

SQL 문에 별칭을 작성한 이유

컬럼명 == 필드명
카멜케이스 == 언더바케이스
이건 DTO 로 가져올 때만 자동으로 연결

Map 은 안됨

interceptor 사용하기 위한 설정 파일 InterceptorConfig

BoardTypeInterceptor 가 어디서 어떻게 쓰일지 어디랑 연결할지 설정해줘야함

interceptorConfig 클래스

package edu.kh.project.common.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import edu.kh.project.common.interceptor.BoardTypeInterceptor;

// 인터셉터가 어떤 요청을 가로챌지 설정하는 클래스
@Configuration // 서버가 켜지면 내부 메서드를 모두 수행
public class InterceptorConfig implements WebMvcConfigurer {

	// 인터셉터 클래스 Bean 등록
	
	@Bean // 개발자가 만들어서 반환하는 객체를 Bean 등록 + Spring Container 가 관리
	public BoardTypeInterceptor boardTypeInterceptor() {
		return new BoardTypeInterceptor();
		// new 연산자 통해서 기본생성자 사용하기 때문에
	}
	
	// WebMvcConfigurer 상속 받고 난 후 ctrl + space
	// 동작할 인터셉터 객체를 추가하는 메서드
	@Override
	public void addInterceptors(InterceptorRegistry registry) {
	
		// Bean 으로 등록된 BoardTypeInterceptor 를 얻어와서 매개변수로 전달
		registry.addInterceptor(boardTypeInterceptor())
		.addPathPatterns("/**") // 가로챌 요청 주소를 지정
								// /** : / 이하 모든 요청 주소
		// 가로채지 않을 주소를 지정
		.excludePathPatterns("/css/**",
					"/js/**",
					"/images/**",
					"/favicon.ico");
	}
}

어디서 interceptor 사용할지 연결해준 후 application scope 에 세팅된 boardTypeList header 에 보여주기

header.html

  <nav>
    <ul>
      <th:block th:each="boardType : ${application.boardTypeList}">
        <li>
          <a th:text="${boardType.boardName}"
          th:href="@{/board/{boardCode}(boardCode=${boardType.boardCode})}">게시판 이름</a>
        </li>
      </th:block>
    </ul>
  </nav>

특정 게시글 목록 가져오기

1. 게시판 눌렀을 때 받아줄 Controller 생성

공지게시판 정보게시판 자유게시판 누를 때마다 넘어가는 boardCode 가 다름

BoardController

package edu.kh.project.board.controller;

import java.util.Map;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

import edu.kh.project.board.model.service.BoardService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Controller
@RequestMapping("board")
@Slf4j
@RequiredArgsConstructor
public class BoardController {

	private final BoardService service;
	
    /** 게시글 목록 조회
	 * @param boardCode : 게시판 종류 구분 번호
	 * @param cp : 현재 조회 요청한 페이지 (없으면 1) current page
	 * @return
	 * 
	 * - /board/xxx
	 * /board 이하 1레벨 자리에 숫자로된 요청 주소가
	 *  작성되어 있을 때만 동작 -> 정규표현식 이용
	 * 
	 * [0-9] : 한 칸에 0~9 사이 숫자 입력 가능
	 * + : 하나 이상
	 * 
	 * [0-9]+ : 모든 숫자
	 */
	@GetMapping("{boardCode:[0-9]+}")
	public String selectBoardList(@PathVariable("boardCode") int boardCode,
			@RequestParam(value="cp", required=false, defaultValue = "1") int cp,
			Model model) {
		
		log.debug("boardCode : " + boardCode);
		
		// 조회 서비스 호출 후 결과 반환
		Map<String, Object> map = service.selectBoardList(boardCode,cp);
		
		return "board/boardList"; // boardList.html 로 forward
	}
}

/board/xxx
/board 이하 1레벨 자리에 숫자로된 요청 주소가
작성되어 있을 때만 동작 -> 정규표현식 이용

[0-9] : 한 칸에 0~9 사이 숫자 입력 가능
+ : 하나 이상

[0-9]+ : 모든 숫자

@PathVariable

  • 주소 중 일부분을 변수 값처럼 사용
  • 해당 어노테이션으로 얻어온 값은 request scope 에 세팅

ex)

	@GetMapping("ex3/{number}")
	public String pathVariableTest(
			@PathVariable("number") int number) {
			// 주소 중 {number} 부분의 값을 가져와 매개변수에 저장
			// + request scope 에 세팅
		
		log.debug("number : " + number);
		// log 에 number : 1 / number : 2 / number : 3 이런 식으로 찍힘
		// html 에서 넘겨줄 때 값 a 태그 안 href 에 /example/ex3/1 2 3 이런식
		
		return "example/testResult";
	}

Pagination 클래스

필드 생성

기본생성자 X
매개변수 생성자

Getter 만 만들기

Setter 4개만 만들기

toString

Setter 마다 다 다시 계산해야됨 calculate(); // 필드 계산 메서드 호출

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

/* Pagination 뜻 : 목록을 일정 페이지로 분할해서
 *                 원하는 페이지를 볼 수 있게 하는 것
 *                 == 페이징 처리
 *                 
 * Pagination 객체 : 페이징 처리에 필요한 값을 모아두고, 계산하는 객체
 * */
/**
 * 
 */
public class Pagination {

	private int currentPage;		// 현재 페이지 번호 cp
	private int listCount;			// 전체 게시글 수
	
	private int limit = 10;			// 한 페이지 목록에 보여지는 게시글 수
	private int pageSize = 10;		// 보여질 페이지 번호 개수
	
	private int maxPage;			// 마지막 페이지 번호
	private int startPage;			// 보여지는 맨 앞 페이지 번호
	private int endPage;			// 보여지는 맨 뒤 페이지 번호
	
	private int prevPage;			// 이전 페이지 모음의 마지막 번호
	private int nextPage;			// 다음 페이지 모음의 시작 번호
	
	// lombok 사용 X

	// 기본생성자 X
	public Pagination(int currentPage, int listCount) {
		super();
		this.currentPage = currentPage;
		this.listCount = listCount;
		
		calculate(); // 필드 계산 메서드 호출
	}

	public Pagination(int currentPage, int listCount, int limit, int pageSize) {
		super();
		this.currentPage = currentPage;
		this.listCount = listCount;
		this.limit = limit;
		this.pageSize = pageSize;
		
		calculate(); // 필드 계산 메서드 호출
	}

	public int getCurrentPage() {
		return currentPage;
	}

	public int getListCount() {
		return listCount;
	}

	public int getLimit() {
		return limit;
	}

	public int getPageSize() {
		return pageSize;
	}

	public int getMaxPage() {
		return maxPage;
	}

	public int getStartPage() {
		return startPage;
	}

	public int getEndPage() {
		return endPage;
	}

	public int getPrevPage() {
		return prevPage;
	}

	public int getNextPage() {
		return nextPage;
	}

	public void setCurrentPage(int currentPage) {
		this.currentPage = currentPage;
		calculate(); // 필드 계산 메서드 호출
	}

	public void setListCount(int listCount) {
		this.listCount = listCount;
		calculate(); // 필드 계산 메서드 호출
	}

	public void setLimit(int limit) {
		this.limit = limit;
		calculate(); // 필드 계산 메서드 호출
	}

	public void setPageSize(int pageSize) {
		this.pageSize = pageSize;
		calculate(); // 필드 계산 메서드 호출
	}

	@Override
	public String toString() {
		return "Pagination [currentPage=" + currentPage + ", listCount=" + listCount + ", limit=" + limit
				+ ", pageSize=" + pageSize + ", maxPage=" + maxPage + ", startPage=" + startPage + ", endPage="
				+ endPage + ", prevPage=" + prevPage + ", nextPage=" + nextPage + "]";
	}
	
	/** 페이징 처리에 필요한 값을 계산해서
	 *  필드에 대입하는 메서드
	 *  (maxPage, startPage, endPage, prevPage, nextPage)
	 */
	private void calculate() {
		
		// maxPage : 최대 페이지 == 마지막 페이지 == 총 페이지 수
		
		// 한 페이지에 게시글이 10개씩 보여질 경우
		// 게시글 수 :  95개 -> 10 page
		// 게시글 수 : 100개 -> 10 page
		// 게시글 수 : 101개 -> 11 page
		
		maxPage = (int)Math.ceil((double)listCount/limit);
		
		// startPage : 페이지 번호 목록의 시작 번호
		
		// 페이지 번호 목록이 10개(pageSize) 씩 보여질 경우
		
		// 현재 페이지가  1 ~ 10 :  1 page 시작 java int 형이라서 0.xx 는 0
		// 현재 페이지가 11 ~ 20 : 11 page 시작
		
		startPage = (currentPage - 1) / pageSize * pageSize + 1;
		
		// endPage : 페이지 번호 목록의 끝 번호
		// 현재 페이지가  1 ~ 10 : 10 page
		// 현재 페이지가 11 ~ 20 : 20 page
		// 현재 페이지가 21 ~ 30 : 30 page
		
		endPage = pageSize - 1 + startPage;
		
		// 페이지 끝 번호가 최대 페이지 수를 초과한 경우
		if(endPage > maxPage) endPage = maxPage;
		
		// prevPage : "<" 클릭 시 이동할 페이지 번호
		//            (이전 페이지 번호 목록 중 끝 번호)
		
		// 더 이상 이전으로 갈 페이지가 없을 경우
		if(currentPage < pageSize) {
			prevPage = 1;
		} else {
			prevPage = startPage - 1;
		}
		
		// nextPage : ">" 클릭 시 이동할 페이지 번호
		//            (다음 페이지 번호 목록 중 시작 번호)
		
		// 더 이상 넘어갈 페이지가 없을 경우
		if(endPage == maxPage) {
			nextPage = maxPage;
		} else { // 그 외 경우
			nextPage = endPage + 1;
		}
	}

}

Board 클래스 생성

2. ServiceImpl에서 받아서 mapper sql 수행 후 페이지 나눠주기

	// 특정 게시판에 지정된 페이지 목록 조회
	@Override
	public Map<String, Object> selectBoardList(int boardCode, int cp) {
		
		// 1. 지정된 게시판(boardCode)에서
		//    삭제되지 않은 게시글 수를 조회
		int listCount = mapper.getListCount(boardCode);
		
		// 2. 1번의 결과 + cp 를 이용해서
		//    Pagination 객체를 생성
		// * Pagination 객체 : 게시글 목록 구성에 필요한 값을 저장한 객체
		Pagination pagination = new Pagination(cp,listCount);
		
		// 3. 특정 게시판의 지정된 페이지 목록 조회
		/* ROWBOUNDS 객체 (Mybatis 제공 객체)
		 * - 지정된 크기(offset) 만큼 건너뛰고
		 *   제한된 크기(limit) 만큼의 행을 조회하는 객체
		 *   
		 * --> 페이징 처리가 괸장히 간단해짐
		 * */
		int limit = pagination.getLimit();
		int offset = (cp - 1) * limit;
		RowBounds rowBounds = new RowBounds(offset, limit);
		
		/* Mapper 메서드 호출 시
		 * - 첫 번째 매개변수 -> SQL에 전달할 파라미터
		 * - 두 번째 매개변수 -> RowBounds 객체만 전달 가능
		 * */
		List<Board> boardList = mapper.selectBoardList(boardCode, rowBounds);
		
		// 4. 목록 조회 결과 + Pagination 객체를 Map 으로 묶음
		Map<String, Object> map = new HashMap<>();
		
		map.put("pagination", pagination);
		map.put("boardList", boardList);
		
		// 5. 결과 반환
		return map;
	}

4. board-mapper.xml

	<!-- 
		<![CDATA["문자열"]]> - 해당 태그 내부에 작성된 문자열은 특수 기호로 해석하지 말고
		문자(Character) 그대로 인식하라고 명령하는 태그 (순수 문자 데이터임을 지정)
	 -->
	<!-- 특정 게시판의 지정된 페이지 목록 조회 -->
	<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>

5. Controller 로 돌아와서 응답해주기

	@GetMapping("{boardCode:[0-9]+}")
	public String selectBoardList(@PathVariable("boardCode") int boardCode,
			@RequestParam(value="cp", required=false, defaultValue = "1") int cp,
			Model model) {
		
		log.debug("boardCode : " + boardCode);
		
		// 조회 서비스 호출 후 결과 반환
		Map<String, Object> map = service.selectBoardList(boardCode,cp);
		
		model.addAttribute("pagination", map.get("pagination"));
		model.addAttribute("boardList", map.get("boardList"));
		
		return "board/boardList"; // boardList.html 로 forward
	}

6. HTML 화면 보여주기

			<th:block th:each="boardType : ${application.boardTypeList}">
				<h1 class="board-name" 
					th:if="${boardType.boardCode} == ${boardCode}"
					th:text="${boardType.boardName}"
				>게시판 이름</h1>
			</th:block>

			<div class="list-wrapper">
				<table class="list-table">

					<thead>
						<tr>
							<th>글번호</th>
							<th>제목</th>
							<th>작성자</th>
							<th>작성일</th>
							<th>조회수</th>
							<th>좋아요</th>
						</tr>
					</thead>

					<tbody>
						<!-- 게시글이 존재하지 않을 때 -->
						<!-- 여러 행 조회 시 결과가 없을 경우 == List 가 비어있음 -->
						<!-- #lists : 타임리프에서 list 관련 기능을 제공하는 객체 -->
						<!-- ${#lists.size(boardList) == 0} -->
						<th:block th:if="${#lists.isEmpty(boardList)}">
							<tr>
								<th colspan="6">게시글이 존재하지 않습니다.</th>
							</tr>
						</th:block>

						<!-- 게시글이 존재할 때 -->
						<th:block th:unless="${#lists.isEmpty(boardList)}">

							<tr th:each="board : ${boardList}" th:object="${board}">
									
								<td th:text="*{boardNo}">게시글 번호</td>
								
								<td>
									<!-- 썸네일 추가 예정 -->
	
									<a th:text="*{boardTitle}">게시글 제목</a>
	
									<th:block th:text="|[*{commentCount}]|">댓글 수</th:block>
								</td>
	
								<!-- 작성자 닉네임 -->
								<td th:text="*{memberNickname}">닉네임</td>
	
								<!-- 작성일 -->
								<td th:text="*{boardWriteDate}">2023-10-26</td>
	
								<!-- 조회수 -->
								<td th:text="*{readCount}">0</td>
	
								<!-- 좋아요 수 -->
								<td th:text="*{likeCount}">0</td>
	
							</tr>

						</th:block>


					</tbody>
				</table>
			</div>

페이지 네이션 설정 전 http://localhost/board/1?cp=10 이렇게 조회해볼 수 있음

7. html pagination 설정해주기

			<div class="pagination-area">

				<ul class="pagination" th:object="${pagination}">

					<!-- 첫 페이지로 이동 -->
					<!-- &lt; == < // /board/{boardCode}cp = 1 -->
					<li><a th:href="@{/board/{boardCode}(boardCode=${boardCode}, cp=1)}">&lt;&lt;</a></li>

					<!-- 이전 목록 마지막 번호로 이동 -->
					<li><a th:href="@{/board/{boardCode}(boardCode=${boardCode}, cp=*{prevPage})}">&lt;</a></li>

					<!-- 특정 페이지로 이동 -->
					<th:block th:each="i : *{#numbers.sequence(startPage, endPage)}">
						<!-- 현재 보고있는 페이지 -->
						<li th:if="${i} == *{currentPage}">
							<a class="current" th:text="${i}">현재페이지</a>
						</li>
	
						<!-- 보고있지 않은 페이지 -->
						<li th:unless="${i} == *{currentPage}">
							<a th:text="${i}"
							th:href="@{/board/{boardCode}(boardCode=${boardCode}, cp=${i})}">이동할 페이지</a>
						</li>
					</th:block>	
					
					<!-- 다음 목록 시작 번호로 이동 -->
					<li><a th:href="@{/board/{boardCode}(boardCode=${boardCode}, cp=*{nextPage})}">&gt;</a></li>

					<!-- 끝 페이지로 이동 -->
					<li><a th:href="@{/board/{boardCode}(boardCode=${boardCode}, cp=*{maxPage})}">&gt;&gt;</a></li>
				</ul>
			</div>

0개의 댓글