Skip to content

Commit 06a9edd

Browse files
authored
feat: 썸네일 이미지 추가 (#40)
* feat: 썸네일 생성 의존성 추가 * feat: 썸네일 경로 속성 추가 * feat: 썸네일 업로드 기능 추가 * feat: 썸네일 경로 응답 추가 * fix: NPE 수정 * feat: 사진 삭제 시 이미지도 함께 삭제
1 parent 8803b60 commit 06a9edd

File tree

9 files changed

+119
-27
lines changed

9 files changed

+119
-27
lines changed

build.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ dependencies {
5656

5757
// 액추에이터 추가
5858
implementation 'org.springframework.boot:spring-boot-starter-actuator'
59+
60+
// 썸네일 라이브러리
61+
implementation 'net.coobird:thumbnailator:0.4.20'
5962
}
6063

6164
tasks.named('test') {

src/main/java/kr/kro/photoliner/domain/photo/dto/response/MapMarkersResponse.java

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,17 +34,19 @@ public record InnerPhotoMarker(
3434
Long id,
3535
LocalDateTime capturedDt,
3636
String filePath,
37-
double lat,
38-
double lng
37+
String thumbnailPath,
38+
Double lat,
39+
Double lng
3940
) {
4041

4142
public static InnerPhotoMarker from(Photo photo) {
4243
return new InnerPhotoMarker(
4344
photo.getId(),
4445
photo.getCapturedDt(),
4546
photo.getFilePath(),
46-
photo.getLocation().getY(),
47-
photo.getLocation().getX()
47+
photo.getThumbnailPath(),
48+
photo.getLongitude(),
49+
photo.getLatitude()
4850
);
4951
}
5052
}
@@ -67,16 +69,20 @@ public static InnerPoiMarkers from(List<Photo> photos) {
6769
public record InnerPoiMarker(
6870
Long id,
6971
LocalDateTime capturedDt,
70-
double lat,
71-
double lng
72+
String filePath,
73+
String thumbnailPath,
74+
Double lat,
75+
Double lng
7276
) {
7377

7478
public static InnerPoiMarker from(Photo photo) {
7579
return new InnerPoiMarker(
7680
photo.getId(),
7781
photo.getCapturedDt(),
78-
photo.getLocation().getY(),
79-
photo.getLocation().getX()
82+
photo.getFilePath(),
83+
photo.getThumbnailPath(),
84+
photo.getLongitude(),
85+
photo.getLatitude()
8086
);
8187
}
8288
}

src/main/java/kr/kro/photoliner/domain/photo/dto/response/PhotosResponse.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,20 +45,21 @@ public static InnerPageInfo from(Page<Photo> page) {
4545
public record InnerPhotoResponse(
4646
Long id,
4747
String filePath,
48+
String thumbnailPath,
4849
LocalDateTime capturedDt,
4950
Double lat,
5051
Double lng,
5152
Long userId
5253
) {
5354

5455
public static InnerPhotoResponse from(Photo photo) {
55-
Optional<Point> location = Optional.ofNullable(photo.getLocation());
5656
return new InnerPhotoResponse(
5757
photo.getId(),
5858
photo.getFilePath(),
59+
photo.getThumbnailPath(),
5960
photo.getCapturedDt(),
60-
location.map(Point::getY).orElse(null),
61-
location.map(Point::getX).orElse(null),
61+
photo.getLongitude(),
62+
photo.getLatitude(),
6263
photo.getUser().getId());
6364
}
6465
}

src/main/java/kr/kro/photoliner/domain/photo/infra/FileStorage.java

Lines changed: 60 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package kr.kro.photoliner.domain.photo.infra;
22

3+
import static kr.kro.photoliner.global.code.ApiResponseCode.DIRECTORY_CREATION_FAILED;
4+
35
import java.io.IOException;
46
import java.io.InputStream;
57
import java.nio.file.Files;
@@ -10,31 +12,79 @@
1012
import java.util.UUID;
1113
import kr.kro.photoliner.global.code.ApiResponseCode;
1214
import kr.kro.photoliner.global.exception.CustomException;
15+
import net.coobird.thumbnailator.Thumbnails;
1316
import org.springframework.beans.factory.annotation.Value;
1417
import org.springframework.stereotype.Component;
1518
import org.springframework.web.multipart.MultipartFile;
1619

1720
@Component
1821
public class FileStorage {
1922

20-
private final Path uploadLocation;
23+
private final Path originalLocation;
24+
private final Path thumbnailLocation;
25+
26+
private static final String ORIGINAL_DIR = "original";
27+
private static final String THUMBNAIL_DIR = "thumbnail";
28+
private static final String BASE_IMAGES_DIR = "/images";
29+
30+
private static final int THUMBNAIL_WIDTH = 300;
31+
private static final int THUMBNAIL_HEIGHT = 300;
2132

2233
public FileStorage(@Value("${photo.upload.base-dir}") String baseDir) {
23-
this.uploadLocation = Paths.get(baseDir)
24-
.toAbsolutePath()
25-
.normalize();
34+
Path rootLocation = Paths.get(baseDir).toAbsolutePath().normalize();
35+
this.originalLocation = rootLocation.resolve(ORIGINAL_DIR);
36+
this.thumbnailLocation = rootLocation.resolve(THUMBNAIL_DIR);
37+
2638
try {
27-
Files.createDirectories(this.uploadLocation);
39+
Files.createDirectories(this.originalLocation);
40+
Files.createDirectories(this.thumbnailLocation);
2841
} catch (IOException e) {
29-
throw new IllegalArgumentException("Failed to create upload directory", e);
42+
throw CustomException.of(DIRECTORY_CREATION_FAILED);
3043
}
3144
}
3245

3346
public String store(MultipartFile file) {
3447
validateFile(file);
3548
String fileName = generateFileName(file);
36-
saveFile(file, fileName);
37-
return fileName;
49+
saveFile(file, this.originalLocation, fileName);
50+
return BASE_IMAGES_DIR + "/" + ORIGINAL_DIR + "/" + fileName;
51+
}
52+
53+
public String storeThumbnail(String originalRelativePath) {
54+
String fileName = Paths.get(originalRelativePath).getFileName().toString();
55+
Path sourceLocation = this.originalLocation.resolve(fileName);
56+
Path targetLocation = this.thumbnailLocation.resolve(fileName);
57+
58+
try {
59+
Thumbnails.of(sourceLocation.toFile())
60+
.size(THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT)
61+
.toFile(targetLocation.toFile());
62+
return BASE_IMAGES_DIR + "/" + THUMBNAIL_DIR + "/" + fileName;
63+
} catch (IOException e) {
64+
throw CustomException.of(ApiResponseCode.FILE_CREATION_FAILED);
65+
}
66+
}
67+
68+
public void deleteOriginalImage(String originalPath) {
69+
String fileName = Paths.get(originalPath).getFileName().toString();
70+
Path sourceLocation = this.originalLocation.resolve(fileName);
71+
72+
try {
73+
Files.delete(sourceLocation);
74+
} catch (IOException e) {
75+
throw CustomException.of(ApiResponseCode.FILE_DELETE_FAILED);
76+
}
77+
}
78+
79+
public void deleteThumbnailImage(String thumbnailPath) {
80+
String fileName = Paths.get(thumbnailPath).getFileName().toString();
81+
Path sourceLocation = this.thumbnailLocation.resolve(fileName);
82+
83+
try {
84+
Files.delete(sourceLocation);
85+
} catch (IOException e) {
86+
throw CustomException.of(ApiResponseCode.FILE_DELETE_FAILED);
87+
}
3888
}
3989

4090
private void validateFile(MultipartFile file) {
@@ -53,9 +103,9 @@ private String generateFileName(MultipartFile file) {
53103
return UUID.randomUUID() + "." + extension;
54104
}
55105

56-
private void saveFile(MultipartFile file, String fileName) {
106+
private void saveFile(MultipartFile file, Path directory, String fileName) {
57107
try {
58-
Path targetLocation = uploadLocation.resolve(fileName);
108+
Path targetLocation = directory.resolve(fileName);
59109
try (InputStream inputStream = file.getInputStream()) {
60110
Files.copy(inputStream, targetLocation, StandardCopyOption.REPLACE_EXISTING);
61111
}

src/main/java/kr/kro/photoliner/domain/photo/model/Photo.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import jakarta.validation.constraints.NotNull;
1313
import java.time.LocalDate;
1414
import java.time.LocalDateTime;
15+
import java.util.Objects;
1516
import java.util.Optional;
1617
import kr.kro.photoliner.common.model.BaseEntity;
1718
import kr.kro.photoliner.domain.user.model.User;
@@ -42,6 +43,10 @@ public class Photo extends BaseEntity {
4243
@Column(name = "file_path", nullable = false)
4344
private String filePath;
4445

46+
@NotNull
47+
@Column(name = "thumbnail_path", nullable = false)
48+
private String thumbnailPath;
49+
4550
@Column(name = "captured_dt")
4651
private LocalDateTime capturedDt;
4752

@@ -66,4 +71,18 @@ public void updateCapturedDate(LocalDateTime capturedDt) {
6671
public void updateLocation(Point location) {
6772
this.location = location;
6873
}
74+
75+
public Double getLatitude() {
76+
if (Objects.isNull(location)) {
77+
return null;
78+
}
79+
return location.getX();
80+
}
81+
82+
public Double getLongitude() {
83+
if (Objects.isNull(location)) {
84+
return null;
85+
}
86+
return location.getY();
87+
}
6988
}

src/main/java/kr/kro/photoliner/domain/photo/service/PhotoService.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
package kr.kro.photoliner.domain.photo.service;
22

3+
import java.util.List;
34
import kr.kro.photoliner.domain.photo.dto.DeletePhotosRequest;
45
import kr.kro.photoliner.domain.photo.dto.request.MapMarkersRequest;
56
import kr.kro.photoliner.domain.photo.dto.request.PhotoCapturedDateUpdateRequest;
67
import kr.kro.photoliner.domain.photo.dto.request.PhotoLocationUpdateRequest;
78
import kr.kro.photoliner.domain.photo.dto.response.MapMarkersResponse;
89
import kr.kro.photoliner.domain.photo.dto.response.PhotosResponse;
10+
import kr.kro.photoliner.domain.photo.infra.FileStorage;
911
import kr.kro.photoliner.domain.photo.model.AlbumPhotos;
1012
import kr.kro.photoliner.domain.photo.model.Photo;
1113
import kr.kro.photoliner.domain.photo.model.Photos;
@@ -28,6 +30,7 @@ public class PhotoService {
2830
private final AlbumPhotoRepository albumPhotoRepository;
2931
private final PhotoRepository photoRepository;
3032
private final GeometryFactory geometryFactory;
33+
private final FileStorage fileStorage;
3134

3235
@Transactional(readOnly = true)
3336
public PhotosResponse getPhotos(Long userId, Pageable pageable) {
@@ -72,6 +75,9 @@ public void updatePhotoLocation(Long photoId, PhotoLocationUpdateRequest request
7275

7376
@Transactional
7477
public void deletePhotos(DeletePhotosRequest request) {
78+
List<Photo> photos = photoRepository.findAllById(request.ids());
79+
photos.forEach(photo -> fileStorage.deleteOriginalImage(photo.getFilePath()));
80+
photos.forEach(photo -> fileStorage.deleteThumbnailImage(photo.getFilePath()));
7581
photoRepository.deleteAllByIdInBatch(request.ids());
7682
}
7783
}

src/main/java/kr/kro/photoliner/domain/photo/service/PhotoUploadService.java

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,26 +28,27 @@ public class PhotoUploadService {
2828

2929
@Transactional
3030
public PhotoUploadResponse uploadPhotos(Long userId, List<MultipartFile> files) {
31+
User user = userRepository.findUserById(userId).orElseThrow(
32+
() -> CustomException.of(ApiResponseCode.NOT_FOUND_USER, "user id: " + userId));
3133
List<InnerUploadedPhotoInfo> uploadedPhotos = files.stream()
3234
.map(file -> {
3335
ExifData exifData = exifExtractor.extract(file);
3436
String filePath = fileStorage.store(file);
37+
String thumbnailPath = fileStorage.storeThumbnail(filePath);
3538
String fileName = file.getOriginalFilename();
36-
User user = userRepository.findUserById(userId)
37-
.orElseThrow(
38-
() -> CustomException.of(ApiResponseCode.NOT_FOUND_USER, "user id: " + userId));
39-
Photo photo = createPhoto(user, exifData, filePath, fileName);
39+
Photo photo = createPhoto(user, exifData, fileName, filePath, thumbnailPath);
4040
Photo savedPhoto = photoRepository.save(photo);
4141
return InnerUploadedPhotoInfo.from(savedPhoto);
4242
})
4343
.toList();
4444
return PhotoUploadResponse.from(uploadedPhotos);
4545
}
4646

47-
private Photo createPhoto(User user, ExifData exifData, String fileName, String filePath) {
47+
private Photo createPhoto(User user, ExifData exifData, String fileName, String filePath, String thumbnailPath) {
4848
return Photo.builder()
4949
.fileName(fileName)
5050
.filePath(filePath)
51+
.thumbnailPath(thumbnailPath)
5152
.capturedDt(exifData.capturedDt())
5253
.location(exifData.location())
5354
.user(user)

src/main/java/kr/kro/photoliner/global/code/ApiResponseCode.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,11 @@ public enum ApiResponseCode {
5959
*/
6060
INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "서버에서 오류가 발생했습니다."),
6161
FILE_PROCESSING_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "파일 처리 중 오류가 발생했습니다."),
62-
FILE_STORE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "파일 저장 중 오류가 발생했습니다.");
62+
FILE_STORE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "파일 저장 중 오류가 발생했습니다."),
63+
FILE_CREATION_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "파일 생성 중 오류가 발생했습니다."),
64+
DIRECTORY_CREATION_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "폴더 생성 중 오류가 발생했습니다."),
65+
FILE_DELETE_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "파일 삭제 중 오류가 발생했습니다.")
66+
;
6367

6468
private final HttpStatus httpStatus;
6569
private final String message;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
alter table photos
2+
add column thumbnail_path text not null

0 commit comments

Comments
 (0)