드래그앤드롭, ajax 방식의 파일 업로드
Back-End/Spring 2019. 6. 28. 17:22ajax (비동기적 방식)을 이용해서 드래그앤드롭을 이용해 파일업로드
이번에는 드래그해서 파일을 갖다놓으면 파일이 업로드되는 방식으로 할 예정
-실습 예제-
menu.jsp에 업로드 링크 코드를 추가
1 | <a href="${path}/upload/uploadAjax">업로드(Ajax)</a> | | cs |
util 패키지를 추가하고, MediaUtils.java 파일을 만듦
MediaUtils.java
이미지와 이미지가 아닌 파일을 구분해서 업로드하고 섬네일을 만들예정
나머지 파일들은 업로드만 할 예정
그럼 구분을 해주어야 하는데 getMediaType( ) 메소드를 호출하게 되면 get( )안에 들어있는 이미지 형식이면 그림파일이고,
안들어있는 형식의 파일이면 일반파일로 생각해 구분할 예정.
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 | package com.example.spring02.util; import java.util.HashMap; import java.util.Map; import org.springframework.http.MediaType; public class MediaUtils { private static Map<String,MediaType> mediaMap; //클래스를 로딩할 때 제일 먼저 실행되는 코드 //파일을 업로드할때 해당되는 형식을 리턴하는 클래스 //static로 만들어놓았기 때문에 이 부분은 처음부터 메모리에 올라가있는 상태이다. static { mediaMap = new HashMap<>(); //키와 값을 저장해야 하기 때문에 HashMap<>를 사용 mediaMap.put("JPG",MediaType.IMAGE_JPEG); //JPG 값을 저장 mediaMap.put("GIF",MediaType.IMAGE_GIF); //GIF 값을 저장 mediaMap.put("PNG",MediaType.IMAGE_PNG); //PNG 값을 저장 } public static MediaType getMediaType(String type) { // toUpperCase() 대문자로 변경 return mediaMap.get(type.toUpperCase()); //mediaMap안에 저장되어 있는 형식 (jpg,gif,png)이면 그림파일, 아니면 일반파일로 구분함 } //toUpperCase() 메소드는 대문자로 바꿔주는 메소드 } | cs |
util.UploadFileUtils.java
- 파일 업로드를 할 때 파일의 이름이 중복되지 않도록 하기 위해 uuid를 사용했음
- uuid는 16바이트 숫자로 구성되며 중복가능성이 없다고 보장할 수는 없으나 거의 중복이 없다고 본다.
- uuid는 밀리세컨드 + db시퀀스로 구성되며 16바이트 (128비트), 32개의 십육진수로 표현, 총 36개 문자 (32개 문자와 4개의 하이픈)
8 - 4 - 4 - 4 - 12
예) 550e8400-e29b-41d4-a716-446655440000
340,282,366,920,938,463,463,374,607,431,768,211,456개의 사용 가능한 UUID를 만들 수 있다.
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 161 162 163 164 165 166 167 168 | 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); //업로드를 하면 이 메소드가 호출된다. //File name를 똑같이 쓰면 덮어쓰기가 될 수 있으므로 //uuid를 생성해서 파일이름 앞쪽에 붙여주어서 파일이름이 서로 중복되지 않게끔 한다. public static String uploadFile(String uploadPath , String originalName, byte[] fileData) throws Exception { // uuid 발급, 랜덤한uuid를 만들어 uid에 저장 UUID uid = UUID.randomUUID(); //uuid를 추가한 파일 이름 String savedName = uid.toString() + "_" + originalName; // 업로드할 디렉토리 생성 (월, 일 날짜별로 디렉토리를 만든다.) // calcPath는 년도, 월, 일이 출력되게하는 메소드이고, 밑에서 static으로 선언되었으므로 메모리에 제일 처음 올려져 있다. // calcpath에 업로드 경로를 매개값으로 줘서, 업로드한 날짜를 savedPath에 저장하고, target변수에 File String savedPath = calcPath(uploadPath); File target = new File(uploadPath + savedPath, savedName); //업로드경로와 저장경로에 저장한파일의 이름에 대한 File 객체를 생성한다. // 임시 디렉토리에 업로드된 파일을 지정된 디렉토리로 복사 FileCopyUtils.copy(fileData, target); //target에 저장된 파일경로와 이름, 그리고 fileData(파일용량)을 복사 // 파일의 확장자 검사 // a.jpg / aaa.bbb.ccc.jpg String formatName = originalName.substring( originalName.lastIndexOf(".") + 1); //lastIndexOf() 메소드는 String오브젝트에서 "." 문자열부터 끝쪽 방향으로 문자열을 찾는다. //"."이 여러개 있을 수도 있으므로 마지막 "." 뒤쪽부터 확장자이다. //그러니까 파일의 확장자가 뭔지 찾기 위해서 "." 뒷부분의 문자를 검색한다는 뜻이다. String uploadedFileName = null; //uploadedFileName의 초기값을 지정 // 이미지 파일은 썸네일 사용 // 타입을 집어넣으면 이미지인지 아닌지 확인이 가능하다. if (MediaUtils.getMediaType(formatName) != null) { // 만약 이미지 이면 // 썸네일 생성 (해당 그림이 드래그하면 작게보임) uploadedFileName = makeThumbnail(uploadPath , savedPath, savedName); } else { //이미지 파일이 아니면 아이콘을 생성한다. uploadedFileName = makeIcon(uploadPath, savedPath , savedName); } 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, '/'); //파일 이름이라고 생각하면 된다. } private static String makeThumbnail(String uploadPath , String path, String fileName) throws Exception { // 이미지를 읽기 위한 버퍼 BufferedImage sourceImg = ImageIO.read( new File(uploadPath + path, fileName)); // 100픽셀 단위의 썸네일 생성 // 작은 썸네일이기 때문에 이미지의 크기를 줄여야 한다. // 높이가 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); //섬네일의 경로를 newFile변수에 저장 String formatName = fileName.substring( fileName.lastIndexOf(".") + 1); //lastIndexOf() 메소드는 String오브젝트에서 "." 문자열부터 끝쪽 방향으로 문자열을 찾는다. //"."이 여러개 있을 수도 있으므로 마지막 "." 뒤쪽부터 확장자이다. //그러니까 파일의 확장자가 뭔지 찾기 위해서 "." 뒷부분의 문자를 검색한다는 뜻이다. // 썸네일 생성 // 아까 사이즈를 조정한 이미지파일형식, 파일이름(대문자로바꿔서), 아까 섬네일의 경로를 저장한 newFile변수를 넣어 // write 메소드를 사용해 썸네일을 생성 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); // "월"이랑 "일"은 10보다 작을때가 있으므로 (1월,2월....은 01월, 02월 이런식으로 붙이기 위해) // 그러니까 자릿수를 맞춰주기 위해서 DecimalFormat를 사용 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(uploadPath, yearPath, monthPath, datePath); logger.info(datePath); return datePath; } private static void makeDir( // 위에서 makeDir을 호출할때는 매개변수가 4개 이지만, String뒤에 있는 ...이 가변사이즈 매개변수이기 // 때문에 호출시에 매개변수의 숫자가 많아도 호출시에 매개변수가 배열로 만들어져 paths로 다 들어가기 때문에 쌓일수 있다. // ex) paths 0번이 yearPath, 1번이 monthPath가 되고, 2번이 datePath가 된다. String uploadPath, String... paths) { //디렉토리가 존재하면 skip, (디렉토리가 기존에 존재하면 만들지 않는다는 뜻) 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(); // 디렉토리 생성, mkdir()메소드는 패스명이 나타내는 디렉토리를 생성하는 메소드. } } } } | cs |
pom.xml
이미지 썸네일을 만들어주는 라이브러리를 추가함
1 2 3 4 5 6 | <!-- 이미지 썸네일을 만들어주는 라이브러리 --> <dependency> <groupId>org.imgscalr</groupId> <artifactId>imgscalr-lib</artifactId> <version>4.2</version> </dependency> | cs |
menu.jsp (view)
1 | <a href="${path}/upload/uploadAjax">업로드(Ajax)</a> | | 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 | 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; // 파일첨부 페이지로 이동 @RequestMapping(value = "/upload/uploadAjax", method = RequestMethod.GET) public String uploadAjax() { return "/upload/uploadAjax"; } // 업로드한 파일은 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(0, 12); 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); } } | cs |
servlet-context.xml
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 |
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 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 161 162 163 164 165 166 167 168 | <%@ 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" %> <!-- ----------------------------------css파일 부분 -----------------------------------------------------------> <style> .fileDrop { width: 100%; height: 200px; border: 1px dotted blue; } small { margin-left:3px; font-weight: bold; color: gray; } </style> <!-- ----------------------------------css파일 부분 -----------------------------------------------------------> <script> $(function(){ //드래그 기본 효과를 막음 $(".fileDrop").on("dragenter dragover", function(event){ event.preventDefault(); //기본효과를 막는다. }); //기본효과를 막지 않으면 파일 업로드를 할 시에 사진이 실행되어버리기 때문에 기본효과를 막아야 한다. // event : jquery의 이벤트 // originalEvent : javascript의 이벤트 $(".fileDrop").on("drop",function(event){ //drop이 될 때 기본 효과를 막음 event.preventDefault(); //드래그된 파일의 정보 //첨부파일 배열 var files=event.originalEvent.dataTransfer.files; //드래그된 파일정보를 files변수에 저장 var file=files[0]; //첫번째 첨부파일 (컨트롤을 누르고 파일을 여러개를 올릴수도 있기 때문에, 첫번째 첨부파일이라고 지정한다.) /* -ajax란??- Asynchronous JavaScript and XML의 약자 입니다. javascript와 xml을 이용하여 비동기 통신을 하는 기능입니다. 쉽게 말해서 새로고침 없이 새로 고침 효과를 볼 수 있는 기능 (실시간으로 변화하는 페이지..) ajax는 데이터를 갱신하는 곳에서 사용한다면 페이지 전환 없이 동적으로 변화하는 모습을 자연스럽게 구현할 수 있습니다. -동기식과 비동기식- 동기식 방식 : 클라이언트가 서버에게 요청하고 응답이 올 때까지 기다리는 것. 비동기식 방식 : 클라이언트가 서버에게 요청을 하고 응답이 오기 전까지 다른일을 할 수 있다. */ //ajax로 전달할 폼 객체 var formData=new FormData(); //폼 객체 formData.append("file",file); //만들어진 폼 객체에 위에서 저장한 file를 ifle란 이름의 변수로 저장한다 //서버에 파일 업로드(백그라운드에서 실행됨) // processData : false => post 방식 // contentType : false => multipart/form-data로 처리됨 //비동기적 방식으로 보낸다. $.ajax({ type: "post", //post 방식으로 보냄 url: "${path}/upload/uploadAjax", //보낼 url data: formData, //formData를 보냄 dataType: "text", processData: false, contentType: false, success: function(data,status,req){ //alert(data); console.log("data:"+data); //업로드된 파일 이름 console.log("status:"+status); //성공,실패 여부 console.log("req:"+req.status);//요청코드값 var str=""; if(checkImageType(data)){ //이미지 파일일 경우 밑에있는 checkImageType메소드에서 리턴한값으로 참, 거짓 판단 //이미지파일이 맞을 경우에 실행되는 구문 //파일이름으로 이미지링크를 거는 하이퍼링크를 건다. str="<div><a href='${path}/upload/displayFile?fileName=" +getImageLink(data)+"'>"; str+="<img src='${path}/upload/displayFile?fileName=" +data+"'></a>"; //이미지 파일일때는 이미지 자체를 보여준다. }else{ //이미지가 아닌 경우 텍스트만 보여준다 (text파일이나 기타 응용파일같은 것들....) str="<div>"; str+="<a href='${path}/upload/displayFile?fileName=" +data+"'>"+getOriginalName(data)+"</a>"; } str+="<span data-src="+data+">[삭제]</span></div>"; //삭제링크를 클릭하면 파일이름을 보내게 된다. $(".uploadedList").append(str); } }); }); // 태그.on("이벤트","자손태그",이벤트핸들러) // data : "fileName="+$(this).attr("data-src") // 태그.attr("속성") // $("#userid").attr("type") //fileDrop 함수 //첨부파일 삭제 함수 $(".uploadedList").on("click","span",function(event){ //현재 클릭한 태그 //span태그를 click하면 deleteFile로 이동 var that=$(this); //this는 클릭한 태그가 된다 (즉, span태그를 클릭핤 ) //data: "fileName="+$(this).attr("data-src"), $.ajax({ url: "${path}/upload/deleteFile", type: "post", data: {fileName: $(this).attr("data-src")}, //deleteFile로 이동하면 data-src속성을 뽑아서 //보낸다. dataType: "text", success: function(result){ if(result=="deleted"){ //클릭한 span 태그가 속한 div를 제거 that.parent("div").remove(); //dete } } }); }); 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; //함수를 종료시킨다. } // 이미지 파일일 경우 연월일 경로와 s_를 제거해서 리턴시킨다. 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; //정규표현식(i는 대소문자 무시하기 때문에 넣은것.) return fileName.match(pattern); //규칙에 맞으면 true //그러니까 파일의 확장명을 검사해서 jpg,png,jpeg형식이 있으면 fileName과 매칭해서 true를 리턴한다. } }); </script> </head> <body> <%@ include file="../include/menu.jsp" %> <h2>Ajax File Upload</h2> <!-- 파일을 업로드할 영역 --> <div class="fileDrop"></div> <!-- 업로드된 파일 목록을 출력할 영역 --> <div class="uploadedList"></div> </body> </html> | cs |
'Back-End > Spring' 카테고리의 다른 글
Spring xml bean property 정리 (0) | 2019.07.01 |
---|---|
jQuery 이벤트 바인딩 (0) | 2019.06.30 |
Spring 자바 코드 난독화 (Java Code Obfuscation) (0) | 2019.06.28 |
Spring 자바스크립트 난독화 (0) | 2019.06.28 |
인터셉터 (Interceptor) (0) | 2019.06.27 |