JSP/Servlet - 게시판 - 게시글 기능!

게시글 기능

전편에서 회원 관련 기능(로그인, 로그아웃, 비밀번호 변경)을 구현하였고

이번엔 게시판 관련 기능(게시판 목록보기, 게시글 쓰기, 게시글 보기, 게시글 수정, 게시글 삭제) 를 구현해보자.

지금까지 게시판 구현은 2번이나 하였다.

https://kouzie.github.io/jsp/JSP-게시판/#

https://kouzie.github.io/jdbc/JDBC.-4일차/#

기존에 있던 소스를 사용해서 게시글 목록보기를 구현해보자.

게시글 DB 생성

먼저 게시글 DB를 생성해야 한다.

CREATE SEQUENCE seq_article21;
    
CREATE TABLE article21 (
    article_no NUMBER PRIMARY KEY, 
    writer_id VARCHAR(50) NOT NULL,
    writer_name VARCHAR(50) NOT NULL,
    title VARCHAR(255) NOT NULL,
    content CLOB,
    regdate DATE DEFAULT SYSDATE,
    moddate DATE DEFAULT SYSDATE,
    read_cnt NUMBER DEFAULT 0
);

게시글 목록 보기

게시글 목록 보기도 .../article/list.do url을 요청하면 아래 그림처럼 handler를 통해 Article객체가 들어간 ArrayList와 페이징 처리를 위해 사용했던 PageBlock객체가 포함된 ArticlePage를 반환한다.

image43

commandHandler.properties파일에 핸드러 추가

/article/list.do=board21.article.command.ListArticleHandler

게시글 관련 url패턴은 모두 앞에 /article/을 붙일 것이다.

ArticleDao - 게시글 SELECT 쿼리를 갖고있는 객체

먼저 sql문을 가지고 있는 ArticleDao를 정의하자.

https://kouzie.github.io/jsp/JSP-게시판/#게시글-검색하기

위 주소에서 사용했던 페이징 처리 기법과 게시글 검색 SELECT 쿼리 그대로 사용해서 구현할 것이다.

사용자가 입력한 검색조건, 검색문자열, 페이지 위치를 파라미터로 가져올 것이기 때문에 매개변수로 Connection객체와 request를 받는다.

public class ArticleDao {

	public ArrayList<Article> getArticleList(Connection conn, HttpServletRequest request) throws SQLException
	{
		System.out.println("ListArticleService process");
		String curPage_param = request.getParameter("currentPage");
		String searchCondition = request.getParameter("searchCondition");
		String searchWord = request.getParameter("searchWord");

		if(searchCondition == null || searchCondition.isEmpty())
			searchCondition = "1";
		if(searchWord == null || searchWord.isEmpty())
			searchWord = "*";
		int curPage;
		if(curPage_param == null || curPage_param.isEmpty())
			curPage = 1;
		else
			curPage = Integer.parseInt(curPage_param);

		int pageSize = 15; //뿌릴 게시글 개수
		int start = (curPage - 1)* pageSize + 1 ;
		int end = curPage * pageSize;

		StringBuffer sql = new StringBuffer();
		sql.append(" WITH temp AS( ");
		sql.append(" SELECT ROWNUM AS no, temp.* ");
		sql.append(" FROM ");
		sql.append(" ( ");
		sql.append("    SELECT article_no, writer_name, title, content, regdate, read_cnt ");
		sql.append("    FROM article21 ");
		System.out.printf("searchCondition: %s\n", searchCondition);
		switch (Integer.parseInt(searchCondition)) {
		case 1: //제목
			sql.append("    WHERE REGEXP_LIKE(title, ?, 'i') ");
			break;
		case 2: //내용
			sql.append("    WHERE REGEXP_LIKE(content, ?, 'i') ");
			break;
		case 3: //글쓴이
			sql.append("    WHERE REGEXP_LIKE(writer_name, ?, 'i') ");
			break;
		case 4: //제목+내용
			sql.append("    WHERE REGEXP_LIKE(title, ?, 'i') OR REGEXP_LIKE(content, ?, 'i') ");
			break;

		default:
			break;
		}
		sql.append("    ORDER BY article_no desc ");
		sql.append("    )temp ");
		sql.append(" ) ");
		sql.append(" SELECT temp.* FROM temp ");
		sql.append(" WHERE temp.no BETWEEN ? AND ? ");

		PreparedStatement pstmt = null;
		ResultSet rs = null;
		ArrayList<Article> article_list = null;
		try {
			pstmt = conn.prepareStatement(sql.toString());
			pstmt.setString(1,  searchWord);
			if(Integer.parseInt(searchCondition) == 4)
			{
				pstmt.setString(2,  searchWord);
				pstmt.setInt(3,  start);
				pstmt.setInt(4,  end);
			}
			else
			{
				pstmt.setInt(2,  start);
				pstmt.setInt(3,  end);
			}
			rs = pstmt.executeQuery();
			if(rs.next())
			{
				Article adto = null;
				article_list = new ArrayList<>();
				do {
					adto = new Article(
							rs.getInt("article_no"),
							new Writer("#", rs.getString("writer_name")),
							"title",
							"content",
							rs.getDate("regdate"),
							null,
							rs.getInt("read_cnt")
							);
					String title = rs.getString("title");
					if(!searchWord.equals("*"))
						title = title.replaceAll("(?i)"+searchWord, "<span class='searchWord'>"+searchWord+"</span>");
					adto.setTitle(title);
					article_list.add(adto);
				} while (rs.next());
			}
		} 
		finally {
			JdbcUtil.close(rs);
			JdbcUtil.close(pstmt);
		}
		return article_list;
	}
  ...



PageBlock객체를 반환하는 getPageBlock()메서드 역시 페이징 처리를 하려면 검색조건, 검색명, 몇페이지인지 알아야 한다.

따라서 Connection객체와 request객체를 매개변수로 받는다.

