package kr.co.jsp.board.service;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import kr.co.jsp.board.model.BoardDAO;
import kr.co.jsp.board.model.BoardVO;
public class ContentService implements IBoardService {
@Override
public void execute(HttpServletRequest request, HttpServletResponse response) {
int bId = Integer.parseInt(request.getParameter("bId"));
/*
# 쿠키로 조회수를 컨트롤 해 보자.
1. 사용자가 글 제목을 눌러서 상세보기 요청을 보낼 때
글 번호로 된 쿠키를 하나 만들어 줄 겁니다. (String)
쿠키 이름과 쿠키에 저장할 값을 모두 글 번호로 만들어 주겠습니다.
쿠키의 수명은 15초로 설정하겠습니다.
2. 요청을 보낼 때 같이 넘어온 쿠키 중에,
현재 글 번호와 일치하는 쿠키가 존재한다면 조회수를 올려주지 않을 겁니다.
현재 글 번호와 일치하는 쿠키가 없다면 조회수를 올려주도록 하겠습니다.
*/
String bNum = request.getParameter("bId");
boolean flag = false;
Cookie[] cookies = request.getCookies();
if(cookies != null) {
for(Cookie c : cookies) {
if(c.getName().equals(bNum)) {
flag = true;
break;
}
}
if(!flag) {
Cookie hitCoo = new Cookie(bNum, bNum);
hitCoo.setMaxAge(15);
response.addCookie(hitCoo);
BoardDAO.getInstance().upHit(bId);
}
}
BoardVO vo = BoardDAO.getInstance().contentBoard(bId);
request.setAttribute("content", vo);
}
}
쿠키를 배열로 받아오고 혹시 쿠키 중에 게시물 번호와 일치하는 쿠키가 발견되는 경우, 현재 게시물을 15초 이내에 클릭했던 적이 있다는 의미이므로 flag
를 true
로 해 주고 다른 쿠키를 더 이상 찾을 필요가 없으므로 break
한다.
만약 일치하는 쿠키가 없는 경우 (눌렀던 적이 없거나, 수명이 다했거나) 쿠키를 새로 생성하고 DAO.upHit()
를 호출한다.
게시글 등록 시 장문의 글을 써서 엔터 키를 이용해 줄 개행을 하는 경우, 조회를 해 보았을 때 개행이 반영되지 않는 문제를 확인할 수 있다.
이는 \r
\n
등이 브라우저상에 줄 바꿈으로 인식되지 않기 때문인데, 이를 <br>
로 대치하여 해결할 수 있다.
@Override
public BoardVO contentBoard(int bId) {
BoardVO vo = null;
String sql = "SELECT * FROM my_board WHERE board_id=?";
try(Connection conn = ds.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setInt(1, bId);
ResultSet rs = pstmt.executeQuery();
if(rs.next()) {
vo = new BoardVO(
rs.getInt("board_id"),
rs.getString("writer"),
rs.getString("title"),
rs.getString("content").replace("\r\n", "<br>"),
rs.getTimestamp("reg_date"),
rs.getInt("hit")
);
}
} catch (Exception e) {
e.printStackTrace();
}
return vo;
}
rs.getString("content").replace("\r\n", "<br>")
바로 이 부분인데, 캐리지 리턴이나 이스케이프 시퀀스를 <br>
로 치환하도록 한다.
하지만 이렇게 하면 글을 수정하려고 했을 때, <br>
태그가 그대로 수정 창에 묻어나오는 문제가 있어서, 역으로 한번 더 변환을 해 줘야 한다.
package kr.co.jsp.board.service;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import kr.co.jsp.board.model.BoardDAO;
import kr.co.jsp.board.model.BoardVO;
public class ModifyService implements IBoardService {
@Override
public void execute(HttpServletRequest request, HttpServletResponse response) {
int bId = Integer.parseInt(request.getParameter("bId"));
BoardVO vo = BoardDAO.getInstance().contentBoard(bId);
vo.setContent(vo.getContent().replace("<br>", "\r\n"));
request.setAttribute("article", vo);
}
}
vo.setContent(vo.getContent().replace("<br>", "\r\n"));
이 부분인데, 기존의 값(게시물 내용) 을 가져와 <br>
태그를 전부 \r
\n
으로 치환하도록 한다.
페이징 알고리즘 만들기
# 1. 사용자가 보게 될 페이지 화면
- 한 화면에 페이지 버튼을 10개씩 끊어서 보여준다면?
ex) 1 2 3 4 ..... 9 10 [다음] // [이전] 31 32 33 34 ..... 39 40 [다음]
- 만약에 총 게시물의 수가 67개라면?
1 2 3 4 5 6 7
- 총 게시물 수가 142개이고, 현재 사용자가 12페이지를 클릭했다면?
[이전] 11 12 13 14 15
# 2. 우선 총 게시물의 개수를 조회해야 합니다.
- 총 게시물 수는 DB로부터 수를 조회하는 SQL문을 작성합니다.
# 3. 사용자가 현재 위치한 페이지를 기준으로
끝 페이지 번호를 계산하는 로직을 작성.
- 만약 현재 사용자가 보고 있는 페이지가 3페이지고,
한 화면에 보여줄 페이지 버튼이 10개라면?
-> 끝 페이지 번호: 10번
- 만약 현재 페이지가 36페이지고, 한 화면에 보여줄 페이지 수가
20개라면?
-> 끝 페이지 번호: 40번
공식: Math.ceil(현재 위치한 페이지 번호 / 한 화면당 보여질 페이지 버튼 수)
* 한 화면당 보여질 페이지 버튼 수
# 4. 시작 페이지 번호 계산
- 현재 위치한 페이지가 15페이지고, 한 화면에 보여줄 페이지 버튼이 10개면?
-> 시작 페이지 번호: 11번
- 현재 위치한 페이지가 73번이고, 한 화면에 버튼 20개씩 보여준다면?
-> 시작 페이지 번호: 61번
공식: (끝 페이지 번호 - 한 화면에 보여줄 페이지 버튼 수) + 1
# 5. 끝 페이지 보정
- 총 게시물 수가 324개이고, 한 페이지당 10개의 게시물을 보여준다.
- 그리고 이 사람은 현재 31페이지를 보고 있다.
- 그리고 한 화면에 페이지 버튼은 10개가 배치된다.
- 그렇다면, 위 공식에 의한 끝 페이지는 몇 번으로 계산되는가? -> 40번
- 하지만, 실제 끝 페이지는 몇 번에서 끝나면 되는가? -> 33번
# 5-1. 이전 버튼 활성화 여부 설정
- 시작 페이지 번호가 1로 구해진 시점에서는 비활성, 나머지는 활성 처리.
# 5-2. 다음 버튼 활성화 여부 설정
공식: 보정 전 끝 페이지 번호 x 한 페이지에 들어갈 게시물 수 >= 총 게시물 수
-> 비활성
# 5-3. 끝 페이지 값 보정
- 다음 버튼이 비활성화 되었다면 총 게시물 수에 맞춰 끝 페이지 번호를
재 보정합니다.
공식: Math.ceil(총 게시물의 개수 / 한 페이지에 보여줄 게시물 수)
IBoardDAO
에 다음과 같은 메서드 선언
//총 게시물 수를 알려주는 메서드
int countArticles();
//글 전체 목록을 가지고 오는 메서드
List<BoardVO> listBoard(PageVO paging);
BoardDAO
에 메서드 오버라이딩하여 구현
@Override
public List<BoardVO> listBoard(PageVO paging) {
List<BoardVO> articles = new ArrayList<>();
String sql = "SELECT * FROM" +
" (" +
" SELECT ROWNUM AS rn, tbl.* FROM" +
" (" +
" SELECT * FROM my_board" +
" ORDER BY board_id DESC" +
" ) tbl" +
" )" +
"WHERE rn > " + (paging.getPage()-1) * paging.getCountPerPage()
+ " AND rn <= " + paging.getPage() * paging.getCountPerPage();
try(Connection conn = ds.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql);
ResultSet rs = pstmt.executeQuery()) {
while(rs.next()) {
BoardVO vo = new BoardVO(
rs.getInt("board_id"),
rs.getString("writer"),
rs.getString("title"),
rs.getString("content"),
rs.getTimestamp("reg_date"),
rs.getInt("hit")
);
articles.add(vo);
}
} catch (Exception e) {
e.printStackTrace();
}
return articles;
}
쿼리 부분이 상당히 복잡해 보이지만 별 거 없다.
먼저 가장 안쪽 쿼리는 모든 게시물을 board_id
기준 내림차순으로 가져오는 부분이고, 이를 tbl이라고 테이블 이름을 명명한 다음 각 row
들에 대해서 rownum
을 붙여준 후, 밑의 WHERE
절 파트에서 가져올 페이지 번호를 지정하고 있다.
@Override
public int countArticles() {
int count = 0;
String sql = "SELECT COUNT(*) FROM my_board";
try(Connection conn = ds.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql);
ResultSet rs = pstmt.executeQuery()) {
if(rs.next()) {
count = rs.getInt("count(*)");
}
} catch (Exception e) {
e.printStackTrace();
}
return count;
}
package kr.co.jsp.board.commons;
//사용자가 선택하는 페이지의 전반적인 정보를 담아놓을 클래스
public class PageVO {
private int page; //사용자가 선택한 페이지 번호
private int countPerPage; //한 화면에 보여질 게시물의 수
public PageVO() {
page = 1;
countPerPage = 10;
}
public int getPage() {
return page;
}
public void setPage(int page) {
this.page = page;
}
public int getCountPerPage() {
return countPerPage;
}
public void setCountPerPage(int countPerPage) {
this.countPerPage = countPerPage;
}
@Override
public String toString() {
return "PageVO [page=" + page + ", countPerPage=" + countPerPage + "]";
}
}
package kr.co.jsp.board.commons;
public class PageCreator {
//페이지번호와 한 페이지당 들어갈 게시물의 개수를 갖고 있는 객체
private PageVO paging;
private int articleTotalCount; //총 게시물의 개수
private int beginPage; //시작 페이지 번호
private int endPage; //끝 페이지 번호
private boolean prev; //이전 버튼 활성화 여부
private boolean next; //다음 버튼 활성화 여부
//한 화면에 보여질 페이지 버튼 수
private final int displayBtn = 10;
//페이징 알고리즘을 수행할 메서드 선언.
private void calcDataOfPage() {
//보정 전 끝 페이지 구하기
endPage = (int) (Math.ceil(paging.getPage() / (double)displayBtn) * displayBtn);
//시작 페이지 번호 구하기
beginPage = (endPage - displayBtn) + 1;
//현재 시작 페이지가 1이라면 이전버튼 비활성화
prev = (beginPage == 1) ? false : true;
//마지막 페이지인지 여부 확인 후 다음 버튼 비활성 판단
next = (articleTotalCount <= (endPage * paging.getCountPerPage())) ? false : true;
//다음 버튼 비활성화라면 끝 페이지 보정하기
if(!next) {
endPage = (int) Math.ceil(articleTotalCount / (double) paging.getCountPerPage());
}
}
public PageVO getPaging() {
return paging;
}
public void setPaging(PageVO paging) {
this.paging = paging;
}
public int getArticleTotalCount() {
return articleTotalCount;
}
public void setArticleTotalCount(int articleTotalCount) {
this.articleTotalCount = articleTotalCount;
//Service클래스가 이 메서드를 통해 총 게시물의 개수를 전달할 때
//즉시 알고리즘이 가동될 수 있도록.
calcDataOfPage();
}
public int getBeginPage() {
return beginPage;
}
public void setBeginPage(int beginPage) {
this.beginPage = beginPage;
}
public int getEndPage() {
return endPage;
}
public void setEndPage(int endPage) {
this.endPage = endPage;
}
public boolean isPrev() {
return prev;
}
public void setPrev(boolean prev) {
this.prev = prev;
}
public boolean isNext() {
return next;
}
public void setNext(boolean next) {
this.next = next;
}
@Override
public String toString() {
return "PageCreator [paging=" + paging + ", articleTotalCount=" + articleTotalCount + ", beginPage=" + beginPage
+ ", endPage=" + endPage + ", prev=" + prev + ", next=" + next + ", displayBtn=" + displayBtn + "]";
}
}
package kr.co.jsp.board.service;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import kr.co.jsp.board.commons.PageCreator;
import kr.co.jsp.board.commons.PageVO;
import kr.co.jsp.board.model.BoardDAO;
import kr.co.jsp.board.model.BoardVO;
public class GetListService implements IBoardService {
@Override
public void execute(HttpServletRequest request, HttpServletResponse response) {
PageVO paging = new PageVO();
BoardDAO dao = BoardDAO.getInstance();
//사용자가 처음 게시판에 들어올 때는 페이지 선택을 하지 않기 때문에
//페이지 선택을 1페이지로, 게시물 개수를 10개로 지정해 주겠습니다.
if(request.getParameter("page") != null) {
paging.setPage(Integer.parseInt(request.getParameter("page")));
paging.setCountPerPage(Integer.parseInt(request.getParameter("cpp")));
}
System.out.println("선택한 페이지: " + paging.getPage());
System.out.println("게시물 수: " + paging.getCountPerPage());
List<BoardVO> boardList = dao.listBoard(paging);
//페이지 버튼 배치를 위해 PageCreator 객체를 생성.
PageCreator pc = new PageCreator();
//페이징 버튼 알고리즘을 위해 PageVO객체와 총 게시물 수를 setter로 전달.
pc.setPaging(paging);
pc.setArticleTotalCount(dao.countArticles());
//알고리즘 결과가 잘 초기화 되었는지를 간단하게 확인.
System.out.println(pc); // pc.toString()
//1일 이내 신규글 new마크 처리 로직
for(BoardVO vo : boardList) {
//운영체제의 현재 시간을 읽어서 밀리초로 리턴하는 메서드
//1970년 1월 1일 자정을 기준으로 현재까지 흐른 시간을
//밀리초로 리턴합니다.
long now = System.currentTimeMillis();
//게시물의 작성 시간을 밀리초로 읽어오기
long regTime = vo.getRegDate().getTime();
if(now - regTime < 60 * 60 * 24 * 1000) {
vo.setNewMark(true);
}
}
request.setAttribute("bList", boardList);
//jsp 파일에서 버튼 배치를 위해, 모든 정보가 완성된 PageCreator 객체를
//request 객체에 담아서 forwarding 하겠다~
request.setAttribute("pc", pc);
}
}
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
<style>
tbody{
font-size: 20px;
}
.btn-countPerPage {
background-color: gray;
color: white;
}
</style>
</head>
<body>
<%--
로그인하지 않은 사용자가 게시판에 들어왔을 경우 돌려보내는 코드를 작성.
--%>
<c:if test="${user == null}">
<script>
alert("회원만 이용 가능한 게시판입니다. 로그인 해 주세요.");
//board_list.jsp로 직접 요청이 들어가는 경우는 없기 때문에
//컨트롤러를 기준으로 상대 경로로 작성하시든지, 절대 경로로 작성해야 합니다.
location.href="user/user_login.jsp";
</script>
</c:if>
<%-- <c:if test="${searchFail}">
<script>
alert("조회 결과가 없습니다.");
location.href="/MyWeb/list.board";
</script>
</c:if> --%>
<jsp:include page="../include/header.jsp"/>
<div class="container">
<h2>My Web게시판</h2>
<span style="float: right; margin-bottom: 15px">
<input class="btn btn-countPerPage" type="button" value="10" onclick="location.href='/MyWeb/list.board?page=1&cpp=10'">
<input class="btn btn-countPerPage" type="button" value="20" onclick="location.href='/MyWeb/list.board?page=1&cpp=20'">
<input class="btn btn-countPerPage" type="button" value="30" onclick="location.href='/MyWeb/list.board?page=1&cpp=30'">
</span>
<table class="table table-secondary table-hover table-bordered">
<thead style="font-size: 25px">
<tr>
<th>글 번호</th>
<th>작성자</th>
<th>제목</th>
<th>날짜</th>
<th>조회수</th>
</tr>
</thead>
<tbody>
<c:forEach var="b" items="${bList}">
<tr>
<td>${b.boardId}</td>
<td>${b.writer}</td>
<td>
<a href="/MyWeb/content.board?bId=${b.boardId}&page=${param.page}&cpp=${param.cpp}">${b.title}</a>
<c:if test="${b.newMark}">
<img alt="newMark" src="/MyWeb/img/icon_new.gif">
</c:if>
</td>
<td>
<fmt:formatDate value="${b.regDate}" pattern="yyyy년 MM월 dd일 a hh시 mm분"/>
</td>
<td>${b.hit}</td>
</tr>
</c:forEach>
</tbody>
<%-- 페이징을 처리할 구간 --%>
<tbody>
<tr>
<td colspan="5" align="center">
<ul class="pagination pagination-lg">
<%-- 이전 버튼 --%>
<c:if test="${pc.prev}">
<li class="page-item"><a class="page-link"
href="/MyWeb/list.board?page=${pc.beginPage-1}&cpp=${pc.paging.countPerPage}"
style="background-color: #643691; margin-top: 0; height: 40px; color: white; border: 0px solid #f78f24; opacity: 0.8">이전</a>
</li>
</c:if>
<%-- 페이지 버튼 --%>
<c:forEach var="pageNum" begin="${pc.beginPage}" end="${pc.endPage}">
<li class="page-item">
<a href="/MyWeb/list.board?page=${pageNum}&cpp=${pc.paging.countPerPage}" class="page-link"
style="margin-top: 0; height: 40px; color: pink; border: 1px solid #643691; ${pageNum == pc.paging.page ? 'background-color: orange;' : ''}">${pageNum}</a>
</li>
</c:forEach>
<%-- 다음 버튼 --%>
<c:if test="${pc.next}">
<li class="page-item"><a class="page-link"
href="/MyWeb/list.board?page=${pc.endPage + 1}&cpp=${pc.paging.countPerPage}"
style="background-color: #643691; margin-top: 0; height: 40px; color: white; border: 0px solid #f78f24; opacity: 0.8">다음</a>
</li>
</c:if>
</ul>
</td>
</tr>
</tbody>
<tbody>
<tr>
<td colspan="5" align="right">
<form action="/MyWeb/search.board" class="form-inline" >
<div class="form-group">
<select name="category" class="form-control">
<option value="title">제목</option>
<option value="writer">작성자</option>
<option value="content">내용</option>
</select>
<input type="text" name="search" placeholder="검색어 입력" class="form-control" >
<input type="submit" value="검색" class="btn btn-default">
<input type="button" value="글 작성" class="btn btn-default" onclick="location.href='/MyWeb/write.board'">
</div>
</form>
</td>
</tr>
</tbody>
</table>
</div>
<jsp:include page="../include/footer.jsp"/>
</body>
</html>
주목 포인트
<%-- 이전 버튼 --%> <c:if test="${pc.prev}"> <li class="page-item"><a class="page-link" href="/MyWeb/list.board?page=${pc.beginPage-1}&cpp=${pc.paging.countPerPage}" style="background-color: #643691; margin-top: 0; height: 40px; color: white; border: 0px solid #f78f24; opacity: 0.8">이전</a> </li> </c:if>
예. 56페이지에 있을 때 이전 페이지를 누르면 51번 페이지로 넘어가고, -1을 해서 50번부터 보여줌. (5페이지씩 넘어가도록 되어 있음 현재는)
로그인하기 전에 사용자가 게시판으로 접근하는 경우를 미연에 방지하고자 한 기존 방법과 다른 방법을 사용할 예정
package kr.co.jsp.board.commons;
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
@WebFilter("/*")
public class TestFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
System.out.println("필터 동작!");
chain.doFilter(request, response);
}
}
package kr.co.jsp.board.commons;
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
@WebFilter("*.board") //.board로 끝나는 모든 요청에 반응.
public class LoginAuthFilter implements Filter {
public LoginAuthFilter() {}
public void destroy() {
//필터 객체가 제거될 때 실행하는 메서드
//보통 초기화 시 생성했던 자원들을 종료하는 기능에 사용합니다.
}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
/*
- 필터의 핵심 메서드! doFilter()는 클라이언트의 요청이 있을 때마다 실행됩니다.
- request객체와 response객체를 넘겨주기 때문에 이를 가지고 요청과 응답을 조작할 수 있습니다.
- 그리고 FilterChain을 통해 조작 이후 요청을 원래 목적지인 서블릿으로 전달할 수 있습니다.
*/
System.out.println("doFilter 메서드 발동!");
//우리가 평소에 사용하던 타입은 HttpServletRequest입니다.
//부모타입의 ServletRequest를 자식타입으로 끌어 내리시면 사용이 가능합니다.
HttpSession session = ((HttpServletRequest)request).getSession();
}
public void init(FilterConfig fConfig) throws ServletException {
// 웹컨테이너(서버)가 시작될 때 필터 객체를 생성하는데,
// 이 때 객체가 생성되면서 최초 한 번 호출되는 메서드입니다.
// 필터에서 처리 시 필요한 객체등을 초기화 (JDBC관련 설정) 하는 데 사용합니다.
System.out.println("로그인 권한 필터 객체 생성!");
}
}
*.board 요청이 들어올때마다 filter를 거쳐서 실행될 것.