게시판 - 글 목록 조회 기능 구현 (23.08.21)

·2023년 8월 21일
0

Spring

목록 보기
18/36
post-thumbnail

🌷 게시글 목록 조회


이번 포스팅에서는 게시판 별 게시글 목록을 조회하는 기능을 구현해 볼 것이다.
구현하기에 앞서 @PathVariable의 개념에 대해 알아보자.

🌼 @PathVariable

URL 경로에 있는 값매개변수로 이용할 수 있게 하는 어노테이션

🍄 예시

	// 게시글 목록 조회
	@GetMapping("/{boardCode}")
	public String selectBoardList(@PathVariable("boardCode") int boardCode) {
		
		// boardCode 확인
		System.out.println("boardCode : " + boardCode);
		
		return "board/boardList";
	}

위와 같이 코드를 작성한 뒤 서버를 실행한다.
이후 nav의 게시판 이름을 누를 때마다 Eclipse의 콘솔창에 아래와 같이 boardCode가 찍히는 것을 볼 수 있다.

여기서 드는 궁금증 하나.
왜 쿼리 스트링을 쓰지 않는 걸까? 🤔

이유는 @PathVariableQuery String을 사용하는 경우가 서로 다르기 때문이다.


🌼 @PathVariable VS Query String

🌱 @PathVariable를 사용하는 경우

자원(resource)를 구분하거나 식별할 때

🍄 예시

https://velog.io/@suhyun_zip
https://velog.io/@testUser
/board/1 -> 1번(공지사항) 게시판
/board/2 -> 2번(자유) 게시판

🌱 Query String을 사용하는 경우

정렬 또는 필터링 할 때

🍄 예시 - 정렬

/board/list?order=recent (최신순)
/board/list?order=most (인기순)

🍄 예시 - 필터링

search.naver.com?query=날씨
search.naver.com?query=점심

/board2/insert?code=1
/board2/insert?code=2
-> 삽입이라는 공통된 동작 수행
   단, code에 따라 어디에 삽입할지 지정(필터링)

이처럼 게시글 목록을 조회할 때는 게시판 코드 번호(boardCode)에 따라 어떤 게시판인지 구분하는 과정이 필요하다. 때문에 @PathVariable를 사용하는 것이다!

이제 준비는 끝났다. 코드를 작성해 보자. 😉


👀 코드로 살펴보기

🌱 Oracle DBMS

🍄 BOARD, COMMENT, BOARD_IMG 샘플 데이터 삽입

-- BOARD 테이블 샘플 데이터 삽입(PL/SQL)
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, 1, 
              CEIL(DBMS_RANDOM.VALUE(0,5))
      );
   END LOOP;
END;
/

SELECT * FROM MEMBER
WHERE MEMBER_NO = 1;

SELECT * FROM BOARD_TYPE;

SELECT COUNT(*) FROM BOARD;

COMMIT;


-- BOARD_CODE가 1(공지사항)인 게시글을 최신순으로 조회
-- 단, 삭제된 글은 제외
SELECT * FROM BOARD
WHERE BOARD_CODE = 1
AND BOARD_DEL_FL = 'N'
ORDER BY BOARD_NO DESC;


-- COMMENT 테이블 샘플 데이터 삽입(PL/SQL)
BEGIN
   FOR I IN 1..1000 LOOP
      INSERT INTO "COMMENT" 
      VALUES(SEQ_COMMENT_NO.NEXTVAL, 
            SEQ_COMMENT_NO.CURRVAL || '번째 댓글',
            DEFAULT, DEFAULT,
             CEIL(DBMS_RANDOM.VALUE(0,2000)),
             1, NULL);
   END LOOP;
END;
/

COMMIT;


-- 게시글 샘플 이미지 삽입
INSERT INTO BOARD_IMG
VALUES(SEQ_IMG_NO.NEXTVAL, '/resources/images/board/',
        '20230821141913_00001.png', 'bear.png', 0, 1999);
