Spring - 게시판 목록

맑은 눈의 코드 👀·2023년 8월 21일
0

06_framework 이론

목록 보기
15/23
post-thumbnail

🐷아래 순서대로 게시판 목록을 만들어보자!

1) DB에 데이터 삽입
2) boardList.jsp 파일 생성
3) boardList-style.css 파일 생성
4) header.jsp 파일에 nva <a>태그 경로 지정하기
5) DTO 패키지에 Board 클래스 생성(Setter/Getter/ToString)
6) Pagination/ BoardImage / Comment DTO 생성하기
6) BoardController 파일 생성
7) BoardService인터페이스 / BoardServiceImpl 클래스
8) BoardDAO 클래스 생성
9) mybatis-config.xml 별칭(aliase)과 mapper 경로 지정
10) board-mapper.xml 생성

🐽DB데이터 삽입

-- 게시판 종류
CREATE TABLE "BOARD_TYPE"(
   "BOARD_CODE" NUMBER CONSTRAINT "PK_BOARD_TYPE" PRIMARY KEY,
   "BOARD_NAME" VARCHAR2(30) NOT NULL
);

COMMENT ON COLUMN "BOARD_TYPE"."BOARD_CODE" 
IS '게시판 코드(SEQ_BOARD_CODE)';

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

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, '테스트 게시판');

INSERT INTO "BOARD_TYPE" VALUES(SEQ_BOARD_CODE.NEXTVAL, '질문 게시판');

INSERT INTO "BOARD_TYPE" VALUES(SEQ_BOARD_CODE.NEXTVAL, '점심 게시판');

COMMIT;

SELECT * FROM "BOARD_TYPE";

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

-- [게시판 DB 설정]


CREATE TABLE "BOARD" (
   "BOARD_NO"   NUMBER      NOT NULL,
   "BOARD_TITLE"   VARCHAR2(150)      NOT NULL,
   "BOARD_CONTENT"   VARCHAR2(4000)      NOT NULL,
   "B_CREATE_DATE"   DATE   DEFAULT SYSDATE   NOT NULL,
   "B_UPDATE_DATE"   DATE      NULL,
   "READ_COUNT"   NUMBER   DEFAULT 0   NOT NULL,
   "BOARD_DEL_FL"   CHAR(1)   DEFAULT 'N'   NOT NULL,
   "MEMBER_NO"   NUMBER      NOT NULL,
   "BOARD_CODE"   NUMBER      NOT NULL
);

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

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

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

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

COMMENT ON COLUMN "BOARD"."B_UPDATE_DATE" IS '마지막 수정일(수정 시 UPDATE)';

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

COMMENT ON COLUMN "BOARD"."BOARD_DEL_FL" IS '삭제 여부(N : 삭제X , Y : 삭제O)';

COMMENT ON COLUMN "BOARD"."MEMBER_NO" IS '작성자 회원 번호';

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

CREATE TABLE "BOARD_IMG" (
   "IMG_NO"   NUMBER      NOT NULL,
   "IMG_PATH"   VARCHAR2(300)      NOT NULL,
   "IMG_RENAME"   VARCHAR2(30)      NOT NULL,
   "IMG_ORIGINAL"   VARCHAR2(300)      NOT NULL,
   "IMG_ORDER"   NUMBER      NOT NULL,
   "BOARD_NO"   NUMBER      NOT NULL
);

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

COMMENT ON COLUMN "BOARD_IMG"."IMG_PATH" IS '이미지 저장 폴더 경로';

COMMENT ON COLUMN "BOARD_IMG"."IMG_RENAME" IS '변경된 이미지 파일 이름';

COMMENT ON COLUMN "BOARD_IMG"."IMG_ORIGINAL" IS '원본 이미지 파일 이름';

COMMENT ON COLUMN "BOARD_IMG"."IMG_ORDER" IS '이미지 파일 순서 번호';

COMMENT ON COLUMN "BOARD_IMG"."BOARD_NO" IS '이미지가 첨부된 게시글 번호';


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


CREATE TABLE "BOARD_LIKE" (
   "BOARD_NO"   NUMBER      NOT NULL,
   "MEMBER_NO"   NUMBER      NOT NULL
);

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

COMMENT ON COLUMN "BOARD_LIKE"."MEMBER_NO" IS '좋아요 누른 회원 번호';


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


CREATE TABLE "COMMENT" (
   "COMMENT_NO"   NUMBER      NOT NULL,
   "COMMENT_CONTENT"   VARCHAR2(4000)      NOT NULL,
   "C_CREATE_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_NO"   NUMBER   
);

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

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

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

COMMENT ON COLUMN "COMMENT"."COMMENT_DEL_FL" IS '댓글 삭제 여부(N : 삭제X, Y : 삭제O)';