	...
	public PageBlock getPageBlock(Connection conn, HttpServletRequest request) throws SQLException, NamingException
	{
		
		int pageSize = 15; //뿌릴 게시글 개수
		int numberOfBlock = 10; //페이지 블록 10page 생성
		
		String curPage_param = request.getParameter("currentPage");
		String searchCondition = request.getParameter("searchCondition");
		String searchWord = request.getParameter("searchWord");

		if(searchCondition == null || searchCondition.isEmpty())
			searchCondition = "1";
		if(searchWord == null || searchWord.isEmpty())
			searchWord = "*";
		int curPage;
		if(curPage_param == null || curPage_param.isEmpty())
			curPage = 1;
		else
			curPage = Integer.parseInt(curPage_param);
		
		PageBlock pageBlock = new PageBlock();
		pageBlock.setCurPage(curPage);
		pageBlock.setNumberPerPage(pageSize);
		pageBlock.setNumberOfBlock(numberOfBlock); //페이지 블록 10개씩 출력
		pageBlock.getSearchNumberOfPages(conn, pageSize, Integer.parseInt(searchCondition), searchWord); 
		//바뀐 페이징 처리 함수
		int pageBlockStart = (curPage-1)/numberOfBlock * numberOfBlock + 1;
		int pageBlockEnd = (curPage-1)/numberOfBlock * numberOfBlock + numberOfBlock;
		if(pageBlockEnd > pageBlock.getNumberOfBlocks())
			pageBlockEnd = pageBlock.getNumberOfBlocks();
		pageBlock.setStart(pageBlockStart);
		pageBlock.setEnd(pageBlockEnd);
		pageBlock.prev = pageBlock.getStart() == 1 ? false : true; //start가 1이면 << 보일 필요 없음
		pageBlock.next = pageBlock.getEnd() == pageBlock.getNumberOfBlocks() ? false : true; //end가 pageBlock.numberOfBlock이면 >> 보일 필요 없음
		return pageBlock;
	}
}

ListArticleHandler - /article/list.do 이벤트 처리 핸들러

ListArticleHandlerListArticleService를 같이 설명하겠다.

게시글 리스트를 가져오는 과정인 ListArticleDao만 복잡하지 HandlerService객체는 얻어온 ArrayList객체와 PageBlock객체를 listArticle.jsp 페이지로 넘기기만 하면 된다.

public class ListArticleHandler implements CommandHandler {