INSERT INTO BOARD_IMG
VALUES(SEQ_IMG_NO.NEXTVAL, '/resources/images/board/',
        '20230821141913_00002.png', 'bear2.png', 0, 1988);
INSERT INTO BOARD_IMG
VALUES(SEQ_IMG_NO.NEXTVAL, '/resources/images/board/',
        '20230821141913_00003.jpg', 'bear3.jpg', 0, 1986);
INSERT INTO BOARD_IMG
VALUES(SEQ_IMG_NO.NEXTVAL, '/resources/images/board/',
        '20230821141913_00004.jpg', 'bear4.jpg', 0, 1976);
INSERT INTO BOARD_IMG
VALUES(SEQ_IMG_NO.NEXTVAL, '/resources/images/board/',
        '20230821141913_00005.png', 'bear5.png', 0, 1975);
        
COMMIT;

SELECT BOARD_NO FROM BOARD
WHERE BOARD_CODE = 1
ORDER BY 1 DESC;


-- 특정 게시판의 삭제되지 않은 게시글 수 조회
SELECT COUNT(*) FROM BOARD
WHERE BOARD_DEL_FL = 'N'
AND BOARD_CODE = 1;

-- 특정 게시판의 목록 조회
-- 1. 최신 순서
-- 2. 1 page(1~10행) 조회
-- 3. 삭제된 글은 제외

-- 마이바티스 O
--> RowBounds 객체 이용
SELECT BOARD_NO, BOARD_TITLE, MEMBER_NICKNAME,
    TO_CHAR(B_CREATE_DATE, 'YYYY-MM-DD') B_CREATE_DATE,
    READ_COUNT
FROM "BOARD"
JOIN "MEMBER" USING(MEMBER_NO)
WHERE BOARD_DEL_FL = 'N'
AND BOARD_CODE = 1
ORDER BY BOARD_NO DESC;

-- 마이바티스 X
-- 1~10행 조회
SELECT * FROM(
    SELECT A.* FROM(
        SELECT ROWNUM NUM, BOARD_NO, BOARD_TITLE, MEMBER_NICKNAME,
            TO_CHAR(B_CREATE_DATE, 'YYYY-MM-DD') B_CREATE_DATE,
            READ_COUNT
        FROM "BOARD"
        JOIN "MEMBER" USING(MEMBER_NO)
        WHERE BOARD_DEL_FL = 'N'
        AND BOARD_CODE = 1
        ORDER BY BOARD_NO DESC
    ) A
)
WHERE NUM BETWEEN 1 AND 10;