COMMENT ON COLUMN "COMMENT"."BOARD_NO" IS '댓글이 작성된 게시글 번호';

COMMENT ON COLUMN "COMMENT"."MEMBER_NO" IS '댓글 작성자 회원 번호';

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

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


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

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

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

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

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

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

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

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

ALTER TABLE "BOARD_LIKE" ADD CONSTRAINT "FK_MEMBER_TO_BOARD_LIKE_1" FOREIGN KEY (
   "MEMBER_NO"
)
REFERENCES "MEMBER" (
   "MEMBER_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_NO"
)
REFERENCES "COMMENT" (
   "COMMENT_NO"
);

-- 시퀀스 생성
CREATE SEQUENCE SEQ_BOARD_NO NOCACHE; -- 게시글 번호
CREATE SEQUENCE SEQ_IMG_NO NOCACHE; -- 게시글 이미지 번호
CREATE SEQUENCE SEQ_COMMENT_NO NOCACHE; -- 댓글 번호

-------------------------------------------------------------------------
-- BOARD 테이블 샘플 데이터 삽입(PL/SQL)
BEGIN
   FOR I IN 1..1500 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;
-- BOARD_TYPE에 컬럼 몇개 인지 조회 
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,1500)),
             1, NULL);
   END LOOP;
END;
/

SELECT * FROM "COMMENT";
COMMIT;
--게시글 샘플 이미지 넣기 
INSERT INTO BOARD_IMG
VALUES (SEQ_IMG_NO.NEXTVAL, '/resources/images/board/',
    '20230821141913_00001.png', '빵빵이1', 0, 1498);

INSERT INTO BOARD_IMG
VALUES (SEQ_IMG_NO.NEXTVAL, '/resources/images/board/',
    '20230821141913_00002.png', '빵빵이2', 0, 1497);

INSERT INTO BOARD_IMG
VALUES (SEQ_IMG_NO.NEXTVAL, '/resources/images/board/',
    '20230821141913_00003.png', '빵빵이3', 0, 1488);
    
INSERT INTO BOARD_IMG
VALUES (SEQ_IMG_NO.NEXTVAL, '/resources/images/board/',
    '20230821141913_00004.png', '빵빵이5', 0, 1495);
    
INSERT INTO BOARD_IMG
VALUES (SEQ_IMG_NO.NEXTVAL, '/resources/images/board/',
    '20230821141913_00005.png', '빵빵이4', 0, 1479);
    
COMMIT;
    
SELECT BOARD_NO FROM BOARD
WHERE BOARD_CODE =1
ORDER BY 1 DESC;

🐽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);
 }
 

🥕haeder.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>

🐽board.model.dto

🥕Board.java

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

import java.util.List;

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

@Setter
@Getter
@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;

@Setter
@Getter
@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;
      }
   }
}

🐽board.Controller

🥕BoardController.java 클래스

@PathVariable: URL 경로에 있는 값을 매개변수로 이용할 수 있게 하는 어노테이션
+ request Scope에 세팅

@PathVariable과 Query String

  • @PathVariable을 사용하는 경우 = 자원(resource) 구분/식별하는 용도
    	ex) https://github.com/complete0415
    	ex) https://github.com/testUser
    	ex) /board/1 -> 1번 (공지사항)게시판
    	ex) /board/2 -> 2번 (자유게시판)게시판
    
  • Query String을 사용하는 경우 = 정렬 필터링
    	ex) search.naver.com?query=날씨
    	ex) search.naver.com?query=점심
    	ex) /board2/insert?code=1
    	ex) /board2/insert?code=2
    		-> 삽입이라는 공통된 동작을 수행 
            단, code에 따라 어디에 삽입할 지 지정(필터링)
    	
    ex) /board/list?order=recent (최신순) ex) /board/list?order=most (인기순)
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/inssert?code=1(code ==BOARD_CODE , 게시판 종류)
	 * 수정 : /board2/update?code=1&no=1500 (no == BOARD_NO , 게시글 번호)
	 * 삭제 : /board2/delete?code=1&no=1500
	 * */

	// @PathVariable
	// URL 경로에 있는 값을 메개 변수로 이용할 수 있게 하는 어노테이션
	// + request Scope에 세팅
	
	// @PathVariable을 사용하는 경우 
	// - 자원(resource) 구분/식별
	// ex) https://github.com/complete0415
	// ex) https://github.com/testUser
	// ex) /board/1 -> 1번 (공지사항)게시판
	// ex) /board/2 -> 2번 (자유게시판)게시판
	
	
	// Query String을 사용하는 경우
	// - 정렬, 필터링 
	// ex) search.naver.com?query=날씨
	// ex) search.naver.com?query=점심
	// ex) /board2/insert?code=1
	// ex) /board2/insert?code=2
	//	-> 삽입이라는 공통된 동작을 수행
	//		단, code에 따라 어디에 삽입할 지 지정(필터링)
	
	// ex) /board/list?order=recent (최신순)
	// ex) /board/list?order=most (인기순)
	
	
	//게시글 목록 조회
	@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);
		
		// 게시글을 목록 조회하는 service호출 
		Map<String, Object> map = service.selectBoardList(boardCode, cp);
		
		//조회 결과를 request scope에 세팅 후 forward
		model.addAttribute("map", map);
		
		return "board/boardList";
	}
}

