today_is
[ jsp ] MVC패턴 - Servlet 활용, 게시판 페이징 및 검색 본문
오늘의 목표
앞서 MVC패턴의 로직 분리에 대해서 배웠으니,
페이징을 적용한 게시판 목록 및 검색 기능을 구현해보자
BoardDTO
idx | number | default board2_seq.nextval | primary key |
title | varchar2(500) | not null | |
writer | varchar2(100) | not null | |
content | varchar2(4000) | not null | |
image | varchar2(500) | ||
ipaddr | varchar2(100) | not null | |
viewCount | number | default 0 | |
writeDate | date | default sysdate |
BoardDAO
package board2;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.sql.DataSource;
public class BoardDAO {
private Connection conn;
private PreparedStatement pstmt;
private ResultSet rs;
private Context init;
private DataSource ds;
private static BoardDAO instance = new BoardDAO();
public static BoardDAO getInstance() {
return instance;
}
private BoardDAO() {
try {
init = new InitialContext();
ds = (DataSource) init.lookup("java:comp/env/jdbc/oracle");
} catch (Exception e) {
e.printStackTrace();
}
}
private void close() {
try {
if(rs != null) rs.close();
if(pstmt != null) pstmt.close();
if(conn != null) conn.close();
} catch (Exception e) {}
}
private BoardDTO mapping(ResultSet rs) throws SQLException {
BoardDTO dto = new BoardDTO();
dto.setIdx(rs.getInt("idx"));
dto.setTitle(rs.getString("title"));
dto.setWriter(rs.getString("writer"));
dto.setContent(rs.getString("content"));
dto.setImage(rs.getString("image"));
dto.setIpaddr(rs.getString("ipaddr"));
dto.setViewCount(rs.getInt("viewCount"));
dto.setWriteDate(rs.getDate("writeDate"));
dto.setDeleted(rs.getInt("deleted"));
return dto;
}
// 게시글 목록 불러오기
public List<BoardDTO> selectList(String search, Paging paging) {
ArrayList<BoardDTO> list = new ArrayList<>();
String sql = "select * from board2 "
+ " where "
+ " deleted = 0 and"
+ " (title like '%' || ? || '%' or "
+ " writer like '%' || ? || '%' or "
+ " content like '%' || ? || '%') "
+ " order by idx desc"
+ " offset ? rows"
+ " fetch next ? rows only";
try {
conn = ds.getConnection();
pstmt = conn.prepareStatement(sql);
pstmt.setString(1, search);
pstmt.setString(2, search);
pstmt.setString(3, search);
pstmt.setInt(4, paging.getOffset()); // 페이징
pstmt.setInt(5, paging.getFetch());
rs = pstmt.executeQuery();
while(rs.next()) {
list.add(mapping(rs));
}
} catch (Exception e) {
e.printStackTrace();
}
finally {
close();
}
return list;
}
// 게시글 조회 1개 (ip 주소를 일부 가리기)
public BoardDTO selectOne(int idx) {
BoardDTO dto = null;
String sql = "select * from board2 where idx = ?";
try {
conn = ds.getConnection();
pstmt = conn.prepareStatement(sql);
pstmt.setInt(1, idx);
rs = pstmt.executeQuery();
while(rs.next()) {
dto = mapping(rs);
dto.setIpaddr(maskIPaddr(dto.getIpaddr()));
}
} catch (Exception e) {
e.printStackTrace();
}
finally {
close();
}
return dto;
}
private String maskIPaddr(String src) { // 아이피 주소 원본(DB에서 가져온 값)
String dst = "";
int dotCount = 0;
for(int i = 0; i < src.length(); i++) {
char ch = src.charAt(i);
if(ch == '.') dotCount += 1;
if(dotCount >= 2 && '0' <= ch && ch <= '9') {
dst += '*';
}
else {
dst += ch;
}
}
return dst;
}
// 게시글 작성
public int insert(BoardDTO dto) {
int row = 0;
String sql = "insert into board2 (title, writer, content, image, ipaddr) "
+ " values (?, ?, ?, ?, ?)";
try {
conn = ds.getConnection();
pstmt = conn.prepareStatement(sql);
pstmt.setString(1, dto.getTitle());
pstmt.setString(2, dto.getWriter());
pstmt.setString(3, dto.getContent());
pstmt.setString(4, dto.getImage());
pstmt.setString(5, dto.getIpaddr());
row = pstmt.executeUpdate();
} catch (Exception e) {
e.printStackTrace();
}
finally {
close();
}
return row;
}
// 삭제(update) : 삭제 처리하고 나서
// 리스트를 불러올때 deleted 값이 0 인 항목만 불러오기
// (== 진짜 삭제가 아님. deleted = 1 인 애들만 보이지 않게)
// deleted = 1 - deleted : 업데이트 할때마다 deleted 의 값이 1 <-> 0 로 왔다갔다하기
public int delete(int idx) {
int row = 0;
String sql = "update board2 set "
+ " deleted = 1 - deleted "
+ " where idx = ?";
try {
conn = ds.getConnection();
pstmt = conn.prepareStatement(sql);
pstmt.setInt(1, idx);
row = pstmt.executeUpdate();
} catch (Exception e) {
e.printStackTrace();
}
finally {
close();
}
return row;
}
// 내가 쓴 글만 볼 수 있는 list
public List<BoardDTO> selectListByWriter(String userid) {
ArrayList<BoardDTO> list = new ArrayList<>();
String sql = "select * from board2 "
+ " where writer = ?";
try {
conn = ds.getConnection();
pstmt = conn.prepareStatement(sql);
pstmt.setString(1, userid);
rs = pstmt.executeQuery();
while(rs.next()) {
list.add(mapping(rs));
}
} catch (Exception e) {
e.printStackTrace();
}
finally {
close();
}
return list;
}
// 페이징
// 게시글 개수 불러오기
public int selectCount(String search) {
int count = 0;
String sql = "select count(*) from board2 "
+ " where "
+ " deleted = 0 and"
+ " (title like '%' || ? || '%' or "
+ " writer like '%' || ? || '%' or "
+ " content like '%' || ? || '%') ";
try {
conn = ds.getConnection();
pstmt = conn.prepareStatement(sql);
pstmt.setString(1, search);
pstmt.setString(2, search);
pstmt.setString(3, search);
rs = pstmt.executeQuery();
while(rs.next()) {
count = rs.getInt(1);
}
} catch (Exception e) {
e.printStackTrace();
}
finally {
close();
}
return count;
}
// 조회수 증가
public int updateViewCount(int idx) {
int row = 0;
String sql = "update board2 set viewCount = viewCount + 1 where idx = ?";
try {
conn = ds.getConnection();
pstmt = conn.prepareStatement(sql);
pstmt.setInt(1, idx);
row = pstmt.executeUpdate();
} catch (Exception e) {
e.printStackTrace();
}
finally {
close();
}
return row;
}
// 댓글 작성
public int insertReply(ReplyDTO dto) {
int row = 0;
String sql = "insert into reply2 (parent_idx, reply_depth, board_idx, writer, content) "
+ " values (? ,? ,? ,? ,?)";
try {
conn = ds.getConnection();
pstmt = conn.prepareStatement(sql);
pstmt.setInt(1, dto.getParent_idx());
pstmt.setInt(2, dto.getReply_depth());
pstmt.setInt(3, dto.getBoard_idx());
pstmt.setString(4, dto.getWriter());
pstmt.setString(5, dto.getContent());
row = pstmt.executeUpdate();
} catch (Exception e) {
e.printStackTrace();
}
finally {
close();
}
return row;
}
// 댓글 목록 불러오기
public List<ReplyDTO> selectReplyList(int idx) {
ArrayList<ReplyDTO> list = new ArrayList<ReplyDTO>();
String sql = "select * from reply2"
+ " where board_idx = ?"
+ " start with parent_idx = 0"
+ " connect by prior idx = parent_idx"
+ " order SIBLINGS by idx";
try {
conn = ds.getConnection();
pstmt = conn.prepareStatement(sql);
pstmt.setInt(1, idx);
rs = pstmt.executeQuery();
while(rs.next()) {
ReplyDTO dto = new ReplyDTO();
dto.setBoard_idx(rs.getInt("board_idx"));
dto.setContent(rs.getString("content"));
dto.setIdx(rs.getInt(idx));
dto.setParent_idx(rs.getInt("parent_idx"));
dto.setReply_depth(rs.getInt("reply_depth"));
dto.setWriteDate(rs.getDate("writeDate"));
dto.setWriter(rs.getString("writer"));
list.add(dto);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
close();
}
return list;
}
}
Paging
package board2;
public class Paging {
private int page;
private int perPage;
private int boardCount;
private int offset; // offset : 건너뛸 페이지 개수
private int fetch; // fetch : 그 다음 불러올 게시글 개수 (== perPage)
private int pageCount; // 전체 페이지 개수
private int section; // 페이지를 10개씩 묶어서 하나의 구역으로 설정하고 0부터 시작
private boolean prev; // 이전 구역이 있으면 true
private boolean next; // 다음 구역이 있으면 true
private int begin; // 구역 시작 페이지
private int end; // 구역 끝 페이지(pageCount 보다 크면 안된다 !! == 게시글 개수보다 클 수가 없음)
// 생성자를 대신할 static method
// 싱글톤으로 객체를 생성하면 모두 같은 페이지만 볼 수 밖에 없음
// 사용자에 따라서 페이지 번호를 전달받아서
// 각기 다르게 처리해주어야하기 때문이다
public static Paging newInstance(int page, int boardCount) {
return new Paging(page, boardCount);
}
// 생성자 : 페이지와 보드 카운트를 전달 받았을때
// 나머지 컬럼의 값을 채울 수 있게
private Paging(int page, int boardCount) { // boardCount 에서 page 내용만 불러오기
this.page = page;
this.boardCount = boardCount;
perPage = 15; // 화면에 출력될 페이지 개수
offset = (page - 1) * perPage;
fetch = perPage;
pageCount = boardCount / perPage;
pageCount += (boardCount % perPage != 0) ? 1: 0;
section = (page - 1) / 10; // 구역 : 1 ~ 10(== 0) , 11 ~ 20(== 1)
begin = section * 10 + 1;
end = begin + 9;
prev = section != 0; // 이전은 섹션이 0이 아닐때만 존재 (prev = true)
next = pageCount > end;
if(end > pageCount) { // end는 pageCount 보다 크면 안된다 !! == 게시글 개수보다 클 수가 없음
end = pageCount;
next = false; // 끝 페이지이기 때문에 더 이상 다음 페이지가 없음(false 로 next 를 출력시키지 않음)
}
}
public int getPage() {
return page;
}
public void setPage(int page) {
this.page = page;
}
public int getPerPage() {
return perPage;
}
public void setPerPage(int perPage) {
this.perPage = perPage;
}
public int getBoardCount() {
return boardCount;
}
public void setBoardCount(int boardCount) {
this.boardCount = boardCount;
}
public int getOffset() {
return offset;
}
public void setOffset(int offset) {
this.offset = offset;
}
public int getFetch() {
return fetch;
}
public void setFetch(int fetch) {
this.fetch = fetch;
}
public int getPageCount() {
return pageCount;
}
public void setPageCount(int pageCount) {
this.pageCount = pageCount;
}
public int getSection() {
return section;
}
public void setSection(int section) {
this.section = section;
}
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;
}
public int getBegin() {
return begin;
}
public void setBegin(int begin) {
this.begin = begin;
}
public int getEnd() {
return end;
}
public void setEnd(int end) {
this.end = end;
}
}
Java코드 | JSP |
Servlet | View |
데이터를 가져오는 역할을 수행 | 처리한 데이터를 화면에 출력하는 역할 |
BoardServlet
데이터 준비 및 로직에 필요한 자바 객체는 미리 생성해두기
-> private BoardDAO boardDAO = BoardDAO.getInstance();
[ doGet이 실행될때의 결과 ]
: getRequestDispatcher 를 사용해서 url 값으로 다시 넘긴다
이때, forward 를 이용하여 req, res 를 같이 보내게 됨
-> req.getRequestDispatcher(prefix + "board" + suffix).forward(req, resp)
즉, 실행할때마다 search 값을 받아서 값들을 보내고,
board.jsp 를 통해 list 를 화면에 출력시킴
package day22;
import java.io.IOException;
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 board2.BoardDAO;
import board2.Paging;
@WebServlet("/board")
public class BoardServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private final String prefix = "/WEB-INF/views/";
private final String suffix = ".jsp";
private BoardDAO boardDAO = BoardDAO.getInstance();
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String search = req.getParameter("search");
if(search == null) {
search = "";
}
int count = boardDAO.selectCount();
String paramPage = req.getParameter("page");
int page = Integer.parseInt(paramPage == null ? "1" : paramPage);
Paging paging = Paging.newInstance(page, count);
req.setAttribute("list", boardDAO.selectList(search, paging));
req.setAttribute("paging", paging);
req.getRequestDispatcher(prefix + "board" + suffix).forward(req, resp);
}
}
board.jsp
: BoardServlet 에서 paging 객체를 넣어준다면 jsp에서도 페이징을 참조함
BoardServlet 에 있는 setAttribute 의 결과가 board.jsp의 c:forEach 로 출력된다
[ BoardServlet ] req.setAttribute("list", boardDAO.selectList(search, paging))
[ board.jsp ] <c:forEach var="dto" items="${list }">
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<c:set var="cpath" value="${pageContext.request.contextPath }" />
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<table id="boardList">
<thead>
<tr>
<th>번호</th>
<th>제목</th>
<th>작성자</th>
<th>조회수</th>
<th>날짜</th>
</tr>
</thead>
<c:forEach var="dto" items="${list }">
<tr>
<td>${dto.idx }</td>
<td><a href="${cpath }/view.jsp?idx=${dto.idx }">
${dto.title }
${not empty dto.image ? '💾' : '' }
</a>
</td>
<td>${dto.writer }</td>
<td>${dto.viewCount }</td>
<td>${dto.writeDate }</td>
</tr>
</c:forEach>
</table>
<div class="center">
<c:if test="${paging.prev }">
<a href="${cpath }/board?page=${paging.begin - 10}&search=${param.search}">[이전]</a>
</c:if>
<c:forEach var="i" begin="${paging.begin }" end="${paging.end }">
<a class="${paging.page == i ? 'bold' : '' }"
href="${cpath }/board?page=${i}&search=${param.search }">[${i }]</a>
</c:forEach>
<c:if test="${paging.next }">
<a href="${cpath }/board?page=${paging.end + 1}&search=${param.search}">[다음]</a>
</c:if>
</div>
<div>
<form>
<input type="search" name="search" value="${param.search }" placeholder="검색어 입력">
<input type="submit" value="검색">
</form>
</div>
</body>
</html>
study_review
기존에는 jsp 페이지로 모든것을 작성하고,
form과 데이터 처리를 합쳐서 작성하는 경우가 많았지만
이번 실습을 통해서,
같은 기능을 구현하더라도 로직을 분리해서 구현하는 방법에 대해서 배우게 되었다.
앞전 실습에서는 doGet 과 doPost를 나누어서 다루었기 때문에
form의 제출 여부에 따라서 코드를 따로 작성했었다.
이번에는 doGet으로만으로도 search 와 paging 을 처리해보았다.
또한 Servlet에서 getParameter 를 이용하여 search 값을 받아올때,
search 가 빈문자열인 경우까지 생각해서, if문을 추가하여 코드의 완성도를 높일 수 있었다 !!
MVC패턴에 어느정도 익숙해졌으니
다음은 Spring 을 본격적으로 배워볼 예정이다
'java' 카테고리의 다른 글
[ jsp ] MVC 패턴 (0) | 2023.12.24 |
---|---|
[ jsp ] 회원 + 게시판 + 댓글 (0) | 2023.12.16 |
[ jsp ] 회원 + 게시판 (0) | 2023.12.14 |
[ jsp ] 회원기능 구현 (2) - 수정, 삭제 (0) | 2023.12.12 |
[ jsp ] 회원기능 구현 (1) - 회원가입, 로그인, 로그아웃 (0) | 2023.12.12 |