[Springboot] 이미지 업로드 - 1 이미지 저장(로컬 및 DB)
Image.class
- Image 도메인을 만든다.
- postImageUrl: 사진을 전송받아서 그 사진을 서버의 특정 폴더에 저장 -Db에 그 저장된 경로를 insert
- 한명의 유저는 여러 이미지를 업로드를 할 수 있고, 하나의 이미지는 하나의 유저만이 만들 수 있으므로 @ManyToOne
- user는 db에서 User객체가 아닌, FK로 저장되기 때문에 userId라는 이름으로 컬럼명을 설정해준다.
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Data
@Entity
public class Image {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String caption;
private String postImageUrl;
@JoinColumn(name = "userId") // DB컬럼명 설정
@ManyToOne
private User user;
//이미지 좋아요 정보-> 추후추가
//댓글-> 추후추가
private LocalDateTime createDate;
@PrePersist
public void createDate(){
this.createDate = LocalDateTime.now();
}
}
ImageRepository
public interface ImageRepository extends JpaRepository<Image, Integer> {
}
ImageController
- imageUploadDto와 유저정보인 principalDetails를 받도록 한다.
- 업로드가 완료되면 유저의 프로필 화면으로 이동하도록 설정한다.
- MultipartFile은 @Notblank 어노테이션이 지원되지 않으므로 이미지 첨부 유효성 검사는 if문으로 처리해준다.
@RequiredArgsConstructor
@Controller
public class ImageController {
private final ImageService imageService;
...
@PostMapping("/image")
public String imageUpload(ImageUploadDto imageUploadDto,
@AuthenticationPrincipal PrincipalDetails principalDetails){
if(imageUploadDto.getFile().isEmpty()){
throw new CustomValidationException("이미지가 첨부되지 않았습니다.", null);
}
imageService.upload(imageUploadDto, principalDetails);
return "redirect:/user/"+principalDetails.getUser().getId();
}
}
ControllerExceptionHandler
- 업로드 이미지가 없을때 CustomValidationException을 throw하고 그것을 ControllerExceptionHandler의 validationException메소드가 낚아챈다.
- 이 경우 errorMap 매개변수를null로 전달하기 때문에 errorMap이 null일 경우, null이 아닐 경우를 분기해야한다.
@RestController
@ControllerAdvice //모든 Exception들을 낚아챔
public class ControllerExceptionHandler {
@ExceptionHandler(CustomValidationException.class) //CustomValidationException 발동하는 모든 Exception을 이 함수가 가로챔
public String validationException(CustomValidationException e){
if(e.getErrorMap() ==null){
return Script.back(e.getMessage());
}else{
return Script.back(e.getErrorMap().toString());
}
}
....
}
CustomValidationException
public class CustomValidationException extends RuntimeException{
//객체를 구분할때
private static final long serialVersionUID = 1L;
private Map<String, String> errorMap;
public CustomValidationException(String message, Map<String, String> errorMap){
super(message);
this.errorMap = errorMap;
}
public Map<String, String > getErrorMap(){
return errorMap;
}
}
ImageUploadDto
- 파일을 받을거기 때문에 요청을 위한 dto(ImageUploadDto)를 만들어준다.
- ImageUploadDto는 file 과 caption을 받을 수 있도록 한다.
- toEntity 메소드를 통해 Image객체로 형변환할 수 있도록 한다.
@Data
public class ImageUploadDto {
//MultipartFile 에는 @Notblank가 지원안된다.
private MultipartFile file;
private String caption;
public Image toEntity(String postImagUrl, User user){
return Image.builder()
.caption(caption)
.postImageUrl(postImagUrl)
.user(user)
.build();
}
}
ImageService
- upload메소드에는 imageUploadDto와 principalDetails를 받는다.
- imageFilePath : 실제 저장될 경로
- uploadFolder: 이미지가 저장될 로컬경로다, @Value 어노테이션으로 해당 값을 가져올 수 있다.
- imageUploadDto의 toEntity() 메소드를 통해 db에 저장할Image객체로 변환한다.
이때 매개변수로 uuid가 포함된 이미지명과 현재 로그인한 유저의 정보를 전달한다.
@RequiredArgsConstructor
@Service
public class ImageService {
@Value("${file.path}") //application.properties에서 가져옴
private String uploadFolder;
private final ImageRepository imageRepository;
@Transactional
public void upload(ImageUploadDto imageUploadDto, PrincipalDetails principalDetails){
UUID uuid = UUID.randomUUID();
String imageFilename = uuid + "_"+ imageUploadDto.getFile().getOriginalFilename(); //db에 저장되는 파일명
Path imageFilePath = Paths.get(uploadFolder+imageFilename);
// 통신, I/O -> 예외가 발생할 수 있다.
try {
Files.write(imageFilePath, imageUploadDto.getFile().getBytes()); //(실제 저장될 경로, 실제 이미지 파일)
} catch (Exception e){
e.printStackTrace();
}
//Image 테이블에 저장
Image image = imageUploadDto.toEntity(imageFilename, principalDetails.getUser());
imageRepository.save(image);
}
}
upload.html
- 파일은 전송하는것이므로 enctype 로 multipart/form-data 로 설정해준다.
<!--사진업로드 Form-->
<form class="upload-form" th:action="@{/image}" method="post" enctype="multipart/form-data">
<input type="file" name="file" onchange="imageChoose(this)"/>
<div class="upload-img">
<img src="/images/person.jpeg" alt="" id="imageUploadPreview" />
</div>
<!--사진설명 + 업로드버튼-->
<div class="upload-form-detail">
<input type="text" placeholder="사진설명" name="caption">
<button class="cta blue">업로드</button>
</div>
<!--사진설명end-->
</form>