	private ListArticleService listService = new ListArticleService();
	@Override
	public String process(HttpServletRequest request, HttpServletResponse response) 
	throws Exception {
		System.out.println("ListArticleHandler process");
		ArticlePage articlePage = listService.getArticlePage(request);
		request.setAttribute("articlePage", articlePage);
		return "/listArticle.jsp";
	}
}
public class ListArticleService {
	private ArticleDao articleDao = new ArticleDao();
	
	public ArticlePage getArticlePage(HttpServletRequest request) {
		
		try (Connection conn = ConnectionProvider.getConncection())
		{
			ArrayList<Article> article_list = articleDao.getArticleList(conn, request);
			PageBlock pageBlock = articleDao.getPageBlock(conn, request);
			return new ArticlePage(article_list, pageBlock);
		} catch (Exception e) {
			throw new RuntimeException();
		}
	}
}



request객체에 ArrayList, PageBlock을 따로 담아도 되지만 ArticlePage란 하나의 객체로 감싸서 전송한다.

ArticlePage도 필드로 ArrayListPageBlock을 가지고 있는 간단한 클래스이다.

public class ArticlePage {
	ArrayList<Article> article_list;
	PageBlock pageBlock;
	public ArticlePage(ArrayList<Article> article_list, PageBlock pageBlock) {
		this.article_list = article_list;
		this.pageBlock = pageBlock;
	}
	public ArrayList<Article> getArticle_list() {
		return article_list;
	}
	public PageBlock getPageBlock() {
		return pageBlock;
	}
}



View역할을 하는 listArticle.jsp파일은 아래와 같다.

사실 listArticle.jsp도 전에 구현했던 것을 그대로 가져다 썼다.

https://kouzie.github.io/jsp/JSP-게시판/#게시판-리스트-페이징-처리

<!-- listArticle.jsp -->
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>게시판 목록</title>
</head>
<body>
	<div align="center">
	<h2>목록보기</h2>
	<table style="border: solid 1px gray; width: 600px;">
		<thead>
			<tr>
				<th>번호</th>
				<th>제목</th>
				<th>작성자</th>
				<th>등록일</th>
				<th>조회</th>
			</tr>
		</thead>
		<tbody>
			<c:if test="${empty articlePage.article_list }">
				<tr align="center">
					<td colspan="5">등록된 게시가 없습니다</td>
				</tr>
			</c:if>
			
			<c:if test="${not empty articlePage.article_list }">
				<c:forEach items="${ articlePage.article_list }" var="dto">
					<tr align="center">
						<td>${dto.article_no }</td>
						<td>
							<a href="/jspPro/article/read.do?no=${dto.article_no }" class="subjectLink">${dto.title }</a>
						</td>
						<td>${dto.writer.name }</td>
						<td>${dto.regdate }</td>
						<td>${dto.read_cnt }</td>
					</tr>	
				</c:forEach>
			</c:if>
		</tbody>
		<tfoot>
			<tr>
				<td colspan="5" align="center">
					<div class="pagination">
						<c:if test="${ articlePage.pageBlock.prev }">
							<a href="/jspPro/article/list.do
							?currentPage=${articlePage.pageBlock.start - 1 }
							&searchCondition=${param.searchCondition}
							&searchWord=${param.searchWord}">&laquo;</a>
						</c:if>
						
						<c:forEach begin="${articlePage.pageBlock.start }" end="${articlePage.pageBlock.end }" step="1" var="i">
							<c:if test="${i eq articlePage.pageBlock.curPage }">
								<a class="active" href="#">${i }</a>
							</c:if>
							
							<c:if test="${i ne articlePage.pageBlock.curPage }">
								<a href="/jspPro/article/list.do
								?currentPage=${i }
								&searchCondition=${param.searchCondition}
								&searchWord=${param.searchWord}">${i }</a>
							</c:if>
						</c:forEach>
						<c:if test="${articlePage.pageBlock.next }">
							<a href="/jspPro/article/list.do
							?currentPage=${articlePage.pageBlock.end + 1 }
							&searchCondition=${param.searchCondition}
							&searchWord=${param.searchWord}">&raquo;</a>
						</c:if>
					</div>
				</td>
			</tr>
			<tr>
				<td colspan="5" align="center">
					<form action="" method="get">
						<select name="searchCondition" id="searchCondition">
							<option value="1">제목</option>
							<option value="2">내용</option>
							<option value="3">글쓴이</option>
							<option value="4">제목+내용</option>
						</select>
						<input type="text" name="searchWord" id="searchWord" value="${param.searchWord}"/>
						<input type="submit" value="검색"/>
						<%-- <input type="hidden" name="currentPage" value="${param.currentPage }"/> --%>
					</form>
				</td>
			</tr>
			<tr>
				<td colspan="5" align="center">
					<a href="/jspPro/index.do" style="float: left;"></a>
					<a href="/jspPro/article/write.do">글쓰기</a>
				</td>
			</tr>
		</tfoot>
	</table>
	</div>
	<script>
		$("#searchCondition").val("${ empty param.searchCondition ? 1 : param.searchCondition}");
		
		$(".subjectLink").attr( "href", function( i, val ) {
			return val + "&currentPage=${param.currentPage }&searchCondition=${param.searchCondition}&searchWord=${param.searchWord}";
		})
	</script>
