이때까지 회원정보에 대해 CRUD 작업을 진행하였다.
이번에는 이 회원들의 정보를 통해 답변형 게시판을 구현해 볼 것이다.
먼저 DB를 좀 조정해야한다. 이제까지는 회원에 대한 DB테이블을 통해 회원의 ID, PW, e-mail 등의 정보를 가졌지만
여기에 게시판글을 저장하는 테이블이 필요하다.
이 테이블은 작성자 ID가 외래키의 역할을하며 테이블의 내용에는 글 번호, 부모 글 번호(댓글에서 필요), 글 제목, 글 내용 등의 속성이 필요하다.
아래 그림의을 참고하여 이해할 수 있다.
'SQL Developer'를 사용하여 테이블을 만들고 테스트 글을 추가해보자.
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
|
DROP TABLE t_Board CASCADE CONSTRAINTS;
create table t_Board(
articleNO number(10) primary key,
parentNO number(10) default 0,
title varchar(500) not null,
content varchar2(4000),
imageFileName varchar2(30),
writedate date default sysdate not null,
id varchar2(10),
CONSTRAINT FK_ID FOREIGN KEY(id)
REFERENCES t_member(id)
);
insert into t_Board(articleNO,parentNO, title, content, imageFileName, writedate, id)
values(1,0,'테스트글....','테스트임...',null,sysdate,'PARK');
insert into t_Board(articleNO,parentNO, title, content, imageFileName, writedate, id)
values(2,0,'하이....','하이임...',null,sysdate,'SON');
insert into t_Board(articleNO,parentNO, title, content, imageFileName, writedate, id)
values(3,2,'하이의 댓글?....','...',null,sysdate,'PARK');
commit;
select * from t_board;
http://colorscripter.com/info#e" target="_blank" style="color:#e5e5e5text-decoration:none">Colored by Color Scripter
|
11,12 행은 ID 컬럼을 회원 테이블의 ID 컬럼에 대해 외래키로 지정하는 의미이핟.
첫 행의 DROP은 테이블을 지우는 명령으로 혹시 오류가 생기면 지우고 수정해서 진행하면 된다.
모두 진행 후, commit 명령으로 커밋한다.
*혹시 오류가 생길수 있는데 필자는 여기서 힌트를 얻었다.
insert 구문에서 오류가 있을 수 있는데 primary key가 없다고? 하는것 같았다.
insert 할때 t_member 테이블에 있는 ID값을 insert해야 한다. 'PARK' 와 같이 자신의 현재 테이블에 있는 ID를 확인하길 바란다.
이제 DB도 새로 만들었으니 게시판을 제대로 만들어보자.
우리가 진행할 구조이다.
뷰와 컨트롤러는 JSP와 Servlet의 기능을 수행하지만, Model은 기존의 DAO클래스 외 BoardService 클래스가 추가된다. 모델 기능은 DAO클래스가 수행하지만 실제 개발시, Service 클래스를 거쳐 DAO 클래스의 기능을 수행하도록 할 것이다.
왜냐하면 이 Service는 업무 단위, 즉 트랜잭션으로 작업을 수행하며 하나의 논리적인 기능을 하도록 할것이다.
결론적으로 유지보수나 확정성에 유리하기 때문이다. 각 글과 관련해 세부 기능을 수행하는 SQL문들을 DAO클래스에서 구현하고 Service 클래스의 단위 기능 메서드에서 DAO에 만들어 놓은 SQL문들을 조합해 단위 기능을 구현하는 방식이다.
위의 구조와 방식을 바탕으로 이제 조회, 생성, 수정, 삭제의 게시판을 만들어보자.
글 목록 보기, 새 글 추가하기
글 목록을 보기에앞서 여기서는 계층형SQL문을 이용하여 구현할 수 있다. 자세한 내용은 SQL 관련 자료를 찾아보기 바라고 우리가 사용할 DAO클래스의 query를 참고하자.
새 프로젝트를 만들어 진행해도 되고. 기존 프로젝트에 패키지를 추가하여 진행해도 무방하다.
여기서는 4개의 클래스와 2개의 JSP파일이 필요하다. webcontent에 2개의 jsp를 생성하고 패키지안에 4개의 클래스를 추가하자.
현재 각 클래스와 jsp에는 글 목록 조회하기와 글쓰기 기능이 모두 담겨있다. 함께 보도록 하자. JSP는 코드만 첨부한다.
BoardController.java
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
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
|
package sec03.board02;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.RequestDispatcher;
import javax.servlet.Servlet;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
/**
* Servlet implementation class BoardController
*/
@WebServlet("/board/*")
public class BoardController extends HttpServlet { //요청시 글 목록 출력의 역할,
private static String ARTICLE_IMAGE_REPO = "C:\\board\\image"; //글에 첨부한 이미지 저장 위치를 상수로 변환
BoardService boardService;
ArticleVO articleVO;
public void init(ServletConfig config) throws ServletException {
boardService = new BoardService(); //서블릿 초기화시 BoardService 객체를 생성
articleVO = new ArticleVO();
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doHandle(request, response);
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doHandle(request, response);
}
private void doHandle(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String nextPage = "";
request.setCharacterEncoding("utf-8");
response.setContentType("text/html; charset=utf-8");
String action = request.getPathInfo(); //요청명을 가져옴
System.out.println("action:" + action);
try {
List<ArticleVO> articlesList = new ArrayList<ArticleVO>();
if (action == null) {
articlesList = boardService.listArticles();
request.setAttribute("articlesList", articlesList);
nextPage = "/board02/listArticles.jsp";
}
articlesList = boardService.listArticles(); // 전체글을 조회
request.setAttribute("articlesList", articlesList); // 조회한 글을 바인딩한 후 jsp로 포워딩
nextPage = "/board02/listArticles.jsp";
}
nextPage = "/board02/articleForm.jsp";
}
//------------------------------ 조회에 관한 내용 ------------------------------------------------------
int articleNO=0;
Map<String, String> articleMap = upload(request, response);
//articleMap에 저장된 글 정보를 다시 가져옴
articleVO.setParentNO(0);
articleVO.setTitle(title);
articleVO.setContent(content);
articleVO.setImageFileName(imageFileName);
articleNO = boardService.addArticle(articleVO); //테이블에 새 글을 추가한 후 새 글에 대한 글 번호를 가져옴
if(imageFileName != null && imageFileName.length() != 0) {//파일을 첨부한 경우에만
File srcFile = new File(ARTICLE_IMAGE_REPO+ "\\" + "tep" + "\\" + imageFileName);//tmp폴더에 임시로 업로드된 파일 객체 생성
File desDir = new File(ARTICLE_IMAGE_REPO + "\\" + articleNO);
desDir.mkdirs();//해당 경로에 글 번호로 폴더를 생성한다.
FileUtils.moveFileToDirectory(srcFile, desDir, true);//tmp 폴더의 파일을 글 번호를 이름으로 하는 폴더로 이동
}
PrintWriter pw = response.getWriter();
pw.print("<script>"
+" alert('새글을 추가했습니다.');"
+"</script>");
//새 글 등록 메세지를 나타낸 후 자바스크립트 location객체의 href로 글 목록을 요청.
return;
//boardService.addArticle(articleVO);
//nextPage = "/board/listArticles.do";
//글쓰기 창에서 입력된 정보를 ArticleVO객체에 설정한 후 addArticle()로 전달
}
RequestDispatcher dispatch = request.getRequestDispatcher(nextPage);
dispatch.forward(request, response);
} catch (Exception e) {
e.printStackTrace();
}
}
private Map<String, String> upload(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
Map<String, String> articleMap = new HashMap<String, String>();
String encoding = "utf-8";
File currentDirPath = new File(ARTICLE_IMAGE_REPO); //글 이미지 저장폴더에 대해 파일 객체를 생성
DiskFileItemFactory factory = new DiskFileItemFactory();
factory.setRepository(currentDirPath);
factory.setSizeThreshold(1024 * 1024);
ServletFileUpload upload = new ServletFileUpload(factory);
try {
List items = upload.parseRequest(request);
FileItem fileItem = (FileItem) items.get(i);
if (fileItem.isFormField()) {
System.out.println(fileItem.getFieldName() + "=" + fileItem.getString(encoding));
//파일 업로드로 같이 전송된 새 글 관련 매개변수를 Map<>에 저장한 후 반환
//새 글과 관련된 title 등을 map에 저장한다.
} else {
System.out.println("파라미터명:" + fileItem.getFieldName());
//System.out.println("파일명:" + fileItem.getName());
System.out.println("파일크기:" + fileItem.getSize() + "bytes");
//업로드된 파일의 파일이름을 map에 (imagefilename,업로드 파일이름) 으로 저장
if (fileItem.getSize() > 0) {
int idx = fileItem.getName().lastIndexOf("\\");
if (idx == -1) {
idx = fileItem.getName().lastIndexOf("/");
}
String fileName = fileItem.getName().substring(idx + 1);
System.out.println("파일명:" + fileName);
articleMap.put(fileItem.getFieldName(), fileName); //익스플로러에서 업로드 파일의 경로 제거 후 map에 파일명 저장
File uploadFile = new File(currentDirPath + "\\tep\\" + fileName);
//File uploadFile = new File(currentDirPath + "\\" + fileName);
//업로드한 파일이 존재하는 경우 업로드한 파일의 파일이름으로 저장소에 업로드
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
return articleMap;
}
}
http://colorscripter.com/info#e" target="_blank" style="color:#e5e5e5text-decoration:none">Colored by Color Scripter
|
이 클래스는 /board2/listArticles.do 로 요청시 화면에 글 목록을 가져오는 부분이 있다. getPathInfo() 메서드를 통해 요청명을 가져오고 가져온 값에 따라 BoardService 클래스의 listArticles() 메서드로 전체글을 조회한다.
이후의 내용은 글쓰기에 관한 내용이다. 우리가 정의한 upload() 메서드에 의해 글쓰기창에 전송된 글 관련 정보를 Map의 key/value 쌍으로 저장한다.
파일을 천부한 경우 파일 이름을 Map에 저장한 후 첨부한 파일을 저장소에 업로드한다.
upload() 함수가 호출된 후, 반환한 Map에서 새 글 정보를 가져오고 Service클래스의 addArticle() 메서드 인자로 새 글 정보를 전달하여 새글을 등록한다.
BoardService.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
package sec03.board02;
import java.util.List;
public class BoardService { // BoardDAO 객체를 생성한후 select...()메서드로 전체글 가져옴
BoardDAO boardDAO;
public BoardService() {
boardDAO = new BoardDAO(); // 생성자 호출시 BoardDAO 객체를 생성
}
public List<ArticleVO> listArticles(){
List<ArticleVO> articleList = boardDAO.selectAllArticle();
return articleList;
}
public int addArticle(ArticleVO article) {
return boardDAO.insertNewArticle(article);
}
}
http://colorscripter.com/info#e" target="_blank" style="color:#e5e5e5text-decoration:none">Colored by Color Scripter
|
BoardDAO 객체를 생성한다. 이 후 selectAllArticle(), addArticle() 메서드로 조회와 새 글 등록에 대한 메서드를 DAO로부터 호출하는 기능을 가지고 있다.
BoardDAO.java
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
109
110
111
112
113
114
|
package sec03.board02;
import java.sql.Connection;
import java.sql.Date;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.List;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.sql.DataSource;
public class BoardDAO { //BoardService 클래스에서 BoardDAO의 select..()메서드 호출하면 계층형 SQL문 실행
private DataSource dataFactory;
Connection con;
PreparedStatement pstmt;
public BoardDAO() {
try {
Context ctx = new InitialContext();
}catch(Exception e) {
e.printStackTrace();
}
}
public List selectAllArticle() {
List articlesList = new ArrayList();
try {
con = dataFactory.getConnection();
String query = "SELECT LEVEL,articleNO,parentNO,title,content,id,writeDate"
+ " from t_board"
+ " START WITH parentNO=0" + " CONNECT BY PRIOR articleNO=parentNO"
+ " ORDER SIBLINGS BY articleNO DESC";
System.out.println(query); //계층형 SQL문
pstmt = con.prepareStatement(query);
ResultSet rs = pstmt.executeQuery();
while (rs.next()) {
String title = rs.getString("title");
String content = rs.getString("content");
String id = rs.getString("id");
Date writeDate = rs.getDate("writeDate");
ArticleVO article = new ArticleVO();
article.setLevel(level);
article.setArticleNO(articleNO);
article.setParentNO(parentNO);
article.setTitle(title);
article.setContent(content);
article.setWriteDate(writeDate); //위의 글 정보를 ArticleVO 객체의 속성에 설정
articlesList.add(article);
}
}catch(Exception e) {
e.printStackTrace();
}
return articlesList;
}
private int getNewArticleNO() {
try {
con = dataFactory.getConnection();
String query = "SELECT max(articleNO) from t_board ";
System.out.println(query);
pstmt = con.prepareStatement(query);
ResultSet rs = pstmt.executeQuery(query);
if (rs.next())
} catch (Exception e) {
e.printStackTrace();
}
return 0;
} //기본 글 번호중 가장 큰 번호를 조회하기 위함
public int insertNewArticle(ArticleVO article) {
int articleNO = getNewArticleNO(); // 새글에 대한 글 번호를 가져옴
try {
con = dataFactory.getConnection();
int parentNO = article.getParentNO();
String title = article.getTitle();
String content = article.getContent();
String imageFileName = article.getImageFileName();
String query = "INSERT INTO t_board (articleNO, parentNO, title, content, imageFileName, id)"
+ " VALUES (?, ? ,?, ?, ?, ?)";
System.out.println(query);
pstmt = con.prepareStatement(query);
pstmt.setInt(1, articleNO);
pstmt.setInt(2, parentNO);
pstmt.setString(3, title);
pstmt.setString(4, content);
pstmt.setString(5, imageFileName);
pstmt.setString(6, id);
pstmt.executeUpdate();
} catch (Exception e) {
e.printStackTrace();
}
return articleNO;
}
}
http://colorscripter.com/info#e" target="_blank" style="color:#e5e5e5text-decoration:none">Colored by Color Scripter
|
3개의 주요 메서드가 있다. 주석으로 있지만 간단하게 알아보자.
selectAllArticle() 메서드로 계층형 SQL문을 이용해 전체글을 조회한 후 반환하는 메서드이다.
getNewArticle() 메서드로 먼저 새 글에 대한 글 번호를 가져와야 한다. 추가하는 새 글의 번호를 얻은 다음 새 글에 대한 정보를 추가해야한다.
insertNewArticle() 메서드로 이제 새 글을 DB에 추가하도록 한다.
ArticleVO.java
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
|
package sec03.board02;
import java.net.URLEncoder;
import java.sql.Date;
public class ArticleVO { // 조회한글을 저장하는 ArticleVO
private int level;
private int articleNO;
private int parentNO;
private String title;
private String content;
private String imageFileName;
private String id;
private Date writeDate;
public ArticleVO() {
}
public ArticleVO(int level, int articleNO, int parentNO, String title, String content, String imageFileName,
String id) {
super();
this.level = level;
this.articleNO = articleNO;
this.parentNO = parentNO;
this.title = title;
this.content = content;
this.imageFileName = imageFileName;
this.id = id;
}
public int getLevel() {
return level;
}
public void setLevel(int level) {
this.level = level;
}
public int getArticleNO() {
return articleNO;
}
public void setArticleNO(int articleNO) {
this.articleNO = articleNO;
}
public int getParentNO() {
return parentNO;
}
public void setParentNO(int parentNO) {
this.parentNO = parentNO;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public String getImageFileName() {
return imageFileName;
}
public void setImageFileName(String imageFileName) {
try {
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public Date getWriteDate() {
return writeDate;
}
public void setWriteDate(Date writeDate) {
this.writeDate = writeDate;
}
}
http://colorscripter.com/info#e" target="_blank" style="color:#e5e5e5text-decoration:none">Colored by Color Scripter
|
조회된 들을 저장하는 ArticleVO 클래스에 글의 깊이(답글이 달린 깊이)를 추가하여 각 속성들을 만들어준다.
마지막으로 JSP파일들은 아래와 같다.
listArticle.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
|
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"
isELIgnored="false" %>
<c:set var="contextPath" value="${pageContext.request.contextPath}" />
<%
request.setCharacterEncoding("UTF-8");
%>
<!DOCTYPE html>
<html>
<head>
<style>
.cls1 {text-decoration:none;}
.cls2{text-align:center; font-size:30px;}
</style>
<meta charset="UTF-8">
<title>글목록창</title>
</head>
<body>
<table align="center" border="1" width="80%" >
<tr height="10" align="center" bgcolor="lightgreen">
<td >글번호</td>
<td >작성자</td>
<td >제목</td>
<td >작성일</td>
</tr>
<c:choose>
<c:when test="${articlesList ==null }" >
<tr height="10">
<td colspan="4">
<p align="center">
<b><span style="font-size:9pt;">등록된 글이 없습니다.</span></b>
</p>
</td>
</tr>
</c:when>
<c:when test="${articlesList !=null }" >
<c:forEach var="article" items="${articlesList }" varStatus="articleNum" >
<tr align="center">
<td align='left' width="35%">
<span style="padding-right:30px"></span>
<c:choose>
<span style="padding-left:10px"></span>
</c:forEach>
<span style="font-size:12px;">[답변]</span>
<a class='cls1' href="${contextPath}/board/viewArticle.do?articleNO=${article.articleNO}">${article.title}</a>
</c:when>
<c:otherwise>
<a class='cls1' href="${contextPath}/board/viewArticle.do?articleNO=${article.articleNO}">${article.title }</a>
</c:otherwise>
</c:choose>
</td>
<td width="10%"><fmt:formatDate value="${article.writeDate}" /></td>
</tr>
</c:forEach>
</c:when>
</c:choose>
</table>
</body>
</html>
http://colorscripter.com/info#e" target="_blank" style="color:#e5e5e5text-decoration:none">Colored by Color Scripter
|
articleForm.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
|
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"
isELIgnored="false" %>
<%
request.setCharacterEncoding("UTF-8");
%>
<c:set var="contextPath" value="${pageContext.request.contextPath}" />
<head>
<meta charset="UTF-8">
<title>글쓰기창</title>
<script type="text/javascript">
function readURL(input) {
var reader = new FileReader();
reader.onload = function (e) {
}
reader.readAsDataURL(input.files[0]);
}
}
function backToList(obj){
}
</script>
<title>새글 쓰기 창</title>
</head>
<body>
<h1 style="text-align:center">새글 쓰기</h1>
<form name="articleForm" method="post" action="${contextPath}/board/addArticle.do" enctype="multipart/form-data">
<table border=0 align="center">
<tr>
<td align="right">글제목: </td>
<td colspan="2"><input type="text" size="67" maxlength="500" name="title" /></td>
</tr>
<tr>
<td align="right" valign="top"><br>글내용: </td>
<td colspan=2><textarea name="content" rows="10" cols="65" maxlength="4000"></textarea> </td>
</tr>
<tr>
<td align="right">이미지파일 첨부: </td>
<td> <input type="file" name="imageFileName" onchange="readURL(this);" /></td>
<td><img id="preview" src="#" width=200 height=200/></td>
</tr>
<tr>
<td align="right"> </td>
<td colspan="2">
<input type="submit" value="글쓰기" />
</td>
</tr>
</table>
</form>
</body>
</html>
http://colorscripter.com/info#e" target="_blank" style="color:#e5e5e5text-decoration:none">Colored by Color Scripter
|
이제 http://localhost:8090/MVC/board/listArticle.do 로 접속해 결과를 확인해보자.
필자가 글쓰기 테스트를 하는바람에 게시글이 좀 많다. 이와 같이 현재 게시판의 각 목록들이 출력될 것이다.
글쓰기 버튼을 클릭하면 새로운 Form이 나타난다.
이와같다. 여기서 이미지 파일첨부에 대한 내용이 필요한데 BoardController의 주석을 확인하기 바란다.
이상태에서 글을 쓰면 우리가 BoardController.java 85번째 라인과 upload함수를 살펴보면 지정한 경로에 해당 이미지가 저장될 것이다.
그리고 지금은 이미지가 업로드될때 해당 글 번호의 디렉토리를 생성하고 그 안에 업로드한 이미지가 저장되도록 되어있다. 그 내용이 85번째 라인의 if()문부터 적용된다.
새로 작성한 글이 추가된것을 확인할 수 있다.
Refernce
자바 웹을 다루는 기술<길벗>
'Servlet JSP MVC Spring' 카테고리의 다른 글
[MVC] 게시판 구현하기: 삭제기능, 답글기능 (0) | 2020.03.31 |
---|---|
[MVC] 게시판 구현하기: 글 상세보기, 글 수정하기 (0) | 2020.03.19 |
[MVC] 정보 수정 및 삭제 (0) | 2020.03.05 |
[Servlet] 포워드(dispatch), 바인딩 (0) | 2020.03.03 |
[MVC] MVC 모델, 테이블 조회 및 생성 (0) | 2020.03.03 |