본문 바로가기
05_Server (04. JSP 프로그래밍 구현)

[3-2] 일반게시판서비스 목록조회요청(페이징처리)

by moca7 2024. 9. 11.



 

 

ㅁ 일반게시판서비스

- 일반게시판서비스_목록조회요청(페이징처리) 

- 일반게시판서비스_작성요청(파일업로드) 

- 일반게시판서비스_수정요청 

 

 

 

 

 

 

src/main/webapp/views/board에 boardList.jsp를 만든다.

 

- "일반게시판목록페이지.html"의 body 태그 안의 구문들을 복붙하고, header와 footer를 include한다.

 

- 페이징바를 부트스트랩에서 긁어왔는데, 매번 다르게 동적으로 제작해야 한다.

- 이 페이지(일반게시글목록페이지)에 올 때 게시글 데이터와 페이징 바 제작을 위한 데이터가 필요하다.

 

 

 

- 헤더의 메뉴바에서 '일반게시판' 클릭시 /list.bo라는 url mapping값을 가지는 서블릿을 호출한다.

- 이때 딱히 몇번 페이지를 요청하겠다고 데이터를 넘기지는 않는다. 

그러나 기본적으로 1번 페이지가 보여지게끔 한다.

 

 

 

ㅁ src/main/java/com.br.web.board.controller에 BoardListController를 만든다. 

 

 
package com.br.web.board.controller;

import java.io.IOException;
import java.util.List;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.br.web.board.model.service.BoardService;
import com.br.web.board.model.vo.Board;
import com.br.web.common.model.vo.PageInfo;

/**
 * Servlet implementation class BoardListController
 */
@WebServlet("/list.bo")
public class BoardListController extends HttpServlet {
    private static final long serialVersionUID = 1L;
       
    /**
     * @see HttpServlet#HttpServlet()
     */
    public BoardListController() {
        super();
    }

    /**
     * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)
     */
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
       
        // 메뉴바에 있는 메뉴 클릭시    /list.bo                       => 1번 페이지 요청
        // 목록페이지의 페이징바 클릭시 /list.bo?page=클릭한페이지번호  => 클릭한 페이지 요청
 
       
        // ------------- 페이징 처리 -------------
        // * listCount   : 현재 게시글 총 갯수 (db로부터 조회)
        int listCount = new BoardService().selectBoardListCount();
 
        // * currentPage : 사용자가 요청한 페이지 번호 (요청시 전달됨|전달된게 없으면 1로 간주)
        int currentPage = 1;
        if(request.getParameter("page") != null) {
            currentPage = Integer.parseInt(request.getParameter("page")); // 문자열이어서 parsing한다.
        }
 
        // * pageLimit   : 페이징바의 목록 수 (몇개 단위씩 보여지게 할건지)
        int pageLimit = 10;
 
        // * boardLimit  : 한 페이지에 보여질 게시글 수 (몇개 단위씩 보여지게 할건지)
        int boardLimit = 10;
 
       
        // 위의 4개를 가지고 사용자가 요청한 페이지 하단에 보여질
        // 페이징바의 시작수, 끝수, 가장마지막페이지수를 구해야됨
 
 
       
        /*
         * * maxPage : 가장 마지막 페이지 수 (즉, 총 페이지 수)
         *   listCount, boardLimit 가지고 구하기
         *  
         *   listCount  boardLimit  maxPage
         *      100         10        10
         *      101         10        11
         *      105         10        11
         *      110         10        11
         *      
         *      즉,
         *      101~110  /  10  => 10.1~11.0  => 올림 =>  11        <- 자바에서 정수/정수는 항상 정수다.
         *      111~120  /  10  => 11.1~12.0  => 올림 =>  12        <- 소수점이 있을 경우 올림처리 하면 된다.
         */
 
 
        int maxPage = (int)Math.ceil( (double)listCount / boardLimit );   
        // ceil은 소수점이 조금(10.000001)이라도 있으면 올림해서 소수점으로 반환한다. 
 
 
       
