본문 바로가기

Server Programming/Spring Boot Backend Programming

[Spring 부트 - 영화 리뷰 프로젝트] 2. 파일 업로드 처리 (2) 섬네일 이미지를 통한 화면 처리와 파일 삭제

반응형

원본 이미지를 보여주는 것보다 섬네일 이미지로 변환해 보여주는 것이 성능에 도움이 된다.

특히 목록 페이지의 경우, 보여주는 이미지가 많기 때문에 주의해야 한다.

 

섬네일 이미지의 처리

  1. 업로드된 파일 저장하고 섬네일 라이브러리 활용해 섬네일 파일 생성
  2. 섬네일 파일을 구분하기 위해 앞에 's_'를 붙여 구분한다.
  3. UploadResultDTO에 getThumbnailURL()을 추가해 섬네일 경로를 <img>태그로 처리

섬네일 이미지 처리 라이브러리

: Thumbnailator 이용

-> 적은 양의 코드로 제작 가능하고, 가로 세로 사이즈 결정하면 비율에 맞게 조정해준다.

 

https://github.com/coobird/thumbnailator

 

GitHub - coobird/thumbnailator: Thumbnailator - a thumbnail generation library for Java

Thumbnailator - a thumbnail generation library for Java - GitHub - coobird/thumbnailator: Thumbnailator - a thumbnail generation library for Java

github.com

 

<!-- 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>삭제

 

반응형