SpringBoot with JPA 프로젝트(M:N) 4.이미지 업로드,썸네일,이미지삭제

mingki·2022년 3월 12일
4

SpringBoot & JPA

목록 보기
23/26


📚 공부한 책 : 코드로배우는 스프링 부트 웹프로젝트
❤️ github 주소 : https://github.com/qkralswl689/LearnFromCode/tree/main/mreview2022

1.파일업로드

스프링부트 파일업로드 설정 방법
1) 파일 업로드라이브러리 등록(commons-fileupload)
2) Servlet3 버전부터 추가된 자체적인 파일 업로드 라이브러리 이용 -> 사용할 것

1-1.properties 설정

# 파일 업로드

# 파일 업로드 기능여부 선택
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

1-2.Controller 작성

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;
    }
}

1-3.html 화면 작성

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>
  • 실행화면

2.파일 업로드 결과 반환과 화면처리

결과 데이터는 JSON으로 전송한다, 어떤 구조의 데이터를 전송할 것인지 결정한다

  • 브라우저에서 필요한 정보
    - 업로드된 파일의 원래 이름
    - 파일의 UUID 값
    - 업로드된 파일의 저장경로

2-1.UploadResultDTO 생성

브라우저에서 파일 저장 처리가 간단할 수 있도록 클래스와 객체를 구성해 처리한다

  • 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 "";
    }
}

2-2.Controller 수정

업로드 결과를 반환하기 위해 리턴타입 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;
    }
}

2-3.html 수정

업로드 된 이미지들을 보여줄 수 있는 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>
  • 실행화면

3.썸네일 이미지 생성과 화면처리

원본이미지가 그대로 화면에 표시되면 데이터를 많이 소비해야하기 때문에 썸네일을 만들어 전송해주고 원본이미지를 보려고 할 때 원본 파일을 보여주는것이 좋다
★ 썸네일 이미지 처리 과정
- 업로드된 파일을 저장하고 썸네일 라이브러리를 활용해 썸네일 파일을 만든다
- 썸네일 파일은 파일의 맨 앞에 's_'를 붙혀 일반 파일과 구분한다
- UploadResultDTO에 getThumbnailURL()을 추가해 썸네일의 경로를 태그로 처리한다

3-1.라이브러리 추가

build.gradle 파일에 Thumbnailator를 추가한다

  • Thumbnailator 라이브러리는 적은양의 코드만을 이용해 썸네일을 제작할 수 있고 가로 세로 사이즈를 결정하면 비율에 맞게 조정해 주는 기능이 제공된다
dependencies {
	//...생략
	implementation group: 'net.coobird', name: 'thumbnailator', version: '0.4.11'
}

3-2.Controller 수정

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);
    }
    //... 생략
}

3-3.DTO파일 수정


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 "";
    }
}

3-3.html 수정

    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+"'>");

        }

    }
  • 실행화면

4.업로드 파일 삭제

파일 삭제는 파일의 URL로 처리할 수 있다
-> 파일의 URL 자체가 년/월/일/uuid_파일명 으로 구성되어있기 때문에 이를 이용해 삭제할 파일의 위치를 찾아 삭제할 수 있다

4-1.Controller 수정

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);
        }
    }

}

4-2.html 수정

파일을 삭제할 수 있도록 버튼을 추가하고, 버튼과 이미지를 하나의 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();
            }
        } )

    });
  • 실행화면
profile
비전공초보개발자

0개의 댓글