</body>
</html>

테스트를 위해 테이블에 랜덤으로 게시글을 200개정도 삽입해보자.

DECLARE
BEGIN
    FOR i IN REVERSE 1 .. 200
    LOOP
        INSERT INTO article21
        (article_no, writer_id, writer_name, title, content)
        VALUES(
          seq_article21.nextval
        , dbms_random.string('U', 5)
        , dbms_random.string('A', 5)
        , dbms_random.string('A', dbms_random.value(10,20))
        , dbms_random.string('A', dbms_random.value(10,20))
        );
        commit;
    END LOOP;
END;

image54

정상적으로 페이징 처리까지 완성되었다!
사실 예전에 해놓은것…

게시글 쓰기 기능 구현

위의 listArticle.jsp파일의 글쓰기 링크는 아래와 같다.

<a href="/jspPro/article/write.do">글쓰기</a>

/article/write.do url을 요청한다.

commandHandler.properties에 아래와 같이 핸들러 추가

/article/write.do=board21.article.command.WriteArticleHandler

그리고 게시글 쓰기는 로그인 해야 사용 요청 가능하도록 /article/write.do url패턴이 LoginCheckFilter에 걸리도록 web.xml에서 설정하자.

<filter>
  <filter-name>loginCheckFilter</filter-name>
  <filter-class>board21.filter.LoginCheckFilter</filter-class>
</filter>
<filter-mapping>
  <filter-name>loginCheckFilter</filter-name>
  <url-pattern>/changePwd.do</url-pattern>
  <url-pattern>/article/write.do</url-pattern>
</filter-mapping>

이제 LoginCheckFilter에 걸리는 url패턴은 총 2개이다.


WrtieArticle 이벤트 처리과정을 살펴보자.

image44

WriteArticleHandler역시 get방식, post방식에 따라
get방식의 경우 newArticleForm.jsp로 포워딩 시키고
post방식의 경우 WriteArticleServiceAricleDao(sql)을 실행시키고 입력 결과를 출력하는 newArticleSuccess.jsp로 포워딩 시킨다.

먼저 입력 form이 있는 newArticleForm.jsp페이지와
성공화면인 newArticleSuccess.jsp페이지를 보자.

<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>게시글 입력 성공</title>
</head>
<body>
	<h3>newArticleSuccess.jsp</h3>
	게시글을 등록했습니다.
	<br> ${ctxPath = pageContext.request.contextPath ; ''}
	<a href="${ctxPath}/article/list.do">[게시글목록보기]</a>
	<a href="${ctxPath}/article/read.do?no=${ newArticleNo }">[게시글내용보기]</a>
</body>
</html>

image53

<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
	<h3>newArticleForm.jsp</h3>
	<form method="post">
		<p>
			제목:<br>
			<input type="text" name="title" value="${param.title}">
			<c:if test="${errors.title}">제목을 입력하세요.</c:if>
		</p>
		<p>
			내용:<br>
			<textarea name="content" rows="5" cols="30">${param.content}</textarea>
		</p>
		<input type="submit" value="새 글 등록">
	</form>
</body>
</html>

image52

WriteArticleHandler - /article/write.do 쓰기 이벤트 헨들러

