diff --git a/src/main/java/com/prgrms2/java/bitta/jobpost/entity/JobPost.java b/src/main/java/com/prgrms2/java/bitta/jobpost/entity/JobPost.java index 9317ac2..867bd06 100644 --- a/src/main/java/com/prgrms2/java/bitta/jobpost/entity/JobPost.java +++ b/src/main/java/com/prgrms2/java/bitta/jobpost/entity/JobPost.java @@ -1,6 +1,7 @@ package com.prgrms2.java.bitta.jobpost.entity; import com.prgrms2.java.bitta.apply.entity.Apply; +import com.prgrms2.java.bitta.media.entity.Media; import com.prgrms2.java.bitta.member.entity.Member; import jakarta.persistence.*; import lombok.*; @@ -62,6 +63,9 @@ public boolean isClosed() { return LocalDate.now().isAfter(this.endDate); } + @OneToOne(mappedBy = "jobPost", cascade = CascadeType.ALL, orphanRemoval = true) + private Media media; + // 해당 게시글에 대한 신청 목록 가져야함 @OneToMany(mappedBy = "jobPost", cascade = CascadeType.ALL, orphanRemoval = true) private List apply = new ArrayList<>(); diff --git a/src/main/java/com/prgrms2/java/bitta/media/dto/MediaDto.java b/src/main/java/com/prgrms2/java/bitta/media/dto/MediaDto.java index 64a48c9..c6f8858 100644 --- a/src/main/java/com/prgrms2/java/bitta/media/dto/MediaDto.java +++ b/src/main/java/com/prgrms2/java/bitta/media/dto/MediaDto.java @@ -1,5 +1,6 @@ package com.prgrms2.java.bitta.media.dto; +import com.fasterxml.jackson.annotation.JsonInclude; import com.prgrms2.java.bitta.media.entity.MediaCategory; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.Min; @@ -17,6 +18,7 @@ @AllArgsConstructor @NoArgsConstructor @Schema(title = "미디어 DTO", description = "미디어 파일의 요청 및 응답에 사용하는 DTO입니다.") +@JsonInclude(JsonInclude.Include.NON_NULL) public class MediaDto { @Schema(title = "미디어 ID (PK)", description = "미디어 파일의 고유 ID 입니다.") @Min(value = 1, message = "ID는 0 또는 음수가 될 수 없습니다.") @@ -43,6 +45,14 @@ public class MediaDto { @Min(value = 1, message = "ID는 0 또는 음수가 될 수 없습니다.") private Long feedId; + @Schema(title = "회원 ID (FK)", description = "회원의 ID입니다.") + @Min(value = 1, message = "ID는 0 또는 음수가 될 수 없습니다.") + private Long memberId; + + @Schema(title = "일거리 ID (FK)", description = "일거리의 ID입니다.") + @Min(value = 1, message = "ID는 0 또는 음수가 될 수 없습니다.") + private Long jobPostId; + @Schema(title = "파일 저장일시", description = "파일이 저장된 날짜 및 시간입니다.", example = "2023-09-24T14:45:00") private LocalDateTime createdAt; diff --git a/src/main/java/com/prgrms2/java/bitta/media/entity/Media.java b/src/main/java/com/prgrms2/java/bitta/media/entity/Media.java index 03b6826..2129322 100644 --- a/src/main/java/com/prgrms2/java/bitta/media/entity/Media.java +++ b/src/main/java/com/prgrms2/java/bitta/media/entity/Media.java @@ -1,6 +1,8 @@ package com.prgrms2.java.bitta.media.entity; import com.prgrms2.java.bitta.feed.entity.Feed; +import com.prgrms2.java.bitta.jobpost.entity.JobPost; +import com.prgrms2.java.bitta.member.entity.Member; import jakarta.persistence.*; import lombok.*; import org.hibernate.annotations.ColumnDefault; @@ -40,6 +42,14 @@ public class Media { @JoinColumn(name = "feed_id") private Feed feed; + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id") + private Member member; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "jobpost_id") + private JobPost jobPost; + @CreatedDate @Column(updatable = false, nullable = false) private LocalDateTime createdAt; diff --git a/src/main/java/com/prgrms2/java/bitta/media/entity/MediaCategory.java b/src/main/java/com/prgrms2/java/bitta/media/entity/MediaCategory.java index 2f558a9..aeed17f 100644 --- a/src/main/java/com/prgrms2/java/bitta/media/entity/MediaCategory.java +++ b/src/main/java/com/prgrms2/java/bitta/media/entity/MediaCategory.java @@ -1,5 +1,5 @@ package com.prgrms2.java.bitta.media.entity; public enum MediaCategory { - IMAGE, VIDEO, PROFILE + IMAGE, VIDEO } diff --git a/src/main/java/com/prgrms2/java/bitta/media/exception/MediaException.java b/src/main/java/com/prgrms2/java/bitta/media/exception/MediaException.java new file mode 100644 index 0000000..bf54fac --- /dev/null +++ b/src/main/java/com/prgrms2/java/bitta/media/exception/MediaException.java @@ -0,0 +1,18 @@ +package com.prgrms2.java.bitta.media.exception; + +public enum MediaException { + BAD_REQUEST(400, "올바르지 않은 접근 경로입니다."), + NOT_FOUND(404, "해당 파일은 존재하지 않습니다."), + INTERNAL_ERROR(500, "파일 처리에 실패했습니다."), + INVALID_FORMAT(500, "올바르지 않은 파일 포맷입니다."); + + private MediaTaskException mediaTaskException; + + MediaException(int code, String message) { + mediaTaskException = new MediaTaskException(code, message); + } + + public MediaTaskException get() { + return mediaTaskException; + } +} diff --git a/src/main/java/com/prgrms2/java/bitta/media/exception/MediaFileException.java b/src/main/java/com/prgrms2/java/bitta/media/exception/MediaFileException.java deleted file mode 100644 index 95b06ba..0000000 --- a/src/main/java/com/prgrms2/java/bitta/media/exception/MediaFileException.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.prgrms2.java.bitta.media.exception; - -public enum MediaFileException { - NOT_FOUND(404, "해당 파일은 존재하지 않습니다."), - INTERNAL_ERROR(500, "파일 처리에 실패했습니다."), - INVALID_FORMAT(500, "올바르지 않은 파일 포맷입니다."); - - private MediaFileTaskException mediaFileTaskException; - - MediaFileException(int code, String message) { - mediaFileTaskException = new MediaFileTaskException(code, message); - } - - public MediaFileTaskException get() { - return mediaFileTaskException; - } -} diff --git a/src/main/java/com/prgrms2/java/bitta/media/exception/MediaFileTaskException.java b/src/main/java/com/prgrms2/java/bitta/media/exception/MediaTaskException.java similarity index 74% rename from src/main/java/com/prgrms2/java/bitta/media/exception/MediaFileTaskException.java rename to src/main/java/com/prgrms2/java/bitta/media/exception/MediaTaskException.java index 59f6497..9c08adf 100644 --- a/src/main/java/com/prgrms2/java/bitta/media/exception/MediaFileTaskException.java +++ b/src/main/java/com/prgrms2/java/bitta/media/exception/MediaTaskException.java @@ -3,9 +3,9 @@ import lombok.AllArgsConstructor; import lombok.Getter; -@AllArgsConstructor @Getter -public class MediaFileTaskException extends RuntimeException { +@AllArgsConstructor +public class MediaTaskException extends RuntimeException { private int code; private String message; } diff --git a/src/main/java/com/prgrms2/java/bitta/media/repository/MediaRepository.java b/src/main/java/com/prgrms2/java/bitta/media/repository/MediaRepository.java index 0986fbb..6d58345 100644 --- a/src/main/java/com/prgrms2/java/bitta/media/repository/MediaRepository.java +++ b/src/main/java/com/prgrms2/java/bitta/media/repository/MediaRepository.java @@ -7,9 +7,12 @@ import org.springframework.stereotype.Repository; import java.util.List; +import java.util.Optional; @Repository public interface MediaRepository extends JpaRepository { @Query("SELECT m FROM Media m WHERE m.feed.id = :feedId") List findAllByFeedId(@Param("feedId") Long feedId); + + Optional findByFilename(@Param("filename") String filename); } diff --git a/src/main/java/com/prgrms2/java/bitta/media/service/MediaService.java b/src/main/java/com/prgrms2/java/bitta/media/service/MediaService.java index 7d8f08c..723c447 100644 --- a/src/main/java/com/prgrms2/java/bitta/media/service/MediaService.java +++ b/src/main/java/com/prgrms2/java/bitta/media/service/MediaService.java @@ -7,12 +7,22 @@ import java.util.List; public interface MediaService { - void upload(List files, Long feedId); + void uploads(List files, Long feedId); + + void upload(MultipartFile file, Long memberId, Long jobPostId); + + void delete(Media media); void delete(List mediaDtos); + void delete(MediaDto mediaDto); + void delete(Long feedId); + String getMediaUrl(Media media); + + Media getMedia(String mediaUrl); + List convertDTOs(List mediaDTOs); List convertEntities(List medias); diff --git a/src/main/java/com/prgrms2/java/bitta/media/service/MediaServiceImpl.java b/src/main/java/com/prgrms2/java/bitta/media/service/MediaServiceImpl.java index 3a3cbad..9ea8e4f 100644 --- a/src/main/java/com/prgrms2/java/bitta/media/service/MediaServiceImpl.java +++ b/src/main/java/com/prgrms2/java/bitta/media/service/MediaServiceImpl.java @@ -2,14 +2,19 @@ import com.prgrms2.java.bitta.feed.entity.Feed; import com.prgrms2.java.bitta.feed.service.FeedProvider; +import com.prgrms2.java.bitta.jobpost.entity.JobPost; +import com.prgrms2.java.bitta.jobpost.util.JobPostProvider; import com.prgrms2.java.bitta.media.dto.MediaDto; import com.prgrms2.java.bitta.media.entity.MediaCategory; import com.prgrms2.java.bitta.media.entity.Media; -import com.prgrms2.java.bitta.media.exception.MediaFileException; +import com.prgrms2.java.bitta.media.exception.MediaException; import com.prgrms2.java.bitta.media.repository.MediaRepository; +import com.prgrms2.java.bitta.member.entity.Member; +import com.prgrms2.java.bitta.member.service.MemberProvider; import lombok.RequiredArgsConstructor; +import net.coobird.thumbnailator.Thumbnails; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -30,53 +35,92 @@ public class MediaServiceImpl implements MediaService { private final FeedProvider feedProvider; + private final MemberProvider memberProvider; + + private final JobPostProvider jobPostProvider; + @Value("${file.root.path}") private String rootPath; @Override @Transactional - public void upload(List files, Long feedId) { - List categories = checkFileType(files); - List media = new ArrayList<>(); + public void uploads(List multipartFiles, Long feedId) { + List categories = checkFileType(multipartFiles); + List medias = new ArrayList<>(); Feed feed = feedProvider.getById(feedId); - for (int i = 0; i < files.size(); i++) { - MultipartFile file = files.get(i); + for (int i = 0; i < multipartFiles.size(); i++) { + MultipartFile multipartFile = multipartFiles.get(i); String filename = UUID.randomUUID().toString(); - String extension = "." + StringUtils.getFilenameExtension(file.getOriginalFilename()); + String extension = "." + StringUtils.getFilenameExtension(multipartFile.getOriginalFilename()); try { - file.transferTo(Paths.get(rootPath + filename + extension)); + multipartFile.transferTo(Paths.get(rootPath, filename + extension)); } catch (IOException e) { - throw MediaFileException.INTERNAL_ERROR.get(); + throw MediaException.INTERNAL_ERROR.get(); } - media.add(Media.builder() + medias.add(Media.builder() .filename(filename) .extension(extension) - .size(file.getSize()) + .size(multipartFile.getSize()) .type(categories.get(i)) .feed(feed) .build()); } - mediaRepository.saveAll(media); + mediaRepository.saveAll(medias); } @Override - public void delete(List mediaDtos) { - mediaDtos.forEach(dto -> { - String filepath = rootPath + dto.getFilename() + dto.getExtension(); + public void upload(MultipartFile multipartFile, Long memberId, Long jobPostId) { + String filename = UUID.randomUUID().toString(); + String extension = "." + StringUtils.getFilenameExtension(multipartFile.getOriginalFilename()); + String filepath = rootPath + filename + extension; + MediaCategory category = checkFileType(multipartFile); + + File file = new File(filepath); + Member member = null; + JobPost jobPost = null; + + try { + if (memberId != null && jobPostId != null) { + throw MediaException.BAD_REQUEST.get(); + } - File file = new File(filepath); + if (memberId != null) { + member = memberProvider.getById(memberId); + multipartFile.transferTo(file); + } - if (file.exists()) { - file.delete(); - } else { - throw MediaFileException.NOT_FOUND.get(); + if (jobPostId != null) { + jobPost = jobPostProvider.getById(jobPostId); + Thumbnails.of(multipartFile.getInputStream()) + .size(200, 200) + .keepAspectRatio(true) + .toFile(file); } - }); + } catch (IOException e) { + throw MediaException.INTERNAL_ERROR.get(); + } + + Media media = Media.builder() + .filename(filename) + .extension(extension) + .size(multipartFile.getSize()) + .type(category) + .member(member) + .jobPost(jobPost) + .build(); + + mediaRepository.save(media); + } + + @Override + @Transactional(readOnly = true) + public void delete(Media media) { + delete(entityToDto(media)); } @Override @@ -92,27 +136,40 @@ public void delete(Long feedId) { if (file.exists()) { file.delete(); } else { - throw MediaFileException.NOT_FOUND.get(); + throw MediaException.NOT_FOUND.get(); } }); } - private List checkFileType(List files) { - List categories = new ArrayList<>(); + @Override + public void delete(List mediaDTOs) { + mediaDTOs.forEach(this::delete); + } - files.forEach(file -> { - String contentType = file.getContentType(); + @Override + public void delete(MediaDto mediaDto) { + String filepath = rootPath + mediaDto.getFilename() + mediaDto.getExtension(); - if (contentType.startsWith("image/")) { - categories.add(MediaCategory.IMAGE); - } else if (contentType.startsWith("video/")) { - categories.add(MediaCategory.VIDEO); - } else { - throw MediaFileException.INVALID_FORMAT.get(); - } - }); + File file = new File(filepath); - return categories; + if (file.exists()) { + file.delete(); + } else { + throw MediaException.NOT_FOUND.get(); + } + } + + @Override + public String getMediaUrl(Media media) { + return rootPath + media.getFilename() + media.getExtension(); + } + + @Override + public Media getMedia(String mediaUrl) { + String filename = mediaUrl.substring(rootPath.length()) + .substring(0, mediaUrl.indexOf(".")); + + return mediaRepository.findByFilename(filename).orElse(null); } @Override @@ -125,27 +182,62 @@ public List convertEntities(List medias) { return medias.stream().map(this::entityToDto).toList(); } + private List checkFileType(List multipartFiles) { + List categories = new ArrayList<>(); + + multipartFiles.forEach(multipartFile + -> categories.add(checkFileType(multipartFile))); + + return categories; + } + + private MediaCategory checkFileType(MultipartFile multipartFile) { + String contentType = multipartFile.getContentType(); + + if (contentType.matches("image/(jpeg|png|gif|bmp|webp|svg\\+xml)")) { + return MediaCategory.IMAGE; + } + + if (contentType.matches("video/(mp4|webm|ogg|x-msvideo|x-matroska)")) { + return MediaCategory.VIDEO; + } + + throw MediaException.INVALID_FORMAT.get(); + } + private Media dtoToEntity(MediaDto mediaDto) { + Long feedId = mediaDto.getFeedId(); + Long memberId = mediaDto.getMemberId(); + Long jobPostId = mediaDto.getJobPostId(); + return Media.builder() .id(mediaDto.getId()) .filename(mediaDto.getFilename()) .extension(mediaDto.getExtension()) .size(mediaDto.getSize()) .type(mediaDto.getType()) - .feed(feedProvider.getById(mediaDto.getFeedId())) + .feed(feedId != null ? feedProvider.getById(feedId) : null) + .member(memberId != null ? memberProvider.getById(memberId) : null) + .jobPost(jobPostId != null ? jobPostProvider.getById(jobPostId) : null) .createdAt(mediaDto.getCreatedAt()) .updatedAt(mediaDto.getUpdatedAt()) .build(); } private MediaDto entityToDto(Media media) { + Feed feed = media.getFeed(); + Member member = media.getMember(); + JobPost jobPost = media.getJobPost(); + return MediaDto.builder() .id(media.getId()) .filename(media.getFilename()) .extension(media.getExtension()) .size(media.getSize()) .type(media.getType()) - .feedId(media.getFeed().getId()) + .feedId(feed != null ? feed.getId() : null) + .memberId(member != null ? member.getId() : null) + .jobPostId(jobPost != null ? jobPost.getId() : null) .createdAt(media.getCreatedAt()) .updatedAt(media.getUpdatedAt()) .build(); diff --git a/src/main/java/com/prgrms2/java/bitta/member/service/ProfileImageService.java b/src/main/java/com/prgrms2/java/bitta/member/service/ProfileImageService.java deleted file mode 100644 index e59dc37..0000000 --- a/src/main/java/com/prgrms2/java/bitta/member/service/ProfileImageService.java +++ /dev/null @@ -1,82 +0,0 @@ -package com.prgrms2.java.bitta.member.service; - -import net.coobird.thumbnailator.Thumbnails; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; -import org.springframework.web.multipart.MultipartFile; - -import java.io.File; -import java.io.IOException; -import java.io.OutputStream; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; - -@Service -public class ProfileImageService { - - @Value("${file.root.path}") - private String fileRootPath; - - private static final String PROFILE_DIR = "/uploads/profile_images/"; - - public String getDefaultProfileImgPath() { - return fileRootPath + PROFILE_DIR + "default_avatar.png"; - } - - public String saveProfileImage(MultipartFile profileImage) throws IOException { - String directory = fileRootPath + PROFILE_DIR; - Path directoryPath = Paths.get(directory); - - if (!Files.exists(directoryPath)) { - Files.createDirectories(directoryPath); - } - - String originalFilename = profileImage.getOriginalFilename(); - Path filePath = directoryPath.resolve(originalFilename); - profileImage.transferTo(filePath.toFile()); - - return filePath.toString(); - } - - public void deleteProfileImage(String profileImgPath) { - if (profileImgPath != null && !profileImgPath.isBlank()) { - File profileImgFile = new File(profileImgPath); - File thumbnailFile = getThumbnailFile(profileImgPath); - - if (profileImgFile.exists() && profileImgFile.isFile()) { - profileImgFile.delete(); - } - - if (thumbnailFile.exists() && thumbnailFile.isFile()) { - thumbnailFile.delete(); - } - } - } - - public File getThumbnailFile(String profileImg) { - String thumbnailImgPath = profileImg.replace("profile_images", "profile_images/thumbnail"); - String thumbnailFileName = "thumb_" + Paths.get(profileImg).getFileName().toString(); - return new File(thumbnailImgPath.replace(Paths.get(profileImg).getFileName().toString(), thumbnailFileName)); - } - - public String createThumbnail(String imagePath) throws IOException { - String thumbnailDirectory = fileRootPath + PROFILE_DIR + "thumbnail/"; - Path thumbnailPath = Paths.get(thumbnailDirectory); - - if (!Files.exists(thumbnailPath)) { - Files.createDirectories(thumbnailPath); - } - - String originalFileName = Paths.get(imagePath).getFileName().toString(); - String thumbnailFileName = "thumb_" + originalFileName; - Path thumbnailFilePath = thumbnailPath.resolve(thumbnailFileName); - - Thumbnails.of(new File(imagePath)) - .size(200, 200) - .keepAspectRatio(true) - .toFile(thumbnailFilePath.toFile()); - - return thumbnailFilePath.toString(); - } -}