ㅁ 일반게시판서비스
- 일반게시판서비스_목록조회요청(페이징처리)
- 일반게시판서비스_작성요청(파일업로드)
- 일반게시판서비스_수정요청
ㅁ 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씩 증가하면서 요소가 동적으로 만들어져야 한다.