TIL

day 47 TIL

dalooong 2023. 7. 9. 13:53

File Handling

정적 파일

사용자에게 변환 없이 전달되는 파일

  • CSS
  • 이미지, 영상파일
  • 몇몇 HTML 등
  • spring boot 기본 설정으로 정적 파일을 전달할 수 있다.
  • 설정을 바꾸면 요청 경로 변경 가능

form

HTML에서 JS없이 데이터를 보낼 때는? ⇒ form 사용

  • 내부에 input 요소를 이용해 전달할 데이터 정의
  • input type=”submit”을 이용해 form 요소 내부의 데이터 수합
  • enctype 속성으로 데이터 인코딩 방식 정의 가능

multipart/form-data

요청을 여러 부분으로 구분해서 전송하는 형태

  • 텍스트와 파일이 혼합된 요청이라는 의미
  • Form을 이용해 파일을 보낼 경우 선택해야 하는 방식
  • application/x-www-form-urlencoded (기본값) : input 데이터를 모아 하나의 문자열로 표현해 전송합니다.
  • mutlipart/form-data : 각각의 input 데이터를 개별적으로 인코딩해, 여러 부분 (multipart)로 나눠서 전송합니다.

이때 저희가 일반적인 문자 데이터를 보낼때는 기본값인 application/x-www-form-urlencoded 를 사용하지만, 만약 파일 같이 별도의 인코딩이 필요한 경우 multipart/form-data 를 활용해야 합니다.

<form enctype="multipart/form-data">
  <input type="text" name="name">
  <input type="file" name="photo">
  <input type="submit">
</form>

참고) type="image" 같은 경우 이미지를 업로드 하기 위함이 아닌, 이미지를 이용해 제출 버튼을 표현하고 싶을 때 사용하는 형식입니다.

Postman을 통해 multipart/form-data 요청을 보내고 싶다면, Body 탭에서 form-data 를 선택하면 됩니다. 이때 Key 에 해당하는 값이 input 요소의 name 의 역할을 합니다.

  • Postman에서 multipart/form-data를 보낼 경우
    • Value에서 파일선택하면 된다.
     

  • .html 코드
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<form
        action="/multipart"
        method="post"
        enctype="multipart/form-data">
    <input type="text" name="name">
    <input type="file" name="photo">
    <input type="submit">
</form>
</body>
</html>

Spring에서 MultipartFile 받기

@RequestParam으로 MultipartFile 인자를 받을 수 있다

  • getBytes() 메소드로 byte[] 데이터로 사전 확인 가능
//저장할 파일 이름
File file = new File("./filename.png");
//파일에 저장하기 위한 OutputStream
try(OutputStream outputStream = new FileOutputStream(file));
//byte[]에 데이터를 받는다.
byte[] fileBytes = multipartFile.getBytes();
//여기에서 추가 작업

OutputStream에 MultipartFile의 byte[]를 저장한다.
outputStream.write(fileBytes);
  • form.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<form
        action="/multipart"
        method="post"
        enctype="multipart/form-data">
    <input type="text" name="name">
    <input type="file" name="photo">
    <input type="submit">
</form>
</body>
</html>
  • ResponseDto
@Data
public class ResponseDto {
    private String message;
}
  • filecontroller
@Slf4j
@RestController
public class FileController {
    @PostMapping(
            value = "/multipart",
            consumes = MediaType.MULTIPART_FORM_DATA_VALUE
    )
    public ResponseDto multipart(
            @RequestParam("name") String name,
            @RequestParam("photo")MultipartFile multipartFile
            ) throws IOException {
        Path uploadTo = Path.of("filename.png");
        multipartFile.transferTo(uploadTo);

        ResponseDto response = new ResponseDto();
        response.setMessage("success");
        return response;
    }
}

postman에서 실행

인텔리제이에 설정한 filename.png 파일이 생긴 걸 확인할 수 있다.

@RequestParam으로 MultipartFile 인자를 받을 수 있다

  • 이때 @RequestMapping의 consumes 설정 필요

  • transferTo 메소드로 파일의 형태로 저장할 수 있다. → 한 파일에서 파일 관리 할 수 있음(media)
//저장할 경로를 생성한다. 
Files.createDirectories(Path.of("media"));
//저할 파일이름을 경로를 포함해 지정한다. 
Path path = Path.of("media/filename.png");
//저장한다.
multipartFile.transferTo(path);
  • media 폴더가 생성되고 그 안에 png 파일이 들어가있는걸 확인할 수 있다.

업로드된 데이터 돌려주기

  • 한 폴더에 사용자가 업로드한 파일을 다 저장하고
  • 해당 경로의 파일을 정적 파일의 형태로 전달하자

  • application.yaml 설정
