📚 공부한 책 : 코드로배우는 스프링 부트 웹프로젝트
❤️ github 주소 : https://github.com/qkralswl689/LearnFromCode/tree/main/mreview2022
스프링부트 파일업로드 설정 방법
1) 파일 업로드라이브러리 등록(commons-fileupload)
2) Servlet3 버전부터 추가된 자체적인 파일 업로드 라이브러리 이용 -> 사용할 것
# 파일 업로드
# 파일 업로드 기능여부 선택
spring.servlet.multipart.enabled=true
# 업로드된 파일의 임시 저장경로
spring.servlet.multipart.location=volumes/ming/git/learnfromcode:\\upload
# 한번에 최대 업로드 가능 용량
spring.servlet.multipart.max-request-size=30MB
# 파일 하나의 최대 크기
spring.servlet.multipart.max-file-size=10MB
# 업로드된 파일 저장
com.example.upload.path =/Volumes/ming/git/LearnFromCode\upload
uploadFile()메서드는 파라미터로 MultipartFile 배열을 받도록 작성한다
-> 배열을 활용하면 동시에 여러 개의 파일 정보를 처리할 수 있으므로 화면에서 여러 개의 파일을 동시에 업로드 할 수 있다
★ 파일 저장 단계에서 해야할 고려사항
- 업로드된 확장자가 이미지만 가능하도록 검사한다 -> 파일 확장자 체크 (MultipartFile 에서 제공하는 getContentType() 이용)
- 동일한 이름의 파일이 업로드 된다면 기존 파일을 덮어쓴다 -> UUID를 용해 고유한 값을 만들어 사용
- 업로드된 파일을 저장하는 폴더의 용량 ->년/월/일 폴더를 따로 생성해 파일을 저장한다
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.UUID;
@RestController
public class UploadController {
@Value("${com.example.upload.path}") // application.properties의 변수
private String uploadPath;
@PostMapping("/uploadAjax")
public void uploadFile(MultipartFile[] uploadFiles){
for (MultipartFile uploadFile : uploadFiles) {
// 이미지 파일만 업로드 가능
if(uploadFile.getContentType().startsWith("image") == false){
return;
}
// 실제 파일 이름 IE나 Edge는 전체 경로가 들어오므로
String originalName = uploadFile.getOriginalFilename();
String fileName = originalName.substring(originalName.lastIndexOf("\\") + 1);
// 날짜 폴더 생성
String folderPath = makeFolder();
//UUID
String uuid = UUID.randomUUID().toString();
//저장할 파일 이름 중간에 "_"를 이용해 구분
String saveName = uploadPath + File.separator + folderPath + File.separator + uuid + "_" + fileName;
Path savePath = Paths.get(saveName);
try {
uploadFile.transferTo(savePath);
}catch (IOException e){
e.printStackTrace();
}
}
}
private String makeFolder() {
String str = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
String folderPath = str.replace("/", File.separator);
// make folder ----
File uploadPatheFolder = new File(uploadPath,folderPath);
if(uploadPatheFolder.exists() == false){
uploadPatheFolder.mkdirs();
}
return folderPath;
}
}
Ajax로 파일 업로드를 하기위해 가상의 Form 객체를 만들어 사용한다 ,Form Data 라는 객체로 전송하려는 내용을 추가할 수 있다
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<input name="uploadFiles" type="file" multiple>
<button class="uploadBtn">Upload</button>
</div>
<script
src="https://code.jquery.com/jquery-3.5.1.min.js"
integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0="
crossorigin="anonymous"></script>
<script>
$('.uploadBtn').click(function( ) {
var formData = new FormData();
var inputFile = $("input[type='file']");
var files = inputFile[0].files;
for (var i = 0; i < files.length; i++) {
console.log(files[i]);
formData.append("uploadFiles", files[i]);
}
//실제 업로드 부분
//upload ajax
//파일업로드를 위해 contentType 속성을 반드시 false로 지정한다 -> multipart/form-data 타입을 사용하기위해
$.ajax({
url: '/uploadAjax',
processData: false,
contentType: false,
data: formData,
type: 'POST',
dataType:'json',
success: function(result){
console.log(result);
//나중에 화면 처리
},
error: function(jqXHR, textStatus, errorThrown){
console.log(textStatus);
}
}); //$.ajax
}); //end click
</script>
</body>
</html>
결과 데이터는 JSON으로 전송한다, 어떤 구조의 데이터를 전송할 것인지 결정한다
- 브라우저에서 필요한 정보
- 업로드된 파일의 원래 이름
- 파일의 UUID 값
- 업로드된 파일의 저장경로
브라우저에서 파일 저장 처리가 간단할 수 있도록 클래스와 객체를 구성해 처리한다
- UploadResultDTO는 실제 파일과 관련된 모든 정보를 가지는데 나중에 전체 경로가 필요한 경우를 대비해 getImageURL()을 제공한다
import lombok.AllArgsConstructor;
import lombok.Data;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
@Data
@AllArgsConstructor
public class UploadResultDTO {
private String fileName;
private String uuid;
private String folderPath;
public String getImageURL(){
try {
return URLEncoder.encode(folderPath+"/" +uuid + fileName,"UTF-8");
}catch (UnsupportedEncodingException e){
e.printStackTrace();
}
return "";
}
}
업로드 결과를 반환하기 위해 리턴타입 void 에서 ResponseEntity로 변경하고,이미지 파일이 아닌경우 예외처리대신 403 Forbidden을 반환하도록 한다
-> 브라우저는 업로드 후 JSON의 배열형태로 결과를 전달받는다
- getFile() 메서드 추가
-> URL 인코딩된 파일 이름을 파라미터로 받아 해당 파일을 byte[]로 만들어 브라우저로 전송한다, 파일의 확장자에 따라 브라우저에 전송하는 MIME 타입이 달라져야 하는 문제는 java.nio.file 패키지의 Files.probeContentType()을 이용해 처리하고 파일 데이터의 처리는 스프링에서 제공하는 FileCopyUtils를 이용해 처리한다
import com.example.mreview2022.dto.UploadResultDTO;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@RestController
public class UploadController {
@Value("${com.example.upload.path}") // application.properties의 변수
private String uploadPath;
@PostMapping("/uploadAjax")
public ResponseEntity<List<UploadResultDTO>> uploadFile(MultipartFile[] uploadFiles){
List<UploadResultDTO> resultDTOList = new ArrayList<>();
for (MultipartFile uploadFile : uploadFiles) {
// 이미지 파일만 업로드 가능
if(uploadFile.getContentType().startsWith("image") == false){
// 이미지가 아닌경우 403 Forbidden 반환
return new ResponseEntity<>(HttpStatus.FORBIDDEN);
}
// 실제 파일 이름 IE나 Edge는 전체 경로가 들어오므로
String originalName = uploadFile.getOriginalFilename();
String fileName = originalName.substring(originalName.lastIndexOf("\\") + 1);
// 날짜 폴더 생성
String folderPath = makeFolder();
//UUID
String uuid = UUID.randomUUID().toString();
//저장할 파일 이름
String saveName = uploadPath + File.separator + folderPath + File.separator + uuid + fileName;
Path savePath = Paths.get(saveName);
try {
uploadFile.transferTo(savePath);// 실제 이미지 저장
resultDTOList.add(new UploadResultDTO(fileName,uuid,folderPath));
}catch (IOException e){
e.printStackTrace();
}
}
return new ResponseEntity<>(resultDTOList, HttpStatus.OK);
}
private String makeFolder() {
String str = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
String folderPath = str.replace("/", File.separator);
// make folder ----
File uploadPatheFolder = new File(uploadPath,folderPath);
if(uploadPatheFolder.exists() == false){
uploadPatheFolder.mkdirs();
}
return folderPath;
}
}
업로드 된 이미지들을 보여줄 수 있는 div를 추가한다, Ajax 업로드 이후 이미지들을 호출하는 함수 showUploadedImages()를 작성하고 ,Ajax 호출 성공 후 '/display?fileName=XXX'을 호출한다
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<input name="uploadFiles" type="file" multiple>
<button class="uploadBtn">Upload</button>
<div class="uploadResult"> <!--추가-->
</div>
<script
src="https://code.jquery.com/jquery-3.5.1.min.js"
integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0="
crossorigin="anonymous"></script>
<script>
$('.uploadBtn').click(function( ) {
var formData = new FormData();
var inputFile = $("input[type='file']");
var files = inputFile[0].files;
for (var i = 0; i < files.length; i++) {
console.log(files[i]);
formData.append("uploadFiles", files[i]);
}
//실제 업로드 부분
//upload ajax
$.ajax({
url: '/uploadAjax',
processData: false,
contentType: false,
data: formData,
type: 'POST',
dataType:'json',
success: function(result){
console.log(result);
//나중에 화면 처리
// 추가
showUploadedImages(result);
},
error: function(jqXHR, textStatus, errorThrown){
console.log(textStatus);
}
}); //$.ajax
}); //end click
// 추가
// Ajax 업로드 이후 이미지들을 호출하는 함수
function showUploadedImages(arr){
console.log(arr);
var divArea = $(".uploadResult");
for(var i = 0; i < arr.length; i++){
divArea.append("<img src='/display?fileName="+arr[i].imageURL+"'>");
}
}
});
</script>
</body>
</html>
원본이미지가 그대로 화면에 표시되면 데이터를 많이 소비해야하기 때문에 썸네일을 만들어 전송해주고 원본이미지를 보려고 할 때 원본 파일을 보여주는것이 좋다
★ 썸네일 이미지 처리 과정
- 업로드된 파일을 저장하고 썸네일 라이브러리를 활용해 썸네일 파일을 만든다
- 썸네일 파일은 파일의 맨 앞에 's_'를 붙혀 일반 파일과 구분한다
- UploadResultDTO에 getThumbnailURL()을 추가해 썸네일의 경로를 태그로 처리한다
build.gradle 파일에 Thumbnailator를 추가한다
- Thumbnailator 라이브러리는 적은양의 코드만을 이용해 썸네일을 제작할 수 있고 가로 세로 사이즈를 결정하면 비율에 맞게 조정해 주는 기능이 제공된다
dependencies {
//...생략
implementation group: 'net.coobird', name: 'thumbnailator', version: '0.4.11'
}
import com.example.mreview2022.dto.UploadResultDTO;
import net.coobird.thumbnailator.Thumbnailator;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.util.FileCopyUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.net.URLDecoder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@RestController
public class UploadController {
@PostMapping("/uploadAjax")
public ResponseEntity<List<UploadResultDTO>> uploadFile(MultipartFile[] uploadFiles){
List<UploadResultDTO> resultDTOList = new ArrayList<>();
for (MultipartFile uploadFile : uploadFiles) {
//...생략
// 추가할 부분
try {
uploadFile.transferTo(savePath);// 실제 이미지 저장(원본 파일)
//섬네일 생성 -> 섬네일 파일 이름은 중간에 s_로 시작
String thubmnailSaveName = uploadPath + File.separator + folderPath + File.separator +"s_" + uuid +"_"+ fileName;
File thumbnailFile = new File(thubmnailSaveName);
// 섬네일 생성
Thumbnailator.createThumbnail(savePath.toFile(),thumbnailFile,100,100);
resultDTOList.add(new UploadResultDTO(fileName,uuid,folderPath));
}catch (IOException e){
e.printStackTrace();
}
}
return new ResponseEntity<>(resultDTOList, HttpStatus.OK);
}
//... 생략
}
import lombok.AllArgsConstructor;
import lombok.Data;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
@Data
@AllArgsConstructor
public class UploadResultDTO {
//...생략
// 추가
public String getThumbnailURL(){
try {
return URLEncoder.encode(folderPath + "/s_" +uuid + "_" +fileName,"UTF-8");
}catch (UnsupportedEncodingException e){
e.printStackTrace();
}
return "";
}
}
function showUploadedImages(arr){
console.log(arr);
var divArea = $(".uploadResult");
for(var i = 0; i < arr.length; i++){
divArea.append("<img src='/display?fileName="+arr[i].thumbnailURL+"'>");
}
}
파일 삭제는 파일의 URL로 처리할 수 있다
-> 파일의 URL 자체가 년/월/일/uuid_파일명 으로 구성되어있기 때문에 이를 이용해 삭제할 파일의 위치를 찾아 삭제할 수 있다
removeFile()은 경로와 UUID가 포함된 파일 이름을 파라미터로 받아 삭제결과를 Boolean 타입으로 만들어 전송한다
- ★ 주의점!
- 원본 파일과 함께 썸네일 파일도 같이 삭제해야 한다. removeFile()은 원본 파일의 이름을 파라미터로 전송받은 후 File 객체를 이용해 원본과 썸네일을 같이 삭제한다
import com.example.mreview2022.dto.UploadResultDTO;
import net.coobird.thumbnailator.Thumbnailator;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.util.FileCopyUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@RestController
public class UploadController {
// ... 생략
@PostMapping("/removeFile")
public ResponseEntity<Boolean> removeFile(String fileName){
String srcFileName = null;
try {
srcFileName = URLDecoder.decode(fileName,"UTF-8");
File file = new File(uploadPath + File.separator + srcFileName);
boolean result = file.delete();
File thumbnail = new File(file.getParent(),"s_" + file.getName());
result = thumbnail.delete();
return new ResponseEntity<>(result,HttpStatus.OK);
}catch (UnsupportedEncodingException e){
e.printStackTrace();
return new ResponseEntity<>(false,HttpStatus.INTERNAL_SERVER_ERROR);
}
}
}
파일을 삭제할 수 있도록 버튼을 추가하고, 버튼과 이미지를 하나의 div 로 묶는다
-> 한 번에 버튼과 이미지를 같이 삭제할 수 있다, 함수 내부에서 div 태그를 생성하고 img,button 태그를 div 태그 안쪽으로 추가한다, 추가된 button 태그는 data-name 이라는 커스텀 속성을 지정해 버튼을 클릭했을 때 삭제해야 하는 파일의 이름을 알아내는 용도로 사용한다
- REMOVE 버튼 클릭시 동작
-> 업로드 결과로 만들어지는 div 태그는 동적으로 생성되기 때문에 바로 클릭 이벤트 처리를 할 수 없어 위임(delegate)하는 방식으로 이벤트를 처리해야한다
=> POST방식으로 호출하고 정상적으로 서버에서 원본 파일과 썸네일 파일이 삭제된 후 화면에서 해당 이미지가 포함된 div를 삭제한다
function showUploadedImages(arr){
console.log(arr);
var divArea = $(".uploadResult");
var str = "";
for(var i = 0; i < arr.length; i++){
str += "<div>";
str += "<img src='/display?fileName="+arr[i].thumbnailURL+"'>";
str += "<button class='removeBtn' data-name='"+arr[i].imageURL+"'>REMOVE</button>"
str += "<div>"
}
divArea.append(str);
}
$(".uploadResult").on("click", ".removeBtn", function(e){
var target = $(this);
var fileName = target.data("name");
var targetDiv = $(this).closest("div");
console.log(fileName);
$.post('/removeFile', {fileName: fileName}, function(result){
console.log(result);
if(result === true){
targetDiv.remove();
}
} )
});