        /*
         * * startPage : 사용자가 요청한 페이지 하단에 보여질 페이징바의 시작수
         *   pageLimit, currentPage 가지고 구함
         *  
         *   ex) pageLimit이 10이라는 가정하에
         *       startPage는 1, 11, 21, 31이 나올 수 있음
         *                  => n * pageLimit + 1 (즉, pageLimit 배수 + 1)
         *      
         *       currentPage    pageLimit   startPage
         *           1              10          1   (0 * pageLimit + 1)
         *           5              10          1   (0 * pageLimit + 1)
         *          10              10          1   (0 * pageLimit + 1)
         *          11              10         11   (1 * pageLimit + 1)
         *          15              10         11   (1 * pageLimit + 1)
         *          20              10         11   (1 * pageLimit + 1)
         *      
         *          즉,
         *          1~10    => n 자리가 0
         *         11~20    => n 자리가 1
         *         21~30    => n 자리가 2
         *         ...
         *        
         *         n 자리는 (currentPage - 1) / pageLimit    <-  자바에서 정수 나누기 정수는 정수다.
         *  
         */
        int startPage = (currentPage - 1) / pageLimit * pageLimit + 1;
 
 
       
        /*
         * * endPage : 사용자가 요청한 페이지 하단에 보여질 페이징바의 끝 수
         *   startPage, pageLimit으로 구하기
         *  
         *   ex) pageLimit이 10이라는 가정하에
         *  
         *       startPage  endPage
         *          1         10
         *         11         20
         *         21         30    
         *        
         *         즉, startPage + pageLimit - 1
         */
        int endPage = startPage + pageLimit - 1;
 
        // 단, 위의 과정으로 구해진 endPage가 maxPage보다 클 경우 수정
        if(endPage > maxPage) {
            endPage = maxPage;
        }
 
 
       
        // * 페이징바를 제작하기 위한 데이터 => PageInfo vo 객체
        PageInfo pi = new PageInfo(listCount, currentPage, pageLimit, boardLimit, maxPage, startPage, endPage);
       
        // * 사용자가 요청한 페이지상에 필요한 게시글 데이터 조회 (전체 데이터가 아닌 일부 데이터만)
        List<Board> list = new BoardService().selectBoardList(pi);
 
 
   
        // 응답페이지 : 일반게시글 목록페이지 (/views/board/boardList.jsp)
        // 응답데이터 : 페이징바 제작할 데이터, 게시글 데이터
        request.setAttribute("pi", pi);
        request.setAttribute("list", list);
       
        request.getRequestDispatcher("/views/board/boardList.jsp").forward(request, response);
   
    }

    /**
     * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response)
     */
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        doGet(request, response);
    }

}

 

 

- /list.bo라는 url은 헤더 메뉴바에서 일반게시판을 클릭할 때 뿐만 아니라, 목록페이지에서 보여지는 페이징 바의 숫자들을 클릭 시에도 요청된다.

 

- url은 2가지 경우가 있다.

메뉴바에 있는 메뉴 클릭시         /list.bo                                            =>     1번 페이지 요청

목록페이지의 페이징바 클릭시   /list.bo?page=클릭한페이지번호     =>     클릭한 페이지 요청

 

- page라는 key값으로 클릭한 페이지 번호를 보낸다.

page라는 parameter는 있을 수도 있고 없을 수도 있다.

 

 

 

※ 페이징 처리를 위해 필요한 데이터들

 

(1) 해당 게시글의 총 개수 (listCount)

- count함수를 써서 db로부터 알아낸다.

 

(2) 사용자가 요청한 페이지 번호 (currentPage)

- 요청시 page라는 key값으로 전달되고, 전달되지 않으면(null이면) 1로 간주한다.

 

(3) 페이징바의 목록 수(pageLimit)

- 몇개 단위씩 보여지게 할건지

 

(4) 한 페이지에 보여질 게시글 수(boardLimit) 

- 몇개 단위씩 보여지게 할건지

 

(5) 위의 4개를 가지고 페이징 바의 시작 수, 끝 수, 가장 마지막 페이지 수를 구한다.

 

 

 

- 만약 화면에서 '10개씩 보기', '20개씩 보기' 같은 기능을 구현한다면, url 요청시 page라는 key-value 세트를 보내듯이 boardLimit에 들어갈 key-value를 보내서 서블릿에서 받아서 대입하면 된다. 

 

 

 