너무많이 봐서 WriteHandler의 구조가 대충 예상이 간다.

public class WriteArticleHandler implements CommandHandler{

	private static final String FORM_VIEW = "/newArticleForm.jsp";
	private WriteArticleService writeService = new WriteArticleService();
	
	@Override
	public String process(HttpServletRequest request, HttpServletResponse response) throws Exception {
		if (request.getMethod().equalsIgnoreCase("GET")) {
			System.out.println("LoginHandler preocess GET");
			return processForm(request, response);
		}
		else if (request.getMethod().equalsIgnoreCase("POST")) {
			System.out.println("LoginHandler preocess POST");
			return processSubmit(request, response);
		}
		else {
			response.setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
			return null;
		}
	}
	private String processForm(HttpServletRequest request, HttpServletResponse response) {
		return FORM_VIEW;
	}
	private String processSubmit(HttpServletRequest request, HttpServletResponse response) throws IOException {
		Map<String, Boolean> errors = new HashMap<>();
		
		User user = (User) request.getSession().getAttribute("authUser");
		WriteRequest writeReq = createWriteRequest(user, request);
		writeReq.validate(errors);
		// title만 들어가있으면 나머지는 모두 들어가 있는것, content는 null일 수 도 있다.  
		request.setAttribute("errors", errors);
		if (!errors.isEmpty()) {
			System.out.println("errors is not empty");
			System.out.println(errors.get("title"));
			return FORM_VIEW;
		}

		int newArticleNo = writeService.write(writeReq);
		request.setAttribute("newArticleNo", newArticleNo);
		return "/newArticleSuccess.jsp";
	}
	private WriteRequest createWriteRequest(User user, HttpServletRequest request) {
		Writer writer = new Writer(user.getId(), user.getName());
		return new WriteRequest(writer, request.getParameter("title"), request.getParameter("content"));
	}
}

processSubmit()메서드만 달라졌는데 특이한점은 회원가입 때 처럼 입력값을 검증하는 WriteRequest객체를 정의하였다.

public class WriteRequest {
	private Writer writer;
	private String title;
	private String content;
	...
	...(생성자, get, set메서드)

	public void validate(Map<String, Boolean> errors) {
		if (title == null || title.trim().isEmpty()) {
			errors.put("title", Boolean.TRUE);
		}
	}
}

validate()메서드를 보면 title값이 공백이거나 null이라면 errors객체에 엔트리를 삽입

form태그로부터 정상적으로 값이 넘어왔다면 WriteRequest객체를 DB에 INSERT하기위해 WriteArticleServicewrite메서드로 넘긴다.

ArticleDao - insert메서드를 추가

게시글을 INSERT하기 위해 ArticleDao클레스에 insert메서드를 추가하자

public class ArticleDao {
	public Article insert(Connection conn, Article article) throws SQLException 
	{
		PreparedStatement pstmt = null;
		Statement stmt = null;
		ResultSet rs = null;
		
		try {
			pstmt = conn.prepareStatement("INSERT INTO article21 "
					+ "(article_no, writer_id, writer_name, title, content) "
					+ "VALUES (seq_article21.nextval, ?, ?, ?, ?)");
			pstmt.setString(1, article.getWriter().getId());
			pstmt.setString(2, article.getWriter().getName());
			pstmt.setString(3, article.getTitle());
			pstmt.setString(4, article.getContent());
			
			int insertedCount = pstmt.executeUpdate();
			
			if (insertedCount > 0) {
				stmt = conn.createStatement();
				rs = stmt.executeQuery("SELECT * FROM (SELECT * FROM article21 ORDER BY article_no desc) WHERE ROWNUM = 1");
				if (rs.next()) {
					return new Article(
							rs.getInt("article_no"), 
							new Writer(rs.getString("writer_id"), rs.getString("writer_name")), 
							rs.getString("title"), 
							rs.getString("content"), 
							rs.getDate("regdate"),
							rs.getDate("moddate"),
							rs.getInt("read_cnt")
							);
				}
			}
			return null;
		} finally {
			JdbcUtil.close(rs);
			JdbcUtil.close(stmt);
			JdbcUtil.close(pstmt);
		}
	}
	public PageBlock getPageBlock(Connection conn, HttpServletRequest request) 
	throws SQLException, NamingException { ... }

