Coding 공부/IntelliJ

[SpringBoot_MariaDB] 파일 업로드 코드 추가

CBJH 2024. 6. 11.
728x90
반응형

1. 추가한 디렉토리

 

  • Cotroller
    • UpDownController
  • DTO
    • UploadResultDTO
    • UploadFileDTO
  • Domain
    • BoardImage

 

 

2. 추가한 설정

 

  • application.properties 파일

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

 

  • 업로드관련 설정 추가

 

  • build.gradle dependencies 추가
implementation 'net.coobird:thumbnailator:0.4.16'
  • 썸네일 사용을 위해 모듈 추가

 

3. 추가한 코드

@Data
public class UploadFileDTO {
    private List<MultipartFile> files;
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class UploadResultDTO {
    private String uuid;
    private String fileName;
    private boolean img;    //이미지이면 트루, 썸네일을 만들 예정
    public String getLink(){
        if(img){
            return "s_"+uuid+"_"+fileName;  //이미지파일이면 앞에 s_를 붙인다.
        }else{
            return uuid+"_"+fileName;
        }
    }
}
@Entity
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class Board extends BaseEntity{
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long bno;

    @Column(length = 500, nullable = false)
    private String title;
    @Column(length = 2000, nullable = false)
    private String content;
    @Column(length = 50, nullable = false)
    private String writer;

    @OneToMany(mappedBy = "board", cascade = {CascadeType.ALL}, fetch = FetchType.LAZY, orphanRemoval = true)
    @Builder.Default
    @BatchSize(size=20)
    private Set<BoardImage> imageset = new HashSet<>();

    public void change(String title, String content){
        this.title = title;
        this.content = content;
    }

    public void addImage(String uuid, String fileName){
        BoardImage boardImage = BoardImage.builder()
                .uuid(uuid)
                .fileName(fileName)
                .board(this)
                .ord(imageset.size())
                .build();
        imageset.add(boardImage);
    }

    public void clearImage(){
        imageset.forEach(boardImage -> boardImage.changeBoard(null));
        this.imageset.clear();
    }
}
@Entity
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@ToString(exclude = "board")
public class BoardImage implements Comparable<BoardImage> { //BoardImage를 비교하기 위해 인터페이스 구현
    @Id
    private String uuid;
    private String fileName;
    private int ord; //파일 업로드 순서

    @ManyToOne
    private Board board;

    @Override
    public int compareTo(BoardImage other){
        return this.ord - other.ord;
    }

    public void changeBoard(Board board){
        this.board = board;
    }
}
@RestController
@Log4j2
public class UpDownController {
    // application.properties에 설정된 업로드 경로를 주입받습니다.
    @Value("${org.zerock.upload.path}")
    private String uploadPath;

    // POST 방식의 /upload 요청을 처리하며, multipart/form-data 형식의 데이터를 받습니다.
    @PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) //consume : 데이터 바인딩, 필터링 역할을 한다. 파일 입출력으로 매개변수 타입을 검증한다. 아니면 415에러 발생.
    public List<UploadResultDTO> upload(UploadFileDTO uploadFileDTO) {

        // 업로드된 파일이 존재하는지 확인합니다.
        if (uploadFileDTO.getFiles() != null) {
            // 업로드 결과를 저장할 리스트를 생성합니다. (final 키워드는 재할당 방지)
            final List<UploadResultDTO> list = new ArrayList<>();
            // 각 파일을 처리하는 로직을 수행합니다.
            uploadFileDTO.getFiles().forEach(multipartFile -> {
                // 파일의 원본 이름을 가져옵니다.
                String originalName = multipartFile.getOriginalFilename();
                log.info("@@originalName: " + originalName);
                // UUID를 생성하여 파일 이름 중복을 방지합니다.
                String uuid = UUID.randomUUID().toString();
                // 저장 경로를 생성합니다.
                Path savePath = Paths.get(uploadPath, uuid + "_" + originalName);
                // 이미지 파일 여부를 판단하기 위한 변수입니다.
                boolean image = false;

                try {
                    // 파일을 저장합니다.
                    multipartFile.transferTo(savePath);
                    // 파일의 MIME 타입을 확인하여 이미지 파일인지 판별합니다.
                    if (Files.probeContentType(savePath).startsWith("image")) {
                        image = true;
                        // 썸네일 파일 경로를 생성합니다.
                        File thumbFile = new File(uploadPath, "s_" + uuid + "_" + originalName);
                        // 썸네일을 생성합니다. (가로, 세로 100px)
                        Thumbnailator.createThumbnail(savePath.toFile(), thumbFile, 100, 100);
                    }
                } catch (IOException e) {
                    // 예외 발생 시 로그를 출력합니다.
                    e.printStackTrace();
                }
                // 업로드 결과 정보를 DTO에 담아 리스트에 추가합니다.
                list.add(UploadResultDTO.builder()
                        .uuid(uuid)
                        .fileName(originalName)
                        .img(image).build());
            });
            // 업로드 결과 리스트를 반환합니다.
            return list;
        }
        // 업로드된 파일이 없으면 null을 반환합니다.
        return null;
    }

