24. 스프링 게시판 만들기 / 첨부파일 업로드, 다운로드(1)
안녕하세요?
오늘은 게시물에 첨부파일 업로드와 다운로드를 구현해보려고 합니다.
1. MP_FILE 테이블 만들기
테이블을 생성합니다.
컬럼이 무엇을 하는 컬럼인지는 주석을 보시면 알 수 있습니다.
ORG_FILE_NAME과 STORED_FILE_NAME 여기서 파일 이름에 관련된 컬럼을 2개 만든 이유는
파일 업로드를 하게 되면 그 파일은 서버에 저장이 되어야하는데 이미 파일의 이름이 저장된 상태에서 같은 이름으로 저장을 또 하게 된다면 저장중에 문제가 발생하거나 파일 이름이 변경될 수도 있습니다.
그래서 파일을 저장할 때, 원본파일(ORG_FILE_NAME)의 이름을 저장하고 서버로는 변경된 파일(STORED_FILE_NAME)의 이름으로 파일을 저장합니다.
시퀀스도 생성해 줍니다.
CREATE TABLE MP_FILE
(
FILE_NO NUMBER, --파일 번호
BNO NUMBER NOT NULL, --게시판 번호
ORG_FILE_NAME VARCHAR2(260) NOT NULL, --원본 파일 이름
STORED_FILE_NAME VARCHAR2(36) NOT NULL, --변경된 파일 이름
FILE_SIZE NUMBER, --파일 크기
REGDATE DATE DEFAULT SYSDATE NOT NULL, --파일등록일
DEL_GB VARCHAR2(1) DEFAULT 'N' NOT NULL,--삭제구분
PRIMARY KEY(FILE_NO) --기본키 FILE_NO
);
COMMIT;
CREATE SEQUENCE SEQ_MP_FILE_NO
START WITH 1
INCREMENT BY 1
NOMAXVALUE NOCACHE;
COMMIT;
2. pom.xml 작성
첨부파일을 하기위한 Maven Dependency를 추가해 줍니다.
<!-- 첨부파일 START-->
<!-- MultipartHttpServletRequset -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.0.1</version>
</dependency>
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.2.2</version>
</dependency>
<!-- 첨부파일 END -->
3. context-common.xml 작성
study/src/main/webapp/WEB-INF/spring 경로에 context-common.xml을 생성하고 코드를 추가해 줍니다.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:context="http://www.springframework.org/schema/context"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.springframework.org/schema/beans"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:cache="http://www.springframework.org/schema/cache"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/cache
http://www.springframework.org/schema/cache/spring-cache.xsd">
<!-- MultipartResolver 설정 -->
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<property name="maxUploadSize" value="100000000" />
<property name="maxInMemorySize" value="100000000" />
</bean>
</beans>
4. web.xml 작성
파일을 읽을수 있게 web.xml에서 <param-value></param-value>사이에
/WEB-/WEB-INF/spring/context-common.xml을 추가 해줍니다.
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
<!-- The definition of the Root Spring Container shared by all Servlets and Filters -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring/root-context.xml</param-value>
</context-param>
<!-- Creates the Spring Container shared by all Servlets and Filters -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- Processes application requests -->
<servlet>
<servlet-name>appServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>
/WEB-INF/spring/appServlet/servlet-context.xml
/WEB-INF/spring/spring-security.xml
/WEB-INF/spring/context-common.xml
</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>appServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<!-- 한글 인코딩 Start -->
<filter>
<filter-name>encodingFilter</filter-name>
<filter-class>
org.springframework.web.filter.CharacterEncodingFilter
</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<init-param>
<param-name>forceEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>encodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- 한글 인코딩 End -->
</web-app>
5. FileUtils.java 작성
src/main/java에 kr.co.util 패키지를 만들어주고 FileUtils.java를 생성합니다.
FileUtils은 첨부파일의 정보를 이용하여 여러가지 조작을 할 클래스입니다.
Iterator은 데이터들의 집합체? 에서 컬렉션으로부터 정보를 얻어올 수 있는 인터페이스입니다.
List나 배열은 순차적으로 데이터의 접근이 가능하지만, Map등의 클래스들은 순차적으로 접근할 수가 없습니다.
Iterator을 이용하여 Map에 있는 데이터들을 while문을 이용하여 순차적으로 접근합니다.
getRandomString() 메서드는 32글자의 랜덤한 문자열(숫자포함)을 만들어서 반환해주는 기능을 합니다.
package kr.co.util;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import kr.co.vo.BoardVO;
@Component("fileUtils")
public class FileUtils {
private static final String filePath = "C:\\mp\\file\\"; // 파일이 저장될 위치
public List<Map<String, Object>> parseInsertFileInfo(BoardVO boardVO,
MultipartHttpServletRequest mpRequest) throws Exception{
/*
Iterator은 데이터들의 집합체? 에서 컬렉션으로부터 정보를 얻어올 수 있는 인터페이스입니다.
List나 배열은 순차적으로 데이터의 접근이 가능하지만, Map등의 클래스들은 순차적으로 접근할 수가 없습니다.
Iterator을 이용하여 Map에 있는 데이터들을 while문을 이용하여 순차적으로 접근합니다.
*/
Iterator<String> iterator = mpRequest.getFileNames();
MultipartFile multipartFile = null;
String originalFileName = null;
String originalFileExtension = null;
String storedFileName = null;
List<Map<String, Object>> list = new ArrayList<Map<String,Object>>();
Map<String, Object> listMap = null;
int bno = boardVO.getBno();
File file = new File(filePath);
if(file.exists() == false) {
file.mkdirs();
}
while(iterator.hasNext()) {
multipartFile = mpRequest.getFile(iterator.next());
if(multipartFile.isEmpty() == false) {
originalFileName = multipartFile.getOriginalFilename();
originalFileExtension = originalFileName.substring(originalFileName.lastIndexOf("."));
storedFileName = getRandomString() + originalFileExtension;
file = new File(filePath + storedFileName);
multipartFile.transferTo(file);
listMap = new HashMap<String, Object>();
listMap.put("BNO", bno);
listMap.put("ORG_FILE_NAME", originalFileName);
listMap.put("STORED_FILE_NAME", storedFileName);
listMap.put("FILE_SIZE", multipartFile.getSize());
list.add(listMap);
}
}
return list;
}
public static String getRandomString() {
return UUID.randomUUID().toString().replaceAll("-", "");
}
}
6. root-context.xml 작성
root-context.xml에서 kr.co.utill패키지를 스캔할 수 있게 추가해 줍니다.
<context:component-scan base-package="kr.co.util"></context:component-scan>
7. BoardController.java 작성
BoardController.java에서 게시판 글 작성쪽에 첨부파일의 파라미터값을 받을수 있는
MultipartHttpServlertRequest mpRequest를 추가해 줍니다.
서비스를 실행할때에 받는 파라미터(mpRequest)도 추가해줍니다. service.write(boardVO, mpRequest);
// 게시판 글 작성
@RequestMapping(value = "/write", method = RequestMethod.POST)
public String write(BoardVO boardVO, MultipartHttpServletRequest mpRequest) throws Exception{
logger.info("write");
service.write(boardVO, mpRequest);
return "redirect:/board/list";
}
8. BoardService 작성
@Resource로 FileUtils를 사용할수 있게 추가해 줍니다.
BoardService와 BoardServiceImpl에 첨부파일 파라미터를 받을 MultipartHttpServletRequest를 추가해줍니다.
impl에는 Map타입의 List타입으로 list라는 이름에 fileUtils.parseInsertFileInfo(boardVO, mpRequest);를 받아옵니다.
for문을 써서 list의 size만큼 넣어주는 이유는 나중에 여러개의 첨부파일을 등록하기 위해서 입니다.
BoardService.java
// 게시글 작성
public void write(BoardVO boardVO, MultipartHttpServletRequest mpRequest) throws Exception;
BoardServiceImpl.java
@Resource(name="fileUtils")
private FileUtils fileUtils;
@Inject
private BoardDAO dao;
// 게시글 작성
@Override
public void write(BoardVO boardVO, MultipartHttpServletRequest mpRequest) throws Exception {
dao.write(boardVO);
List<Map<String,Object>> list = fileUtils.parseInsertFileInfo(boardVO, mpRequest);
int size = list.size();
for(int i=0; i<size; i++){
dao.insertFile(list.get(i));
}
}
9. BoardDAO 작성
BoardDAO와 BoardDAOImpl에 첨부파일 업로드 메소드를 작성해줍니다.
BoardDAO.java
public void insertFile(Map<String, Object> map) throws Exception;
BoardDAOImpl.java
// 첨부파일 업로드
@Override
public void insertFile(Map<String, Object> map) throws Exception {
// TODO Auto-generated method stub
sqlSession.insert("boardMapper.insertFile", map);
}
10. writerView.jsp 작성
form태그에 enctype="multipart/form-data"를 추가해주고
파일 업로드를 할 수 있는 input태그를 추가해줍니다.
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<html>
<head>
<!-- 합쳐지고 최소화된 최신 CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap.min.css">
<!-- 부가적인 테마 -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.2/css/bootstrap-theme.min.css">
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<title>게시판</title>
</head>
<script type="text/javascript">
$(document).ready(function(){
var formObj = $("form[name='writeForm']");
$(".write_btn").on("click", function(){
if(fn_valiChk()){
return false;
}
formObj.attr("action", "/board/write");
formObj.attr("method", "post");
formObj.submit();
});
})
function fn_valiChk(){
var regForm = $("form[name='writeForm'] .chk").length;
for(var i = 0; i<regForm; i++){
if($(".chk").eq(i).val() == "" || $(".chk").eq(i).val() == null){
alert($(".chk").eq(i).attr("title"));
return true;
}
}
}
</script>
<body>
<div id="root">
<header>
<h1> 게시판</h1>
</header>
<hr />
<div>
<%@include file="nav.jsp" %>
</div>
<hr />
<section id="container">
<form name="writeForm" method="post" action="/board/write" enctype="multipart/form-data">
<table>
<tbody>
<c:if test="${member.userId != null}">
<tr>
<td>
<label for="title">제목</label><input type="text" id="title" name="title" class="chk" title="제목을 입력하세요."/>
</td>
</tr>
<tr>
<td>
<label for="content">내용</label><textarea id="content" name="content" class="chk" title="내용을 입력하세요."></textarea>
</td>
</tr>
<tr>
<td>
<label for="writer">작성자</label><input type="text" id="writer" name="writer" class="chk" title="작성자를 입력하세요." value="${member.userId}" />
</td>
<tr>
<tr>
<td>
<input type="file" name="file">
</td>
<tr>
<td>
<button class="write_btn" type="submit">작성</button>
</td>
</tr>
</c:if>
<c:if test="${member.userId == null}">
<p>로그인 후에 작성하실 수 있습니다.</p>
</c:if>
</tbody>
</table>
</form>
</section>
<hr />
</div>
</body>
</html>
11. boardMapper.xml 작성
게시글 insert작성하는 곳에 원래는 bno가 시퀀스로 들어갔었는데요.
<intert>에는 useGeneratedKeys와 keyProperty 속성이 추가 되었고,
그 아래에는 <selectKey>를 추가한것을 보실수 있습니다.
useGeneratedKeys는 자동적으로 증가되는 키를 받는 getGeneratedKeys()를 사용할 수 있게 true로 설정.
keyProperty는 selectKey에의해 선택된 키(keyProperty="bno")가 무엇인지 설정.
order="BEFORE"는 insert를 실행하기전에 실행한다는 설정.
그렇다면
1) insert쿼리를 실행기전에 SELECT MP_BOARD_SEQ.NEXTVAL FROM DUAL을 실행
2) 실행한 쿼리에서 가져올값 (keyProperty="bno") bno를 int형으로 가져옴
3) <insert>안에 있는 keyProperty에 의해 <selectKey>에 있는 keyProperty값 bno를 가져옴
4) bno값 파라미터로 넣어줌.
이런식으로 됩니다.
파일첨부 업로드할 쿼리를 작성해 줍니다.
게시판의 글을 등록하면 자동으로 시퀀스에의해 1씩 증가되게 만들었습니다. 그런데
파일첨부가 생기고나서 파일첨부 BNO컬럼에 증가된 BNO를 받을수가 없습니다.
그래서 selectKey를 이용하여 따로 증가된 bno를 셋팅해주고 BoardServiceImpl에 게시글작성쪽 메소드를 보면
게시글이 작성되고 나서 그 다음에 첨부파일이 되는것을 보실수 있는데요.
게시글이 작성되면 bno의 값은 1이 증가된 상태에서 첨부파일업로드메소드에 증가된 bno를 넘겨주기 때문에
같은 bno의 값을 가질 수 있게 됩니다.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="boardMapper">
<!-- 게시판 글 작성 -->
<insert id="insert" parameterType="kr.co.vo.BoardVO" useGeneratedKeys="true" keyProperty="bno">
<selectKey keyProperty="bno" resultType="int" order="BEFORE">
SELECT MP_BOARD_SEQ.NEXTVAL FROM DUAL
</selectKey>
INSERT INTO MP_BOARD( BNO
, TITLE
, CONTENT
, WRITER )
VALUES( #{bno}
, #{title}
, #{content}
, #{writer} )
</insert>
<select id="listPage" resultType="kr.co.vo.BoardVO" parameterType="kr.co.vo.SearchCriteria">
SELECT BNO,
TITLE,
CONTENT,
WRITER,
REGDATE
FROM (
SELECT BNO,
TITLE,
CONTENT,
WRITER,
REGDATE,
ROW_NUMBER() OVER(ORDER BY BNO DESC) AS RNUM
FROM MP_BOARD
WHERE 1=1
<include refid="search"></include>
) MP
WHERE RNUM BETWEEN #{rowStart} AND #{rowEnd}
ORDER BY BNO DESC
</select>
<select id="listCount" parameterType="kr.co.vo.SearchCriteria" resultType="int">
SELECT COUNT(BNO)
FROM MP_BOARD
WHERE 1=1
<include refid="search"></include>
AND BNO > 0
</select>
<sql id="search">
<if test="searchType != null">
<if test="searchType == 't'.toString()">AND TITLE LIKE '%' || #{keyword} || '%'</if>
<if test="searchType == 'c'.toString()">AND CONTENT LIKE '%' || #{keyword} || '%'</if>
<if test="searchType == 'w'.toString()">AND WRITER LIKE '%' || #{keyword} || '%'</if>
<if test="searchType == 'tc'.toString()">AND (TITLE LIKE '%' || #{keyword} || '%') or (CONTENT LIKE '%' || #{keyword} || '%')</if>
</if>
</sql>
<select id="read" parameterType="int" resultType="kr.co.vo.BoardVO">
SELECT BNO
, TITLE
, CONTENT
, WRITER
, REGDATE
FROM MP_BOARD
WHERE BNO = #{bno}
</select>
<update id="update" parameterType="kr.co.vo.BoardVO">
UPDATE MP_BOARD
SET TITLE = #{title},
CONTENT = #{content}
WHERE BNO = #{bno}
</update>
<delete id="delete" parameterType="int">
DELETE
FROM MP_BOARD
WHERE BNO = #{bno}
</delete>
<insert id="insertFile" parameterType="hashMap">
INSERT INTO MP_FILE(
FILE_NO,
BNO,
ORG_FILE_NAME,
STORED_FILE_NAME,
FILE_SIZE
)VALUES(
SEQ_MP_FILE_NO.NEXTVAL,
#{BNO},
#{ORG_FILE_NAME},
#{STORED_FILE_NAME},
#{FILE_SIZE}
)
</insert>
</mapper>
12. 실행 테스트
파일 선택하는 버튼이 생기고, 버튼을 클릭하여 파일을 추가한 후 작성 버튼을 눌러봅니다.
470번 글에 첨부파일이 된 것을 확인 할 수 있습니다.
경로를 설정한 곳에 랜덤으로 만든 파일이 생성된것을 확인할 수 있습니다.
아래에는 프로젝트 파일입니다. 7z압축프로그램으로 푸셔야 합니다.