본문 바로가기
Spring/study

spring 17강 드래그앤드롭, ajax 방식의 파일업로드

by avvin 2019. 7. 1.

spring 17강 드래그앤드롭, ajax 방식의 파일업로드



controller.upload / AjaxUploadController.java


util / MediaUtils.java

  UploadFileUtils.java



view/uploadAjax.jsp



menu.jsp

1
<a href="${path}/upload/uploadAjax">업로드(Ajax)</a> | 




 MediaUtils.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.example.spring02.util;
 
import java.util.HashMap;
import java.util.Map;
 
import org.springframework.http.*MediaType;
 
public class MediaUtils {
    
    //이 이부분은 static이기때문에 처음부터 메모리에 올라가 있다.
    //하단의 메서드를 호출하게되면 올라온 파일의 타입 이름을 대문자로 변경하여 key로 준다.
    //mediaMap.get은 key값을 받아서 vaule를 리턴한다.
    //해당되는 key(JPG,GIF,PNG)가 없으면 null을 리턴
    private static Map<String,*MediaType> mediaMap;
    
    //클래스를 로딩할 때 제일 먼저 실행되는 코드
    //이미지 파일에 해당되는 확장자만 받기 위해 key와 Mediatype value 등록
    static {
        mediaMap = new HashMap<>();
        mediaMap.put("JPG",MediaType.IMAGE_JPEG);
        mediaMap.put("GIF",MediaType.IMAGE_GIF);
        mediaMap.put("PNG",MediaType.IMAGE_PNG);
    }
    //UploadFileUtils.java에서 호출하여 이미지파일이 등록됐는지 여부 체크해
    public static MediaType getMediaType(String type) {
        // toUpperCase() 대문자로 변경
        return mediaMap.get(type.toUpperCase());
    }
}
cs




*스프링프레임워크의 MediaType 객체는 파일 확장자에 대한 직렬화(자바코드를 바이트코드화) 정보를 담고 있음


getMediaType 메서드는 올라온 파일 확장자 이름이 이미지 확장자인 경우에 해당하는 MediaType을 리턴한다.




UploadFileUtils.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
package com.example.spring02.util;
 
import java.awt.image.BufferedImage;
import java.io.File;
import java.text.DecimalFormat;
import java.util.Calendar;
import java.util.UUID;
import javax.imageio.ImageIO;
import org.imgscalr.Scalr;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.FileCopyUtils;
 
public class UploadFileUtils {
    //로깅을 위한 변수
    private static final Logger logger 
        = LoggerFactory.getLogger(UploadFileUtils.class);
 
