Spring

[Spring Boot] 파일 업로드와 섬네일 처리

오은이 2023. 10. 9. 22:11

주요 기능

 

  • 이미지 파일 업로드
  • 이미지 여러개 동시 업로드
  • 날짜 폴더 생성
  • 섬네일 처리
  • 이미지 삭제 처리

나중에 구현할 기능

 

  • 모든 형식 파일 업로드 가능
  • 파일 정보를 DB에 저장

 

 

 


 

 

 

application.yml

org:
  zerock:
      upload:
        path: "C:\\upload"
  
spring:
  servlet:
    multipart:
      enabled: 'true'
      max-request-size: 30MB
      max-file-size: 10MB
      location: C:\upload

yml 파일에서 멀티파트의 (파일)데이터 처리 관련 속성 중, 파일 업로드 가능 여부를 true, 멀티파트 요청에 대한 최대 크기 제한, 멀티파트 요청 내의 개별 파일에 대한 최대 크기 제한, 업로드 된 파일이 서버의 파일 시스템에 저장될 위치를 지정한다.

 

 

참고로 아래의 yml과 properties 파일을 서로 변환시켜주는 사이트가 아주 유용하다.

Yaml Converter - MAGEDDO

 

 

 

 

UploadTestController

@Controller
public class UploadTestController {

	@GetMapping("/uploadEx")
	public void uploadEx() {
	}
}

 
 
 
 
 

UploadController

@RestController
@Log4j2
public class UploadController {

	
	@Value("${org.zerock.upload.path}")
	private String uploadPath;
	
	@PostMapping("/uploadAjax")
	public ResponseEntity<List<UploadResultDTO>> uploadFile(MultipartFile[] uploadFiles) throws IOException{
		
		List<UploadResultDTO> resultDTOList = new ArrayList<>();
		
		for(MultipartFile uploadFile : uploadFiles) {
			
			//이미지 파일만 업로드 가능
			if(uploadFile.getContentType().startsWith("image") == false) {
				log.warn("this file is not image type");
				return new ResponseEntity<>(HttpStatus.FORBIDDEN);
			}
			
			//실제 파일 이름 IE나 Edge나 전체 경로가 들어오므로
			String originalName = uploadFile.getOriginalFilename();
			String fileName = originalName.substring(originalName.lastIndexOf("\\") + 1);
			
			log.info("fileName : " + fileName);
			
			//날짜 폴더 생성
			String folderPath = makeFolder();
			
			String uuid = UUID.randomUUID().toString();
			
			//저장할 파일 이름 중간에 "_"를 사용해 구분
			String saveName = uploadPath + File.separator + folderPath +
					File.separator + uuid + "_" + fileName;
			
			Path savePath = Paths.get(saveName);
			
			try {
				uploadFile.transferTo(savePath);	//원본 이미지 저장
				
				//섬네일 생성
				String thumbnailSaveName = uploadPath + File.separator + folderPath + File.separator
						 + "s_" + uuid + "_" + fileName;
				//섬네일 파일 이름은 s_로 시작하도록함
				
				File thumbnailFile = new File(thumbnailSaveName);
				//섬네일 생성
				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);
	}

	private String makeFolder() {
		
		String str = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
		
		String folderPath = str.replace("/", File.separator);
		
		//make folder
		File uploadPathFolder = new File(uploadPath, folderPath);
		
		if(uploadPathFolder.exists() == false) {
			uploadPathFolder.mkdirs();
		}
		return folderPath;
	}
	
	
	@GetMapping("/display")
	public ResponseEntity<byte[]> getFile(String fileName) {
		
		ResponseEntity<byte[]> result = null;
		
		try {
			String srcFileName = URLDecoder.decode(fileName, "UTF-8");
			
			log.info("fileName : " + srcFileName);
			
			File file = new File(uploadPath + File.separator + srcFileName);
			
			log.info("file : " + file);

			HttpHeaders header = new HttpHeaders();
			
			//MIME 타입 처리
			header.add("Content-Type", Files.probeContentType(file.toPath()));
			//파일 데이터 처리
			result = new ResponseEntity<>(FileCopyUtils.copyToByteArray(file),
					header, HttpStatus.OK);
			
		} catch (Exception e) {
			log.error(e.getMessage());
			return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
		}
		return result;
	}
	
	
	//업로드 파일 삭제
	@PostMapping("/removeFile")
	public ResponseEntity<Boolean> removeFiles(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);
		}
	}
}

 
 
 
 
 

UploadResultDTO

@Data
@AllArgsConstructor
public class UploadResultDTO implements Serializable {

	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 "";
	}
	
	public String getThumbnailURL() {
		try {
			return URLEncoder.encode(folderPath + "/s_" + uuid + "_" + fileName, "UTF-8"); 
		} catch (UnsupportedEncodingException e) {
			e.printStackTrace();
		}
		return "";
	}
	
}

 
 
 
 

uploadEx.html

<!DOCTYPE html>
<html leng="en" xmlns="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Insert title here</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]);
			}
		
			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);
				}
			}
			
			//실제 업로드 부분
			$.ajax({
				url: '/uploadAjax',
				processData: false,
				contentType: false,
				data: formData,
				type: 'POST',
				dataType: 'json',
				success: function(result) {
					showUploadedImages(result);
					//나중에 화면 처리
				},
				error: function(jqXHR, textStatus, errorThrown) {
					console.log(textStatus);
				}
			});
			
			$(".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();
					}
				})
			});
			
		});
	
	</script>
</body>
</html>

 
 


 
 
실행 화면 - 포트 번호 : 82

 
전용 날짜 폴더를 생성하고, UUID로 이름이 중복되지 않게 저장한다.
섬네일 처리가 되어 크기가 100X100으로 맞춰진 상태이며 REMOVE 버튼 클릭 시 삭제된다.