spring:
  mvc:
    static-path-pattern: /static/**
  web:
#    spring이 정적 파일 요청을 받을 때 그 파일을 찾는 경로들 
    resources:
      static-locations: file:media/,classpath:/static

  • 파일에 현재 시간 나오게 저장하기
@Slf4j
@RestController
public class FileController {
    @PostMapping(
            value = "/multipart",
            consumes = MediaType.MULTIPART_FORM_DATA_VALUE
    )
    public ResponseDto multipart(
            @RequestParam("name") String name,
            @RequestParam("photo")MultipartFile multipartFile
        ) throws IOException {
        //저장할 경로를 생성한다.
        Files.createDirectories(Path.of("media"));
        //저할 파일이름을 경로를 포함해 지정한다.
//        Path path = Path.of("media/filename.png");

//        multipartFile.transferTo(path);
        LocalDateTime now = LocalDateTime.now();
        log.info(now.toString());
        String filename = now.toString().replace(":", "");

        Path uploadTo
                = Path.of(String.format("media/%s.png", filename));
        //저장한다.
        multipartFile.transferTo(uploadTo);

        ResponseDto response = new ResponseDto();
        response.setMessage(String.format("/static/%s.png",filename));
        return response;
    }
  • 포스트맨 실행

실행결과 → 파일이름에 현재 날짜가 적힌 걸 확인할 수 있다.

UserEntity.avatar

  • userService 전체 코드 updateUserAvatar,
  • updateUserAvatar
  •  
package com.example.contents;

import com.example.contents.dto.UserDto;
import com.example.contents.entity.UserEntity;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.server.ResponseStatusException;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Optional;
@Slf4j
@Service
@RequiredArgsConstructor
public class UserService {
    private final UserRepository repository;

    // createUser
    public UserDto createUser(UserDto dto) {
        // 1. 회원가입 ==> 프로필 이미지가 아직 필요없다.
        UserEntity entity = new UserEntity();
        entity.setUsername(dto.getUsername());
        entity.setPhone(dto.getPhone());
        entity.setBio(dto.getBio());
        entity.setEmail(dto.getEmail());
        entity.setAvatar(dto.getAvatar());
        throw new ResponseStatusException(HttpStatus.NOT_IMPLEMENTED);
    }

    // readUserByUsername
    public UserDto readUserByUsername(String username) {
        throw new ResponseStatusException(HttpStatus.NOT_IMPLEMENTED);
    }

    // updateUser
    public UserDto updateUser(Long id, UserDto dto) {

        throw new ResponseStatusException(HttpStatus.NOT_IMPLEMENTED);
    }

    // updateUserAvatar
    public UserDto updateUserAvatar(Long id, MultipartFile avatarImage) {
        // 사용자가 프로필 이미지를 업로드 한다.

        // 1. 유저 존재 확인
        Optional<UserEntity> optionalUser
                = repository.findById(id);
        if (optionalUser.isEmpty())
            throw new ResponseStatusException(HttpStatus.NOT_FOUND);

        // media/filename.png
        // media/<업로드 시각>.png
        // 2. 파일을 어디에 업로드 할건지
        // media/{userId}/profile.{파일 확장자}

        // 2-1. 폴더만 만드는 과정
        String profileDir = String.format("media/%d/", id);
        log.info(profileDir);
        try {
            Files.createDirectories(Path.of(profileDir));
        } catch (IOException e) {
            log.error(e.getMessage());
            throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR);
        }

        // 2-2. 확장자를 포함한 이미지 이름 만들기 (profile.{확장자})
        String originalFilename = avatarImage.getOriginalFilename();
        String[] fileNameSplit = originalFilename.split("\\\\.");
        String extension = fileNameSplit[fileNameSplit.length - 1];
        String profileFilename = "profile." + extension;
        log.info(profileFilename);

        // 2-3. 폴더와 파일 경로를 포함한 이름 만들기
        String profilePath = profileDir + profileFilename;
        log.info(profilePath);

        // 3. MultipartFile 을 저장하기
        try {
            avatarImage.transferTo(Path.of(profilePath));
        } catch (IOException e) {
            log.error(e.getMessage());
            throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR);
        }

        // 4. UserEntity 업데이트 (정적 프로필 이미지를 회수할 수 있는 URL)
        // <http://localhost:8080/static/1/profile.png>
        log.info(String.format("/static/%d/%s", id, profileFilename));

        UserEntity userEntity = optionalUser.get();
        userEntity.setAvatar(String.format("/static/%d/%s", id, profileFilename));
        return UserDto.fromEntity(repository.save(userEntity));
    }
}

  • 만약 Multipart 이미지 크기 제한이 걸려서 사진이 로당 안된다면 .yaml 에서 크기 용량을 변경해준다
spring:
  servlet:
    multipart:
      max-file-size: 10MB
      max-request-size: 10MB