원본 이미지를 보여주는 것보다 섬네일 이미지로 변환해 보여주는 것이 성능에 도움이 된다.
특히 목록 페이지의 경우, 보여주는 이미지가 많기 때문에 주의해야 한다.
섬네일 이미지의 처리
- 업로드된 파일 저장하고 섬네일 라이브러리 활용해 섬네일 파일 생성
- 섬네일 파일을 구분하기 위해 앞에 's_'를 붙여 구분한다.
- UploadResultDTO에 getThumbnailURL()을 추가해 섬네일 경로를 <img>태그로 처리
섬네일 이미지 처리 라이브러리
: Thumbnailator 이용
-> 적은 양의 코드로 제작 가능하고, 가로 세로 사이즈 결정하면 비율에 맞게 조정해준다.
https://github.com/coobird/thumbnailator
<!-- https://mvnrepository.com/artifact/net.coobird/thumbnailator -->
<dependency>
<groupId>net.coobird</groupId>
<artifactId>thumbnailator</artifactId>
<version>0.4.17</version>
</dependency>
진행 순서
1. 컨트롤러에서 섬네일 생성
2. 브라우저에서 섬네일 처리
3. 업로드된 파일 삭제 메서드 추가
1. 컨트롤러에서 섬네일 생성
섬네일 추가 전, UploadController
package com.movie.boot4.controller;
import com.movie.boot4.dto.UploadResultDTO;
import lombok.extern.log4j.Log4j2;
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 javax.xml.ws.Response;
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
@Log4j2
public class UploadController {
//업로드된 파일 저장 결로 설정
@Value("${com.movie.upload.path}") //애플리케이션 설정 변수
private String uploadPath;
//MultipartFile 타입을 이용한 파일 사용
//: Ajax를 이용한 파일 업로드 처리 -> 업로드 결과에 대한 화면 작성 X
//따라서 업로드 결과는 JSON 형태로 제공
@PostMapping("/uploadAjax")
//ResponseEntity는 HttpHeader와 HttpBody를 포함하는 HttpEntity를 상속받아 구현한 클래스
//즉, 사용자의 HttpRequest에 대한 응답 데이터를 포함하는 클래스로 HttpStatus, HttpHeaders, HttpBody를 포함
public ResponseEntity<List<UploadResultDTO>> uploadFile(MultipartFile[] uploadFiles) {
List<UploadResultDTO> resultDTOList = new ArrayList<>();
for (MultipartFile uploadFile : uploadFiles) {
//1. 확장자 검사
if (uploadFile.getContentType().startsWith("image") == false) {
log.warn("this is not image type");
return new ResponseEntity<>(HttpStatus.FORBIDDEN);
//이미지가 아닐경우 HttpRequest에 포함하는 HttpStatus 403을 포함해 반환
}
//실제 파일 이름이 전체 경로가 들어오기 때문에 원본 이름 처리와 실제 파일 이름 처리
String originalName = uploadFile.getOriginalFilename();
String fileName = originalName.substring(originalName.lastIndexOf("\\") + 1);
log.info("filename: " + fileName);
//2. 동일한 폴더에 많은 파일 방지
//(1) 날짜 폴더 생성
String folderPath = makeFolder();
//(2) UUID 클래스를 이용한 고유한 파일 이름 생성
String uuid = UUID.randomUUID().toString();
//(3) UUID를 이용해 저장 파일 이름 중간에 "_"를 이용해 구분
String saveName = uploadPath + File.separator + folderPath + File.separator + uuid + "_" + fileName;
Path savePath = Paths.get(saveName);
try {
uploadFile.transferTo(savePath); //실제 이미지 저장부
//이미지 저장 후, DTOList에 추가한다.
resultDTOList.add(new UploadResultDTO(fileName, uuid, folderPath));
} catch (IOException e) {
e.printStackTrace();
}
}//end for
return new ResponseEntity<>(resultDTOList, HttpStatus.OK);
//성공할 경우엔 HttpStatus OK를 담아 resultDTOList를 반환한다.
}
//(3) 폴더 생성 메서드 작성
private String makeFolder(){
String str= LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
String folderPath = str.replace("/", File.separator);
//경로에 폴더가 없을 경우 폴더 생성
File uploadPathFolder =new File(uploadPath, folderPath);
if(uploadPathFolder.exists()==false){
uploadPathFolder.mkdirs();
}
return folderPath;
}
//URL로 이미지 전송을 위한 메서드
@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()));
//: 확장자를 통해 마임타입을 판단하는데, 확장자가 없으면 null을 반환한다.
//파일 데이터 처리
result = new ResponseEntity<>(FileCopyUtils.copyToByteArray(file), header, HttpStatus.OK);
//ResponseEntity에 바이트 배열로 만든 파일, 헤더, 상태코드 전달
}
catch (Exception e){
log.error(e.getMessage());
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
}
return result;
}
}
섬네일 추가 후, UploadController
package com.movie.boot4.controller;
import com.movie.boot4.dto.UploadResultDTO;
import lombok.extern.log4j.Log4j2;
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 javax.xml.ws.Response;
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
@Log4j2
public class UploadController {
//업로드된 파일 저장 경로 설정
@Value("${com.movie.upload.path}") //애플리케이션 설정 변수
private String uploadPath;
//MultipartFile 타입을 이용한 파일 사용
//: Ajax를 이용한 파일 업로드 처리 -> 업로드 결과에 대한 화면 작성 X
//따라서 업로드 결과는 JSON 형태로 제공
@PostMapping("/uploadAjax")
//ResponseEntity는 HttpHeader와 HttpBody를 포함하는 HttpEntity를 상속받아 구현한 클래스
//즉, 사용자의 HttpRequest에 대한 응답 데이터를 포함하는 클래스로 HttpStatus, HttpHeaders, HttpBody를 포함
public ResponseEntity<List<UploadResultDTO>> uploadFile(MultipartFile[] uploadFiles) {
List<UploadResultDTO> resultDTOList = new ArrayList<>();
for (MultipartFile uploadFile : uploadFiles) {
//1. 확장자 검사
if (uploadFile.getContentType().startsWith("image") == false) {
log.warn("this is not image type");
return new ResponseEntity<>(HttpStatus.FORBIDDEN);
//이미지가 아닐경우 HttpRequest에 포함하는 HttpStatus 403을 포함해 반환
}
//실제 파일 이름이 전체 경로가 들어오기 때문에 원본 이름 처리와 실제 파일 이름 처리
String originalName = uploadFile.getOriginalFilename();
String fileName = originalName.substring(originalName.lastIndexOf("\\") + 1);
log.info("filename: " + fileName);
//2. 동일한 폴더에 많은 파일 방지
//(1) 날짜 폴더 생성
String folderPath = makeFolder();
//(2) UUID 클래스를 이용한 고유한 파일 이름 생성
String uuid = UUID.randomUUID().toString();
//(3) UUID를 이용해 저장 파일 이름 중간에 "_"를 이용해 구분
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;
File thumbnailFile = new File(thumbnailSaveName);
//섬네일 생성 메서드
//: 경로, 해당 파일, 원하는 사이즈
Thumbnailator.createThumbnail(savePath.toFile(), thumbnailFile, 100, 100);
//이미지 저장, 섬네일 생성 후, DTOList에 추가한다.
resultDTOList.add(new UploadResultDTO(fileName, uuid, folderPath));
} catch (IOException e) {
e.printStackTrace();
}
}//end for
return new ResponseEntity<>(resultDTOList, HttpStatus.OK);
//성공할 경우엔 HttpStatus OK를 담아 resultDTOList를 반환한다.
}
//(3) 폴더 생성 메서드 작성
private String makeFolder(){
String str= LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
String folderPath = str.replace("/", File.separator);
//경로에 폴더가 없을 경우 폴더 생성
File uploadPathFolder =new File(uploadPath, folderPath);
if(uploadPathFolder.exists()==false){
uploadPathFolder.mkdirs();
}
return folderPath;
}
//URL로 이미지 전송을 위한 메서드
@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()));
//: 확장자를 통해 마임타입을 판단하는데, 확장자가 없으면 null을 반환한다.
//파일 데이터 처리
result = new ResponseEntity<>(FileCopyUtils.copyToByteArray(file), header, HttpStatus.OK);
//ResponseEntity에 바이트 배열로 만든 파일, 헤더, 상태코드 전달
}
catch (Exception e){
log.error(e.getMessage());
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
}
return result;
}
}
2. 브라우저에서 섬네일 처리
: JSON으로 전달되는 UploadResultDTO에서 getImageURL()과 같은 섬네일 링크 처리를 위한 메서드 추가
UploadResultDTO에 getThumbnailURL() 추가
package com.movie.boot4.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import java.io.Serializable;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
@Data
@AllArgsConstructor
//직렬화 인터페이스 구현
//: 객체 데이터를 바이트 형태로 반환해 JSON으로 결과 데이터를 반환하기 위해서
//-> 직렬화 대상은 필드만 적용되며, 생성자와 메서드는 대상에 포함되지 않는다.
//JPA 엔티티를 사용한다면, 기본값으로 직렬화가 적용되어 있는것을 알 수 있다.
public class UploadResultDTO implements Serializable {
private String fileName;
private String uuid;
private String folderPath;
//해당 DTO는 실제 파일과 관련된 모든 정보를 가지는데, 나중에 전체 경로가 필요할 경우 사용하기 위한 메서드
//: 컨트롤러에서 업로드 결과 반환하기 위해 DTO를 엔티티로 반환해 사용
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에서 추가한 섬네일 이미지로 보여주도록 코드 변경
변경 전, showUploadedImages()
//Ajax 업로드 이후 이미지 호출하는 showUploadedImages() 함수 작성
//: Ajax 호출 성공시, '/display?fileName=xxx' 호출
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+"'>");
}
}
변경 후, showUploadedImages()
//Ajax 업로드 이후 이미지 호출하는 showUploadedImages() 함수 작성
//: Ajax 호출 성공시, '/display?fileName=xxx' 호출
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+"'>");
}
}
3. 업로드된 파일 삭제 메서드 추가
: 파일 URL 규칙을 이용해 삭제할 파일의 위치를 찾아 삭제하는 메서드를 UploadController에 removeFile()작성
[파일 URL 규칙 : 년/월/일/uuid_파일명]
경로와 UUID가 포함된 파일 이름을 파라미터로 받아 삭제 결과를 Boolean 타입으로 전송하는 removeFile() 메서드
//업로드된 파일 삭제를 위한 메서드
@PostMapping("/removeFile")
public ResponseEntity<Boolean> removeFile(String fileName) {
String srcFileName = null;
try {
//삭제할 파일 이름으로 URL디코딩
srcFileName= URLDecoder.decode(fileName, "UTF-8");
File file = new File(uploadPath+File.separator+srcFileName);
//(1) 원본 파일 삭제
boolean result = file.delete();
//파일의 디렉토리를 가져오는 함수 :getParent()
File thumbnail = new File(file.getParent(), "s_"+file.getName());
//(2) 섬네일 파일 삭제
result=thumbnail.delete();
return new ResponseEntity<>(result, HttpStatus.OK);
}
//Exception 클래스 : 사용자 실수와 같은 외적 요인으로 발생하는 예외로 예외처리 필수
//: IOException / ClassNotFoundException
//IOException 예외로 지정된 문자 부호화 형식을 지원하고 있지 않을때 발생
catch (UnsupportedEncodingException e) {
e.printStackTrace();
return new ResponseEntity<>(false, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
: 원본 파일과 함께 섬네일 파일도 함께 삭제하는데
-> 원본 파일 이름을 파라미터로 전송받아 File 객체를 이용해 원본과 섬네일 함께 삭제
4. 브라우저에서 파일 삭제
: 각 파일 삭제할 수 있도록 버튼 추가 후, 버튼과 이미지를 하나로 묶는다.
-> 하나로 묶은 <div>를 삭제해 한 번에 버튼과 이미지를 삭제할 수 있도록 처리
변경 전의, showUploadedImages()
//Ajax 업로드 이후 이미지 호출하는 showUploadedImages() 함수 작성
//: Ajax 호출 성공시, '/display?fileName=xxx' 호출
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+"'>");
}
}
변경 후의, showUploadedImages()와 삭제 이벤트 처리
//Ajax 업로드 이후 이미지 호출하는 showUploadedImages() 함수 작성
//: Ajax 호출 성공시, '/display?fileName=xxx' 호출
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();
}
} )
});
showUploadedImages()
(1) 함수 내부에 <div> 태그 생성해 <img>, <button>태그를 <div>로 묶는다.
(2) <button>태그는 'data-name' 커스텀 속성을 지정해 버튼 클릭시 삭제 파일 이름을 알아내는 용도로 사용
(3) 이로인해 업로드시 버튼이 추가되었고, 업로드시 서버에 원본과 섬네일 파일 생성된다.
remove버튼 이벤트처리
(1) 업로드 결과 만들어지는 <div>는 동적으로 생성되므로, 이벤트처리가 바로 불가능하다.
(2) 따라서 위임delegate하는 방식으로 이벤트 처리
(3) 삭제 작업은 POST 방식으로, 원본 파일과 섬네일 파일 삭제 후 화면에서 해당 이미지 포함된 <div>삭제
'Server Programming > Spring Boot Backend Programming' 카테고리의 다른 글
[Spring 부트 - 영화 리뷰 프로젝트] 4. 영화 목록 처리 (0) | 2022.10.18 |
---|---|
[Spring 부트 - 영화 리뷰 프로젝트] 3. 영화 등록 처리 (0) | 2022.10.18 |
[Spring 부트 - 영화 리뷰 프로젝트] 2. 파일 업로드 처리 (1) Ajax를 통한 JSON으로 이미지 업로드 (0) | 2022.10.18 |
[Spring 부트 - 영화 리뷰 프로젝트] 1. M:N (다대다) 관계 설계와 구현 [+ N+1 문제와 엔티티의 특정 속성 로딩 방법] (0) | 2022.10.17 |
[Spring 부트 - 댓글 프로젝트] 3-4. 댓글 비동기처리를 위한 @RestController와 JSON 처리 (1) | 2022.10.04 |