ㅁ listCount를 구하기 위해 service, dao를 호출해서 아래 쿼리를 실행하고 결과를 반환받는다. 

 
    <entry key="selectBoardListCount">
        SELECT
               COUNT(*) as COUNT
          FROM
               BOARD
         WHERE
               BOARD_TYPE = 1
           AND STATUS = 'Y'
    </entry>
 

 

 
    public int selectBoardListCount(Connection conn) { // dao
 
        // select => ResultSet(게시글갯수, 숫자한개) => int
        int listCount = 0;
        PreparedStatement pstmt = null;
        ResultSet rset = null;
        String sql = prop.getProperty("selectBoardListCount");
       
        try {
            pstmt = conn.prepareStatement(sql);
            rset = pstmt.executeQuery();
           
            if(rset.next()) {
                listCount = rset.getInt("count");
            }
           
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            close(rset);
            close(pstmt);
        }
       
        return listCount;
       
    }
 

 

 

- BOARD 테이블에 일반게시글, 사진게시글이 같이 있다. 일반게시글이고 삭제되지 않은 게시글만 조회한다. 

- 함수를 쓰면 별칭을 부여해야 한다.

SQL 쿼리에서 함수 결과에 별칭을 주지 않으면 JDBC에서 해당 값을 참조하기가 어려워집니다.

 

 

 

 

 

ㅁ com.br.web.common.model.vo 패키지에 PageInfo라는 클래스를 만든다.

 

 

- 페이징 바 제작을 위한 변수가 7개 있다. 일일이 전달하기 번거로우니 vo 객체를 만들어서 한번에 넘긴다.

- vo객체가 꼭 db의 테이블과 연관있을 필요는 없다.

- 기본생성자, 매개변수생성자, 게터세터, toString 메소드를 만들어 놓는다. 

 

- 응답페이지에서 페이징 바 제작을 위한 데이터가 필요하다. 그 데이터들을 pageInfo 객체로 만들었다.

- 그리고 사용자가 요청한 페이지에 뿌려줄 게시글 데이터도 조회한다.

사용자가 몇번 페이지를 요청할지는 모르지만, 요청한 페이지에 필요한 데이터만을 조회해야 한다. 전체 데이터가 아니라.

 

 

 

 

 

ㅁ 목록페이지

 

 

 

- 화면 구현한 목록 페이지를 보고 어떤 데이터가 필요한지 파악한다.

- 목록 페이지에서는 해당 게시글의 내용은 필요없다. 번호, 카테고리, 글제목, 작성자, 조회수, 작성일이 필요하다. 

 

 

 

 

 

ㅁ 사용자가 요청한 페이지에 필요한 게시글 데이터를 아래 쿼리를 실행해서 List에 담아서 가져온다.

 

 
    <entry key="selectBoardList">
        SELECT *
          FROM (
                SELECT
                       BOARD_NO
                     , CATEGORY_NAME
                     , BOARD_TITLE
                     , USER_ID
                     , BOARD_COUNT
                     , REGIST_DATE
                     , ROW_NUMBER() OVER(ORDER BY BOARD_NO DESC) as RNUM
                  FROM
                       BOARD B
                  JOIN CATEGORY USING(CATEGORY_NO)
                  JOIN MEMBER ON (USER_NO = BOARD_WRITER)
                 WHERE
                       BOARD_TYPE = 1
                   AND B.STATUS = 'Y'
                )
         WHERE RNUM BETWEEN ? AND ?
    </entry>
 

 

- 1번 페이지 요청이라면 WHERE RNUM BETWEEN 1 AND 10;

- 2번 페이지 요청이라면 WHERE RNUM BETWEEN 11 AND 20;

 

 

 
    public List<Board> selectBoardList(Connection conn, PageInfo pi){ // dao
 
        // select => ResultSet (여러행) => List<Board>
        List<Board> list = new ArrayList<>();
        PreparedStatement pstmt = null;
        ResultSet rset = null;
 
        String sql = prop.getProperty("selectBoardList");
       
        try {
            pstmt = conn.prepareStatement(sql);
           
            /*
             * ex) boardLimit이 10이라는 가정하
             *     currentPage 1일 경우 => 시작값 : 1 / 끝값 : 10
             *     currentPage 2일 경우 => 시작값 : 11/ 끝값 : 20
             *    
             *     시작값 : (currentPage - 1) * boardLimit + 1
             *     끝값   : 시작값 + boardLimit - 1
             */
           
            int startRow = (pi.getCurrentPage() - 1) * pi.getBoardLimit() + 1;
            int endRow = startRow + pi.getBoardLimit() - 1;
           
            pstmt.setInt(1, startRow);
            pstmt.setInt(2, endRow);
           
            rset = pstmt.executeQuery();
           
            while(rset.next()) {
                list.add(new Board(rset.getInt("board_no")
                                 , rset.getString("category_name")
                                 , rset.getString("board_title")
                                 , rset.getString("user_id")
                                 , rset.getInt("board_count")
                                 , rset.getDate("regist_Date")));
            }
           
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            close(rset);
            close(pstmt);
        }
       
        return list;
       
    }
 

 

 