	public ArrayList<Article> getArticleList(Connection conn, HttpServletRequest request) 
	throws SQLException { ... }

ArticleDao객체엔 아직 추가할 메서드가 많다.

게시글 삭제, 수정, 조회, 조회수 증가 등 의 메서드가 더 추가될 예정이다.

WriteArticleService - insert메서드를 수행

ArticleDao에 추가한 insert메서드를 수행하는 서비스 객체를 정의하자.

게시글 추가작업에선 게시글 추가만 할 뿐 다른 부가적인 작업이 없기 때문에 트랜잭션으로 묶을 필요는 없겠지만
후의 유지보수를 생각해서 트랜잭션 처리를 해야한다.

insert와중에 예외가 발생하면 rollback시키도록 하자.

public class WriteArticleService {
	private ArticleDao articleDao = new ArticleDao();
	
	public int write(WriteRequest req) {
		Connection conn = null;
		try {
			conn = ConnectionProvider.getConnection();
			conn.setAutoCommit(false);
			
			Article article = toArticle(req);
			Article savedArticle = articleDao.insert(conn, article);
			if (savedArticle == null) {
				throw new RuntimeException("faild to insert article");
			}
			conn.commit();
			return savedArticle.getArticle_no();
		} catch (SQLException | NamingException e) {
			JdbcUtil.rollback(conn);
			throw new RuntimeException(e);
		}
		finally {
			JdbcUtil.close(conn);
		}
	}

	private Article toArticle(WriteRequest req) {
		Date now = new Date();
		Article article = new Article(
				0, 
				req.getWriter(), 
				req.getTitle(), 
				req.getContent(), 
				now, 
				now, 
				0);
		return article;
	}
}

toArticle()메서드를 통해 request객체를 매개변수로 받아 insert()메서드를 위한 DTO객체를 생성한다.

게시글 쓰기 외에도 기타 클래스에도 이런 메서드를 정의해 두면 가독성을 높일 수 있다.

게시글 조회 기능

게시글 목록에서 게시글을 클릭하거나 게시글 쓰기 완료 후 newArticleSuccess.jsp페이지에서 [게시글내용보기] 링크를 클릭하면 게시글을 보여주어야 한다.

게시글 조회 이벤트 처리 구조는 간단하다.

get, post도 나뉘지 않으며 조회할 게시글이 없어 예외가 발생할경우 404에러를 reponse객체에 담아 반환하기만 하면 된다.

image55

정상적으로 게시글을 찾았다면 Article객체에 담아 readArticle.jsp에서 출력하면 된다.

ReadArticleHandler - /article/read.do 조회 이벤트 헨들러

따라서 ReadArticleHandler정의도 몇줄 되지 않는다.
조회할 게시글 넘버만 파라미터로 전달받아 DB에서 SELECT한 뒤 View페이지 readArticle.jsp 로 이동하여 결과를 출력한다.

public class ReadArticleHandler implements CommandHandler{
	private ReadArticleService readService = new ReadArticleService();
	@Override
	public String process(HttpServletRequest request, HttpServletResponse response) throws Exception {
		int articleNum = Integer.parseInt(request.getParameter("no"));
		try {
			Article article = readService.getArticle(articleNum, true);
			request.setAttribute("article", article);
			return "/readArticle.jsp";
		} catch(ArticleNotFoundException e) {
			request.getServletContext().log("no article", e);
			response.sendError(HttpServletResponse.SC_NOT_FOUND);
			return null;
		}
	}
}

ArticleDao - 게시글조회, 조회수 증가 메서드 추가

게시글 조회는 조회작업, 조회수 증가작업 2가지를 같이 수행해햐 한다.

ArticleDaoselectById(), increaseReadCount()메서드를 정의하자.

public class ArticleDao {
	public Article insert(Connection conn, Article article) throws SQLException 
	{ ... }

	public ArrayList<Article> getArticleList(Connection conn, HttpServletRequest request) 
	throws SQLException 
	{ ...	}
	
	public PageBlock getPageBlock(Connection conn, HttpServletRequest request) 
	throws SQLException, NamingException 
	{ ...	}