    @GetMapping("/view/{fileName}")
    public ResponseEntity<Resource> viewFileGET(@PathVariable String fileName) {

        // 1. 파일 경로 생성:
        //    - 업로드 경로 (uploadPath)와 파일 이름 (fileName)을 결합하여 파일의 전체 경로를 생성합니다.
        //    - File.separator를 사용하여 운영체제에 맞는 파일 경로 구분자를 자동으로 적용합니다.
        Resource resource = new FileSystemResource(uploadPath + File.separator + fileName);

        // 2. 파일 이름 추출:
        //    - 리소스 객체에서 파일 이름을 가져옵니다.
        String resourceName = resource.getFilename();

        // 3. HTTP 응답 헤더 설정:
        //    - HttpHeaders 객체를 생성하여 응답 헤더를 설정합니다.
        HttpHeaders headers = new HttpHeaders();

        try {
            // 4. Content-Type 헤더 설정:
            //    - Files.probeContentType() 메서드를 사용하여 파일의 MIME 타입을 확인하고,
            //      Content-Type 헤더에 해당 MIME 타입을 추가합니다.
            //      (예: 이미지 파일이면 "image/jpeg", 텍스트 파일이면 "text/plain" 등)
            headers.add("Content-Type", Files.probeContentType(resource.getFile().toPath()));
        } catch (Exception e) {
            // 5. 예외 처리:
            //    - MIME 타입 확인 과정에서 예외가 발생하면, 500 Internal Server Error 응답을 반환합니다.
            return ResponseEntity.internalServerError().build();
        }

        // 6. 파일 데이터 응답:
        //    - ResponseEntity를 생성하여 200 OK 상태 코드, 설정된 헤더, 파일 리소스를 포함하여 반환합니다.
        //    - 이를 통해 클라이언트는 웹 브라우저에서 이미지를 보거나,
        //      텍스트 파일 내용을 확인하는 등 파일을 직접 열어볼 수 있습니다.
        return ResponseEntity.ok().headers(headers).body(resource);
    }

    @DeleteMapping("/remove/{fileName}")
    public Map<String, Boolean> removeFile(@PathVariable String fileName) {

        // 1. 파일 경로 생성:
        //    - 업로드 경로 (uploadPath)와 파일 이름 (fileName)을 결합하여 파일의 전체 경로를 생성합니다.
        //    - File.separator를 사용하여 운영체제에 맞는 파일 경로 구분자를 자동으로 적용합니다.
        Resource resource = new FileSystemResource(uploadPath + File.separator + fileName);

        // 2. 삭제 결과 저장할 Map 생성:
        //    - 파일 삭제 성공 여부를 저장할 Map을 생성합니다. 키는 "result", 값은 boolean 타입입니다.
        Map<String, Boolean> resultMap = new HashMap<>();

        // 3. 삭제 성공 여부 초기값 설정:
        //    - 파일 삭제 성공 여부를 나타내는 변수를 false로 초기화합니다.
        boolean removed = false;

        try {
            // 4. 파일 MIME 타입 확인:
            //    - Files.probeContentType() 메서드를 사용하여 파일의 MIME 타입을 확인합니다.
            String contentType = Files.probeContentType(resource.getFile().toPath());

            // 5. 파일 삭제:
            //    - resource.getFile().delete() 메서드를 사용하여 파일을 삭제하고, 결과를 removed 변수에 저장합니다.
            removed = resource.getFile().delete();

            // 6. 이미지 파일인 경우 썸네일 삭제:
            //    - 삭제된 파일이 이미지 파일인 경우 (MIME 타입이 "image"로 시작하는 경우),
            //      해당 파일의 썸네일 파일도 함께 삭제합니다.
            if (contentType.startsWith("image")) {
                File thumbFile = new File(uploadPath + File.separator + "s_" + fileName);
                thumbFile.delete();
            }
        } catch (Exception e) {
            // 7. 예외 처리:
            //    - 파일 삭제 과정에서 예외가 발생하면, 에러 메시지를 로그에 출력합니다.
            log.error(e.getMessage());
        }

        // 8. 삭제 결과 저장:
        //    - resultMap에 "result" 키와 함께 파일 삭제 성공 여부 (removed)를 저장합니다.
        resultMap.put("result", removed);

        // 9. 결과 반환:
        //    - 파일 삭제 성공 여부를 담은 Map 객체를 반환합니다.
        return resultMap;
    }
}

 

4. 잡담

 

  • 파일 업로드를 하는 모든 코드를 오늘 배우진 못 했다. 생각보다 설정할 부분도 많고, DB 설계도 새로해야되서 시간이 오래 걸렸다.

댓글