spring boot 파일, 이미지 업로드(multipartfile)
게시판을 만들 때 필수적인 사진 저장 코드를 구현해봤다.
1. application.properties
#Multipart file
spring.servlet.multipart.enabled=true
spring.servlet.multipart.max-file-size=200MB
spring.servlet.multipart.max-request-size=215MB
2. Photo 클래스
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import solux.wansuki.OurNeighbor_BE.domain.UsedGoods.UsedGoods;
import javax.persistence.*;
@Getter
@NoArgsConstructor
@Entity
public class Photo {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "photo_id")
private Long id;
@Column(nullable = false)
private String originalName;
@Column(nullable = false)
private String filePath;
private Long fileSize;
@JsonIgnore
@ManyToOne
@JoinColumn(name = "usedGoods_id")
private UsedGoods usedGoods;
@Builder
public Photo(String originalName, String filePath, Long fileSize) {
this.originalName = originalName;
this.filePath = filePath;
this.fileSize = fileSize;
}
public void setUsedGoods(UsedGoods usedGoods) {
if (this.usedGoods != null) {
this.usedGoods.getPhotos().remove(this);
}
this.usedGoods = usedGoods;
//무한 루프 빠지지 않도록
if (!usedGoods.getPhotos().contains(this))
usedGoods.getPhotos().add(this);
}
}
사진의 이름, 경로, 사이즈를 저장할 클래스를 만든다.
그 후 usedGoods라는 중고 거래 게시판의 글 하나당 사진 여러개를 저장할 수 있기 때문에
다대일 양방향 연관관계를 만들어준다.
3. UsedGoods 클래스
@OneToMany(mappedBy = "usedGoods")
private List<Photo> photos = new ArrayList<>();
public void addPhoto(Photo photo) {
this.photos.add(photo);
if (photo.getUsedGoods() != this)
photo.setUsedGoods(this);
}
양방향이기 때문에 usedGoods 클래스에도 photo와 연관 관계를 맺을 수 있도록 하는 addPhoto 메소드를 쓴다.
- usedGoods 클래스의 addPhoto 메소드를 실행하면 photo의 setUsedGoods 메소드도 같이 실행되도록 편의 메소드를 작성했다.
4. UsedGoodsController
@PostMapping("/usedGoods")
public Long save(
@RequestParam(value = "file", required = false) List<MultipartFile> files,
@RequestParam("title") String title,
@RequestParam(value = "content", required = false) String content
) throws Exception{
UsedGoodsSaveDto saveDto = UsedGoodsSaveDto.builder()
.title(title)
.content(content)
.build();
return usedGoodsService.save(saveDto, files);
}
multipartfile은 @RequestBody로 받으면 오류가 발생하기 때문에 이처럼 @RequestParam으로 받아줬다.
중고 거래 게시판 작성글의 제목, 내용도 마찬가지로 @RequestParam으로 받았다.
5. FileHandler
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.web.multipart.MultipartFile;
import solux.wansuki.OurNeighbor_BE.domain.Photo.Photo;
import java.io.File;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
@Component
public class FileHandler {
public List<Photo> parseFileInfo(List<MultipartFile> multipartFiles) throws Exception {
List<Photo> fileList = new ArrayList<>();
if (multipartFiles.isEmpty())
return fileList;
if (!CollectionUtils.isEmpty(multipartFiles)) {
LocalDateTime now = LocalDateTime.now();
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyyMMdd");
String currentDate = now.format(dateTimeFormatter);
String absolutePath = new File("").getAbsolutePath() + File.separator + File.separator;
//프로젝트 폴더 내에 photo 폴더 생성 후 그 안에 현재 날짜로 파일 생성
String path = "photo" + File.separator + currentDate;
File file = new File(path);
//경로가 존재하지 않으면 디렉토리 생성
if (!file.exists()) {
file.mkdirs();
}
for (MultipartFile multipartFile : multipartFiles) {
String originalFileExtension;
String contentType = multipartFile.getContentType();
if (ObjectUtils.isEmpty(contentType))
break;
else {
if (contentType.contains("image/jpeg"))
originalFileExtension = ".jpg";
else if (contentType.contains("image/png"))
originalFileExtension = ".png";
else
break;
}
//파일 이름이 중복되지 않도록 생성 시간의 나노초까지 구해서 이름으로 사용
String newFileName = System.nanoTime() + originalFileExtension;
Photo photo = Photo.builder()
.originalName(multipartFile.getOriginalFilename())
//photo 파일 아래 날짜 파일 아래 파일을 저장
.filePath(path + File.separator + newFileName)
.fileSize(multipartFile.getSize())
.build();
fileList.add(photo);
file = new File(absolutePath + path + File.separator + newFileName);
multipartFile.transferTo(file);
file.setWritable(true);
file.setReadable(true);
}
}
return fileList;
}
}
파일 핸들러를 통해 여러 개의 파일들의 이름을 각각 system의 나노초로 변환 후 photo 객체 생성 후 fileList에 넣어 반환한다.
6. UsedGoodsService
@Transactional
public Long save(UsedGoodsSaveDto saveDto, List<MultipartFile> files) throws Exception{
UsedGoods usedGoods = saveDto.toEntity();
List<Photo> photoList = fileHandler.parseFileInfo(files);
if (!photoList.isEmpty()) {
for (Photo photo : photoList)
usedGoods.addPhoto(photoRepository.save(photo));
}
return usedGoodsRepository.save(usedGoods).getId();
}
fileHandler를 통해 반환받은 포토 리스트들을 각각 usedGoods 객체의 photo list에 추가한 후 usedGoods 객체를 저장한다.
참고
[Spring Boot] 게시판 구현 4 - 다중 파일(이미지) 업로드 MultipartFile (velog.io)
[Spring Boot] 파일(이미지) 업로드 구현하기 (velog.io)