	public Article selectById(Connection conn, int no) throws SQLException {
		PreparedStatement pstmt = null;
		ResultSet rs = null;
		try {
			pstmt = conn.prepareStatement("SELECT * FROM article21 WHERE article_no = ?");
			pstmt.setInt(1, no);
			rs = pstmt.executeQuery();
			Article article = null;
			if (rs.next()) {
				article = new Article(
						  rs.getInt("article_no")
						, new Writer(rs.getString("writer_id"), rs.getString("writer_name"))
						, rs.getString("title")
						, rs.getString("content")
						, rs.getDate("regdate")
						, rs.getDate("moddate")
						, rs.getInt("read_cnt"));
			}
			return article;
		} finally {
			JdbcUtil.close(rs);
			JdbcUtil.close(pstmt);
		}
	}
	public void increaseReadCount(Connection conn, int no) throws SQLException {
		try (PreparedStatement pstmt = conn.prepareStatement(
				"UPDATE article21 SET read_cnt = read_cnt+1 WHERE article_no = ?"
				))
		{
			pstmt.setInt(1, no);
			pstmt.executeUpdate();
		} 
	}
}

selectById()메서드는 매개변수로 조회할 게시글 no를 받고 결과값 Article객체를 반환한다.

ReadArticleService - 게시글 조회, 조회수 증가 메서드 호출

public class ReadArticleService {
	private ArticleDao articleDao = new ArticleDao();
	
	public Article getArticle(int articleNum, boolean increaseReadCount) throws SQLException {
		Connection conn = null;
		try	{
			conn = ConnectionProvider.getConnection();
			conn.setAutoCommit(false);
			if(increaseReadCount) { //조회수 증가여부를 결정
				articleDao.increaseReadCount(conn, articleNum);
			}
			Article article = articleDao.selectById(conn, articleNum);
			if (article == null) {
				throw new ArticleNotFoundException();
			}
			conn.commit();
			return article; 
		} catch (NamingException | SQLException e) {
			conn.rollback();
			throw new RuntimeException();
		} 
		finally {
			JdbcUtil.close(conn);
		}
	}
}

조회수를 먼저 증가시키고 Article객체를 받아온다.

increaseReadCount(), selectById()중 하나라도 예외가 발생한다면 rollback하기 때문에 조회수가 증가 됬더라도 게시글 조회에서 실패했다면 조회수 증가는 무효된다.

사실 게시판 프로젝트에서 트랜잭션다운 트랜젝션은 이 작업밖에 없는것 같다….

최종적으로 ReadArticleService객체가 게시글을 조회해서 ReadArticleHandler에 반환하면 readArticle.jsp에 출력된다.

<%@ page language="java" contentType="text/html; charset=UTF-8"	pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>JSP / Servelet Class - kouzie(2019. 5. 14.-오전 9:31:26)</title>
<style>
table, td, tr {
	border: solid 1px gray;
}

table {
	border-spacing: 3px;
	border-collapse: separate;
}
</style>
</head>
<body>
	<div align="center">
		<table>
			<tr>
				<td>이름</td>
				<td>${ article.writer.name }</td>
				<td>등록일 / 수정일</td>
				<td>${ article.regdate }/ ${ article.moddate }</td>
			</tr>
			<tr>
				<td>ID</td>
				<td>${ article.writer.id }</td>
				<td>조회</td>
				<td>${ article.read_cnt }</td>
			</tr>
			<tr>
				<td>제목</td>
				<td colspan="3">${article.title }</td>
			</tr>
			<tr>
				<td colspan="4" style="padding: 15px;">${article.content }</td>
			</tr>
			<tr>
				<td colspan="4" align="center"><a
					href="/jspPro/article/edit.do?seq=${article.article_no }">수정하기</a> <a
					href="/jspPro/article/list.do?currentPage=${ param.currentPage }
								&searchCondition=${param.searchCondition}
								&searchWord=${param.searchWord}">목록으로</a>
					<a href="/jspPro/article/delete.do?seq=${article.article_no }">삭제하기</a>
				</td>
			</tr>
		</table>
	</div>
</body>
</html>

image56

게시글 삭제 기능

게시글 수정 기능

카테고리:

업데이트: