댓글게시판 : 댓글 작성 기능 (이어서)
현재 작성된 글과 변수 상태
- 목록페이지에서 목록을 보여줄때 ref 컬럼으로 내림차순 정렬, re_step 컬럼으로 오름차순 정렬
- ref 컬럼으로 정렬시 최근글이 위로 간다, 그리고 부모글과 자식글(댓글)은 ref 값이 같다
- 그러므로 re_step 컬럼으로 오름차순 정렬을 한다
만약
- '다섯번째 원문'을 선택하고 '네번째 댓글' 을 달면 다섯번째 원문에 달린 댓글과 대댓글들의 re_step 값이 모두 1씩 증가
- 'Re.두번째 댓글' 을 선택하고 'Re.3대댓글'을 달면 부모의 re_step 인 2 보다 큰 re_step 값을 가진 3,4,5 를 re_step 으로 가진 댓글의 re_step 을 1씩 증가하고 자신이 3이 됨
reply() 메소드 SQL문 정리
- 원문이 부모일때, 댓글이 부모일떄를 나눠서 생각해야한다
* 주의 : 작성 순서를 자세히 보기
* 먼저 달린 댓글 = re_step 값이 크다, 즉 부모보다 큰 re_step 값을 가진 댓글은 부모보다 먼저 달린 댓글임
1. 원문이 부모일때
1-1) update 문
- 조건 a. 'ref= 부모의 ref' 의미 = 기존에 그 부모 원문글에 달려있던 댓글들 지칭
- 조건 b. 're_step > 부모의 re_step' 의미 = 부모의 re_step 은 0 이므로 그 원문의 모든 댓글 지칭
- 조건 a & b. 그 부모의 모든 댓글을 의미
- 조건 a & b 에 해당하는 데이터의 re_step 값을 1 증가시킴
1-2) insert 문
- ref 값은 부모와 똑같이 설정
- re_level 은 부모보다 +1 을 해서 1 로 설정
- re_step 은 부모보다 +1 을 해서 1 로 설정, 기존의 댓글들이 모두 update 문에 의해 re_step값이 +1 되어서 2부터 시작하므로 새 댓글의 re_step 값을 1로 설정할 수 있다
2. 원문이 댓글일때
2-1) update 문
- 조건 a. 'ref= 부모의 ref' 의미 = 기존에 그 부모 댓글의 원문 + 그 원문의 모든 댓글들 + 그 원문의 대댓글들 (즉 그림에서 빨간색으로 묶은 모든 글)
- 조건 b. 're_step > 부모의 re_step' 의미 = 그 부모 댓글과 형제인 댓글 중 부모 댓글보다 먼저 달린 모든 댓글과, 그 댓글의 대댓글들
- 조건 a & b. 그 부모댓글보다 먼저 달린 모든 댓글과, 그 댓글의 대댓글들 의미
- 조건 a & b 에 해당하는 데이터의 re_step 값을 1 증가시킴
2-2) insert 문
- ref 값은 부모와 똑같이 설정
- re_level 은 부모보다 +1 을 해서 2 로 설정
- re_step 은 부모보다 +1 을 해서 설정, 그러므로 부모 바로 밑에 출력된다, 부모댓글보다 먼저 작성되었던 댓글과 그 댓글의 대댓글들이 모두 update 문에 의해 re_step 값이 +1 이 되어서 비어있는 숫자를 가지는 것
+ 그림처럼 정렬하기 위해서 이 작업을 해야한다, 단순 작성 최신순이 아님, re_step 기준으로 오름차순 정렬이다
+ 원문 글 작성양식과 댓글 작성 양식이 따로 되어있어야만 시퀀스를 사용 가능하다
+ 댓글을 작성할때 num, page, 부모글에 대한 3가지 정보 (ref, re_step, re_level) 의 변수값을 넘겼다
- 목록페이지 list.jsp 로 부터 시작해서 값이 계속 넘어가고 있는 것을 URL 창에서 확인 가능
댓글게시판 : 글 수정
- 수정 폼부터 만들어야 한다
- 상세페이지 content.jsp 에서 버튼 4개 중 '수정' 버튼에 수정폼인 updateForm.jsp 를 연결
+ content.jsp 전체 코드는 삭제 구현 이후에 올리기
- 목록에서 상세페이지로 넘어갈때 전달되었던 num, nowpage 값을 다시 상세페이지에서 수정폼으로 전달한다
- 이후 updateForm.jsp 를 생성 및 작성하자
- updateForm.jsp
<%@page import="reboard.BoardDataBean"%>
<%@page import="reboard.BoardDBBean"%>
<%@ page contentType="text/html; charset=utf-8" %>
<%@ include file="color.jsp"%>
<%
int num = Integer.parseInt(request.getParameter("num"));
String nowpage = request.getParameter("page");
BoardDBBean dao = BoardDBBean.getInstance();
// 상세 정보 구하기
BoardDataBean board = dao.getContent(num);
%>
<html>
<head>
<title>글수정</title>
<link href="style.css" rel="stylesheet" type="text/css">
<script src="http://code.jquery.com/jquery-latest.js"></script>
<script src="check.js"></script>
</head>
<body bgcolor="<%=bodyback_c%>">
<center><b>글수정</b>
<br>
<form method="post" action="updatePro.jsp">
<input type="hidden" name="num" value="<%=num%>">
<input type="hidden" name="page" value="<%=nowpage%>">
<input type="hidden" name="ref" value="<%=board.getRef()%>">
<input type="hidden" name="re_step" value="<%=board.getRe_step()%>">
<input type="hidden" name="re_level" value="<%=board.getRe_level()%>">
<table width="400" border="1" cellspacing="0" cellpadding="0" bgcolor="<%=bodyback_c%>" align="center">
<tr>
<td align="right" colspan="2" bgcolor="<%=value_c%>">
<a href="list.jsp?page=<%=nowpage%>"> 글목록</a>
</td>
</tr>
<tr>
<td width="70" bgcolor="<%=value_c%>" align="center">이 름</td>
<td width="330">
<input type="text" size="10" maxlength="10" id="writer"
name="writer" value="<%=board.getWriter() %>"></td>
</tr>
<tr>
<td width="70" bgcolor="<%=value_c%>" align="center" >제 목</td>
<td width="330">
<input type="text" size="40" maxlength="50" id="subject"
name="subject" value="<%=board.getSubject() %>"></td>
</tr>
<tr>
<td width="70" bgcolor="<%=value_c%>" align="center">Email</td>
<td width="330">
<input type="text" size="40" maxlength="30" id="email"
name="email" value="<%=board.getEmail() %>"></td>
</tr>
<tr>
<td width="70" bgcolor="<%=value_c%>" align="center" >내 용</td>
<td width="330" >
<textarea id="content" name="content" rows="13" cols="40"><%=board.getContent() %></textarea> </td>
</tr>
<tr>
<td width="70" bgcolor="<%=value_c%>" align="center" >비밀번호</td>
<td width="330" >
<input type="password" size="8" maxlength="12" id="passwd" name="passwd">
</td>
</tr>
<tr>
<td colspan=2 bgcolor="<%=value_c%>" align="center">
<input type="submit" value="글수정" >
<input type="reset" value="다시작성">
<input type="button" value="목록보기" OnClick="window.location='list.jsp?page=<%=nowpage%>'">
</td></tr></table>
</form>
</body>
</html>
- writeForm.jsp 의 내용을 복붙 후 수정
- 먼저 상세페이지 content.jsp 에서 넘어간 num, nowpage 의 값을 받아주는 내용을 추가함
- 글 수정시, 글의 정보를 보여줘야하므로 DB와 연동해서 글 1개의 상세정보를 가져와야한다
- updateContent()와 달리 getContent() 에서는 조회수 증가시키는 작업을 하지 않음
- getContent()로 받아온 상세정보에서 ref, re_step, re_level 값을 getter 메소드로 가져와서 hidden 으로 updatePro.jsp로 넘김
- 즉, 5가지 정보인 num, nowpage, re_step, re_level, re_step 값을 form의 hidden 을 통해 updatePro.jsp 로 넘긴다
- 또한 수정 폼에 입력한 5개의 정보들도 form 을 통해 updatePro.jsp 로 넘긴다
- 입력양식에 받아온 상세정보들을 value 속성으로 출력함, 다만 textarea 는 value 속성이 아닌 태그 사이에 작성해야함
- list.jsp 로 넘어가는 링크들에 ?page=<%=nowpage%> 를 추가해서 원래 페이지로 돌아가도록 하기
- DAO 클래스로 가서 getContent() 메소드를 추가하자
DAO 클래스 글 상세 정보 구하기 메소드 작성
- BoardDBBean.java 전체 코드는 완성 후 올리기 (검색어 : BoardDBBean.java 전체 코드 )
- 추가한 코드인 getContent() 메소드 부분 코드만
// 수정 폼 : 데이터 1개 추출
public BoardDataBean getContent(int num) {
BoardDataBean board = new BoardDataBean();
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
con = getConnection();
String sql = "select * from board where num=?";
pstmt = con.prepareStatement(sql);
pstmt.setInt(1, num);
rs = pstmt.executeQuery(); // SQL문 실행
if(rs.next()) {
board.setNum(rs.getInt("num"));
board.setWriter(rs.getString("writer"));
board.setEmail(rs.getString("email"));
board.setSubject(rs.getString("subject"));
board.setPasswd(rs.getString("passwd"));
board.setReg_date(rs.getTimestamp("reg_date"));
board.setReadcount(rs.getInt("readcount"));
board.setRef(rs.getInt("ref"));
board.setRe_level(rs.getInt("re_level"));
board.setRe_step(rs.getInt("re_step"));
board.setContent(rs.getString("content"));
board.setIp(rs.getString("ip"));
}
} catch(Exception e) {
e.printStackTrace();
} finally {
if(rs != null) try { rs.close(); } catch(Exception e) {}
if(pstmt != null) try { pstmt.close(); } catch(Exception e) {}
if(con != null) try { con.close(); } catch(Exception e) {}
}
return board;
}
- 글 1개에 대한 상세정보를 구해줘야하므로 리턴자료형은 DTO 이다
- 글 번호인 num 으로 상세 정보를 구해와야하므로 매개변수는 int 형
- updateContent() 메소드 안의 내용을 통째로 복붙 후, 조회수 증가시키는 코드 부분만 지우기
- 다음으로 updatePro.jsp 파일을 생성 및 작성하자
- updatePro.jsp
<%@page import="reboard.BoardDataBean"%>
<%@page import="reboard.BoardDBBean"%>
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%
request.setCharacterEncoding("utf-8");
%>
<jsp:useBean id="board" class="reboard.BoardDataBean"/>
<jsp:setProperty property="*" name="board"/>
<%
board.setIp(request.getRemoteAddr());
String nowpage = request.getParameter("page");
BoardDBBean dao = BoardDBBean.getInstance();
BoardDataBean old = dao.getContent(board.getNum());
// 비번 비교
if(old.getPasswd().equals(board.getPasswd())) { // 비번 일치시
int result = dao.update(board); // update SQL문 실행
if(result == 1) {
%>
<script>
alert("글 수정 성공");
location.href = "list.jsp?page=<%=nowpage%>";
</script>
<%
}
} else { // 비번 불일치시
%>
<script>
alert("비번이 일치하지 않습니다.");
history.go(-1);
</script>
<%
}
%>
- 폼에서 한글값이 post 방식으로 넘어오므로 한글 인코딩
- useBean, setProperty action tag 를 사용해서 폼에서 넘어온 값을 DTO 객체에 저장
- 이때, 폼에서 ip 주소값은 전달되지 않았고, DTO 프로퍼티중 page 값을 저장할 곳이 없으므로 따로 처리
- DB에서 getContent() 메소드로 비번을 끄집어 내서 수정폼에서 사용자가 입력한 비번과 일치하는지 확인
- getContent() 매개변수에는 글의 번호인 num 이 들어가므로 수정폼에서 넘어온 값들을 저장한 board 객체에서 getNum() 으로 num 값을 꺼내서 매개변수로 넣기
- DB의 비밀번호인 old.getPasswd() 와 사용자가 수정폼에서 입력한 비밀번호인 getPasswd() 가 일치하면 수정 메소드인 update() 호출
- DAO 클래스로 가서 update() 메소드를 추가하자
DAO 클래스 글 수정 메소드 작성
- BoardDBBean.java 전체 코드는 완성 후 올리기 (검색어 : BoardDBBean.java 전체 코드 )
- 추가한 코드인 update() 메소드 부분 코드만
// 글 수정
public int update(BoardDataBean board) {
int result = 0;
Connection con = null;
PreparedStatement pstmt = null;
try {
con = getConnection();
String sql = "update board set writer=?,email=?,subject=?,";
sql += "content=? where num=?";
pstmt = con.prepareStatement(sql);
pstmt.setString(1, board.getWriter());
pstmt.setString(2, board.getEmail());
pstmt.setString(3, board.getSubject());
pstmt.setString(4, board.getContent());
pstmt.setInt(5, board.getNum());
result = pstmt.executeUpdate(); // SQL문 실행
} catch(Exception e) {
e.printStackTrace();
} finally {
if(pstmt != null) try { pstmt.close(); } catch(Exception e) {}
if(con != null) try { con.close(); } catch(Exception e) {}
}
return result;
}
- update 된 데이터 개수를 반환하기 위해 리턴자료형은 int
- 사용자가 수정폼에서 입력한 값을 전달받아야하므로 매개변수 자료형은 DTO
- 수정폼에서 넘어온 데이터를 저장하는 board 객체에서 값을 가져와서 update 문을 실행
- list.jsp 를 실행 후 글 '다섯번째 원문' 을 선택하여 목록페이지->상세페이지-> 수정폼 에서 글 수정을 시도해보자
- 수정 완료
댓글게시판 : 글 삭제
- 삭제 폼부터 만들어야 한다
- 상세페이지 content.jsp 에서 버튼 4개 중 '삭제' 버튼에 수정폼인 deleteForm.jsp 를 연결
- content.jsp (최종)
<%@page import="java.text.SimpleDateFormat"%>
<%@page import="reboard.BoardDataBean"%>
<%@page import="reboard.BoardDBBean"%>
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%
int num = Integer.parseInt(request.getParameter("num"));
String nowpage = request.getParameter("page");
BoardDBBean dao = BoardDBBean.getInstance();
// 조회수 1 증가 + 상세 정보 구하기
BoardDataBean board = dao.updateContent(num);
// 부모글 정보 구하기
int ref = board.getRef();
int re_level = board.getRe_level();
int re_step = board.getRe_step();
SimpleDateFormat sd = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String content = board.getContent().replace("\n","<br>");
%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>상세 페이지</title>
</head>
<body>
<table border=1 width=400 align=center>
<caption>상세 페이지</caption>
<tr>
<td>번호</td>
<td><%=board.getNum() %></td>
<td>조회수</td>
<td><%=board.getReadcount() %></td>
</tr>
<tr>
<td>작성자</td>
<td><%=board.getWriter() %></td>
<td>작성일</td>
<td><%=sd.format(board.getReg_date()) %></td>
</tr>
<tr>
<td>제목</td>
<td colspan=3><%=board.getSubject() %></td>
</tr>
<tr>
<td>내용</td>
<td colspan=3>
<pre><%=board.getContent() %></pre>
<%=content %>
</td>
</tr>
<tr>
<td colspan=4 align=center>
<input type="button" value="댓글"
onClick="location.href='replyForm.jsp?num=<%=num%>&page=<%=nowpage%>&ref=<%=ref%>&re_level=<%=re_level%>&re_step=<%=re_step%>'">
<input type="button" value="수정"
onClick="location.href='updateForm.jsp?num=<%=num %>&page=<%=nowpage %>'">
<input type="button" value="삭제"
onClick="location.href='deleteForm.jsp?num=<%=num %>&page=<%=nowpage %>'">
<input type="button" value="목록"
onClick="location.href='list.jsp?page=<%=nowpage%>'">
</td>
</tr>
</table>
</body>
</html>
- 삭제 폼인 deleteForm.jsp 파일부터 먼저 생성 후 작성
- deleteForm.jsp
<%@page import="reboard.BoardDataBean"%>
<%@page import="reboard.BoardDBBean"%>
<%@ page contentType="text/html; charset=utf-8" %>
<%@ include file="color.jsp"%>
<%
int num = Integer.parseInt(request.getParameter("num"));
String nowpage = request.getParameter("page");
BoardDBBean dao = BoardDBBean.getInstance();
// 상세 정보 구하기
BoardDataBean board = dao.getContent(num);
%>
<html>
<head>
<title>글삭제</title>
<link href="style.css" rel="stylesheet" type="text/css">
<script src="http://code.jquery.com/jquery-latest.js"></script>
<script src="check.js"></script>
</head>
<body bgcolor="<%=bodyback_c%>">
<center><b>글삭제</b>
<br>
<form method="post" action="deletePro.jsp">
<input type="hidden" name="num" value="<%=num%>">
<input type="hidden" name="page" value="<%=nowpage%>">
<input type="hidden" name="ref" value="<%=board.getRef()%>">
<input type="hidden" name="re_step" value="<%=board.getRe_step()%>">
<input type="hidden" name="re_level" value="<%=board.getRe_level()%>">
<table width="400" border="1" cellspacing="0" cellpadding="0" bgcolor="<%=bodyback_c%>" align="center">
<tr>
<td align="right" colspan="2" bgcolor="<%=value_c%>">
<a href="list.jsp?page=<%=nowpage%>"> 글목록</a>
</td>
</tr>
<tr>
<td width="70" bgcolor="<%=value_c%>" align="center" >비밀번호</td>
<td width="330" >
<input type="password" size="8" maxlength="12" id="passwd" name="passwd">
</td>
</tr>
<tr>
<td colspan=2 bgcolor="<%=value_c%>" align="center">
<input type="submit" value="글삭제" >
<input type="reset" value="다시작성">
<input type="button" value="목록보기" OnClick="window.location='list.jsp?page=<%=nowpage%>'">
</td></tr></table>
</form>
</body>
</html>
- updateForm.jsp 파일을 복붙 후 비번 입력 양식 제외 입력 양식 삭제
- DB연동 코드와 ref, re_step, re_level 값을 hidden 으로 넘기는 부분은 지워도 되지만 여기선 놔둠
- form 을 통해 비밀번호가 deletePro.jsp 로 전달됨
- deletePro.jsp 생성 및 작성
<%@page import="reboard.BoardDataBean"%>
<%@page import="reboard.BoardDBBean"%>
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%
request.setCharacterEncoding("utf-8");
%>
<jsp:useBean id="board" class="reboard.BoardDataBean"/>
<jsp:setProperty property="*" name="board"/>
<%
board.setIp(request.getRemoteAddr());
String nowpage = request.getParameter("page");
BoardDBBean dao = BoardDBBean.getInstance();
BoardDataBean old = dao.getContent(board.getNum());
// 비번 비교
if(old.getPasswd().equals(board.getPasswd())) { // 비번 일치시
int result = dao.delete(board); // update SQL문 실행
if(result == 1) {
%>
<script>
alert("글 삭제 성공");
location.href = "list.jsp?page=<%=nowpage%>";
</script>
<%
}
} else { // 비번 불일치시
%>
<script>
alert("비번이 일치하지 않습니다.");
history.go(-1);
</script>
<%
}
%>
- updatePro.jsp 파일 내용을 복붙 후 수정
- ip 주소값을 DTO 객체에 저장하는 코드는 필요없으므로 삭제
- 비번 일치시 update() 메소드 대신 delete() 메소드 호출
삭제시 주의할 점
- 원문을 삭제시 그 아래의 자식 댓글들만 나오게 된다, 보기 좋지 않음
- 일반적으로, 원문을 삭제 시도시 실제로 삭제하진 않고 제목에 삭제되었다는 메세지를 출력하고 링크를 없앤다
- 원문은 re_level 이 0 이거나 re_step 이 0 이므로 원문 구별 가능
- DAO 클래스에서 delete() 메소드를 작성하자
DAO 클래스 글 삭제 메소드 작성
- BoardDBBean.java 전체 코드
// DAO (Data Access Object)
package reboard;
import java.sql.Connection;
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 BoardDBBean {
// 싱글톤 : 객체 생성을 1번만 수행하는 것
private static BoardDBBean instance = new BoardDBBean();
public static BoardDBBean getInstance() { // 정적 메소드
return instance;
}
// 커넥션 풀에서 커넥션을 구해오는 메소드
public Connection getConnection() throws Exception {
Context init = new InitialContext();
DataSource ds = (DataSource) init.lookup("java:comp/env/jdbc/orcl");
return ds.getConnection();
}
// 원문 글작성
public int insert(BoardDataBean board) {
int result = 0;
Connection con = null;
PreparedStatement pstmt = null;
try {
con = getConnection();
String sql = "insert into board values(board_seq.nextval,?,?,?,?,";
sql += "sysdate,?,board_seq.nextval,?,?,?,?)";
pstmt = con.prepareStatement(sql);
pstmt.setString(1, board.getWriter());
pstmt.setString(2, board.getEmail());
pstmt.setString(3, board.getSubject());
pstmt.setString(4, board.getPasswd());
pstmt.setInt(5, 0); // readcount
pstmt.setInt(6, 0); // re_step
pstmt.setInt(7, 0); // re_level
pstmt.setString(8, board.getContent());
pstmt.setString(9, board.getIp());
result = pstmt.executeUpdate(); // SQL문 실행
} catch (Exception e) {
e.printStackTrace();
} finally {
if(pstmt != null) try { pstmt.close(); } catch(Exception e) {}
if(con != null) try { con.close(); } catch(Exception e) {}
}
return result;
}
// 총 데이터 갯수 구하기
public int getCount() {
int result = 0;
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
con = getConnection();
String sql = "select count(*) from board";
pstmt = con.prepareStatement(sql);
rs = pstmt.executeQuery(); // SQL문 실행
if(rs.next()) {
// result = rs.getInt(1); // 아래와 같다
result = rs.getInt("count(*)");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if(rs != null) try { rs.close(); } catch(Exception e) {}
if(pstmt != null) try { pstmt.close(); } catch(Exception e) {}
if(con != null) try { con.close(); } catch(Exception e) {}
}
return result;
}
// 데이터 목록 구하기 : 데이터 10개 추출
public List<BoardDataBean> getList(int start, int end) {
List<BoardDataBean> list = new ArrayList<BoardDataBean>();
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
con = getConnection();
String sql = "select * from (select rownum rnum, board.* from ";
sql += "(select * from board order by ref desc, re_step asc) board) ";
sql += "where rnum >= ? and rnum <= ?";
pstmt = con.prepareStatement(sql);
pstmt.setInt(1, start);
pstmt.setInt(2, end);
rs = pstmt.executeQuery(); // SQL문 실행
while(rs.next()) {
BoardDataBean board = new BoardDataBean();
board.setNum(rs.getInt("num"));
board.setWriter(rs.getString("writer"));
board.setEmail(rs.getString("email"));
board.setSubject(rs.getString("subject"));
board.setPasswd(rs.getString("passwd"));
board.setReg_date(rs.getTimestamp("reg_date"));
board.setReadcount(rs.getInt("readcount"));
board.setRef(rs.getInt("ref"));
board.setRe_level(rs.getInt("re_level"));
board.setRe_step(rs.getInt("re_step"));
board.setContent(rs.getString("content"));
board.setIp(rs.getString("ip"));
list.add(board);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if(rs != null) try { rs.close(); } catch(Exception e) {}
if(pstmt != null) try { pstmt.close(); } catch(Exception e) {}
if(con != null) try { con.close(); } catch(Exception e) {}
}
return list;
}
// 상세 페이지 : 조회수 1 증가 시키고 상세정보 구하기
public BoardDataBean updateContent(int num) {
BoardDataBean board = new BoardDataBean();
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
con = getConnection();
String sql = "update board set readcount=readcount+1 ";
sql += "where num=?";
pstmt = con.prepareStatement(sql);
pstmt.setInt(1, num);
pstmt.executeUpdate(); // SQL문 실행
sql = "select * from board where num=?";
pstmt = con.prepareStatement(sql);
pstmt.setInt(1, num);
rs = pstmt.executeQuery(); // SQL문 실행
if(rs.next()) {
board.setNum(rs.getInt("num"));
board.setWriter(rs.getString("writer"));
board.setEmail(rs.getString("email"));
board.setSubject(rs.getString("subject"));
board.setPasswd(rs.getString("passwd"));
board.setReg_date(rs.getTimestamp("reg_date"));
board.setReadcount(rs.getInt("readcount"));
board.setRef(rs.getInt("ref"));
board.setRe_level(rs.getInt("re_level"));
board.setRe_step(rs.getInt("re_step"));
board.setContent(rs.getString("content"));
board.setIp(rs.getString("ip"));
}
} catch(Exception e) {
e.printStackTrace();
} finally {
if(rs != null) try { rs.close(); } catch(Exception e) {}
if(pstmt != null) try { pstmt.close(); } catch(Exception e) {}
if(con != null) try { con.close(); } catch(Exception e) {}
}
return board;
}
// 댓글 작성
public int reply(BoardDataBean board) {
int result = 0;
// 부모글에 대한 정보
int ref = board.getRef();
int re_step = board.getRe_step();
int re_level = board.getRe_level();
Connection con = null;
PreparedStatement pstmt = null;
try {
con = getConnection();
// 1. 원문이 부모인 경우
// : 원문이 re_step=0 이기 떄문에, 모든 댓글들의 re_step 값이 1 증가 된다.
// 2. 댓글이 부모인 경우
// : 부모의 re_step 보다 큰 댓글만 re_step 값이 1 증가 된다.
String sql = "update board set re_step=re_step+1 ";
sql += "where ref=? and re_step > ?";
pstmt = con.prepareStatement(sql);
pstmt.setInt(1, ref); // 부모의 ref
pstmt.setInt(2, re_step); // 부모의 re_step
pstmt.executeUpdate(); // SQL문 실행
sql = "insert into board values(board_seq.nextval,?,?,?,?,";
sql += "sysdate,?,?,?,?,?,?)";
pstmt = con.prepareStatement(sql);
pstmt.setString(1, board.getWriter());
pstmt.setString(2, board.getEmail());
pstmt.setString(3, board.getSubject());
pstmt.setString(4, board.getPasswd());
pstmt.setInt(5, 0); // readcount
pstmt.setInt(6, ref); // ref
pstmt.setInt(7, re_step+1); // re_step
pstmt.setInt(8, re_level+1); // re_level
pstmt.setString(9, board.getContent());
pstmt.setString(10, board.getIp());
result = pstmt.executeUpdate(); // SQL문 실행
} catch(Exception e) {
e.printStackTrace();
} finally {
if(pstmt != null) try { pstmt.close(); } catch(Exception e) {}
if(con != null) try { con.close(); } catch(Exception e) {}
}
return result;
}
// 수정 폼 : 데이터 1개 추출
public BoardDataBean getContent(int num) {
BoardDataBean board = new BoardDataBean();
Connection con = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
con = getConnection();
String sql = "select * from board where num=?";
pstmt = con.prepareStatement(sql);
pstmt.setInt(1, num);
rs = pstmt.executeQuery(); // SQL문 실행
if(rs.next()) {
board.setNum(rs.getInt("num"));
board.setWriter(rs.getString("writer"));
board.setEmail(rs.getString("email"));
board.setSubject(rs.getString("subject"));
board.setPasswd(rs.getString("passwd"));
board.setReg_date(rs.getTimestamp("reg_date"));
board.setReadcount(rs.getInt("readcount"));
board.setRef(rs.getInt("ref"));
board.setRe_level(rs.getInt("re_level"));
board.setRe_step(rs.getInt("re_step"));
board.setContent(rs.getString("content"));
board.setIp(rs.getString("ip"));
}
} catch(Exception e) {
e.printStackTrace();
} finally {
if(rs != null) try { rs.close(); } catch(Exception e) {}
if(pstmt != null) try { pstmt.close(); } catch(Exception e) {}
if(con != null) try { con.close(); } catch(Exception e) {}
}
return board;
}
// 글 수정
public int update(BoardDataBean board) {
int result = 0;
Connection con = null;
PreparedStatement pstmt = null;
try {
con = getConnection();
String sql = "update board set writer=?,email=?,subject=?,";
sql += "content=? where num=?";
pstmt = con.prepareStatement(sql);
pstmt.setString(1, board.getWriter());
pstmt.setString(2, board.getEmail());
pstmt.setString(3, board.getSubject());
pstmt.setString(4, board.getContent());
pstmt.setInt(5, board.getNum());
result = pstmt.executeUpdate(); // SQL문 실행
} catch(Exception e) {
e.printStackTrace();
} finally {
if(pstmt != null) try { pstmt.close(); } catch(Exception e) {}
if(con != null) try { con.close(); } catch(Exception e) {}
}
return result;
}
// 글 삭제
public int delete(BoardDataBean board) {
int result = 0;
Connection con = null;
PreparedStatement pstmt = null;
String sql = "";
try {
con = getConnection();
if(board.getRe_level() == 0) { // 원문
sql = "update board set subject=?,content=? where num=?";
pstmt = con.prepareStatement(sql);
pstmt.setString(1, "관리자에 의해서 삭제됨");
pstmt.setString(2, " ");
pstmt.setInt(3, board.getNum());
} else { // 댓글
sql = "delete from board where num=?";
pstmt = con.prepareStatement(sql);
pstmt.setInt(1, board.getNum());
}
result = pstmt.executeUpdate(); // SQL문 실행
} catch(Exception e) {
e.printStackTrace();
} finally {
if(pstmt != null) try { pstmt.close(); } catch(Exception e) {}
if(con != null) try { con.close(); } catch(Exception e) {}
}
return result;
}
}
- 추가한 코드인 delete() 메소드 부분 코드만
// 글 삭제
public int delete(BoardDataBean board) {
int result = 0;
Connection con = null;
PreparedStatement pstmt = null;
String sql = "";
try {
con = getConnection();
if(board.getRe_level() == 0) { // 원문
sql = "update board set subject=?,content=? where num=?";
pstmt = con.prepareStatement(sql);
pstmt.setString(1, "관리자에 의해서 삭제됨");
pstmt.setString(2, "");
pstmt.setInt(3, board.getNum());
} else { // 댓글
sql = "delete from board where num=?";
pstmt = con.prepareStatement(sql);
pstmt.setInt(1, board.getNum());
}
result = pstmt.executeUpdate(); // SQL문 실행
} catch(Exception e) {
e.printStackTrace();
} finally {
if(pstmt != null) try { pstmt.close(); } catch(Exception e) {}
if(con != null) try { con.close(); } catch(Exception e) {}
}
return result;
}
- if문을 이용해서 원문인지 아닌지 re_level 을 가져와서 판별
- 원문인 경우 해당 원문글의 제목을 "관리자에 의해 삭제됨" 으로 수정하고 내용을 ""으로 수정함
- 댓글인 경우 그냥 delete문으로 바로 삭제
+ 매개변수에 board 안의 num 변수만 넘겨도 가능하지만 여기선 DTO 객체를 통째로 넘겼다
- 댓글을 삭제할때
- list.jsp 를 실행 후 댓글 'Re1.대댓글' 을 선택하여 목록페이지->상세페이지-> 삭제폼 에서 글 삭제를 시도해보자
- 원문을 삭제할때
- list.jsp 를 실행 후 댓글 '다섯번째 원문(수정)' 을 선택하여 목록페이지->상세페이지-> 삭제폼 에서 글 삭제를 시도해보자
+ 삭제된 글인경우 목록페이지에서 링크를 없애고 싶다면
- 테이블에 특정 컬럼 del 이라는 varchar2 컬럼을 추가하고 insert 당시에 그 컬럼값을 'n' 으로 한다
- 글을 삭제했을때 컬럼값을 'y' 로 설정함
- 그럼 삭제를 한 글인지 판별하는 컬럼이 생긴다
- 그 컬럼을 통해서 삭제된 글은 링크를 달지 않기
댓글게시판 : 초기 페이지
- index.jsp 파일을 작성하자
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
댓글 게시판
<script>
location.href = "reboard/list.jsp";
</script>
</body>
</html>
- 가장 먼저 자동으로 실행되는 파일인 index.jsp 파일에 reboard 폴더 안의 list.jsp 가 실행되도록 함
- 프로젝트 처음부터 실행하는 방법
- 프로젝트를 클릭후 Run on Server 로 실행시켰을때 index.jsp 부터 자동으로 실행된다
delete 원문삭제 가 안됨
update /delete 성공 후 list.jsp 로 돌아가는게 안됨
자료실 게시판 소개
- 첨부파일을 업로드 / 다운로드 가능한게 자료실 기능을 가진 게시판
- 업로드 / 다운로드 하는 방법부터 먼저 학습 후 자료실 기능 게시판 만들 것
첨부파일 저장
- 첨부파일은 일반 값과는 달리 테이블에 저장되지 않고 서버측에 지정된 디렉토리 안에 저장된다
- 첨부파일명만 테이블에 저장된다
- 지정한 upload 디렉토리안에 첨부파일이 저장됨
첨부파일 업로드 / 다운로드 라이브러리
- 첨부파일 업로드 / 다운로드를 위해서 따로 라이브러리가 필요하다
- 여러 라이브러리 중 cos.jar 라는 라이브러리를 사용해서 첨부파일을 업로드 / 다운로드 할 것
- cos.jar 라이브러리를 프로젝트의 lib 폴더 안에 저장해야한다
+ cos 라이브러리 장점
- Model 1, Model 2 에서 많이 사용함
- upload 디렉토리 안에 첨부파일을 저장할때 동일 폴더이므로 파일명 중복문제가 발생시 자동으로 처리해줌
ex) A 가 자료실에 파일명을 kuro.jpg 로 올리고, B도 자료실에 파일명을 kuro.jpg 로 올렸다면 중복 발생
- 자동으로 번호값을 부여해서 중복 문제를 해결함
ex) kuro.jpg, kuro1.jpg, kuro2.jpg 처럼 끝에 숫자를 붙여줌
- 다른 라이브러리는 같은 이름의 파일이 있는지 없는지 검사하며 개발자가 중복 문제를 직접 처리해야함
cos 라이브러리 다운
- zip 파일을 다운
- 다운로드 후 압축 풀기
+ doc 폴더 안에는 API 문서가 있다
+ doc폴더 안에서 index 파일 실행시 API 문서를 볼 수 있음
첨부파일 업로드 / 다운로드
실습 준비 / 환경 구축
- 업로드를 수행하기 위한 예제 파일들이 있는 uploadTest 를 jspproject 프로젝트 로 가져오기
- 이후 WebContent 폴더 하위에 업로드를 하기 위한 폴더인 upload 폴더 생성
- 아까 다운받은 cos-22.05 중 lib 폴더 안의 cos.jar 파일을 이클립스의 jspproject 프로젝트 WEB-INF/lib 폴더 안에 넣기
첨부파일 서버로 전송하기
- 반드시 post 방식만 가능하다
- form 태그안에 속성으로 이 코드가 있어야한다
- uploadTest 폴더 안의 fileUploadForm.jsp 파일 열기
<%@ page language="java" contentType="text/html; charset=utf-8"%>
<html>
<head>
<title>FileUpload Form</title>
</head>
<body>
<center>
<form action="fileUpload.jsp" method="post" enctype="multipart/form-data">
<table border=1>
<tr>
<td colspan=2 align=center><h3>파일 업로드 폼</h3></td>
<tr>
<td>올린 사람 : </td><td><input type="text" name="name"></td><br>
</tr>
<tr>
<td>제목 : </td><td><input type="text" name="subject"></td><br>
</tr>
<tr>
<td>파일명1 : </td><td><input type="file" name="fileName1"></td><br>
</tr>
<tr>
<td>파일명2 : </td><td><input type="file" name="fileName2"></td><p/>
</tr>
<tr>
<td colspan=2 align=center><input type="submit" value="전송"></td>
</tr>
</table>
</form>
</center>
</body>
</html>
- 첨부파일을 서버측으로 전송할 때는 post 방식만 가능하다
- 첨부파일을 서버측으로 전송할 때는 form 태그 안에 반드시 enctype="multipart/form-data" 코드가 있어야한다
- input type=file 로만 해도 버튼이 포함된 이 모양을 만들어준다
+ 첨부파일도 마찬가지로 name 값이 값을 전달하기 위한 변수 역할을 한다
- fileUploadForm.jsp 를 실행한 뒤 HTML 시간에 사용했던 사진들을 업로드 하기
- 클릭 시 다운로드 된다
만약 다시 업로드할때 파일명이 기존 파일명과 중복된다면
- 똑같은 파일명으로 다시 업로드시
- 문제없이 자동으로 잘 처리된다
+ 업로드 관련 클래스
- 업로드는 2개의 클래스로 처리된다
- doc 폴더의 index 를 실행하면 실행되는 API 문서에서 클래스 확인 가능
- form 태그에서 action 으로 지정된 fileUpload.jsp 파일을 살펴보자
- fileUpload.jsp
<%@ page language="java" contentType="text/html; charset=utf-8"%>
<%@ page import="com.oreilly.servlet.MultipartRequest" %>
<%@ page import="com.oreilly.servlet.multipart.DefaultFileRenamePolicy" %>
<%@ page import="java.util.*" %>
<%
// 업로드할 디렉토리 위치를 구해옴
String uploadPath=request.getRealPath("upload");
System.out.println("path="+uploadPath);
int size = 10*1024*1024; // 첨부파일의 크기(단위:Byte) : 10MB
String name="";
String subject="";
String filename1="";
String filename2="";
String origfilename1="";
String origfilename2="";
try{ // 첨부파일은 MultipartRequest 객체를 생성하면서 업로드 된다.
MultipartRequest multi=new MultipartRequest(request,
uploadPath, // 업로드할 디렉토리 위치
size, // 첨부파일의 크기 : 10MB
"utf-8", // 인코딩 타입 설정
new DefaultFileRenamePolicy()); // 중복 문제 해결
name=multi.getParameter("name");
subject=multi.getParameter("subject");
Enumeration files=multi.getFileNames();
// String file1 = "fileName1";
String file1 =(String)files.nextElement();
filename1=multi.getFilesystemName(file1);
origfilename1= multi.getOriginalFileName(file1);
// String file2 = "fileName2";
String file2 =(String)files.nextElement();
filename2=multi.getFilesystemName(file2);
origfilename2=multi.getOriginalFileName(file2);
}catch(Exception e){
e.printStackTrace();
}
%>
<html>
<body>
<form name="filecheck" action="fileCheck.jsp" method="post">
<input type="hidden" name="name" value="<%=name%>">
<input type="hidden" name="subject" value="<%=subject%>">
<input type="hidden" name="filename1" value="<%=filename1%>">
<input type="hidden" name="filename2" value="<%=filename2%>">
<input type="hidden" name="origfilename1" value="<%=origfilename1%>">
<input type="hidden" name="origfilename2" value="<%=origfilename2%>">
</form>
<a href="#" onclick="javascript:filecheck.submit()">업로드 확인 및 다운로드 페이지 이동</a>
</body>
</html>
- 다른 값과 다르게 첨부파일은 useBean, setProperty, getProperty 로 저장 및 가져오기 불가능함
- 그래서 cos 라이브러리에서 지원되는 2개의 클래스를 사용해서 첨부파일을 처리한다
업로드할 디렉토리 위치 구하기 (fileUpload.jsp 부분)
- 파일 업로드 후 upload 폴더로 가보면 파일이 저장되어 있지 않음
- C:\Users\admin\eclipse-workspace\jspproject\WebContent 로 가보면 아무 파일도 없다
// 업로드할 디렉토리 위치를 구해옴
String uploadPath=request.getRealPath("upload");
System.out.println("path="+uploadPath);
- "upload" 라는 폴더를 실제 저장되는 경로로 지정하고 있다
- request.getRealPath() 메소드로 실제 서버에 저장되는 경로값을 구해온다
- 실제로 첨부파일이 서버에서 저장되는 위치는 따로 있고, 콘솔창을 보면 확인 가능
C:\Users\admin\eclipse-workspace\.metadata\.plugins\org.eclipse.wst.server.core\tmp0\wtpwebapps\jspproject\upload
- 이 위치가 실제 저장 위치이다
- 탐색기에 복붙시
- 같은 파일명인 파일이 들어왔을때는 번호를 뒤에 붙여서 중복문제를 자동으로 해결해 줌
cos 라이브러리 업로드 관련 클래스
- 2개의 클래스를 사용해서 업로드
- com.oreilly.servlet.MultipartRequest 클래스 : 실제 업로드를 수행하는 메소드
- com.oreilly.servlet.multipart.DefaultFileRenamePolicy 클래스 : 중복 문제를 해결하는 메소드
- 이후 각 클래스 API 문서 볼 것
업로드 하기 (fileUpload.jsp 부분)
// 첨부파일은 MultipartRequest 객체를 생성하면서 업로드 된다.
MultipartRequest multi=new MultipartRequest(request,
uploadPath, // 업로드할 디렉토리 위치
size, // 첨부파일의 크기 : 10MB
"utf-8", // 인코딩 타입 설정
new DefaultFileRenamePolicy()); // 중복 문제 해결
- cos 라이브러리는 따로 업로드 시켜주는 코드가 없고, MultipartRequest 객체를 생성하면서 자동으로 업로드된다
+ 다른 라이브러리는 따로 업로드 시켜주는 코드가 있다
- 매개변수가 5개인 생성자로 MultipartRequest 객체 생성하면서 첨부파일 업로드가 수행됨
- 아래를 보면서 각 매개변수가 무엇을 의미하는지 보자
MultipartRequest 생성자 매개변수 의미
- MultipartRequest 클래스 API 문서
MultipartRequest
(javax.servlet.http.HttpServletRequest request,
java.lang.String saveDirectory,
int maxPostSize,
java.lang.String encoding,
FileRenamePolicy policy)
- 첫번째 매개변수 : request 객체가 되어야함
- 두번째 매개변수 : 업로드할 디렉토리 위치
- 세번째 매개변수 : 최대크기, 바이트 단위, 현재는 10메가까지 설정되어있다
- 네번째 매개변수 : cos 라이브러리로 전달되는 한글값들은 setCharaterEncoding() 메소드를 사용할 필요 없이 여기서 설정 가능
- 다섯번째 매개변수 : DefaultFileRenamePolicy 객체를 생성함으로서 중복 문제를 해결해준다
+ MultipartRequest 객체 생성시 인코딩 시켰으므로 name값과 subject.값이 넘어오더라도 한글값이 깨지지 않는다
폼에서 넘어온 첨부파일 외의 다른 값들은 어떻게 받을까? (fileUpload.jsp 부분)
- 폼에서 넘어온 올린 사람, 제목을 받는 방법
- request.getParameter() 처럼 request 객체로 값을 받으려고 시도하면 값이 안받아진다
// String na = request.getParameter("name");
// out.println("name : " + na); // name : null (값 전달이 안됨)
- 해결 방법 : request 객체가 아닌 생성했던 MultipartRequest 객체인 multi.getParameter() 메소드로 값을 받을 수 있다
name=multi.getParameter("name");
subject=multi.getParameter("subject");
파일명 구하기 (fileUpload.jsp 부분)
- 사용자가 폼에서 업로드한 첨부파일의 파일명을 구해야한다
- 파일의 name 값을 알지 못하는 경우와 아는 경우로 나뉜다
1. 파일의 name 값을 알지 못하는 경우
- 파일의 name 값인 "fileName1", "fileName2" 를 알지 못하는 경우
- 열거형 Enumeration 에 multi.getFileNames() 로 구해온 이름들을 저장하고 하나씩 가져오기
- multi.getFileNames() 메소드 : input type=file 로 된 양식의 name 값들을 가져옴
// 열거형 : fileName1, fileName2
Enumeration files=multi.getFileNames();
// String file1 = "fileName1";
String file1 =(String)files.nextElement();
// 실제 서버에 저장된 파일명 : Koala1.jpg
filename1=multi.getFilesystemName(file1);
// 클라이언트가 업로드한 파일명 : Koala.jpg
origfilename1= multi.getOriginalFileName(file1);
// String file2 = "fileName2";
String file2 =(String)files.nextElement();
filename2=multi.getFilesystemName(file2);
origfilename2=multi.getOriginalFileName(file2);
- 실제 서버에 저장된 파일명과 클라이언트가 업로드한 파일명이 다를 수 있다
- getFilesystemName() : 실제 서버에 저장된 파일명 구하기 ex) Koala1.jpg
- getOriginalFileName() : 사용자가 실제 선택한 파일명, 클라이언트가 업로드한 파일명 구하기 ex) Koala.jpg
- 중복문제가 없을땐 두가지가 같은 이름, 중복문제가 생겼을때 번호를 붙여 서버에 저장하면 두 이름이 달라짐
2. 파일의 name 값을 아는 경우
- 열거형을 사용할 필요 없이 바로 multi.getFilesystemName() 과 getOriginalFileName() 으로 파일명 구하기
- MultipartRequest 클래스 지원 메소드
- 지원되는 메소드가 MultipartRequest 안에 있으면 그걸 사용
- 지원되는 메소드가 없으면 request 객체 사용하면 됨
- 이후 폼을 통해서 값을 전달함
값을 fileCheck.jsp 파일로 전달 (fileUpload.jsp 부분)
<body>
<form name="filecheck" action="fileCheck.jsp" method="post">
<input type="hidden" name="name" value="<%=name%>">
<input type="hidden" name="subject" value="<%=subject%>">
<input type="hidden" name="filename1" value="<%=filename1%>">
<input type="hidden" name="filename2" value="<%=filename2%>">
<input type="hidden" name="origfilename1" value="<%=origfilename1%>">
<input type="hidden" name="origfilename2" value="<%=origfilename2%>">
</form>
<a href="#" onclick="javascript:filecheck.submit()">업로드 확인 및 다운로드 페이지 이동</a>
</body>
- hidden 을 사용해서 각 파일의 파일명 2개씩과 name, subject 등 6개의 값을 fileCheck.jsp 로 전송
- 전송버튼이 아니라 링크지만 onClick 에 submit() 메소드를 사용해서 전송가능
- 이제 fileCheck.jsp 파일을 살펴보자
- fileCheck.jsp
<%@ page language="java" contentType="text/html; charset=utf-8"%>
<%
request.setCharacterEncoding("utf-8");
String name=request.getParameter("name");
String subject=request.getParameter("subject");
String filename1=request.getParameter("filename1");
String filename2=request.getParameter("filename2");
//아래 두줄 추가
String origfilename1=request.getParameter("origfilename1");
String origfilename2=request.getParameter("origfilename2");
%>
<html>
<head>
<title>파일 업로드 확인 및 다운로드</title>
</head>
<body>
올린 사람 : <%=name %><br>
제목 : <%=subject %><br>
<!-- 파일 명 링크 거는 부분 수정함. -->
파일명1 : <a href="file_down.jsp?file_name=<%=filename1 %>"><%=origfilename1 %></a><br>
파일명2 : <a href="file_down.jsp?file_name=<%=filename2 %>"><%=origfilename2 %></a><p>
</body>
</html>
- hidden 으로 넘어온 값들을 모두 변수에 저장함
+ MultipartRequest 객체 생성시 인코딩 시켰으므로 name값과 subject.값이 계속 넘어가더라도 한글값이 깨지지 않는다
- 파일명을 클릭시 링크를 걸어서 클릭시 file_down.jsp 로 이동해서 파일을 다운하도록 한다
- 클릭시 get 방식으로 file_name 에 실제 서버에 저장된 이름인 filename1, filename2 를 각각 file_down.jsp로 전달함
- file_down.jsp 파일을 살펴보자
- file_down.jsp
<%@ page contentType="text/html;charset=utf-8" %>
<%@ page import="java.io.File"%>
<%@ page import="java.io.*"%>
<%
String fileName = request.getParameter("file_name");
System.out.println("fileName="+fileName);
String savePath = "upload";
ServletContext context = getServletContext();
String sDownloadPath = context.getRealPath(savePath);
String sFilePath = sDownloadPath + "\\" + fileName;
System.out.println("sFilePath="+sFilePath);
// jsp에서 OutputStream 사용시 IllegalStateException 해결법 : 2줄 추가
out.clear();
out = pageContext.pushBody();
try{
byte b[] = new byte[4096];
File oFile = new File(sFilePath);
FileInputStream in = new FileInputStream(sFilePath);
String sMimeType = getServletContext().getMimeType(sFilePath);
System.out.println("sMimeType>>>"+sMimeType );
// 다운로드 파일의 파일형식(마임타입) 설정
// octet-stream은 8비트로 된 일련의 데이터를 뜻합니다. 지정되지 않은 파일 형식을 의미합니다.
if(sMimeType == null) sMimeType = "application/octet-stream";
response.setContentType(sMimeType);
// 한글 파일명 처리 : 한글 파일명이 깨지는 것을 방지해 준다.
String sEncoding = new String(fileName.getBytes("utf-8"),"8859_1");
// 다운로드 파일 헤드 지정
response.setHeader("Content-Disposition", "attachment; filename= " + sEncoding);
// 출력 스트림 생성 : 위의 15, 16라인과 충돌발생함.
ServletOutputStream out2 = response.getOutputStream();
int numRead;
// 바이트 배열b의 0번 부터 numRead번 까지 브라우저로 출력
while((numRead = in.read(b, 0, b.length)) != -1) {
out2.write(b, 0, numRead);
}
out2.flush();
out2.close();
in.close();
}catch(Exception e){
e.printStackTrace();
}
%>
- request.getParameter() 를 통해 get 방식으로 넘어온 file_name 값을 받아서 fileName 변수에 저장함 (실제 저장 이름)
절대경로 구하기 (file_down.jsp 부분)
- 다운로드 받을때도 upload 디렉토리까지의 절대경로를 구해와야한다
String savePath = "upload";
ServletContext context = getServletContext();
String sDownloadPath = context.getRealPath(savePath);
String sFilePath = sDownloadPath + "\\" + fileName;
System.out.println("sFilePath="+sFilePath);
- getServletContext() 로 context 를 구해오고, 이 context 객체는 request 와 같은 역할을 함
- context.getRealPath() 메소드로 upload 폴더의 진짜 경로를 구해와서 sDownloadPath 에 저장
- sDownloadPath 에 저장된 경로값은 upload 디렉토리까지의 절대경로가 출력됨
sFilePath=C:\Users\admin\eclipse-workspace\.metadata\.plugins\org.eclipse.wst.server.core\tmp0\wtpwebapps\jspproject\upload\Penguins3.jpg
마임 타입 설정 (file_down.jsp 부분)
String sMimeType = getServletContext().getMimeType(sFilePath);
System.out.println("sMimeType>>>"+sMimeType );
// 다운로드 파일의 파일형식(마임타입) 설정
// octet-stream은 8비트로 된 일련의 데이터를 뜻합니다. 지정되지 않은 파일 형식을 의미합니다.
if(sMimeType == null) sMimeType = "application/octet-stream";
response.setContentType(sMimeType);
- 다운로드 파일의 파일형식(마임타입) 설정하는 코드
- 콘솔창 출력
한글 파일명 처리 (file_down.jsp 부분)
// 한글 파일명 처리 : 한글 파일명이 깨지는 것을 방지해 준다.
String sEncoding = new String(fileName.getBytes("utf-8"),"8859_1");
- 한글 파일명이 깨지는 것을 방지해주는 코드
다운로드 파일 헤드 지정 (file_down.jsp 부분)
// 다운로드 파일 헤드 지정
response.setHeader("Content-Disposition", "attachment; filename= " + sEncoding);
- 다운로드 받은 파일의 헤더에 관한 설정
- 인코딩 값등 설정
첨부파일을 하나씩 읽어오기 / 다운로드 (file_down.jsp 부분)
// 출력 스트림 생성 : 위의 15, 16라인과 충돌발생함.
ServletOutputStream out2 = response.getOutputStream();
int numRead;
// 바이트 배열b의 0번 부터 numRead번 까지 브라우저로 출력
while((numRead = in.read(b, 0, b.length)) != -1) {
out2.write(b, 0, numRead);
}
out2.flush();
out2.close();
in.close();
- 첨부파일을 하나씩 읽어오는 작업을 한다
- 읽어올때는 read() 메소드 사용
- 출력스트림을 통해서 write() 함수를 사용시 다운로드가 수행
- 쭉 읽어와서 출력을 시키면 그게 다운로드이다
Model 1 DBCP 방식으로 자료실 게시판 만들기
자료실 게시판
- 일반게시판에서 첨부파일을 업로드 / 다운로드 기능을 추가한 게시판
자료실 게시판 : 주요 기능 소개
1. Connection Pool
2. 액션태그
- <jsp:useBean....>, <jsp:setProperty....>
+ 첨부파일이 있는 경우엔 useBean, setProperty 사용 불가, 없는 경우엔 사용 가능
3. DTO, DAO 클래스
4. 페이징 처리 ( inline View )
5. 첨부파일 업로드 – cos.jar 라이브러리 이용
첨부파일 관련 컬럼
1. upload 컬럼
- 테이블에 첨부파일명을 저장해야한다
- 컬럼 upload 을 추가하여, 그 컬럼에 첨부파일명을 저장한다
- 나머지 컬럼은 일반 게시판과 같다
자료실 게시판 : 프로젝트 생성
- 이렇게 구조를 만들기 위해
- upload 라는 프로젝트부터 생성
- 이후 update 프로젝트의 WebContent 폴더 하위에 초기에 보여줄 파일인 index.jsp 를 생성
자료실 게시판 : 기초 파일 가져오기
- 클라우드에서 작성 양식, 환경설정 코드가 있는 upload 폴더를 이클립스 WebContent 폴더로 가져오기
자료실 게시판 : 첨부파일을 저장할 폴더 생성
- WebContent 하위에 업로드된 파일을 저장할 폴더인 upload 를 생성
- 실제 저장되는 폴더는 아니지만 여기에 upload 폴더를 만들어야 실제 서버에 저장되는 위치에도 upload 폴더가 생성된다
자료실 게시판 : 몇가지 환경 구축
1) reboard 폴더 안의 context.xml 파일을 META-INF 폴더로 옮기기
- 커넥션 풀의 환경설정 파일이고, 기존에 작성했던 내용
2) 오라클용 JDBC Driver 인 ojdbc.jar 를 WEB-INF/lib 폴더 안에 가져오기, 다른 프로젝트에서 가져오면 된다
3) cos 라이브러리를 사용하기 위해 cos.jar 파일을 WEB-INF/lib 폴더 안으로 가져오기
자료실 게시판 : Connection Pool 테스트
- dbcpAPITest.jsp 파일을 실행해서 커넥션 풀에서 커넥션 가져오기 테스트
- context.xml
<Context>
<Resource name="jdbc/orcl"
auth="Container"
type="javax.sql.DataSource"
username="totoro"
password="totoro123"
driverClassName="oracle.jdbc.driver.OracleDriver"
factory="org.apache.tomcat.dbcp.dbcp2.BasicDataSourceFactory"
url="jdbc:oracle:thin:@localhost:1521:xe"
maxActive="500"
maxIdle="100"/>
</Context>
<!-- p423 참조
maxActive="500" 컨넥션풀이 관리하는 컨넥션의 최대 갯수
(기본값 : 8)
maxIdle="100" 컨넥션풀이 관리하는 최대 유휴 갯수(기본값 : 8)
-->
- dbcpAPITest.jsp
<%@ page language="java" contentType="text/html; charset=EUC-KR"%>
<%@ page import="java.sql.*"%>
<%@ page import="javax.sql.*" %>
<%@ page import="javax.naming.*" %>
<%
Connection conn = null;
try {
Context init = new InitialContext();
DataSource ds = (DataSource) init.lookup("java:comp/env/jdbc/orcl");
conn = ds.getConnection();
out.println("<h3>연결되었습니다.</h3>");
}catch(Exception e){
out.println("<h3>연결에 실패하였습니다.</h3>");
e.printStackTrace();
}
%>
- 커넥션 풀에서 커넥션 가져오기 테스트 성공
+ 추후 DAO 클래스 만들때 커넥션풀에서 커넥션 구하기 3줄 코드를 DAO 클래스에 넣을 것
Context init = new InitialContext();
DataSource ds = (DataSource) init.lookup("java:comp/env/jdbc/orcl");
conn = ds.getConnection();
자료실 게시판 : 테이블 및 시퀀스 생성
- 이전에 회원관리 프로그램에서 사용했던 오라클계정인 totoro 계정에 upload 테이블 생성
- 아래 양식으로 테이블을 생성한다
create table upload(
num number primary key,
writer varchar2(20) not null,
email varchar2(30),
subject varchar2(50) not null,
passwd varchar2(20) not null,
reg_date timestamp not null,
readcount number default 0,
content varchar2(2000) not null,
ip varchar2(20) not null,
upload varchar2(30) ); -- 첨부파일명
create sequence upload_seq
start with 1
increment by 1
nocache;
- WebContent 하위에 sql 폴더를 만들고 안에 upload.sql 파일 생성
- 커넥션 프로파일을 설정 : Oracle_11, New Oracle(totoro), xe 로 해준다
- 테이블과 시퀀스를 생성한다
- upload 컬럼에는 첨부파일 명이 들어간다.
- upload 컬럼을 제외한 나머지 컬럼은 일반 게시판과 동일하다
- 시퀀스는 num 이라는 기본키 컬럼에 들어갈 시퀀스이다
- 테이블과 시퀀스 생성 확인
- 생성확인 완료
자료실 게시판 : DAO 와 DTO 클래스 만들기
+ 확장자가 .java 인 파일은 src 폴더 안에 있어야 정상동작한다
+ 이후 Model 2 에선 기능에 따라 세분화하므로 DAO, DTO 가 다른 패키지에 들어감
- 지금은 같은 패키지인 upload 패키지 안에 둘 다 넣자
자료실 게시판 : DTO 클래스 작성
- 이걸 가져와서 DTO 클래스에 복붙
- varchar2 는 String 으로, number 는 int 로, timestamp 는 Timestamp로 바꿔서 자바 변수(프로퍼티) 만들기
+ 프로퍼티의 접근제어자는 private
+ java.sql 의 Timestamp import
- getter / setter 메소드 추가
- DTO 클래스 BoardDataBean.java 완성 코드
// DTO (Data Transfer Object)
package upload;
import java.sql.Timestamp;
public class BoardDataBean {
private int num;
private String writer;
private String email;
private String subject;
private String passwd;
private Timestamp reg_date;
private int readcount;
private String content;
private String ip;
private String upload;
public int getNum() {
return num;
}
public void setNum(int num) {
this.num = num;
}
public String getWriter() {
return writer;
}
public void setWriter(String writer) {
this.writer = writer;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getSubject() {
return subject;
}
public void setSubject(String subject) {
this.subject = subject;
}
public String getPasswd() {
return passwd;
}
public void setPasswd(String passwd) {
this.passwd = passwd;
}
public Timestamp getReg_date() {
return reg_date;
}
public void setReg_date(Timestamp reg_date) {
this.reg_date = reg_date;
}
public int getReadcount() {
return readcount;
}
public void setReadcount(int readcount) {
this.readcount = readcount;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public String getIp() {
return ip;
}
public void setIp(String ip) {
this.ip = ip;
}
public String getUpload() {
return upload;
}
public void setUpload(String upload) {
this.upload = upload;
}
}
자료실 게시판 : DAO 클래스 작성
DAO 에 들어갈 내용
1. 싱글톤
2. 정적메소드 getInstance() 생성
3. Connection Pool 에서 커넥션 구해오는 메소드
4. 그 이후 자료실 게시판 CRUD 관련 메소드
1. 싱글톤
- 외부 접근 불가능하게 private, 공유하기 위해 static 해서 자기자신의 클래스로 객체 생성을 한번만 하기
2. 정적메소드 getInstance() 생성
3. Connection Pool 에서 커넥션 구해오는 메소드 getConnection()
- 메소드 호출시 Connection Pool 에서 커넥션을 구해주도록 함
- DBCP 방식으로 DB 와 연결하므로 이 DB 연결시 커넥션을 구해오는 이 메소드를 사용해야한다
- 커넥션 구할때 예외처리를 해야한다, 여기선 throws 로 Exception 던지기
+ Connection 클래스 import
- 테스트할때 사용했던 dbcpAPITest.jsp 3줄을 복사해서 넣는다
- 단축키 Ctrl + Shift + O 로 import 시키기
- javax.naming.Context 를 선택후 Next, 다음은 javax.sql.DataSource 선택후 Finish
+ Context 와 DataSource 둘 다 Interface 이다
- getConnection() 메소드 완성
- 가져온 커넥션을 리턴하는 코드로 바꿈
+ 리턴자료형을 private 으로 해서 DAO 클래스 내에서만 호출가능하도록 함
- 현재까지 DAO 클래스 BoardDBBean.java 코드
// DAO (Data Access Object)
package upload;
import java.sql.Connection;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.sql.DataSource;
public class BoardDBBean {
// 싱글톤 : 객체 생성을 1번만 수행하는 것.
private static BoardDBBean instance = new BoardDBBean();
public static BoardDBBean getInstance() {
return instance;
}
// 커넥션 풀에서 커넥션을 구해오는 메소드
private Connection getConnection() throws Exception{
Context init = new InitialContext();
DataSource ds = (DataSource) init.lookup("java:comp/env/jdbc/orcl");
return ds.getConnection();
}
// 글 작성
}
4. 그 이후 자료실 게시판 CRUD 관련 메소드
- 가장 먼저 글 작성 메소드를 만들어야한다, 그 후 다양한 메소드들을 DAO에 작성
자료실 게시판 : 파일별 기능 및 구조 설명
- writeForm.jsp 에 첨부파일을 선택하는 양식이 포함되어있다