-
Notifications
You must be signed in to change notification settings - Fork 0
feat: 썸네일 이미지 추가 #40
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: 썸네일 이미지 추가 #40
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -45,20 +45,21 @@ public static InnerPageInfo from(Page<Photo> page) { | |
| public record InnerPhotoResponse( | ||
| Long id, | ||
| String filePath, | ||
| String thumbnailPath, | ||
| LocalDateTime capturedDt, | ||
| Double lat, | ||
| Double lng, | ||
| Long userId | ||
| ) { | ||
|
|
||
| public static InnerPhotoResponse from(Photo photo) { | ||
| Optional<Point> location = Optional.ofNullable(photo.getLocation()); | ||
| return new InnerPhotoResponse( | ||
| photo.getId(), | ||
| photo.getFilePath(), | ||
| photo.getThumbnailPath(), | ||
| photo.getCapturedDt(), | ||
| location.map(Point::getY).orElse(null), | ||
| location.map(Point::getX).orElse(null), | ||
| photo.getLongitude(), | ||
| photo.getLatitude(), | ||
|
Comment on lines
-60
to
+62
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. NPE 방지 로직을 |
||
| photo.getUser().getId()); | ||
| } | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,7 @@ | ||
| package kr.kro.photoliner.domain.photo.infra; | ||
|
|
||
| import static kr.kro.photoliner.global.code.ApiResponseCode.DIRECTORY_CREATION_FAILED; | ||
|
|
||
| import java.io.IOException; | ||
| import java.io.InputStream; | ||
| import java.nio.file.Files; | ||
|
|
@@ -10,31 +12,79 @@ | |
| import java.util.UUID; | ||
| import kr.kro.photoliner.global.code.ApiResponseCode; | ||
| import kr.kro.photoliner.global.exception.CustomException; | ||
| import net.coobird.thumbnailator.Thumbnails; | ||
| import org.springframework.beans.factory.annotation.Value; | ||
| import org.springframework.stereotype.Component; | ||
| import org.springframework.web.multipart.MultipartFile; | ||
|
|
||
| @Component | ||
| public class FileStorage { | ||
|
|
||
| private final Path uploadLocation; | ||
| private final Path originalLocation; | ||
| private final Path thumbnailLocation; | ||
|
|
||
| private static final String ORIGINAL_DIR = "original"; | ||
| private static final String THUMBNAIL_DIR = "thumbnail"; | ||
| private static final String BASE_IMAGES_DIR = "/images"; | ||
|
Comment on lines
+26
to
+28
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 사진 저장 경로 하위로 original, thumbnail 폴더를 생성합니다.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
실제 저장 경로와 다릅니다. |
||
|
|
||
| private static final int THUMBNAIL_WIDTH = 300; | ||
| private static final int THUMBNAIL_HEIGHT = 300; | ||
|
|
||
| public FileStorage(@Value("${photo.upload.base-dir}") String baseDir) { | ||
| this.uploadLocation = Paths.get(baseDir) | ||
| .toAbsolutePath() | ||
| .normalize(); | ||
| Path rootLocation = Paths.get(baseDir).toAbsolutePath().normalize(); | ||
| this.originalLocation = rootLocation.resolve(ORIGINAL_DIR); | ||
| this.thumbnailLocation = rootLocation.resolve(THUMBNAIL_DIR); | ||
|
|
||
| try { | ||
| Files.createDirectories(this.uploadLocation); | ||
| Files.createDirectories(this.originalLocation); | ||
| Files.createDirectories(this.thumbnailLocation); | ||
| } catch (IOException e) { | ||
| throw new IllegalArgumentException("Failed to create upload directory", e); | ||
| throw CustomException.of(DIRECTORY_CREATION_FAILED); | ||
| } | ||
| } | ||
|
|
||
| public String store(MultipartFile file) { | ||
| validateFile(file); | ||
| String fileName = generateFileName(file); | ||
| saveFile(file, fileName); | ||
| return fileName; | ||
| saveFile(file, this.originalLocation, fileName); | ||
| return BASE_IMAGES_DIR + "/" + ORIGINAL_DIR + "/" + fileName; | ||
| } | ||
|
|
||
| public String storeThumbnail(String originalRelativePath) { | ||
| String fileName = Paths.get(originalRelativePath).getFileName().toString(); | ||
| Path sourceLocation = this.originalLocation.resolve(fileName); | ||
| Path targetLocation = this.thumbnailLocation.resolve(fileName); | ||
|
|
||
| try { | ||
| Thumbnails.of(sourceLocation.toFile()) | ||
| .size(THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT) | ||
| .toFile(targetLocation.toFile()); | ||
| return BASE_IMAGES_DIR + "/" + THUMBNAIL_DIR + "/" + fileName; | ||
| } catch (IOException e) { | ||
| throw CustomException.of(ApiResponseCode.FILE_CREATION_FAILED); | ||
| } | ||
| } | ||
|
|
||
| public void deleteOriginalImage(String originalPath) { | ||
| String fileName = Paths.get(originalPath).getFileName().toString(); | ||
| Path sourceLocation = this.originalLocation.resolve(fileName); | ||
|
|
||
| try { | ||
| Files.delete(sourceLocation); | ||
| } catch (IOException e) { | ||
| throw CustomException.of(ApiResponseCode.FILE_DELETE_FAILED); | ||
| } | ||
| } | ||
|
|
||
| public void deleteThumbnailImage(String thumbnailPath) { | ||
| String fileName = Paths.get(thumbnailPath).getFileName().toString(); | ||
| Path sourceLocation = this.thumbnailLocation.resolve(fileName); | ||
|
|
||
| try { | ||
| Files.delete(sourceLocation); | ||
| } catch (IOException e) { | ||
| throw CustomException.of(ApiResponseCode.FILE_DELETE_FAILED); | ||
| } | ||
|
Comment on lines
+68
to
+87
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| } | ||
|
|
||
| private void validateFile(MultipartFile file) { | ||
|
|
@@ -53,9 +103,9 @@ private String generateFileName(MultipartFile file) { | |
| return UUID.randomUUID() + "." + extension; | ||
| } | ||
|
|
||
| private void saveFile(MultipartFile file, String fileName) { | ||
| private void saveFile(MultipartFile file, Path directory, String fileName) { | ||
| try { | ||
| Path targetLocation = uploadLocation.resolve(fileName); | ||
| Path targetLocation = directory.resolve(fileName); | ||
| try (InputStream inputStream = file.getInputStream()) { | ||
| Files.copy(inputStream, targetLocation, StandardCopyOption.REPLACE_EXISTING); | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,6 +12,7 @@ | |
| import jakarta.validation.constraints.NotNull; | ||
| import java.time.LocalDate; | ||
| import java.time.LocalDateTime; | ||
| import java.util.Objects; | ||
| import java.util.Optional; | ||
| import kr.kro.photoliner.common.model.BaseEntity; | ||
| import kr.kro.photoliner.domain.user.model.User; | ||
|
|
@@ -42,6 +43,10 @@ public class Photo extends BaseEntity { | |
| @Column(name = "file_path", nullable = false) | ||
| private String filePath; | ||
|
|
||
| @NotNull | ||
| @Column(name = "thumbnail_path", nullable = false) | ||
| private String thumbnailPath; | ||
|
|
||
| @Column(name = "captured_dt") | ||
| private LocalDateTime capturedDt; | ||
|
|
||
|
|
@@ -66,4 +71,18 @@ public void updateCapturedDate(LocalDateTime capturedDt) { | |
| public void updateLocation(Point location) { | ||
| this.location = location; | ||
| } | ||
|
|
||
| public Double getLatitude() { | ||
| if (Objects.isNull(location)) { | ||
| return null; | ||
| } | ||
| return location.getX(); | ||
| } | ||
|
|
||
| public Double getLongitude() { | ||
| if (Objects.isNull(location)) { | ||
| return null; | ||
| } | ||
| return location.getY(); | ||
| } | ||
|
Comment on lines
+74
to
+87
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 위치가 존재하지 않을 경우 npe 가능성이 충분해서 캡슐화했습니다. |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,11 +1,13 @@ | ||
| package kr.kro.photoliner.domain.photo.service; | ||
|
|
||
| import java.util.List; | ||
| import kr.kro.photoliner.domain.photo.dto.DeletePhotosRequest; | ||
| import kr.kro.photoliner.domain.photo.dto.request.MapMarkersRequest; | ||
| import kr.kro.photoliner.domain.photo.dto.request.PhotoCapturedDateUpdateRequest; | ||
| import kr.kro.photoliner.domain.photo.dto.request.PhotoLocationUpdateRequest; | ||
| import kr.kro.photoliner.domain.photo.dto.response.MapMarkersResponse; | ||
| import kr.kro.photoliner.domain.photo.dto.response.PhotosResponse; | ||
| import kr.kro.photoliner.domain.photo.infra.FileStorage; | ||
| import kr.kro.photoliner.domain.photo.model.AlbumPhotos; | ||
| import kr.kro.photoliner.domain.photo.model.Photo; | ||
| import kr.kro.photoliner.domain.photo.model.Photos; | ||
|
|
@@ -28,6 +30,7 @@ public class PhotoService { | |
| private final AlbumPhotoRepository albumPhotoRepository; | ||
| private final PhotoRepository photoRepository; | ||
| private final GeometryFactory geometryFactory; | ||
| private final FileStorage fileStorage; | ||
|
|
||
| @Transactional(readOnly = true) | ||
| public PhotosResponse getPhotos(Long userId, Pageable pageable) { | ||
|
|
@@ -72,6 +75,9 @@ public void updatePhotoLocation(Long photoId, PhotoLocationUpdateRequest request | |
|
|
||
| @Transactional | ||
| public void deletePhotos(DeletePhotosRequest request) { | ||
| List<Photo> photos = photoRepository.findAllById(request.ids()); | ||
| photos.forEach(photo -> fileStorage.deleteOriginalImage(photo.getFilePath())); | ||
| photos.forEach(photo -> fileStorage.deleteThumbnailImage(photo.getFilePath())); | ||
| photoRepository.deleteAllByIdInBatch(request.ids()); | ||
| } | ||
|
Comment on lines
76
to
82
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 사진 삭제 시 실제 이미지도 함께 삭제합니다. |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -59,7 +59,11 @@ public enum ApiResponseCode { | |
| */ | ||
| INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "서버에서 오류가 발생했습니다."), | ||
| FILE_PROCESSING_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "파일 처리 중 오류가 발생했습니다."), | ||
| FILE_STORE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "파일 저장 중 오류가 발생했습니다."); | ||
| FILE_STORE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "파일 저장 중 오류가 발생했습니다."), | ||
| FILE_CREATION_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "파일 생성 중 오류가 발생했습니다."), | ||
| DIRECTORY_CREATION_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "폴더 생성 중 오류가 발생했습니다."), | ||
| FILE_DELETE_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "파일 삭제 중 오류가 발생했습니다.") | ||
| ; | ||
|
Comment on lines
+62
to
+66
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A디테일한 오류 코드 정의는 정말 명확하고 좋네요:) |
||
|
|
||
| private final HttpStatus httpStatus; | ||
| private final String message; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| alter table photos | ||
| add column thumbnail_path text not null | ||
|
Comment on lines
+1
to
+2
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
사진조회 응답 필드로 썸네일 경로를 추가했습니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A
현재 지도 사진 마커 조회 API 에서
filePath(원본 이미지 파일),thumbnailPath(리사이징된 이미지 파일) 을 모두 반환하고 있네요.클라이언트에서 지도 사진 마커 조회를 요청했을 때는 썸네일 이미지만 반환해주고,
클라이언트가 썸네일 이미지를 클릭하면 그 때 원본 이미지를 반환해주는 것은 어떻게 생각하시나요?
어차피 이미지 파일 URL 만 보내주는 것이라 큰 상관은 없을 것이라 생각되지만 고민이 되는 부분이네요:)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
클라이언트가 썸네일 이미지를 클릭하고 원본 이미지를 반환 받기 위해 사진 마커 조회 API에서 썸네일 이미지 url을 같이 반환하는 것이 적절하다고 생각했습니다.