    //파일 업로드메서드, 업로드경로를 매개값으로 받음. 
    //어디서 호출??>> AjaxUploadController의 uploadAjax메서드에서 호출
    public static String uploadFile(String uploadPath
            , String originalName, byte[] fileData) 
                    throws Exception {
        // uuid 발급하여 저장할 이름(uuid + 원래 이름) 설정
        UUID uid = UUID.randomUUID();
        String savedName = uid.toString() + "_" + originalName;
        
        // 업로드할 디렉토리 생성
        //calcPath는 날짜별로 디렉토리 만들어 경로 설정하는 메서드(하단에 작성)
        String savedPath = calcPath(uploadPath);
        File target = new File(uploadPath 
                + savedPath, savedName);
        
        // 임시 디렉토리에 업로드된 파일을 지정된 디렉토리로 복사
        //FileCopyUtils는 스프링이 제공하는 클래스
        FileCopyUtils.copy(fileData, target);
        // 파일의 확장자 검사
        // a.jpg / aaa.bbb.ccc.jpg
        //파일명에 dot이 있는 경우엔 마지막 dot 뒤가 확장자
        //.substring()은 문자열에서 특정 부분만 골라낼 때 사용하는 메서드
        //substring(start, end(없으면 문자열끝까지))
        //lastIndexOf(".")는 해당 문자의 마지막 위치의 인덱스값 반환
        //마지막 dot인덱스 +1 자리부터 끝까지가 formatname(파일 확장자)
        String formatName = originalName.substring(
                originalName.lastIndexOf("."+ 1);
        
        String uploadedFileName = null;
        // 이미지 파일은 썸네일 사용
        // 확장자가 getMediaType()으로 거른 이미지파일 확장자라면,
        //(이미지파일 확장자가 아니라면 null이 리턴됨)//static 메서드
        if (MediaUtils.getMediaType(formatName) != null) {
            // 썸네일 생성
            //하단에 작성한 makeThumnail메서드(썸네일 이름 리턴)
            uploadedFileName = makeThumbnail(uploadPath
                    , savedPath, savedName);
        } else {
            uploadedFileName = makeIcon(uploadPath, savedPath
                    , savedName);
        }
        //썸네일 이름 or 아이콘 이름(경로+파일이름) 리턴
        return uploadedFileName;
    }
    
//아이콘 생성
    private static String makeIcon(String uploadPath
            , String path, String fileName) throws Exception {
        // 아이콘의 이름
        String iconName = uploadPath + path + File.separator 
                + fileName;
        // 아이콘 이름을 리턴
        // File.separatorChar : 디렉토리 구분자
        // 윈도우 \ , 유닉스(리눅스) /
        //???뭘 리턴하지?아이콘 이름에서 디렉토리 구분자 변경한거?
        //아이콘 이름이라고 쓰긴했지만 그냥 경로 + 파일명 리턴하는거
        return iconName.substring(uploadPath.length())
                .replace(File.separatorChar, '/');
    }
 
    //썸네일(이미지 리사이즈) 만들기 위해서는
    //imgscalr(이미지스칼라)라이브러리 필요 (pom.xml에 등록)
    private static String makeThumbnail(String uploadPath
            , String path, String fileName) throws Exception {
        
        // 이미지를 읽기 위한 버퍼
        BufferedImage sourceImg = ImageIO.read(
                new File(uploadPath + path, fileName));
        
        // 100픽셀 단위의 썸네일 생성
        BufferedImage destImg = Scalr.resize(
                sourceImg, Scalr.Method.AUTOMATIC
                , Scalr.Mode.FIT_TO_HEIGHT, 100);
        
        // 썸네일의 이름, 's_'가 들어간 파일은 썸네일 파일, 원본파일과 구분하기위함
        String thumbnailName = uploadPath + path 
                + File.separator + "s_" + fileName;
        
        //썸네일 이름(경로 + 파일이름)으로 파일 생성
        File newFile = new File(thumbnailName);
        
        //확장자
        String formatName = fileName.substring(
                fileName.lastIndexOf("."+ 1);
        
        // 이미지, 확장자, 새로 생성한 파일로 썸네일 파일 생성
        ImageIO.write(
                destImg, formatName.toUpperCase(), newFile);
        
        // 썸네일의 이름을 리턴함
        return thumbnailName.substring(
uploadPath.length()).replace(File.separatorChar, '/');
    }
    
 
    private static String calcPath(String uploadPath) {
        Calendar cal = Calendar.getInstance();
        String yearPath = File.separator 
                + cal.get(Calendar.YEAR);
        
        //DecimalFormat 객체는 10진수의 포맷 변경시 사용
        //한자리수 월, 날짜일 경우 01 02 03...으로 표현하기 위함 
        //format은 포맷에 맞는 문자열을 리턴
        //★★★ Calendar객체에서 1월은 0으로 출력하기때문에
        //현재 월을 알고 싶다면 Calendar.MONTH + 1 을 해줘야한다.
        String monthPath = yearPath + File.separator 
                + new DecimalFormat("00").format(
                        cal.get(Calendar.MONTH) + 1);
        String datePath = monthPath + File.separator 
                + new DecimalFormat("00").format(cal.get(
                        Calendar.DATE));
        
 
        //makeDir은 하단에 작성한 디렉토리 생성 메서드
        //File클래스의  mkdir();아니고 
        //mkdir()메서드를 활용하여 직접 작성한 메서드
        //날짜별로 디렉토리 생성
        makeDir(uploadPath, yearPath, monthPath, datePath);
        
        logger.info(datePath);
        return datePath; //날짜 디렉토리 리턴
    }
 
    //★★★String...은 가변사이즈 매개변수 (가변인자★) 배열!
    private static void makeDir(String uploadPath
            , String... paths) {
        
        //디렉토리가 존재하면 새로 만들지 않고skip//인덱스는 0부터 시작하기때문에 -1
        if (new File(paths[paths.length - 1]).exists()) {
            return;
        }
        
        //가변인자로 받은 paths에서 path를 가져와 
        //해당 디렉토리가 존재하지 않는다면 디렉토리 생성
        for (String path : paths) {
            File dirPath = new File(uploadPath + path);
            if (!dirPath.exists()) {
                dirPath.mkdir(); // 디렉토리 생성
            }
        }
    }
}
cs


*UUID

파일 업로드 시에 중복 방지를 위해 uuid 사용. 16바이트 숫자로 구성되며 중복가능성이 없다는 보장은 없으나 거의 중복 x

파일이름 중복 방지 방법에는 여러가지가 있으나 여기에서는 uuid사용





ajax 방식의 파일 업로드하기


기존의 UploadController가 아닌 AjaxUploadController.java 작성


menu.jsp

1
<a href="${path}/upload/uploadAjax">업로드(Ajax)</a> | 



servlet-context.xml에 등록해놓은 resource

1
2
3
4
5
    <!-- 파일업로드를 위한 디렉토리 설정 -->
    <!-- String uploadPath=new String("d:/upload"); -->
    <beans:bean id="uploadPath" class="java.lang.String">
        <beans:constructor-arg value="d:/upload" />
    </beans:bean>
cs



servlet-context에 등록해놓은 리소스를 쓸 때에는 String 변수 위에 @Resource(name = 등록한 id)를 등록하여 객체 관리



//자바스크립트를 더 공부한 뒤에 다시봐도 좋을 것 같다.

uploadAjax.jsp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
    <!-- views/upload/uploadAjax.jsp -->
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
<%@ include file="../include/header.jsp" %>
<style>
.fileDrop {
    width: 100%;
    height: 200px;
    border: 1px dotted blue;
}
small {
    margin-left:3px;
    font-weight: bold;
    color: gray;
}
</style>
<script>
$(function(){
    //이벤트 설정시에는 jquery의 .on()을 사용한다.
    //드래그 기본 효과를 막음
    $(".fileDrop").on("dragenter dragover"function(event){
        //drop영역에 들어가고, 드롭영역에 드래그 되고있을때의 기본 효과를 막음
        event.preventDefault();
    });
    $(".fileDrop").on("drop",function(event){
        //drop이 될 때 기본 효과를 막음
        //기본 효과를 막지 않으면 드래그시에 브라우저에서 이미지파일이 열려버림
        event.preventDefault();
        
        //첨부파일 배열
        var files=event.originalEvent.dataTransfer.files;
        var file=files[0]; //첫번째 첨부파일
        //AJAX로 (이미지를 넘길때)폼 전송을 가능케해주는 FormData 객체
        var formData=new FormData(); 
        formData.append("file",file); //폼에 file 변수 추가
        //서버에 파일 업로드(백그라운드에서 실행됨)
        // contentType: false => multipart/form-data로 처리됨
        $.ajax({
            //AjaxUploadController에 post방식으로 넘어감
            type: "post",
            url: "${path}/upload/uploadAjax",
            data: formData,
            dataType: "text",
            processData: false,
            contentType: false,
            success: function(data,status,req){
                console.log("data:"+data); //업로드된 파일 이름
                console.log("status:"+status); //성공,실패 여부
                console.log("req:"+req.status);//요청코드값
                var str="";
                if(checkImageType(data)){ //이미지 파일
str="<div><a href='${path}/upload/displayFile?fileName="
        +getImageLink(data)+"'>";
str+="<img src='${path}/upload/displayFile?fileName="
        +data+"'></a>"
                }else//이미지가 아닌 경우
                    str="<div>";
str+="<a href='${path}/upload/displayFile?fileName="
        +data+"'>"+getOriginalName(data)+"</a>";
                }
                str+="<span data-src="+data+">[삭제]</span></div>";
                $(".uploadedList").append(str);
            }
        });
    }); //fileDrop 함수
    //첨부파일 삭제 함수
    $(".uploadedList").on("click","span",function(event){
        //현재 클릭한 태그
        var that=$(this);
//data: "fileName="+$(this).attr("data-src"),        
        $.ajax({
            url: "${path}/upload/deleteFile",
            type: "post",
            data: {
                fileName: $(this).attr("data-src")
            },
            dataType: "text",
            success: function(result){
                if(result=="deleted"){
                    that.parent("div").remove();
                }
            }
        });
    });
    
    function getOriginalName(fileName){
        if(checkImageType(fileName)){ //이미지 파일이면 skip
            return;
        }
        var idx=fileName.indexOf("_")+1//uuid를 제외한 파일이름
        return fileName.substr(idx);
    }
    function getImageLink(fileName){
        if(!checkImageType(fileName)){//이미지 파일이 아니면 skip
            return;
        }
        var front=fileName.substr(0,12);//연월일 경로
        var end=fileName.substr(14);// s_ 제거
        return front+end;
    }
    function checkImageType(fileName){
        // '/i': ignore case
        var pattern=/jpg|png|jpeg/i; //정규표현식(대소문자 무시)
        return fileName.match(pattern); //규칙에 맞으면 true
    }
});
</script>
</head>
<body>
<%@ include file="../include/menu.jsp" %>
<h2>Ajax File Upload</h2>
<!-- 파일을 업로드할 영역 / div태그는 레이아웃 설정 역할-->
<div class="fileDrop"></div>
<!-- 업로드된 파일 목록을 출력할 영역 -->
<div class="uploadedList"></div>
 
</body>
</html>
cs




AjaxUploadController.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
package com.example.spring02.controller.upload;
 
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
 
import javax.annotation.Resource;
import javax.inject.Inject;
 
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;
 
import com.example.spring02.service.board.BoardService;
import com.example.spring02.util.MediaUtils;
import com.example.spring02.util.UploadFileUtils;
 
@Controller
public class AjaxUploadController {
    // 로깅을 위한 변수
    private static final Logger logger = LoggerFactory.getLogger(AjaxUploadController.class);
    