- 카테고리명을 알려면 CATEGORY 테이블과 조인해야 한다. 

- 회원id를 알려면 MEMBER 테이블과 조인해야 한다. 

 

- 시작값의 일의자리는 boardLimit + 1이다.

- 쿼리에서 ROWNUM은 굳이 뽑아서 담을 필요 없다. 조건식에서 쓰는 용도다.

 

 

 

 

 

 

- 전체 게시글이 아닌, 일부 게시글만 조회해야 한다.

- 최신글 순으로 정렬시키고, 사용자가 요청한 페이지에 맞는 일부 데이터만을 조회한다. 

- ROW_NUMBER() 함수를 이용한다. 

정렬 시키고 정렬한 순서대로 순번을 부여하는 함수다. 

 

 

 

 

 

- 지금은 전체 게시글이 다 조회되지만, 사용자가 1번 페이지를 요청하면 RNUM이 1~10 까지만 조회해 가야 한다. 

(한 페이지에 게시글이 10개씩 보여질 때) 

- 2번 페이지를 요청하면 RNUM이 11~20 까지의 데이터만 조회돼야 한다. 

 

- 그런데 RNUM 컬럼은 SELECT절에서 뒤늦게 별칭이 부여돼서 WHERE절에서 쓸 수 없다. 

- RNUM 컬럼을 조건으로 쓰고 싶다면 현재 조회되는 이 결과물을 서브쿼리로 마치 하나의 테이블처럼 쓰면 된다. 

 

 

 

 

ㅁ 응답페이지인 일반게시글 목록페이지(/views/board/boardList.jsp)에서 데이터를 뽑는다.

 

 
<%@ page import="java.util.List" %>
<%@ page import="com.br.web.common.model.vo.PageInfo" %>
<%@ page import="com.br.web.board.model.vo.Board" %>
 
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
 
<%
  PageInfo pi = (PageInfo)request.getAttribute("pi");
  List<Board> list = (List<Board>)request.getAttribute("list");