🐽board.model.service

🥕BoardService.java 인터페이스

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

import java.util.List;
import java.util.Map;

public interface BoardService {

	/** 게시판 종류 목록 조회
	 * @return boardTypeList
	 */
	List<Map<String, Object>> selectBoardTypeList();

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

}

🥕BoardServiceIpml.java 클래스

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

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import edu.kh.project.board.model.dao.BoardDao;
import edu.kh.project.board.model.dto.Board;
import edu.kh.project.board.model.dto.Pagination;

@Service
public class BoardServiceImpl implements BoardService{
	
	@Autowired
	private BoardDao dao;
	
	//게시판 종류 목록 조회
	@Override
	public List<Map<String, Object>> selectBoardTypeList() {
		return dao.selectBoardTypeList();
	}
	
	// 게시글 목록 조회
	@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.crrentPage)에 대한
		// 게시글 몇 개 (pagination.limit) 조회)
		List<Board> boardList = dao.selectBoardList(pagination, boardCode);
		
		// 4. paginatio, boardList를 Map에 담아서 반환
		Map<String, Object> map = new HashMap<String, Object>();
		
		map.put("pagination", pagination);
		map.put("boardList", boardList);
		
		return map;
	}
}

🐽 board.model.dao

🥕BoardDAO.java 클래스

RowBounds 객체

	- 마이바티스 에서 페이징 처리를 위해 제공하는 객체 
	- offset 만큼 건너 뛰고
	  그 다음 지정된 행의 객수 (limit) 만큼 조회
  [순서] 
  1) offset 계산
  2) RowBounds 객체생성 
  3) selectList("namespace.id", 파라미터, Rowbounds)호출
package edu.kh.project.board.model.dao;

import java.util.List;
import java.util.Map;

import org.apache.ibatis.session.RowBounds;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

import edu.kh.project.board.model.dto.Board;
import edu.kh.project.board.model.dto.Pagination;

@Repository
public class BoardDao {
	
	@Autowired
	private SqlSessionTemplate sqlSession;

	/** 게시판 동류목록 조회
	 * @return boardTypeList
	 */
	public List<Map<String, Object>> selectBoardTypeList() {
		return sqlSession.selectList("boardMapper.selectBoardTypeList");
	}

	
	/** 특정기세판에 삭제되지 않은 게시글 수 조회
	 * @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) {
		
		// RowBounds 객체 
		// - 마이바티스 에서 페이징 처리를 위해 제공하는 객체 
		// - offset 만클 건너 뛰고
		// 	 그 다음 지정된 행의 객수 (limit) 만큼 조회 
		
		// 1) offset 계산
		
		int offset 
		= (pagination.getCurrentPage() -1) * pagination.getLimit();
		
		// 2) Row Bounds객체 생성
		RowBounds rowBounds = new RowBounds(offset, pagination.getLimit());
		
		// 3) sselectList("namespace.id", 파라미터, Rowbounds)호출
		
		return sqlSession.selectList("boardMapper.selectBoardList", boardCode, rowBounds);
	}

}

🐽Spring

🥕mybatis-mapper.xml

별칭 및 mapper 위치 등록

<!-- 별칭 작성 -->
<typeAliases>
  <typeAlias type="edu.kh.project.board.model.dto.Board" alias="Board"/>
</typeAliases>

<!-- mapper파일 위치등록 -->
<mappers>
  <mapper resource="/mappers/board-mapper.xml">
</mappers>

🐽mappers

🥕board-mapper.xml

  • resultType 또는 resultMap 꼭 써줄것
  • CDATA 태그 : 해당 태그 내부에 작성된 것은 모두 문자로 취급
  • paramType은 생략가능
<?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">

	<!--boardDTO에 대한 ReaultMap -->
	<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>

🐽 SQL구문 더 살펴보기

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

-- 특정게시판에 목록조회---------------
-- 1. 최신순서 
-- 2. 1page(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;
profile
나를 죽이지 못하는 오류는 내 코드를 더 강하게 만들지ㅋ

1개의 댓글

comment-user-thumbnail
2023년 8월 21일

정리 너무너무 잘하셨네요 >< 🥰

답글 달기