서블릿 3.0 이상은 자체적인 파일 업로드 처리를 API 상에서 지원한다.
파일 업로드 API를 사용하기 위해서는 Tomcat은 버전 7 이상 서블릿은 3.0 이상으로 변경해 주어야 한다.
이 예제는 이전에 사용했던 프로젝트에 업로드 기능을 추가적으로 만들 것 이다.
전체 페이지 소스를 올리게 되면 보기 힘들 것 같아 추가된 내용만 포스팅하고 깃허브에 전체 소스를 올릴 것이다.
github : https://github.com/ming9mon/spring
혹시 파일 업로드를 하는데 Unable to process parts as no multi-part configuration has been provided 에러가 난다면 https://ming9mon.tistory.com/89 이 글을 참고하면 된다
pom.xml
<!-- Servlet -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.8</version>
</dependency>
servlet-api의 버전은 3.0 이상으로 변경시켜 주고, lombok을 사용할 것이기 때문에 디펜던시를 추가시켜 준다.
web.xml
web.xml 상단을 보면 다음과 같이 되어있을 것이다.
2.5로 된 부분을 찾아 다음과 같이 수정해 준다.
수정한 뒤 조금 내려보면 <!-- Processes application requests --> 가 보일 것이다 그 아래에 다음과 같이 작성해 준다.
<!-- 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</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<!-- 파일 업로드 설정 -->
<multipart-config>
<location>D:\\upload</location>
<!-- 1MB * 20 = 20MB -->
<max-file-size>20971520</max-file-size>
<!-- 60MB -->
<max-request-size>62914560</max-request-size>
<!-- 20MB -->
<file-size-threshold>20971520</file-size-threshold>
</multipart-config>
location : 파일을 저장할 경로이다. 절대 경로로 입력해 주어야 하며, 폴더를 미리 생성해 놓아야 한다.
max-file-size : 파일 한 개의 최대 용량 (1024*1024*20 = 20MB)
max-request-size : 파일 여러 개를 추가했을 때의 총 용량
file-size-threshold : 업로드하는 파일이 임시로 파일로 저장되지 않고 메모리에서 바로 스트림으로 전달되는 크기의 한계 (20MB로 설정하면 20MB 이상인 경우 임시 파일로 저장)
web.xml은 was의 설정일 뿐이기 때문에 servlet-context.xml에 MultipartResolver라는 타입의 객체를 빈으로 등록해 주어야 한다.
servlet-context.xml
<beans:bean id="multipartResolver" class="org.springframework.web.multipart.support.StandardServletMultipartResolver"> </beans:bean>
데이터베이스
CREATE TABLE attachments(
uuid varchar2(36) not null,
path varchar2(100) not null,
file_name varchar2(100) not null,
type char(1) not null,
bno number(10,0)
);
alter table attachments add constraint pk_attach primary key(uuid);
alter table attachments add constraint fk_board_attache foreign key(bno)
references boardTest(idx);
첨부파일의 내용을 저장할 데이터 베이스를 만든다.
uuid는 고유한 값을 넣어주기 위해 사용하였고, path는 저장 경로, name은 파일 이름, type은 이미지 파일인지 아닌지, bno는 boardTest의 idx를 참조한다.
insertBoard.jsp
<form id="frm" name="frm" action="insertBoard.do" method="post" enctype="multipart/form-data">
...
<tr>
<td colspan="2" align="center">
<input type="file" name="uploadFile" id="uploadFile" multiple>
<div id="preview"></div>
</td>
</tr>
...
insertBoard에 파일을 업로드 할 수 있도록 <input type=file>을 추가해 주고, form에 enctype 의 속성값을 multipart/form-data로 지정해 주어야 한다.
그리고 업로드 할 이미지를 미리볼 수 있도록 div태그를 추가해 준다.
multiple 속성은 한번에 파일을 여러개 올릴 수 있도록 해주는 것이다.
multiple 속성은 크롬은 6.0 이상 IE는 10.0 이상부터 지원한다고 한다.
<script type="text/javascript">
$(document).ready(function (e){
$('#write').click(function(){
var frmArr = ["title","writer","content"];
//입력 값 널 체크
for(var i=0;i<frmArr.length;i++){
//alert(arr[i]);
if($.trim($('#'+frmArr[i]).val()) == ''){
alert('빈 칸을 모두 입력해 주세요.');
$('#'+frmArr[i]).focus();
return false;
}
}
//전송
$('#frm').submit();
});
$("input[type='file']").change(function(e){
//div 내용 비워주기
$('#preview').empty();
var files = e.target.files;
var arr =Array.prototype.slice.call(files);
//업로드 가능 파일인지 체크
for(var i=0;i<files.length;i++){
if(!checkExtension(files[i].name,files[i].size)){
return false;
}
}
preview(arr);
});//file change
function checkExtension(fileName,fileSize){
var regex = new RegExp("(.*?)\.(exe|sh|zip|alz)$");
var maxSize = 20971520; //20MB
if(fileSize >= maxSize){
alert('파일 사이즈 초과');
$("input[type='file']").val(""); //파일 초기화
return false;
}
if(regex.test(fileName)){
alert('업로드 불가능한 파일이 있습니다.');
$("input[type='file']").val(""); //파일 초기화
return false;
}
return true;
}
function preview(arr){
arr.forEach(function(f){
//파일명이 길면 파일명...으로 처리
var fileName = f.name;
if(fileName.length > 10){
fileName = fileName.substring(0,7)+"...";
}
//div에 이미지 추가
var str = '<div style="display: inline-flex; padding: 10px;"><li>';
str += '<span>'+fileName+'</span><br>';
//이미지 파일 미리보기
if(f.type.match('image.*')){
var reader = new FileReader(); //파일을 읽기 위한 FileReader객체 생성
reader.onload = function (e) { //파일 읽어들이기를 성공했을때 호출되는 이벤트 핸들러
str += '<img src="'+e.target.result+'" title="'+f.name+'" width=100 height=100 />';
str += '</li></div>';
$(str).appendTo('#preview');
}
reader.readAsDataURL(f);
}else{
str += '<img src="resources/img/fileImg.png" title="'+f.name+'" width=100 height=100 />';
$(str).appendTo('#preview');
}
});//arr.forEach
}
});
</script>
insertBoard의 file에 change 옵션으로 파일이 변경 될 때마다 업로드 가능한 파일인지 확인하고 이미지라면 미리보기를 만들어 주고, 이미지 파일이 아니라면 그냥 파일모양의 이미지를 띄워준다.
img에 title 속성으로 마우스를 올렸을 때, 전체 파일명을 볼 수 있다.
첨부파일을 하나씩 지우는 기능을 추가해 주려고 하였지만, file태그는 보안상의 이유로 직접 수정을 하지 못한다고 한다. 그래서 하나씩 지우는 기능은 패스 ..
FileVO
package com.wipia.study.domain;
import lombok.Data;
@Data
public class FileVO {
private String fileName;
private String uuid;
private String path;
private long bno;
}
파일의 이름, 경로 등을 입력받을 DTO를 만들어 주고,
여러 파일을 등록할 경우 한번에 처리하기 위하여 BoardVO에 List<FileVO>를 추가해 준다.
BoardVO
@Data
public class BoardVO {
private int idx;
private String title;
private String writer;
private String content;
private Date regDate;
private int cnt;
private List<FileVO> fileList;
}
BoardController
// 글 쓰기
@RequestMapping(value="/insertBoard.do", method=RequestMethod.POST)
public String insertBoard(BoardVO vo,MultipartFile[] uploadFile) throws IOException {
boardService.insertBoard(vo,uploadFile);
return "redirect:getBoardList.do";
}
컨트롤러는 서비스로 vo와 uploadFile을 다시 넘겨준다.
BoardServiceImpl
@Transactional
@Override
public void insertBoard(BoardVO vo, MultipartFile[] uploadFile) {
List<FileVO> list = new ArrayList<>();
//파일을 저장할 경로 생성
String path = getPath();
//ex) D:\\upload\\2019\\06\\21
File uploadPath = new File("D:"+File.separator+"upload"+File.separator+path);
//폴더가 없다면 생성
if (uploadPath.exists() == false) {
uploadPath.mkdirs();
}
//게시글 디비에 저장
boardDAO.insertBoard(vo);
//게시한 글 번호 얻어오기
long bno = boardDAO.getBno();
//파일이 없으면 에러남
if(!uploadFile[0].getOriginalFilename().equals("")) {
for(MultipartFile file : uploadFile) {
FileVO fvo = new FileVO();
String fileName = file.getOriginalFilename();
//파일명만 추출
fileName = fileName.substring(fileName.lastIndexOf(File.separator)+1);
//확장자 추출
String ext=fileName.substring(fileName.lastIndexOf("."));
//uuid 생성
String uuid= UUID.randomUUID().toString();
//파일명을 UUID+확장자로 서버에 저장
File saveFile = new File(uploadPath,uuid+ext);
//vo 세팅
fvo.setBno(bno);
fvo.setFileName(fileName);
fvo.setPath(path);
fvo.setUuid(uuid+ext);
//파일 저장
try {
file.transferTo(saveFile);
} catch (IllegalStateException | IOException e) {
e.printStackTrace();
}
list.add(fvo); //리스트에 추가
}
//파일 디비에 추가
boardDAO.insertAttach(list);
}
}
private String getPath() {
//날짜를 년-월-일로 포멧
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
Date date = new Date();
String str = sdf.format(date);
//ex) 2019\\06\\21
String path = str.replace("-",File.separator);
return path;
}
if(!uploadFile[0].getOriginalFilename().equals("")) < -- 이 부분은 첨부 파일을 입력하지 않아도 length가 1이여서 에러가 나기에 저렇게 비교해 주었다.
업로드 파일은 파일명의 중복 또는 많은 양의 파일이 생길 수 있기 때문에 D:\upload 폴더에 년\월\일 폴더가 있는지 확인해 없다면 생성해주고, UUID+확장자로 파일을 저장하였다.
insertBoard의 어노테이션을 보면 @Transaction이 있는데 첨부 파일은 등록되지 않았는데 게시물 내용만 저장되는 경우를 방지하기 위하여 Transaction을 이용하였다.
@Transactioni 사용법은 https://ming9mon.tistory.com/87 에서 확일할 수 있다.
BoardDAO
public long getBno() {
return mybatis.selectOne("BoardMapper.getBno");
}
public void insertAttach(List<FileVO> list) {
mybatis.insert("BoardMapper.insertAttach",list);
}
testMapper (BoardMapper)
<select id="getBno" resultType="long">
<![CDATA[
SELECT idx
FROM boardTest
WHERE ROWNUM < 2
ORDER BY 1 DESC
]]>
</select>
<insert id="insertAttach" parameterType="FileVO">
<foreach item="item" index="index" collection="list" separator=" " open="INSERT ALL " close="SELECT * FROM DUAL">
INTO attachments(uuid,path,file_name,bno)
VALUES (#{item.uuid},#{item.path},#{item.fileName},#{item.bno})
</foreach>
</insert>
매퍼에서 getBno는 idx를 역순으로 정렬하여 제일 최근에 등록한 글의 idx를 가져왔다.
insertAttache는 list로 받기 때문에 foreach문을 이용하여 첨부파일을 등록해 주었다.
'프로그래밍 > 스프링 & 스프링 부트' 카테고리의 다른 글
스프링 서블릿 api를 이용한 파일 업로드, 다운로드(2)-다운로드 (0) | 2019.06.24 |
---|---|
스프링 MyBatis foreach 여러개 다중 insert (list insert) (0) | 2019.06.21 |
스프링 Oracle 트랜잭션 관리 예제 (0) | 2019.06.19 |
스프링 AOP란? - (AOP적용 예제) (0) | 2019.06.18 |
스프링 ajax통신 한글 깨짐 (물음표로 나오는 현상) (0) | 2019.06.12 |