    @Inject
    BoardService boardService;
 
    // 업로드 디렉토리 servlet-context.xml에 설정되어 있음
    
    @Resource(name = "uploadPath")
    String uploadPath;
 
    //메뉴에서 [ 업로드(Ajax) ]를 누르면
    // 파일첨부 페이지(jsp페이지)로 이동
    @RequestMapping(value = "/upload/uploadAjax"
            method = RequestMethod.GET)
    public String uploadAjax() {
        return "/upload/uploadAjax";
    }
 
    //uploadAjax에서 받은 데이터를 post방식으로 명시하여 실행
    // 업로드한 파일은 MultipartFile 변수에 저장됨
    @ResponseBody // json 형식으로 리턴
    @RequestMapping(value = "/upload/uploadAjax"
    method = RequestMethod.POST, produces = "text/plain;charset=utf-8")

    public ResponseEntity<String> uploadAjax(MultipartFile file) throws Exception {
        // 업로드한 파일 정보와 Http 상태 코드를 함께 리턴
        return new ResponseEntity<String>(
                UploadFileUtils.uploadFile(uploadPath, file.getOriginalFilename(), file.getBytes()), HttpStatus.OK);
    }
 
    // 이미지 표시 기능
    @ResponseBody // view가 아닌 data 리턴
    @RequestMapping("/upload/displayFile")
    public ResponseEntity<byte[]> displayFile(String fileName) 
            throws Exception {
        // 서버의 파일을 다운로드하기 위한 스트림
        InputStream in = null// java.io
        ResponseEntity<byte[]> entity = null;
        try {
            // 확장자 검사
            String formatName = fileName.substring(
                    fileName.lastIndexOf("."+ 1);
            MediaType mType = MediaUtils.getMediaType(formatName);
            // 헤더 구성 객체
            HttpHeaders headers = new HttpHeaders();
            // InputStream 생성
            in = new FileInputStream(uploadPath + fileName);
//            if (mType != null) { // 이미지 파일이면
//                headers.setContentType(mType);
//            } else { // 이미지가 아니면
                fileName = fileName.substring(
                        fileName.indexOf("_"+ 1);
                // 다운로드용 컨텐트 타입
                headers.setContentType(
                        MediaType.APPLICATION_OCTET_STREAM);
                // 큰 따옴표 내부에 " \" "
                // 바이트배열을 스트링으로
                // iso-8859-1 서유럽언어
                // new String(fileName.getBytes("utf-8"),"iso-8859-1")
                headers.add("Content-Disposition",
                        "attachment; filename=\"" 
                                + new String(
fileName.getBytes("utf-8"), "iso-8859-1"+ "\"");
                // headers.add("Content-Disposition"
                // ,"attachment; filename='"+fileName+"'");
//            }
            // 바이트배열, 헤더
            entity = new ResponseEntity<byte[]>(
                    IOUtils.toByteArray(in), headers, HttpStatus.OK);
        } catch (Exception e) {
            e.printStackTrace();
            entity = new ResponseEntity<byte[]>(
                    HttpStatus.BAD_REQUEST);
        } finally {
            if (in != null)
                in.close(); // 스트림 닫기
        }
        return entity;
    }
    
    @ResponseBody //뷰가 아닌 데이터를 리턴
    @RequestMapping(value="/upload/deleteFile"
        ,method=RequestMethod.POST)
    //메시지와 에러코드를 같이 돌려줌 (??)
    public ResponseEntity<String> deleteFile(String fileName){
        logger.info("fileName:"+fileName); 
        //확장자 검사
        String formatName=fileName.substring(
                fileName.lastIndexOf(".")+1);
        MediaType mType=MediaUtils.getMediaType(formatName);
        if(mType != null) { //이미지 파일이면 원본이미지 삭제
            String front=fileName.substring(012);
            String end=fileName.substring(14);
//         File.separatorChar : 유닉스 / 윈도우즈\    
            new File(uploadPath+(front+end).replace(
                    '/',File.separatorChar)).delete();
        }
        //원본 파일 삭제(이미지이면 썸네일 삭제)
        new File(uploadPath+fileName.replace(
                '/',File.separatorChar)).delete();
        //레코드 삭제
        boardService.deleteFile(fileName); 
        
        return new ResponseEntity<String>("deleted"
                ,HttpStatus.OK);
    }
}