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

게시글 기능

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

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

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

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

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

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

게시글 DB 생성

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

1
2
3
4
5
6
7
8
9
10
11
12
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{: .shadow}

commandHandler.properties파일에 핸드러 추가

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

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

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

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
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객체를 매개변수로 받는다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
	...
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 페이지로 넘기기만 하면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
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";
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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을 가지고 있는 간단한 클래스이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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-게시판/#게시판-리스트-페이징-처리

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
<!-- 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개정도 삽입해보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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{: .shadow}

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

게시글 쓰기 기능 구현

위의 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에서 설정하자.

1
2
3
4
5
6
7
8
9
<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{: .shadow}

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<%@ 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{: .shadow}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<%@ 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{: .shadow}

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
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객체를 정의하였다.

1
2
3
4
5
6
7
8
9
10
11
12
13
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메서드를 추가하자

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
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시키도록 하자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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{: .shadow}

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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()메서드를 정의하자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
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 - 게시글 조회, 조회수 증가 메서드 호출

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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에 출력된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<%@ 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{: .shadow}

게시글 삭제 기능

게시글 수정 기능