SELECT BOARD_NO, BOARD_TITLE, MEMBER_NICKNAME, READ_COUNT, 
         
            CASE  
               WHEN SYSDATE - B_CREATE_DATE < 1/24/60 -- 1분 미만
               THEN FLOOR( (SYSDATE - B_CREATE_DATE) * 24 * 60 * 60 ) || '초 전'
               WHEN SYSDATE - B_CREATE_DATE < 1/24 -- 1시간 미만
               THEN FLOOR( (SYSDATE - B_CREATE_DATE) * 24 * 60) || '분 전'
               WHEN SYSDATE - B_CREATE_DATE < 1 -- 1일 미만
               THEN FLOOR( (SYSDATE - B_CREATE_DATE) * 24) || '시간 전'
               ELSE TO_CHAR(B_CREATE_DATE, 'YYYY-MM-DD')
            END B_CREATE_DATE,
         (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,
          
         (SELECT IMG_PATH || IMG_RENAME FROM BOARD_IMG I
         WHERE I.BOARD_NO = B.BOARD_NO
         AND IMG_ORDER = 0) THUMBNAIL
      FROM "BOARD" B
      JOIN "MEMBER" USING(MEMBER_NO)
      WHERE BOARD_DEL_FL = 'N'
      AND BOARD_CODE = 1
      ORDER BY BOARD_NO DESC;

🍄 삽입된 샘플 데이터 확인

  • BOARD

샘플 데이터가 랜덤으로 2000개 생성되었다.

  • COMMENT

샘플 데이터가 랜덤으로 1000개 생성되었다.

  • BOARD_IMG


🌱 VS Code

🍄 boardList.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"  %>

<%-- map에 저장된 값들을 각각 변수에 저장 --%>
<c:set var="pagination" value="${map.pagination}"/>
<c:set var="boardList" value="${map.boardList}"/>

<%-- <c:set var="boardName" value="${boardTypeList[boardCode - 1].BOARD_NAME}"/> --%>

<c:forEach items="${boardTypeList}" var="boardType">
    <c:if test="${boardType.BOARD_CODE == boardCode}" >
        <c:set var="boardName" value="${boardType.BOARD_NAME}"/>
    </c:if>
</c:forEach>


<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>게시판 이름</title>

    <link rel="stylesheet" href="/resources/css/board/boardList-style.css">

</head>
<body>
    <main>
        <jsp:include page="/WEB-INF/views/common/header.jsp"/>

        <section class="board-list">

            <h1 class="board-name">${boardName}</h1>

            <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>

                    <c:choose>

                        <c:when test="${empty boardList}">
                            <%-- 조회된 게시글 목록이 비어 있거나 null인 경우 --%>
                            <!-- 게시글 목록 조회 결과가 비어있다면 -->
                            <tr>
                                <th colspan="6">게시글이 존재하지 않습니다.</th>
                            </tr>
                        </c:when>
                    
                        <c:otherwise>
                            <!-- 게시글 목록 조회 결과가 있다면 -->

                            <c:forEach items="${boardList}" var="board">
                                <tr>
                                    <td>${board.boardNo}</td>
                                    <td>

                                        <%-- 썸네일이 있을 경우 --%>
                                        <c:if test="${!empty board.thumbnail}" >
                                            <img class="list-thumbnail" src="${board.thumbnail}">
                                        </c:if>

                                        <a href="/board/${boardCode}/${board.boardNo}?cp=${pagination.currentPage}">${board.boardTitle}</a>   
                                        [${board.commentCount}]                        
                                    </td>
                                    <td>${board.memberNickname}</td>
                                    <td>${board.boardCreateDate}</td>
                                    <td>${board.readCount}</td>
                                    <td>${board.likeCount}</td>
                                </tr>
                            </c:forEach>

                        </c:otherwise>

                    </c:choose>

                    </tbody>

                </table>

            </div>

            <div class="btn-area">

            <!-- 로그인 상태일 경우 글쓰기 버튼 노출 -->
                <button id="insertBtn">글쓰기</button>                     

            </div>

            <div class="pagination-area">

                <ul class="pagination">
                
                    <!-- 첫 페이지로 이동 -->
                    <li><a href="/board/${boardCode}?cp=1">&lt;&lt;</a></li>

                    <!-- 이전 목록 마지막 번호로 이동 -->
                    <li><a href="/board/${boardCode}?cp=${pagination.prevPage}">&lt;</a></li>
               
                    <!-- 특정 페이지로 이동 -->
                    <c:forEach var="i" begin="${pagination.startPage}"
                            end="${pagination.endPage}" step="1">

                        <c:choose>
                            <c:when test="${i == pagination.currentPage}">
                                <!-- 현재 보고있는 페이지 -->
                                <li><a class="current">${i}</a></li>
                            </c:when>
                            
                            <c:otherwise>
                                <!-- 현재 페이지를 제외한 나머지 -->
                                <li><a href="/board/${boardCode}?cp=${i}">${i}</a></li>
                            </c:otherwise>
                        </c:choose>
                         
                    </c:forEach>
                    
                    <!-- 다음 목록 시작 번호로 이동 -->
                    <li><a href="/board/${boardCode}?cp=${pagination.nextPage}">&gt;</a></li>

                    <!-- 끝 페이지로 이동 -->
                    <li><a href="/board/${boardCode}?cp=${pagination.maxPage}">&gt;&gt;</a></li>

                </ul>

            </div>

         <!-- 검색창 -->
            <form action="#" method="get" id="boardSearch">

                <select name="key" id="searchKey">
                    <option value="t">제목</option>
                    <option value="c">내용</option>
                    <option value="tc">제목+내용</tion>
                    <option value="w">작성자</option>
                </select>

                <input type="text" name="query"  id="searchQuery" placeholder="검색어를 입력해주세요.">

                <button>검색</button>
            </form>

        </section>
    </main>
    
    
    <!-- 썸네일 클릭 시 모달창 출력 -->
    <div class="modal">
        <span id="modalClose">&times;</span>
        <img id="modalImage" src="/resources/images/user.png">
    </div>


    <jsp:include page="/WEB-INF/views/common/footer.jsp"/>

</body>
</html>

🍄 boardList-style.css

/* section, div{ border : 1px solid black;} */

.board-name{
    font-size : 2em;
    margin : 20px 0;
 }
 
 a{
     text-decoration: none;
     color : black;
 }
 
 a:hover {
     text-decoration: underline;
 }
 
 .board-list{
     /* display: flex; */
     width: 1000px;
     /* height: 800px; */
     margin : 50px auto;
 }
 
 .board-title{
     margin-left : 30px;
     font-size: 2.5em;
 }
 
 
 .list-wrapper{
     min-height: 670px;
     width: 100%;
 }
 
 .list-table{
     width: 100%;
     padding: 20px;
     border-collapse: collapse;
 }
 
 .list-table > thead{ background-color: #455ba8; color: white;}
 
 
 .list-table tr{
     height: 60px;
 }
 
 .list-table > tbody > tr{
     border-bottom: 1px solid gray;
 
 }
 
 
 .list-table tr > *{ text-align: center; }
 .list-table tr > *:nth-of-type(1){ width: 10%; }
 
 .list-table tr > td:nth-of-type(2){
      width: 50%; 
      text-align: left;
      padding-left: 50px;
      text-decoration: none;
 }
 
 .list-table tr > *:nth-of-type(3){ width: 13%; }
 .list-table tr > *:nth-of-type(4){ width: 10%; }
 .list-table tr > *:nth-of-type(5){ width: 8%; }
 .list-table tr > *:nth-of-type(6){ width: 8%; }
 
 
 .btn-area{
     height: 50px;
     display: flex;
     justify-content: flex-end;
     align-items: center;
 }
 
 
 .btn-area > button{
     margin-right: 50px;
     
     width: 80px;
     height: 40px;   
     background-color: white;
 
     font-weight: bold;
     color: #455ba8;
     border: 2px solid #455ba8;
     border-radius: 5px;
 
     cursor: pointer;
 
 }
 
 
 .pagination{
     list-style: none;
     display: flex;
     justify-content: center;
     padding: 0;
 }
 
 .pagination li{
     width: 20px;
     margin: 0 5px;
     text-align: center;
 }
 
 .pagination a{
     display: block;
     width: 100%;
     height: 100%;
 }
 
 .current{
     font-weight: bold;
     background-color: #455ba8;
     border-radius: 50%;
     color: white !important;
     letter-spacing: 0 !important;
 }
 
 #boardSearch{
     text-align: center;
     width: 500px;
     height: 30px;
     margin: 30px auto;
 
     display: flex;
     justify-content: space-between;
 }
 
 
 #boardSearch * {
     box-sizing: border-box;
     margin: 0;
     padding: 0;
 }
 
 #boardSearch select{
     width: 100px;
 }
 
 #boardSearch input{
     flex-grow: 1;
     margin: 0 10px;
     padding-left: 10px;
 }  
 
 #boardSearch button{
     
     width: 100px;
 
     font-weight: bold;
     background-color: #455ba8;
     border: none;
     color: white;
 
     cursor: pointer;
     
 
 }
 
 
 /* 썸네일 */
 .list-table .list-thumbnail{
     position: absolute;
     max-height: 30px;
     max-width: 50px;
     left: -15px;
     top: 17px;
 }
 
 .list-table tr > td:nth-of-type(2){
     position: relative;
 }
 
 /* 모달 */
 .modal {
     position: fixed;
     top: 0;
     left: 0;
 
     width: 100%;
     height: 100%;
 
     display: none;
     z-index: 50;
 
     background-color: rgba(0, 0, 0, 0.4);
 }
 
 .modal.show {
     display: flex;
     animation-name: show;
     animation-duration: 0.5s;
     z-index: 500;
   }
 
 .modal.hide{
     animation-name: hide;
     animation-duration: 0.5s;
 }
 
   
 #modalImage {
     background-color: white;
     margin: auto;
     border-radius: 10px;
     max-width: 100%;
     max-height: 100%;
 }
 
 /* @keyframes : 
 애니메이션 중간중간의 특정 지점들을 거칠 수 있는 키프레임들을 설정함으로써 CSS 애니메이션 과정의 중간 절차를 제어할 수 있게 합니다.  */
 @keyframes show {
     from {opacity: 0;}
     to {opacity: 1;}
 }
 
 @keyframes hide {
     from {opacity: 1;}
     to {opacity: 0;}
 }
 
  
 /* 닫기 버튼 꾸미기 */
 #modalClose {
     position: absolute;
     top: 20px;
     right: 40px;
 
     color: white;
     font-size: 50px;
     font-weight: bold; 
 
     transition-duration: 0.2s;
     cursor: pointer;
 }
 
 #modalClose:hover{
     transform: scale(1.2);
 }
 
 

🍄 header.jsp

...
<nav>
    <ul>
        <%-- <li><a href="#">공지사항</a></li>
        <li><a href="#">자유 게시판</a></li>
        <li><a href="#">질문 게시판</a></li>
        <li><a href="#">FAQ</a></li>
        <li><a href="#">1:1문의</a></li> --%>

        <%-- interceptor를 이용해서 조회된 boardTypeList를
             application scope에서 얻어와 화면에 출력
         --%>

        <c:forEach items="${boardTypeList}" var="boardType">
            <li><a href="/board/${boardType.BOARD_CODE}">${boardType.BOARD_NAME}</a></li>
        </c:forEach>
        
    </ul>
</nav>

🌱 Spring

🍄 Board.java

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

import java.util.List;

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

@Getter
@Setter
@ToString
public class Board {

	private int boardNo;
	private String boardTitle;
	private String boardContent;
	private String boardCreateDate;
	private String boardUpdateDate;
	private int readCount;
	private int boardCode;
	
	// 서브쿼리
	private int commentCount; // 댓글 수
	private int likeCount;    // 좋아요 수
	
	// 회원 join
	private String memberNickname; 
	private int memberNo;
	private String profileImage;
	private String thumbnail;

	// 이미지 목록
	private List<BoardImage> imageList;

	// 댓글 목록
	private List<Comment> commentList;
	
}

🍄 BoardImage.java

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

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

@Getter
@Setter
@ToString
public class BoardImage {
	
	private int imageNo;
	private String imagePath;
	private String imageReName;
	private String imageOriginal;
	private int imageOrder;
	private int boardNo;
	
}

🍄 Comment.java

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

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

@Getter
@Setter
@ToString
public class Comment {

	private int commentNo;
	private String commentContent;
	private String commentCreateDate;
	private int boardNo;
	private int memberNo;
	private String commentDeleteFlag;
	private int parentNo;
	private String memberNickname;
	private String profileImage;

}

🍄 Pagination.java

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

public class Pagination {
	// 페이지네이션(페이징 처리)에 필요한 모든 값들을 저장하고 있는 객체

	private int currentPage;      // 현재 페이지 번호
	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;         // 목록 하단에 노출된 번호의 다음 목록 시작 번호


	// 생성자
	public Pagination(int currentPage, int listCount) {
		this.currentPage = currentPage;
		this.listCount = listCount;

		calculatePagination(); // 계산 메서드 호출
	}


	public int getCurrentPage() {
		return currentPage;
	}


	public void setCurrentPage(int currentPage) {
		this.currentPage = currentPage;

		calculatePagination();
	}


	public int getListCount() {
		return listCount;
	}


	public void setListCount(int listCount) {
		this.listCount = listCount;

		calculatePagination();
	}


	public int getLimit() {
		return limit;
	}


	public void setLimit(int limit) {
		this.limit = limit;

		calculatePagination();
	}


	public int getPageSize() {
		return pageSize;
	}


	public void setPageSize(int pageSize) {
		this.pageSize = pageSize;

		calculatePagination();
	}


	public int getMaxPage() {
		return maxPage;
	}


	public void setMaxPage(int maxPage) {
		this.maxPage = maxPage;
	}


	public int getStartPage() {
		return startPage;
	}


	public void setStartPage(int startPage) {
		this.startPage = startPage;
	}


	public int getEndPage() {
		return endPage;
	}


	public void setEndPage(int endPage) {
		this.endPage = endPage;
	}


	public int getPrevPage() {
		return prevPage;
	}


	public void setPrevPage(int prevPage) {
		this.prevPage = prevPage;
	}


	public int getNextPage() {
		return nextPage;
	}


	public void setNextPage(int nextPage) {
		this.nextPage = nextPage;
	}


	@Override
	public String toString() {
		return "Pagination [currentPage=" + currentPage + ", listCount=" + listCount + ", limit=" + limit
				+ ", pageSize=" + pageSize + ", maxPage=" + maxPage + ", startPage=" + startPage + ", endPage="
				+ endPage + ", prevPage=" + prevPage + ", nextPage=" + nextPage + "]";
	}



	// 페이징 처리에 필요한 값을 계산하는 메서드
	private void calculatePagination() {

		// * maxPage 계산 : 최대 페이지 수 == 마지막 페이지 번호

		// 전체 게시글 수 : 500개  //  보여지는 게시글 수: 10개
		// -> 마지막 페이지 번호는?  500 / 10 =  50

		// 전체 게시글 수 : 501개  //  보여지는 게시글 수: 10개
		// -> 마지막 페이지 번호는?  501 / 10 =  51  (50.1의 올림 처리)

		maxPage = (int)Math.ceil(  (double)listCount / limit  );



		// * startPage : 목록 하단에 노출된 페이지의 시작 번호

		// 목록 하단 페이지 번호의 노출 개수가 10개일 때

		// 현재 페이지가  1~10 인 경우 :  1
		// 현재 페이지가 11~20 인 경우 : 11
		// 현재 페이지가 21~30 인 경우 : 21

		startPage = (currentPage - 1) / pageSize * pageSize + 1;



		// * endPage : 목록 하단에 노출된 페이지의 끝 번호

		// 목록 하단 페이지 번호의 노출 개수가 10개일 때

		// 현재 페이지가  1~10 인 경우 : 10
		// 현재 페이지가 11~20 인 경우 : 20
		// 현재 페이지가 21~30 인 경우 : 30

		endPage = startPage + pageSize - 1;

		// 만약에 endPage가 maxPage를 초과하는 경우
		if(endPage > maxPage) {
			endPage = maxPage;
		}


		// * prevPage(<) : 목록 하단에 노출된 번호의 이전 목록 끝 번호
		// * nextPage(>) : 목록 하단에 노출된 번호의 다음 목록 시작 번호


		// 현재 페이지가  1~10 일 경우
		// < :  1 페이지
		// > : 11 페이지

		// 현재 페이지가 11~20 일 경우
		// < : 10 페이지
		// > : 21 페이지

		// 현재 페이지가 41~50 일 경우  (maxPage가 50)
		// < : 40 페이지
		// > : 50 페이지

		if(currentPage <= pageSize) { 
			prevPage = 1;
		}else {
			prevPage = startPage - 1;
		}


		if(endPage == maxPage) {
			nextPage = maxPage;
		}else {
			nextPage = endPage + 1;
		}

	}

}

