[비트캠프] 77일차(16주차4일) - Servlet(파일 업로드, 멀티파트), myapp-50(파일 업로드)
50 파일 업로드 처리 - 테이블 구조 정의
회원, 게시글, 첨부파일을 ER Diagram 으로 나타내면 다음과 같다.
app_member I OI< app_board I OI< app_board_file
app_member 의 member_id (PK) 가 app_board 의 writer (FK) 이다.
app_board 의 board_id (PK) 가 app_board_file 의 board_id (FK) 이다.
50 파일 업로드 처리 - 테이블과 자바 객체
Board 의 writer 필드에 Member 객체를 필수로 저장한다.
Board 의 attachedFiles 필드에 BoardFile 객체를 0개 이상 저장한다.
50 파일 업로드 처리 - Servlet 과 DAO
BoardInsertServlet 에서 insert() 실행을 BoardDao 에게 지시한다. 여기서 《table》app_board 에 insert 한다.
BoardInsertServle 에서 insert() 실행을 BoardFileDao 에게 지시한다. 여기서 BoardFileMapper.xml 이용해서 《table》app_board_file 에 insert 한다.
입력화면 생성: BoardFormServlet 에서 forwarding 을 form.jsp 로 보낸다.
50 파일 업로드 처리 - 조인 결과를 자바 객체로 받기
DBMS 에서 findByNo 로 board 데이터 찾을 때 Board, Member, BoardFile 3개 객체의 데이터가 사용된다.
Board 데이터의 board_id 는 각 행의 ID 컬럼 값이 같기 때문에 기존 객체 사용하여 객체 생성 및 컬럼 값 저장한다.
Member 데이터의 writer 도 각 행의 ID 컬럼 값이 같기 때문에 기존 객체 사용한다.
BoardFile 데이터의 boardfile_id 는 각 행의 ID 컬럼 값이 다르기 때문에 행마다 객체 생성 및 컬럼 값 저장한다.
50 파일 업로드 처리 - 멀티 파트 데이터 처리
요청 프로토콜
POST /web/board/insert HTTP/1.1
...
Content-Type: multipart/form-data; boundary=~~~
Content-Length: ~~~
...
파라미터 정보를 객체로 변환할 때
Apache.org 의 fileupload 라이브러리를 멀티파트 형식의 요청 데이터를 분석해서 객체에 담아 준다.
Apache 파일 업로드 라이브러리 다운
central.sonatype.com 에서 'commons-fileupload' 검색 후 아래 코드 build.gradle 에 붙이고 $ gradle eclipse 실행한다.
commoms- 라이브러리 생성된다.
apache commons 사용법
apache.org 페이지 아래에서 Commons 클릭한다.
FileUpload 클릭한다.
필요 코드 참고한다.
Web Project 개발과 테스팅 흐름
(프로젝트 폴더) → (톰캣 테스트 환경의 배치 폴더)
src/main/java/ .java 컴파일 된 바이트 코드 → tmp0/wtpwebapps/myapp-server/WEB-INF/classes/ .class 로 배치된다.
src/main/resources/ .xml, .properties → tmp0/wtpwebapps/myapp-server/WEB-INF/classes/ .xml, .properties 등으로 배치된다.
src/main/webapp/ .html, .js, .css, .gif 등 → tmp0/wtpwebapps/myapp-server/ .html, .js, .css, .gif 등으로 배치된다.
프로젝트 폴더는 Eclipse IDE 가 관리한다.
배치 폴더는 Servlet Container 가 web browser 의 요청을 받으면 서블릿을 실행, HTML/CSS/JS 등 정적 파일을 다운로드 하여 응답한다.
### 50. 파일을 업로드하기: multipart/form-data MIME 타입 다루기
- ddl4.sql 실행: app_board_file 테이블 생성
- multipart/form-data 형식으로 파일을 업로드 하는 방법
- apache commons-fileupload 라이브러리 사용법
- Servlet에서 제공하는 API(3.0부터 추가됨)를 사용하여 파일 업로드를 처리하는 방법
util/DaoGenerator.java
SqlSession 객체 얻고나서 자동 close 하기 위해 try-with-resources 의 () 안에 넣는다.
public class DaoGenerator implements InvocationHandler {
SqlSessionFactory sqlSessionFactory;
public DaoGenerator(SqlSessionFactory sqlSessionFactory) {
this.sqlSessionFactory = sqlSessionFactory;
}
@SuppressWarnings("unchecked")
public <T> T getObject(Class<T> classInfo) {
String className = classInfo.getName();
return (T) Proxy.newProxyInstance(
getClass().getClassLoader(), // 현재 클래스의 로딩을 담당한 관리자: 즉 클래스 로딩 관리자
new Class[] {classInfo}, // 클래스가 구현해야 할 인터페이스 정보 목록
this // InvocationHandler 객체
);
}
// 자동 생성된 프록시 객체에 대해 메서드를 호출하면
// 실제 InvocationHandler의 invoke()가 호출된다.
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
String daoName = proxy.getClass().getInterfaces()[0].getSimpleName();
String methodName = method.getName();
String sqlStatementName = String.format("%s.%s", daoName, methodName);
System.out.printf("%s.%s() 호출!\n", daoName, methodName);
Class<?> returnType = method.getReturnType();
if (returnType == int.class || returnType == void.class) {
return args == null ? sqlSession.insert(sqlStatementName) :
sqlSession.insert(sqlStatementName, args[0]);
} else if (returnType == List.class) {
return args == null ? sqlSession.selectList(sqlStatementName) :
sqlSession.selectList(sqlStatementName, args[0]);
} else {
return args == null ? sqlSession.selectOne(sqlStatementName) :
sqlSession.selectOne(sqlStatementName, args[0]);
}
}
}
public static void main(String[] args) throws Exception {
BitcampSqlSessionFactory sqlSessionFactory = new BitcampSqlSessionFactory(
new SqlSessionFactoryBuilder().build(
Resources.getResourceAsStream("bitcamp/myapp/config/mybatis-config.xml")));
DaoGenerator generator = new DaoGenerator(sqlSessionFactory);
BoardDao dao = generator.getObject(BoardDao.class);
// Board b = new Board();
// b.setTitle("테스트1");
// b.setContent("테스트내용1");
// b.setPassword("1111");
// dao.insert(b);
// Board b = new Board();
// b.setNo(13);
// b.setTitle("테스트1xxx");
// b.setContent("테스트내용1xxxx");
// b.setPassword("1111");
// dao.update(b);
// dao.delete(13);
// List<Board> list = dao.findAll();
// for (Board b : list) {
// System.out.println(b);
// }
// System.out.println(dao.findByNo(13));
}
}
doc/ddl4.sql
-- 게시글 첨부파일 정보를 저장하는 테이블 정의
create table app_board_file (
boardfile_id int not null,
filepath varchar(255) not null,
origin_filename varchar(255) not null,
mime_type varchar(30) not null,
board_id int not null
);
alter table app_board_file
add constraint primary key (boardfile_id),
modify column boardfile_id int not null auto_increment;
alter table app_board_file
add constraint app_board_file_fk foreign key (board_id) references app_board (board_id);
BoardFile.java
DBMS 에 맞춰 BoardFile 클래스 정의한다.
package bitcamp.myapp.vo;
import java.io.Serializable;
import java.util.Objects;
public class BoardFile implements Serializable {
private static final long serialVersionUID = 1L;
private int no;
private String filepath;
private String originalFilename;
private String mimeType;
private int boardNo;
/* 후략 */
BoardFileDao.java
package bitcamp.myapp.dao;
import java.util.List;
import bitcamp.myapp.vo.BoardFile;
public interface BoardFileDao {
int insert(BoardFile boardFile);
int insertList(List<BoardFile> boardFiles);
List<BoardFile> findAllOfBoard(int boardNo);
BoardFile findByNo(int boardFileNo);
int delete(int boardFileNo);
int deleteOfBoard(int boardNo);
}
ContextLoaderListener.java
웹 앱 시작시 BoardFileDao 객체 생성 및 Servlet Context 에 보관한다.
package bitcamp.myapp.listener;
import java.io.InputStream;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import bitcamp.myapp.dao.BoardDao;
import bitcamp.myapp.dao.BoardFileDao;
import bitcamp.myapp.dao.MemberDao;
import bitcamp.myapp.dao.StudentDao;
import bitcamp.myapp.dao.TeacherDao;
import bitcamp.util.BitcampSqlSessionFactory;
import bitcamp.util.DaoGenerator;
import bitcamp.util.TransactionManager;
// 웹 애플리케이션이 시작/종료 될 때 실행되는 객체
@WebListener
// 서블릿 컨테이너에게 이 클래스가 리스너 구현체임을 알려줘야 한다.
// 그래야만 서블릿 컨테이너는 이 클래스의 인스턴스를 생성한다.
// 그리고 웹앱이 시작되거나 종료될 때 메서드를 호출해 준다.
public class ContextLoaderListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
// 웹 애플리케이션이 시작될 때 서블릿 컨테이너가 호출한다.
System.out.println("ContextLoaderListener.contextInitialized() 호출됨!");
try {
InputStream mybatisConfigInputStream = Resources.getResourceAsStream(
"bitcamp/myapp/config/mybatis-config.xml");
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
BitcampSqlSessionFactory sqlSessionFactory = new BitcampSqlSessionFactory(
builder.build(mybatisConfigInputStream));
TransactionManager txManager = new TransactionManager(sqlSessionFactory);
BoardDao boardDao = new DaoGenerator(sqlSessionFactory).getObject(BoardDao.class);
MemberDao memberDao = new DaoGenerator(sqlSessionFactory).getObject(MemberDao.class);
StudentDao studentDao = new DaoGenerator(sqlSessionFactory).getObject(StudentDao.class);
TeacherDao teacherDao = new DaoGenerator(sqlSessionFactory).getObject(TeacherDao.class);
BoardFileDao boardFileDao = new DaoGenerator(sqlSessionFactory).getObject(BoardFileDao.class);
// 서블릿 컨텍스트 보관소를 알아낸다.
ServletContext ctx = sce.getServletContext();
// 서블릿들이 공유할 객체를 이 보관소에 저장한다.
ctx.setAttribute("txManager", txManager);
ctx.setAttribute("boardDao", boardDao);
ctx.setAttribute("memberDao", memberDao);
ctx.setAttribute("studentDao", studentDao);
ctx.setAttribute("teacherDao", teacherDao);
ctx.setAttribute("boardFileDao", boardFileDao);
} catch (Exception e) {
System.out.println("웹 애플리케이션 자원을 준비하는 중에 오류 발생!");
e.printStackTrace();
}
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
// 웹 애플리케이션이 종료될 때 서블릿 컨테이너가 호출한다.
System.out.println("ContextLoaderListener.contextDestroyed() 호출됨!");
}
}
Board.java
Board 객체에 BoardFile 을 담을 List 필드를 attachedFiles 로 정의한다.
package bitcamp.myapp.vo;
import java.sql.Date;
import java.util.List;
import java.util.Objects;
public class Board implements java.io.Serializable {
private static final long serialVersionUID = 1L;
private int no;
private String title;
private String content;
private String password;
private Date createdDate;
private int viewCount;
// private int writerNo;
// private String writerName;
private Member writer;
private List<BoardFile> attachedFiles;
webapp/board/view.jsp
<input> 속성에 multiple 로 파일 다중 선택 지정한다.
반복문에서 boardFile.no != 0 (파일이 있으)면 file download 링크 건다. file delete 버튼 생성한다.
<%@ 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>비트캠프 - NCP 1기</title>
</head>
<body>
<h1>게시판(JSP + MVC2 + EL + JSTL)</h1>
<c:if test="${empty board}">
<p>해당 번호의 게시글 없습니다.</p>
<div>
<button id='btn-list' type='button'>목록</button>
</div>
</c:if>
<c:if test="${not empty board}">
<form id='board-form' action='update' method='post' enctype="multipart/form-data">
<table border='1'>
<tr>
<th>번호</th>
<td><input type='text' name='no' value='${board.no}' readonly></td>
</tr>
<tr>
<th>제목</th>
<td><input type='text' name='title' value='${board.title}'></td>
</tr>
<tr>
<th>내용</th>
<td><textarea name='content' rows='10' cols='60'>${board.content}</textarea></td>
</tr>
<tr>
<th>작성자</th>
<td>${board.writer.name}</td>
</tr>
<tr>
<th>등록일</th>
<td>${board.createdDate}</td>
</tr>
<tr>
<th>조회수</th>
<td>${board.viewCount}</td>
</tr>
<tr>
<th>첨부파일</th>
<td>
<input type="file" name='files' multiple>
<ul>
<c:forEach items="${board.attachedFiles}" var="boardFile">
<c:if test="${boardFile.no != 0}">
<li>
<a href="../download/boardfile?fileNo=${boardFile.no}">${boardFile.originalFilename}</a>
[<a href="filedelete?boardNo=${board.no}&fileNo=${boardFile.no}">삭제</a>]
</li>
</c:if>
</c:forEach>
</ul>
</td>
</tr>
</table>
<div>
<button id='btn-list' type='button'>목록</button>
<button>변경</button>
<button id='btn-delete' type='button'>삭제</button>
</div>
</form>
</c:if>
<script>
document.querySelector('#btn-list').onclick = function() {
location.href = 'list';
}
<c:if test="${not empty board}">
document.querySelector('#btn-delete').onclick = function() {
var form = document.querySelector('#board-form');
form.action = 'delete';
form.submit();
}
</c:if>
</script>
</body>
</html>
DownloadServlet.java
package bitcamp.myapp.servlet;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import bitcamp.myapp.dao.BoardFileDao;
import bitcamp.myapp.vo.BoardFile;
@WebServlet("/download/boardfile")
public class DownloadServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private BoardFileDao boardFileDao;
@Override
public void init() {
ServletContext ctx = getServletContext();
boardFileDao = (BoardFileDao) ctx.getAttribute("boardFileDao");
}
@Override
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
try {
// 다운로드 받을 파일 번호를 알아낸다.
int fileNo = Integer.parseInt(request.getParameter("fileNo"));
// 파일 번호를 이용하여 파일 정보를 가져온다.
BoardFile boardFile = boardFileDao.findByNo(fileNo);
if (boardFile == null) {
throw new RuntimeException("파일 정보 없음!");
}
// 파일을 찾는다.
File downloadFile = new File(
this.getServletContext().getRealPath("/board/upload/" + boardFile.getFilepath()));
if (!downloadFile.exists()) {
throw new RuntimeException("파일이 존재하지 않음!");
}
// 파일을 보내기 전에 클라이언트에게 파일에 대한 정보를 알려주기 위해 응답헤더를 추가한다.
// => 보내는 데이터의 MIME 타입을 알려준다.
// 예) Content-Type: image/jpeg
response.setContentType(boardFile.getMimeType());
// => 보내는 데이터의 파일 이름을 알려준다.
// 예) Content-Disposition: attachment; filename="test.gif"
response.setHeader("Content-Disposition",
String.format("attachment; filename=\"%s\"", boardFile.getOriginalFilename()));
try (// 파일을 읽기 위해 준비한다.
BufferedInputStream fileIn = new BufferedInputStream(new FileInputStream(downloadFile));
BufferedOutputStream out = new BufferedOutputStream(response.getOutputStream());) {
// 파일에서 데이터를 읽어 클라이언트로 보낸다.
int b;
while ((b = fileIn.read()) != -1) {
out.write(b);
}
out.flush();
}
} catch (Exception e) {
request.getRequestDispatcher("/downloadfail.jsp").forward(request, response);
}
}
}
webapp/downloadfail.jsp
다운로드 실패시 여기도 이동한다.
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>비트캠프 - NCP 1기</title>
</head>
<body>
<h1>다운로드 오류!</h1>
<p>파일 다운로드 실패!</p>
</body>
</html>
BoardFileDeleteServlet.java
파일 삭제 클릭시 이 서블릿 실행된다.
로그인 사용자의 세션 정보를 가져와서 게시글 작성자 번호와 비교 후 일치시 파일 삭제하고 현재 페이지 리다이렉트 한다.
이 서버 렌더링 방식과 다르게 클라이언트 렌더링 방식은 <li> 만 삭제하면 되므로 간편하다.
package bitcamp.myapp.servlet.board;
import java.io.IOException;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import bitcamp.myapp.dao.BoardDao;
import bitcamp.myapp.dao.BoardFileDao;
import bitcamp.myapp.vo.Board;
import bitcamp.myapp.vo.Member;
@WebServlet("/board/filedelete")
public class BoardFileDeleteServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private BoardDao boardDao;
private BoardFileDao boardFileDao;
@Override
public void init() {
ServletContext ctx = getServletContext();
boardDao = (BoardDao) ctx.getAttribute("boardDao");
boardFileDao = (BoardFileDao) ctx.getAttribute("boardFileDao");
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 로그인 사용자의 정보를 가져온다.
Member loginUser = (Member) request.getSession().getAttribute("loginUser");
int boardNo = Integer.parseInt(request.getParameter("boardNo"));
Board old = boardDao.findByNo(boardNo);
if (old.getWriter().getNo() != loginUser.getNo()) {
response.sendRedirect("../auth/fail");
return;
}
boardFileDao.delete(Integer.parseInt(request.getParameter("fileNo")));
response.sendRedirect("view?no=" + boardNo);
}
}
mapper/BoardFileMapper.xml
BoardFileMapper.xml 정의한다.
insert 로 넘어오는 파라미터 타입이 List 면 <foreach> 속성 collection= "list" 나 "collection" 사용해야 한다.
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="BoardFileDao">
<resultMap type="boardfile" id="boardfileMap">
<id column="boardfile_id" property="no"/>
<result column="filepath" property="filepath"/>
<result column="origin_filename" property="originalFilename"/>
<result column="mime_type" property="mimeType"/>
<result column="board_id" property="boardNo"/>
</resultMap>
<insert id="insert" parameterType="boardfile">
insert into app_board_file(filepath, origin_filename, mime_type, board_id)
values(#{filepath}, #{originalFilename}, #{mimeType}, #{boardNo})
</insert>
<!-- SQL을 실행할 때 넘어오는 파라미터 타입이 List일 경우
foreach 태그의 collection 속성에 사용할 수 있는 파라미터 이름은
list 또는 collection 이 가능하다. -->
<insert id="insertList">
insert into app_board_file(filepath, origin_filename, mime_type, board_id)
values
<foreach collection="list" item="file" separator=",">
(#{file.filepath}, #{file.originalFilename}, #{file.mimeType}, #{file.boardNo})
</foreach>
</insert>
<select id="findAllOfBoard" resultMap="boardfileMap" parameterType="int">
select
boardfile_id,
filepath,
origin_filename,
mime_type,
board_id
from
app_board_file
where
board_id = ${no}
order by
origin_filename asc
</select>
<select id="findByNo" parameterType="int" resultMap="boardfileMap">
select
boardfile_id,
filepath,
origin_filename,
mime_type,
board_id
from
app_board_file
where
boardfile_id = ${no}
</select>
<delete id="delete" parameterType="int">
delete from app_board_file
where boardfile_id=#{no}
</delete>
<delete id="deleteOfBoard" parameterType="int">
delete from app_board_file
where board_id=#{no}
</delete>
</mapper>
config/mybatis-config.xml
여기서 BoardFileMapper.xml 등록한다.
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<properties resource="bitcamp/myapp/config/jdbc.properties"></properties>
<typeAliases>
<!-- 지정된 패키지의 모든 클래스에 대해 클래스 이름과 같은 이름으로 별명을 부여한다. -->
<package name="bitcamp.myapp.vo"/>
</typeAliases>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="bitcamp/myapp/mapper/BoardMapper.xml"/>
<mapper resource="bitcamp/myapp/mapper/MemberMapper.xml"/>
<mapper resource="bitcamp/myapp/mapper/StudentMapper.xml"/>
<mapper resource="bitcamp/myapp/mapper/TeacherMapper.xml"/>
<mapper resource="bitcamp/myapp/mapper/BoardFileMapper.xml"/>
</mappers>
</configuration>
mapper/BoardMapper.xml
resultMap 설정한다.
<collection> 으로 property="attachedFiles" 설정한다. 콜렉션에 담는 타입은 ofType="boardfile" 로 설정한다.
findByNo SQL 에서 left outer join 으로 app_board_file 테이블 가져온다.
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="BoardDao">
<resultMap type="board" id="boardMap">
<id column="board_id" property="no"/>
<result column="title" property="title"/>
<result column="content" property="content"/>
<result column="pwd" property="password"/>
<result column="created_date" property="createdDate"/>
<result column="view_cnt" property="viewCount"/>
<association property="writer" javaType="member">
<id column="writer" property="no"/>
<result column="name" property="name"/>
</association>
<collection property="attachedFiles" ofType="boardfile">
<id column="boardfile_id" property="no"/>
<result column="filepath" property="filepath"/>
<result column="origin_filename" property="originalFilename"/>
<result column="mime_type" property="mimeType"/>
<result column="board_id" property="boardNo"/>
</collection>
</resultMap>
<insert id="insert" parameterType="board"
useGeneratedKeys="true" keyColumn="board_id" keyProperty="no">
insert into app_board(title, content, writer)
values(#{title}, #{content}, #{writer.no})
</insert>
<select id="findAll" resultMap="boardMap" parameterType="string">
select
b.board_id,
b.title,
b.writer,
b.created_date,
b.view_cnt,
m.name
from
app_board b
inner join app_member m on b.writer = m.member_id
<if test="keyword != '' and keyword != null">
where
b.title like(concat('%',#{keyword},'%'))
or b.content like(concat('%',#{keyword},'%'))
</if>
order by
b.board_id desc
</select>
<select id="findByNo" parameterType="int" resultMap="boardMap">
select
b.board_id,
b.title,
b.content,
b.writer,
(select name from app_member where member_id = b.writer) name,
b.created_date,
b.view_cnt,
bf.boardfile_id,
bf.filepath,
bf.origin_filename,
bf.mime_type
from
app_board b
left outer join app_board_file bf on b.board_id = bf.board_id
where
b.board_id=#{no}
</select>
<update id="increaseViewCount" parameterType="int">
update app_board set
view_cnt = view_cnt + 1
where board_id=#{maumdaerohaedodoi}
</update>
<update id="update" parameterType="board">
update app_board set
title=#{title},
content=#{content}
where board_id=#{no}
</update>
<delete id="delete" parameterType="int">
delete from app_board
where board_id=#{no}
</delete>
</mapper>
webapp/board/form.jsp
첨부파일 <input> 추가한다. multiple 로 다수 파일 선택하게 한다.
<form> 속성에 enctype="multipart/form-data" 추가한다.
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset='UTF-8'>
<title>비트캠프 - NCP 1기</title>
</head>
<body>
<h1>게시판(JSP + MVC2)</h1>
<form action='insert' method='post' enctype="multipart/form-data">
<table border='1'>
<tr>
<th>제목</th>
<td><input type='text' name='title'></td>
</tr>
<tr>
<th>내용</th>
<td><textarea name='content' rows='10' cols='60'></textarea></td>
</tr>
<tr>
<th>첨부파일</th>
<td><input type="file" name='files' multiple></td>
</tr>
</table>
<div>
<button>등록</button>
<button id='btn-cancel' type='button'>취소</button>
</div>
</form>
<script>
document.querySelector('#btn-cancel').onclick = function() {
location.href = 'list';
}
</script>
</body>
</html>
BoardInsertServlet.java
insert 시 첨부파일 추가한다.
DiskFileItemFactory 객체 생성 해서 ServletFileUpload 객체 생성 매개변수로 전달한다.
아래부터 전체를 try 로 묶는다.
upload.parseRequest(request) 로 각 파트를 담은 리스트 반환받는다.
일반 파라미터 맵과 첨부파일 보관 리스트로 분리 저장한다.
현재 웹 앱 실제 경로 알아내 파일 저장한다.
파일 1개마다 개별 insert 문 실행하지 않고, 파일 목록 담은 list 를 한번에 insert 한다.
package bitcamp.myapp.servlet.board;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import bitcamp.myapp.dao.BoardDao;
import bitcamp.myapp.dao.BoardFileDao;
import bitcamp.myapp.vo.Board;
import bitcamp.myapp.vo.BoardFile;
import bitcamp.myapp.vo.Member;
import bitcamp.util.TransactionManager;
@WebServlet("/board/insert")
public class BoardInsertServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private TransactionManager txManager;
private BoardDao boardDao;
private BoardFileDao boardFileDao;
@Override
public void init() {
ServletContext ctx = getServletContext();
boardDao = (BoardDao) ctx.getAttribute("boardDao");
boardFileDao = (BoardFileDao) ctx.getAttribute("boardFileDao");
txManager = (TransactionManager) ctx.getAttribute("txManager");
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 멀티파트 데이터를 디스크에 저장한 후 객체를 리턴해주는 일을 한다.
DiskFileItemFactory factory = new DiskFileItemFactory();
// 멀티파트로 전송된 요청 데이터를 읽을 준비를 한다.
ServletFileUpload upload = new ServletFileUpload(factory);
// 위에서 준비한 객체를 사용하여 멀티파트 요청 데이터를 처리한다.
// => 리턴되는 값은 각 파트를 FileItem 객체에 담은 객체 목록이다.
try {
List<FileItem> items = upload.parseRequest(request);
// 일반 파라미터 값을 저장할 맵
Map<String,String> paramMap = new HashMap<>();
// 첨부파일을 보관할 목록
List<FileItem> files = new ArrayList<>();
// 파트의 값을 일반 파라미터와 첨부파일 파라미터로 분리하여 저장한다.
for (FileItem item : items) {
if (item.isFormField()) {
paramMap.put(item.getFieldName(), item.getString("UTF-8"));
} else {
files.add(item);
}
}
Board board = new Board();
board.setTitle(paramMap.get("title"));
board.setContent(paramMap.get("content"));
// 로그인 사용자의 정보를 가져온다.
Member loginUser = (Member) request.getSession().getAttribute("loginUser");
Member writer = new Member();
writer.setNo(loginUser.getNo());
board.setWriter(writer);
txManager.startTransaction();
boardDao.insert(board);
List<BoardFile> boardFiles = new ArrayList<>();
for (FileItem file : files) {
if (file.getSize() == 0) {
continue;
}
String filename = UUID.randomUUID().toString();
// 임시 저장된 첨부파일을 특정 디렉토리로 옮긴다.
// 이때 전체 경로 및 파일명을 File 객체에 담아 넘겨야 한다.
// 1) 서블릿 컨테이너가 실행하는 현재 웹 애플리케이션의 실제 경로 알아내기
String realPath = this.getServletContext().getRealPath("/board/upload/" + filename);
System.out.println(realPath);
file.write(new File(realPath));
BoardFile boardFile = new BoardFile();
boardFile.setOriginalFilename(file.getName());
boardFile.setFilepath(filename);
boardFile.setMimeType(file.getContentType());
boardFile.setBoardNo(board.getNo());
//boardFileDao.insert(boardFile);
boardFiles.add(boardFile);
}
if (boardFiles.size() > 0) {
boardFileDao.insertList(boardFiles);
}
txManager.commit();
} catch (Exception e) {
txManager.rollback();
e.printStackTrace();
request.setAttribute("error", "data");
}
request.getRequestDispatcher("/board/insert.jsp").forward(request, response);
}
}
BoardUpdateServlet.java
여기서는 Servlet 3.0 방법을 쓴다.
@Multipartconfig 로 멀티파트 처리한다.
request.getParts(); 로 파트들을 모두 가져온다.
part 의 이름이 files 가 아닌걸로 List 에 담아야한다.
"file" 로 첨부파일 없어도 빈 파트가 넘어오므로 size 검사한다.
package bitcamp.myapp.servlet.board;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.UUID;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.MultipartConfig;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.Part;
import bitcamp.myapp.dao.BoardDao;
import bitcamp.myapp.dao.BoardFileDao;
import bitcamp.myapp.vo.Board;
import bitcamp.myapp.vo.BoardFile;
import bitcamp.myapp.vo.Member;
import bitcamp.util.TransactionManager;
// multipart/form-data 를 처리할 때 Servlet 3.0 기본 라이브러리를 사용한다면
// 다음 애노테이션을 붙여야 한다.
@MultipartConfig(maxFileSize = 1024 * 1024 * 50)
@WebServlet("/board/update")
public class BoardUpdateServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private TransactionManager txManager;
private BoardDao boardDao;
private BoardFileDao boardFileDao;
@Override
public void init() {
ServletContext ctx = getServletContext();
boardDao = (BoardDao) ctx.getAttribute("boardDao");
boardFileDao = (BoardFileDao) ctx.getAttribute("boardFileDao");
txManager = (TransactionManager) ctx.getAttribute("txManager");
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
txManager.startTransaction();
try {
// 로그인 사용자의 정보를 가져온다.
Member loginUser = (Member) request.getSession().getAttribute("loginUser");
Board board = new Board();
board.setNo(Integer.parseInt(request.getParameter("no")));
board.setTitle(request.getParameter("title"));
board.setContent(request.getParameter("content"));
Board old = boardDao.findByNo(board.getNo());
if (old.getWriter().getNo() != loginUser.getNo()) {
response.sendRedirect("../auth/fail");
return;
}
if (boardDao.update(board) == 0) {
throw new RuntimeException("게시글이 존재하지 않습니다.");
}
// 게시글의 첨부파일 추가하기
Collection<Part> parts = request.getParts();
List<BoardFile> boardFiles = new ArrayList<>();
for (Part part : parts) {
if (!part.getName().equals("files") || part.getSize() == 0) {
continue;
}
String filename = UUID.randomUUID().toString();
// 임시 저장된 첨부파일을 특정 디렉토리로 옮긴다.
// 이때 전체 경로 및 파일명을 File 객체에 담아 넘겨야 한다.
// 1) 서블릿 컨테이너가 실행하는 현재 웹 애플리케이션의 실제 경로 알아내기
String realPath = this.getServletContext().getRealPath("/board/upload/" + filename);
System.out.println(realPath);
part.write(realPath);
BoardFile boardFile = new BoardFile();
boardFile.setOriginalFilename(part.getSubmittedFileName());
boardFile.setFilepath(filename);
boardFile.setMimeType(part.getContentType());
boardFile.setBoardNo(board.getNo());
boardFiles.add(boardFile);
}
if (boardFiles.size() > 0) {
boardFileDao.insertList(boardFiles);
}
txManager.commit();
} catch (Exception e) {
txManager.rollback();
e.printStackTrace();
request.setAttribute("error", "data");
}
request.getRequestDispatcher("/board/update.jsp").forward(request, response);
}
}
BoardDeleteServlet.java
package bitcamp.myapp.servlet.board;
import java.io.IOException;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.MultipartConfig;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import bitcamp.myapp.dao.BoardDao;
import bitcamp.myapp.dao.BoardFileDao;
import bitcamp.myapp.vo.Board;
import bitcamp.myapp.vo.Member;
import bitcamp.util.TransactionManager;
// 게시글 삭제는 게시글 수정 폼을 그대로 사용하기 때문에
// 요청 데이터가 multipart/form-data 형식으로 넘어온다.
@MultipartConfig(maxFileSize = 1024 * 1024 * 50)
@WebServlet("/board/delete")
public class BoardDeleteServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private TransactionManager txManager;
private BoardDao boardDao;
private BoardFileDao boardFileDao;
@Override
public void init() {
ServletContext ctx = getServletContext();
boardDao = (BoardDao) ctx.getAttribute("boardDao");
boardFileDao = (BoardFileDao) ctx.getAttribute("boardFileDao");
txManager = (TransactionManager) ctx.getAttribute("txManager");
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
txManager.startTransaction();
try {
// 로그인 사용자의 정보를 가져온다.
Member loginUser = (Member) request.getSession().getAttribute("loginUser");
int boardNo = Integer.parseInt(request.getParameter("no"));
Board old = boardDao.findByNo(boardNo);
if (old.getWriter().getNo() != loginUser.getNo()) {
response.sendRedirect("../auth/fail");
return;
}
boardFileDao.deleteOfBoard(boardNo);
if (boardDao.delete(boardNo) == 0) {
throw new RuntimeException("게시글이 존재하지 않습니다!");
}
txManager.commit();
} catch (Exception e) {
txManager.rollback();
e.printStackTrace();
request.setAttribute("error", "data");
}
request.getRequestDispatcher("/board/delete.jsp").forward(request, response);
}
}
조언
*
과제
/