%>
 
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>

  <div class="container p-3">

    <!-- Header, Nav start -->
    <%@ include file="/views/common/header.jsp" %>
    <!-- Header, Nav end -->


    <!-- Section start -->
    <section class="row m-3" style="min-height: 500px;">
      <div class="container border m-4 p-5 rounded">

        <h2 class="m-4">일반게시글 목록</h2>

        <% if(loginUser != null){ %>
        <div class="d-flex justify-content-end">
          <a href="<%= contextPath %>/write.bo" class="btn btn-secondary btn-sm">등록하기</a>
        </div>
        <% } %>

        <br>
        <table class="table table-hover" id="board-list">
          <thead>
            <tr>
              <th width="100px">번호</th>
              <th width="100px">카테고리</th>
              <th width="400px">글제목</th>
              <th width="120px">작성자</th>
              <th>조회수</th>
              <th>작성일</th>
            </tr>
          </thead>
          <tbody>
            <!-- 사용자가 요청한 페이지에 뿌려줄 게시글 데이터 조회해와야됨 -->
         
            <% if(list.isEmpty()) { %>
            <!-- case1. 조회된 게시글이 없을 경우 -->
            <tr>
              <td colspan="6" style="text-align: center;">존재하는 게시글이 없습니다.</td>
            </tr>
            <% }else { %>

              <!-- case2. 조회된 게시글이 있을 경우 -->
              <% for(Board b : list){ %>
              <tr>
                <td><%= b.getBoardNo() %></td>
                <td><%= b.getCategory() %></td>
                <td><%= b.getBoardTitle() %></td>
                <td><%= b.getBoardWriter() %></td>
                <td><%= b.getBoardCount() %></td>
                <td><%= b.getRegistDt() %></td>
              </tr>
              <% } %>
           
            <% } %>
           
          </tbody>
        </table>
        <script>
          $(function() {
            $('#board-list tbody>tr').on('click', function() {
             
              // 현재 클릭한 게시글 번호
              let no = $(this).children().eq(0).text();
              // 현재 클릭한 게시글 작성자 아이디
              let writer = $(this).children().eq(3).text();
              // 현재 로그인한 회원 아이디
              let loginUserId = '<%= loginUser == null ? "" : loginUser.getUserId() %>';
             
              if(writer == loginUserId) {
                // 현재 내가 쓴 글일 경우 => 조회수증가없이 상세페이지로 바로 이동
                location.href = "<%=contextPath%>/detail.bo?no=" + no;
              }else{
                // 내가 쓴 글이 아닐 경우 => 조회수증가하면서 상세페이지로 이동
                location.href = "<%=contextPath%>/increase.bo?no=" + no;
              }
            })
          })
        </script>
       
        <!-- 사용자가 현재보고있는 페이지가 뭐냐에 따라서 다르게 보여질 페이징바 -->
        <ul class="pagination d-flex justify-content-center text-dark">
       
          <li class='page-item <%=pi.getCurrentPage() == 1 ? "disabled" : "" %>'>
            <a class="page-link" href="<%=contextPath%>/list.bo?page=<%=pi.getCurrentPage()-1%>">Previous</a>
          </li>
         
          <% for(int p = pi.getStartPage(); p<=pi.getEndPage(); p++) { %>
            <li class='page-item <%= p == pi.getCurrentPage() ? "active" : ""%>'>
              <a class="page-link" href="<%= contextPath %>/list.bo?page=<%= p %>"><%= p %></a>
            </li>
          <% } %>
         
          <li class='page-item <%= pi.getCurrentPage() == pi.getMaxPage() ? "disabled" : "" %>'>
            <a class="page-link" href="<%=contextPath%>/list.bo?page=<%= pi.getCurrentPage()+1 %>">Next</a>
          </li>
         
        </ul>
       
      </div>
    </section>
    <!-- Section end -->

    <!-- Footer start -->
    <%@ include file="/views/common/footer.jsp" %>
    <!-- Footer end -->

</body>
</html>
 

 

 

- 클래스명들에 빨간줄 뜨니까 import 한다.

 

- 페이징바에서 클래스명에 disabled가 들어가면 클릭이 불가능해진다.

- 그리고 현재 페이지는 다른 페이지 번호들과 다르게 선택되었음을 표시하기 위해 active 클래스를 줘서 배경색을 준다. 

- 클래스명을 쌍따옴표에서 홑따옴표로 바꾸고, 한칸 띄어쓰고 3항 연산자로 클래스이름을 준다.

자바에서의 문자열 반환시에는 무조건 쌍따옴표를 안에 써야 한다. 

 

- p는 1일 수도 있고, 11일 수도 있다.

- 페이징 바의 페이지 번호(p) 클릭시 "/list.bo/?page=클릭한페이지번호"를 요청한다. 

 

- previous, next를 누르면 현재 내가 보고있는 페이지보다 -1, +1번 페이지를 요청한다. 

이때 내가 이미 1번 페이지를 보고있다면 요청이 불가능하게끔 한다.

마찬가지로 내가 이미 마지막 페이지를 보고 있다면 요청이 불가능하게끔 한다. 

 

 

※ "일반게시판목록페이지.html"에 있던 페이징 바 부분


        <ul class="pagination d-flex justify-content-center text-dark">
          <li class="page-item disabled"><a class="page-link" href="">Previous</a></li>
 
          <li class="page-item active"><a class="page-link" href="">1</a></li>
          <li class="page-item"><a class="page-link" href="">2</a></li>
          <li class="page-item"><a class="page-link" href="">3</a></li>
          <li class="page-item"><a class="page-link" href="">4</a></li>
          <li class="page-item"><a class="page-link" href="">5</a></li>
 
          <li class="page-item"><a class="page-link" href="">Next</a></li>
        </ul>
 

 

- 정적으로 구현되어 있지만 startPage부터 endPage까지 반복문으로 1씩 증가하면서 요소가 동적으로 만들어져야 한다.