🍄 BoardController.java

package edu.kh.project.board.controller;

import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
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 org.springframework.web.bind.annotation.SessionAttributes;

import edu.kh.project.board.model.service.BoardService;

@SessionAttributes({"loginMember"})
@RequestMapping("/board")
@Controller
public class BoardController {

	@Autowired
	private BoardService service;
	
	/* 목록 조회 : /board/1(게시판 코드)?cp=1 (cp : current page(현재 페이지))
	 * 상세 조회 : /board/1/1500?cp=1
	 * 
	 * ** 컨트롤러 따로 생성 예정
	 * 삽입 : /board2/insert?code=1 (code == BOARD_CODE, 게시판 종류)
	 * 수정 : /board2/update?code=1&no=1500 (no == BOARD_NO, 게시글 번호)
	 * 삭제 : /board2/delete?code=1&no=1500
	 * */

	// 게시글 목록 조회
	@GetMapping("/{boardCode}")
	public String selectBoardList(@PathVariable("boardCode") int boardCode
			, @RequestParam(value = "cp", required = false, defaultValue = "1") int cp
			, Model model) {
		
		// boardCode 확인
		// System.out.println("boardCode : " + boardCode);
		
		// 게시글 목록 조회 서비스 호출
		Map<String, Object> map = service.selectBoardList(boardCode, cp);
		
		// 조회 결과를 request scope에 세팅 후 forward
		model.addAttribute("map", map);
		
		return "board/boardList";
	}
}

🍄 BoardService.java

...
	/** 게시글 목록 조회
	 * @param boardCode
	 * @param cp
	 * @return map
	 */
	Map<String, Object> selectBoardList(int boardCode, int cp);

🍄 BoardServiceImpl.java

...
	// 게시글 목록 조회
	@Override
	public Map<String, Object> selectBoardList(int boardCode, int cp) {
		
		// 1. 특정 게시판의 삭제되지 않은 게시글 수 조회
		int listCount = dao.getListCount(boardCode);
		
		// 2. 1번 조회 결과 + cp를 이용해서 Pagination 객체 생성
		// -> 내부 필드가 모두 계산되어 초기화
		Pagination pagination = new Pagination(cp, listCount);
		
		// 3. 특정 게시판에서 현재 페이지에 해당하는 부분에 대한 게시글 목록 조회
		// (어떤 게시판(boardCode)에서
		//  몇 페이지(pagination.currentPage)에 대한
		//  게시글 몇 개(pagination.limit) 조회)
		List<Board> boardList = dao.selectBoardList(pagination, boardCode);
		
		// 4. pagination, boardList를 Map에 담아서 반환
		Map<String, Object> map = new HashMap<>();
		map.put("pagination", pagination);
		map.put("boardList", boardList);
		
		return map;
	}

🍄 BoardDAO.java

💭 RowBounds 객체란?

마이바티스에서 페이징 처리를 위해 제공하는 객체
-> offset만큼 건너뛰고 그다음 지정된 행의 개수(limit)만큼 조회

...
	/** 특정 게시판의 삭제되지 않은 게시글 수 조회
	 * @param boardCode
	 * @return listCount
	 */
	public int getListCount(int boardCode) {
		return sqlSession.selectOne("boardMapper.getListCount", boardCode);
	}

	/** 특정 게시판에서 현재 페이지에 해당하는 부분에 대한 게시글 목록 조회
	 * @param pagination
	 * @param boardCode
	 * @return boardList
	 */
	public List<Board> selectBoardList(Pagination pagination, int boardCode) {
		
		// 1) offset 계산
		int offset
		= (pagination.getCurrentPage() - 1) * pagination.getLimit();
		
		// 2) RowBounds 객체 생성
		RowBounds rowBounds = new RowBounds(offset, pagination.getLimit());
		
		// 3) selectList("namespace.id", 파라미터, RowBounds)
		return sqlSession.selectList("boardMapper.selectBoardList", boardCode, rowBounds);

	}

🍄 board-mapper.xml

💭 CDATA 태그란?

해당 태그 내부에 작성된 것은 모두 문자로 취급하게 해 준다.

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

	<!-- Board DTO에 대한 resultMap -->
	<resultMap type="Board" id="board_rm">

		<id property="boardNo" column="BOARD_NO" />

		<result property="boardTitle" column="BOARD_TITLE" />
		<result property="boardContent" column="BOARD_CONTENT" />
		<result property="boardCreateDate" column="B_CREATE_DATE" />
		<result property="boardUpdateDate" column="B_UPDATE_DATE" />
		<result property="readCount" column="READ_COUNT" />
		<result property="commentCount" column="COMMENT_COUNT" />
		<result property="likeCount" column="LIKE_COUNT" />
		<result property="memberNickname" column="MEMBER_NICKNAME" />

		<result property="memberNo" column="MEMBER_NO" />
		<result property="profileImage" column="PROFILE_IMG" />

		<result property="thumbnail" column="THUMBNAIL" />
	</resultMap>

	<!--
		resultType이 "map"인 경우
		K : 컬럼 명(BOARD_CODE, BOARD_NAME)
		V : 컬럼 값(	   1    ,   공지사항   )
	 -->

	<!-- 게시판 종류 목록 조회 -->
	<select id="selectBoardTypeList" resultType="map">
		SELECT * FROM BOARD_TYPE ORDER BY 1
	</select>
	
	<!-- 특정 게시판의 삭제되지 않은 게시글 수 조회 -->
	<select id="getListCount" resultType="_int">
		SELECT COUNT(*) FROM BOARD
		WHERE BOARD_DEL_FL = 'N'
		AND BOARD_CODE = #{boardCode}
	</select>
	
	<!-- CDATA 태그 : 해당 태그 내부에 작성된 것은 모두 문자로 취급 -->
	<!-- 게시글 목록 조회 -->
	<select id="selectBoardList" resultMap="board_rm">
		SELECT BOARD_NO, BOARD_TITLE, MEMBER_NICKNAME, READ_COUNT, 
		         
			<![CDATA[
	            CASE  
	               WHEN SYSDATE - B_CREATE_DATE < 1/24/60
	               THEN FLOOR( (SYSDATE - B_CREATE_DATE) * 24 * 60 * 60 ) || '초 전'
	               WHEN SYSDATE - B_CREATE_DATE < 1/24
	               THEN FLOOR( (SYSDATE - B_CREATE_DATE) * 24 * 60) || '분 전'
	               WHEN SYSDATE - B_CREATE_DATE < 1
	               THEN FLOOR( (SYSDATE - B_CREATE_DATE) * 24) || '시간 전'
	               ELSE TO_CHAR(B_CREATE_DATE, 'YYYY-MM-DD')
	            END B_CREATE_DATE,
			]]>
	            
			(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,
	          
			(SELECT IMG_PATH || IMG_RENAME FROM BOARD_IMG I
			 WHERE I.BOARD_NO = B.BOARD_NO
			 AND IMG_ORDER = 0) THUMBNAIL
	         
		FROM "BOARD" B
		JOIN "MEMBER" USING(MEMBER_NO)
		WHERE BOARD_DEL_FL = 'N'
		AND BOARD_CODE = #{boardCode}
		ORDER BY BOARD_NO DESC
	</select>
	
</mapper>

💻 출력 화면

boardCode에 따라 게시판 이름이 바뀌는 것을 볼 수 있다.
또한 게시판에 작성된 게시글도 한 페이지에 10개씩(지정한 limit만큼) 썸네일과 제목, 작성자 이름 등이 출력된다.

하단의 페이지네이션 버튼을 누르면 원하는 페이지로 이동도 가능하다. 😉

페이지네이션은 나올 때마다 어려워서 자주 사용해 보며 익숙해져야겠다...!

profile
풀스택 개발자 기록집 📁

0개의 댓글