From ee708d1bfdda37a96a84cb43fd7e694a84f1110c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=9D=B8=ED=99=94?= Date: Sat, 22 Nov 2025 21:21:26 +0900 Subject: [PATCH 01/11] =?UTF-8?q?feat:=20=EC=82=AC=EC=A7=84=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=EB=82=A0=EC=A7=9C=20=EA=B8=B0=EC=A4=80=20=EC=A0=95?= =?UTF-8?q?=EB=A0=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kro/photoliner/domain/photo/controller/PhotoController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/kr/kro/photoliner/domain/photo/controller/PhotoController.java b/src/main/java/kr/kro/photoliner/domain/photo/controller/PhotoController.java index 34d0cb0..53abe37 100644 --- a/src/main/java/kr/kro/photoliner/domain/photo/controller/PhotoController.java +++ b/src/main/java/kr/kro/photoliner/domain/photo/controller/PhotoController.java @@ -41,7 +41,7 @@ public class PhotoController { @GetMapping public ResponseEntity getPhotos( @RequestParam Long userId, - @PageableDefault(sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable + @PageableDefault(sort = "capturedDt", direction = Sort.Direction.DESC) Pageable pageable ) { return ResponseEntity.ok(photoService.getPhotosByIds(userId, pageable)); } From 1fd2104be1e04fc3dea8005e4463a7252d269cac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=9D=B8=ED=99=94?= Date: Sun, 23 Nov 2025 02:56:23 +0900 Subject: [PATCH 02/11] =?UTF-8?q?feat:=20presigned-url=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=20API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 3 + .../dto/request/PresignedUrlRequest.java | 12 ++++ .../dto/response/PresignedUrlResponse.java | 10 +++ .../photo/infra/S3PresignedUrlGenerator.java | 71 +++++++++++++++++++ .../photoliner/global/config/S3Config.java | 32 +++++++++ 5 files changed, 128 insertions(+) create mode 100644 src/main/java/kr/kro/photoliner/domain/photo/dto/request/PresignedUrlRequest.java create mode 100644 src/main/java/kr/kro/photoliner/domain/photo/dto/response/PresignedUrlResponse.java create mode 100644 src/main/java/kr/kro/photoliner/domain/photo/infra/S3PresignedUrlGenerator.java create mode 100644 src/main/java/kr/kro/photoliner/global/config/S3Config.java diff --git a/build.gradle b/build.gradle index 7d21ce8..cac3648 100644 --- a/build.gradle +++ b/build.gradle @@ -21,6 +21,7 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-test' implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.flywaydb:flyway-core' @@ -59,6 +60,8 @@ dependencies { // 썸네일 라이브러리 implementation 'net.coobird:thumbnailator:0.4.20' + + implementation 'software.amazon.awssdk:s3' } tasks.named('test') { diff --git a/src/main/java/kr/kro/photoliner/domain/photo/dto/request/PresignedUrlRequest.java b/src/main/java/kr/kro/photoliner/domain/photo/dto/request/PresignedUrlRequest.java new file mode 100644 index 0000000..036e695 --- /dev/null +++ b/src/main/java/kr/kro/photoliner/domain/photo/dto/request/PresignedUrlRequest.java @@ -0,0 +1,12 @@ +package kr.kro.photoliner.domain.photo.dto.request; + +import jakarta.validation.constraints.NotBlank; + +public record PresignedUrlRequest( + @NotBlank(message = "파일명은 필수입니다") + String originalFileName, + + @NotBlank(message = "Content-Type은 필수입니다") + String contentType +) { +} \ No newline at end of file diff --git a/src/main/java/kr/kro/photoliner/domain/photo/dto/response/PresignedUrlResponse.java b/src/main/java/kr/kro/photoliner/domain/photo/dto/response/PresignedUrlResponse.java new file mode 100644 index 0000000..b923b0d --- /dev/null +++ b/src/main/java/kr/kro/photoliner/domain/photo/dto/response/PresignedUrlResponse.java @@ -0,0 +1,10 @@ +package kr.kro.photoliner.domain.photo.dto.response; + +public record PresignedUrlResponse( + String presignedUrl, + String uploadFileUrl +) { + public static PresignedUrlResponse of(String presignedUrl, String uploadFileUrl) { + return new PresignedUrlResponse(presignedUrl, uploadFileUrl); + } +} \ No newline at end of file diff --git a/src/main/java/kr/kro/photoliner/domain/photo/infra/S3PresignedUrlGenerator.java b/src/main/java/kr/kro/photoliner/domain/photo/infra/S3PresignedUrlGenerator.java new file mode 100644 index 0000000..22ce95a --- /dev/null +++ b/src/main/java/kr/kro/photoliner/domain/photo/infra/S3PresignedUrlGenerator.java @@ -0,0 +1,71 @@ +package kr.kro.photoliner.domain.photo.infra; + +import java.time.Duration; +import java.util.List; +import java.util.UUID; +import kr.kro.photoliner.domain.photo.dto.request.PresignedUrlRequest; +import kr.kro.photoliner.domain.photo.dto.response.PresignedUrlResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; +import software.amazon.awssdk.services.s3.presigner.S3Presigner; +import software.amazon.awssdk.services.s3.presigner.model.PresignedPutObjectRequest; +import software.amazon.awssdk.services.s3.presigner.model.PutObjectPresignRequest; + +@Service +@RequiredArgsConstructor +public class S3PresignedUrlGenerator { + + private final S3Presigner s3Presigner; + + @Value("${cloud.aws.s3.bucket}") + private String bucketName; + + @Value("${cloud.aws.cdn.base-url}") + private String cdnBaseUrl; + + private static final Duration PRESIGNED_URL_EXPIRATION = Duration.ofMinutes(10); + + public List generatePresignedUrls(List requests) { + return requests.stream() + .map(this::generatePresignedUrl) + .toList(); + } + + private PresignedUrlResponse generatePresignedUrl(PresignedUrlRequest request) { + String objectKey = generateObjectKey(request.originalFileName()); + + PutObjectRequest putObjectRequest = PutObjectRequest.builder() + .bucket(bucketName) + .key(objectKey) + .contentType(request.contentType()) + .build(); + + PutObjectPresignRequest presignRequest = PutObjectPresignRequest.builder() + .signatureDuration(PRESIGNED_URL_EXPIRATION) + .putObjectRequest(putObjectRequest) + .build(); + + PresignedPutObjectRequest presignedRequest = s3Presigner.presignPutObject(presignRequest); + + String presignedUrl = presignedRequest.url().toString(); + String uploadFileUrl = cdnBaseUrl + "/" + objectKey; + + return PresignedUrlResponse.of(presignedUrl, uploadFileUrl); + } + + private String generateObjectKey(String originalFileName) { + String extension = extractExtension(originalFileName); + String uuid = UUID.randomUUID().toString(); + return "images/original/" + uuid + extension; + } + + private String extractExtension(String fileName) { + int lastDotIndex = fileName.lastIndexOf("."); + if (lastDotIndex == -1) { + return ""; + } + return fileName.substring(lastDotIndex); + } +} \ No newline at end of file diff --git a/src/main/java/kr/kro/photoliner/global/config/S3Config.java b/src/main/java/kr/kro/photoliner/global/config/S3Config.java new file mode 100644 index 0000000..22d5c46 --- /dev/null +++ b/src/main/java/kr/kro/photoliner/global/config/S3Config.java @@ -0,0 +1,32 @@ +package kr.kro.photoliner.global.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.presigner.S3Presigner; + +@Configuration +public class S3Config { + + @Value("${cloud.aws.region.static:ap-northeast-2}") + private String region; + + @Bean + public S3Client s3Client() { + return S3Client.builder() + .region(Region.of(region)) + .credentialsProvider(DefaultCredentialsProvider.create()) + .build(); + } + + @Bean + public S3Presigner s3Presigner() { + return S3Presigner.builder() + .region(Region.of(region)) + .credentialsProvider(DefaultCredentialsProvider.create()) + .build(); + } +} \ No newline at end of file From 6262c0be561526e60899548e9a50a220199509f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=9D=B8=ED=99=94?= Date: Sun, 23 Nov 2025 03:05:33 +0900 Subject: [PATCH 03/11] =?UTF-8?q?feat:=20=EC=82=AC=EC=A7=84=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20API=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/photo/controller/PhotoController.java | 16 +++++++++++++--- .../dto/{ => request}/DeletePhotosRequest.java | 2 +- ...3PresignedUrlGenerator.java => S3Client.java} | 13 ++++++++++++- .../domain/photo/service/PhotoService.java | 10 +++++----- 4 files changed, 31 insertions(+), 10 deletions(-) rename src/main/java/kr/kro/photoliner/domain/photo/dto/{ => request}/DeletePhotosRequest.java (63%) rename src/main/java/kr/kro/photoliner/domain/photo/infra/{S3PresignedUrlGenerator.java => S3Client.java} (85%) diff --git a/src/main/java/kr/kro/photoliner/domain/photo/controller/PhotoController.java b/src/main/java/kr/kro/photoliner/domain/photo/controller/PhotoController.java index 53abe37..a1faffd 100644 --- a/src/main/java/kr/kro/photoliner/domain/photo/controller/PhotoController.java +++ b/src/main/java/kr/kro/photoliner/domain/photo/controller/PhotoController.java @@ -2,15 +2,17 @@ import jakarta.validation.Valid; import java.util.List; -import kr.kro.photoliner.domain.photo.dto.DeletePhotosRequest; +import kr.kro.photoliner.domain.photo.dto.request.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.request.PresignedUrlRequest; import kr.kro.photoliner.domain.photo.dto.response.MapMarkersResponse; import kr.kro.photoliner.domain.photo.dto.response.PhotoUploadResponse; import kr.kro.photoliner.domain.photo.dto.response.PhotosResponse; +import kr.kro.photoliner.domain.photo.dto.response.PresignedUrlResponse; +import kr.kro.photoliner.domain.photo.infra.S3Client; import kr.kro.photoliner.domain.photo.service.PhotoService; -import kr.kro.photoliner.domain.photo.service.PhotoUploadService; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; @@ -36,7 +38,7 @@ public class PhotoController { private final PhotoService photoService; - private final PhotoUploadService photoUploadService; + private final S3Client s3Client; @GetMapping public ResponseEntity getPhotos( @@ -51,6 +53,14 @@ public ResponseEntity getMarkersInViewport(@Valid MapMarkers return ResponseEntity.ok(photoService.getMarkersInViewport(request)); } + @PostMapping("/presigned-urls") + public ResponseEntity> getPresignedUrls( + @Valid @RequestBody List requests + ) { + List responses = s3Client.generatePresignedUrls(requests); + return ResponseEntity.ok(responses); + } + @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public ResponseEntity uploadPhotos( @RequestParam("userId") Long userId, diff --git a/src/main/java/kr/kro/photoliner/domain/photo/dto/DeletePhotosRequest.java b/src/main/java/kr/kro/photoliner/domain/photo/dto/request/DeletePhotosRequest.java similarity index 63% rename from src/main/java/kr/kro/photoliner/domain/photo/dto/DeletePhotosRequest.java rename to src/main/java/kr/kro/photoliner/domain/photo/dto/request/DeletePhotosRequest.java index 85458ff..b32318f 100644 --- a/src/main/java/kr/kro/photoliner/domain/photo/dto/DeletePhotosRequest.java +++ b/src/main/java/kr/kro/photoliner/domain/photo/dto/request/DeletePhotosRequest.java @@ -1,4 +1,4 @@ -package kr.kro.photoliner.domain.photo.dto; +package kr.kro.photoliner.domain.photo.dto.request; import java.util.List; diff --git a/src/main/java/kr/kro/photoliner/domain/photo/infra/S3PresignedUrlGenerator.java b/src/main/java/kr/kro/photoliner/domain/photo/infra/S3Client.java similarity index 85% rename from src/main/java/kr/kro/photoliner/domain/photo/infra/S3PresignedUrlGenerator.java rename to src/main/java/kr/kro/photoliner/domain/photo/infra/S3Client.java index 22ce95a..b6e370f 100644 --- a/src/main/java/kr/kro/photoliner/domain/photo/infra/S3PresignedUrlGenerator.java +++ b/src/main/java/kr/kro/photoliner/domain/photo/infra/S3Client.java @@ -8,6 +8,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; +import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; import software.amazon.awssdk.services.s3.model.PutObjectRequest; import software.amazon.awssdk.services.s3.presigner.S3Presigner; import software.amazon.awssdk.services.s3.presigner.model.PresignedPutObjectRequest; @@ -15,9 +16,10 @@ @Service @RequiredArgsConstructor -public class S3PresignedUrlGenerator { +public class S3Client { private final S3Presigner s3Presigner; + private final software.amazon.awssdk.services.s3.S3Client s3Client; @Value("${cloud.aws.s3.bucket}") private String bucketName; @@ -68,4 +70,13 @@ private String extractExtension(String fileName) { } return fileName.substring(lastDotIndex); } + + public void delete(String objectKey) { + DeleteObjectRequest deleteRequest = DeleteObjectRequest.builder() + .bucket(bucketName) + .key(objectKey) + .build(); + + s3Client.deleteObject(deleteRequest); + } } \ No newline at end of file diff --git a/src/main/java/kr/kro/photoliner/domain/photo/service/PhotoService.java b/src/main/java/kr/kro/photoliner/domain/photo/service/PhotoService.java index 07021e1..6447901 100644 --- a/src/main/java/kr/kro/photoliner/domain/photo/service/PhotoService.java +++ b/src/main/java/kr/kro/photoliner/domain/photo/service/PhotoService.java @@ -3,13 +3,13 @@ import java.util.List; import kr.kro.photoliner.domain.album.model.view.AlbumPhotoViews; import kr.kro.photoliner.domain.album.repository.AlbumPhotoRepository; -import kr.kro.photoliner.domain.photo.dto.DeletePhotosRequest; +import kr.kro.photoliner.domain.photo.dto.request.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.infra.S3Client; import kr.kro.photoliner.domain.photo.model.Photo; import kr.kro.photoliner.domain.photo.repository.PhotoRepository; import kr.kro.photoliner.global.code.ApiResponseCode; @@ -29,7 +29,7 @@ public class PhotoService { private final PhotoRepository photoRepository; private final AlbumPhotoRepository albumPhotoRepository; private final GeometryFactory geometryFactory; - private final FileStorage fileStorage; + private final S3Client s3Client; @Transactional(readOnly = true) public PhotosResponse getPhotosByIds(Long userId, Pageable pageable) { @@ -69,8 +69,8 @@ public void updatePhotoLocation(Long photoId, PhotoLocationUpdateRequest request @Transactional public void deletePhotos(DeletePhotosRequest request) { List photos = photoRepository.findAllById(request.ids()); - photos.forEach(photo -> fileStorage.deleteOriginalImage(photo.getFilePath())); - photos.forEach(photo -> fileStorage.deleteThumbnailImage(photo.getFilePath())); + photos.forEach(photo -> s3Client.delete(photo.getFilePath())); + photos.forEach(photo -> s3Client.delete(photo.getThumbnailPath())); photoRepository.deleteAllByIdInBatch(request.ids()); } } From 88a894b3f378e494ada1537afb3cbdb7c343ef34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=9D=B8=ED=99=94?= Date: Sun, 23 Nov 2025 03:51:20 +0900 Subject: [PATCH 04/11] =?UTF-8?q?feat:=20=EC=82=AC=EC=A7=84=20=EC=82=BD?= =?UTF-8?q?=EC=9E=85=20API=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../photo/controller/PhotoController.java | 13 +++--- .../dto/request/CreatePhotosRequest.java | 43 +++++++++++++++++++ .../photo/dto/response/PhotosResponse.java | 4 +- .../dto/response/PresignedUrlResponse.java | 9 ++-- .../domain/photo/infra/S3Client.java | 11 ++--- .../photoliner/domain/photo/model/Photo.java | 9 +--- .../domain/photo/service/PhotoService.java | 19 ++++++++ 7 files changed, 78 insertions(+), 30 deletions(-) create mode 100644 src/main/java/kr/kro/photoliner/domain/photo/dto/request/CreatePhotosRequest.java diff --git a/src/main/java/kr/kro/photoliner/domain/photo/controller/PhotoController.java b/src/main/java/kr/kro/photoliner/domain/photo/controller/PhotoController.java index a1faffd..4b4461e 100644 --- a/src/main/java/kr/kro/photoliner/domain/photo/controller/PhotoController.java +++ b/src/main/java/kr/kro/photoliner/domain/photo/controller/PhotoController.java @@ -2,13 +2,13 @@ import jakarta.validation.Valid; import java.util.List; +import kr.kro.photoliner.domain.photo.dto.request.CreatePhotosRequest; import kr.kro.photoliner.domain.photo.dto.request.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.request.PresignedUrlRequest; import kr.kro.photoliner.domain.photo.dto.response.MapMarkersResponse; -import kr.kro.photoliner.domain.photo.dto.response.PhotoUploadResponse; import kr.kro.photoliner.domain.photo.dto.response.PhotosResponse; import kr.kro.photoliner.domain.photo.dto.response.PresignedUrlResponse; import kr.kro.photoliner.domain.photo.infra.S3Client; @@ -28,9 +28,7 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RequestPart; import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.multipart.MultipartFile; @RestController @RequiredArgsConstructor @@ -62,12 +60,11 @@ public ResponseEntity> getPresignedUrls( } @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE) - public ResponseEntity uploadPhotos( - @RequestParam("userId") Long userId, - @RequestPart("files") List files + public ResponseEntity createPhotos( + @Valid @RequestBody CreatePhotosRequest request ) { - PhotoUploadResponse response = photoUploadService.uploadPhotos(userId, files); - return ResponseEntity.status(HttpStatus.CREATED).body(response); + photoService.createPhotos(request); + return ResponseEntity.status(HttpStatus.CREATED).build(); } @PatchMapping("/{photoId}/captured-date") diff --git a/src/main/java/kr/kro/photoliner/domain/photo/dto/request/CreatePhotosRequest.java b/src/main/java/kr/kro/photoliner/domain/photo/dto/request/CreatePhotosRequest.java new file mode 100644 index 0000000..955f8fd --- /dev/null +++ b/src/main/java/kr/kro/photoliner/domain/photo/dto/request/CreatePhotosRequest.java @@ -0,0 +1,43 @@ +package kr.kro.photoliner.domain.photo.dto.request; + +import jakarta.annotation.Nullable; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Objects; +import org.locationtech.jts.geom.Coordinate; + +public record CreatePhotosRequest( + @NotNull + Long userId, + + @NotNull + @NotEmpty + List photos +) { + + public record InnerPhoto( + @NotNull + String fileName, + + @NotNull + String uploadFileName, + + @Nullable + LocalDateTime capturedDate, + + @Nullable + Integer latitude, + + @Nullable + Integer longitude + ) { + public Coordinate convertToGeo() { + if (Objects.isNull(latitude) || Objects.isNull(longitude)) { + return null; + } + return new Coordinate(latitude, longitude); + } + } +} diff --git a/src/main/java/kr/kro/photoliner/domain/photo/dto/response/PhotosResponse.java b/src/main/java/kr/kro/photoliner/domain/photo/dto/response/PhotosResponse.java index 59e8009..650e8ff 100644 --- a/src/main/java/kr/kro/photoliner/domain/photo/dto/response/PhotosResponse.java +++ b/src/main/java/kr/kro/photoliner/domain/photo/dto/response/PhotosResponse.java @@ -2,9 +2,7 @@ import java.time.LocalDateTime; import java.util.List; -import java.util.Optional; import kr.kro.photoliner.domain.photo.model.Photo; -import org.locationtech.jts.geom.Point; import org.springframework.data.domain.Page; public record PhotosResponse( @@ -60,7 +58,7 @@ public static InnerPhotoResponse from(Photo photo) { photo.getCapturedDt(), photo.getLongitude(), photo.getLatitude(), - photo.getUser().getId()); + photo.getId()); } } } diff --git a/src/main/java/kr/kro/photoliner/domain/photo/dto/response/PresignedUrlResponse.java b/src/main/java/kr/kro/photoliner/domain/photo/dto/response/PresignedUrlResponse.java index b923b0d..b2f893f 100644 --- a/src/main/java/kr/kro/photoliner/domain/photo/dto/response/PresignedUrlResponse.java +++ b/src/main/java/kr/kro/photoliner/domain/photo/dto/response/PresignedUrlResponse.java @@ -2,9 +2,10 @@ public record PresignedUrlResponse( String presignedUrl, - String uploadFileUrl + String uploadFileName ) { - public static PresignedUrlResponse of(String presignedUrl, String uploadFileUrl) { - return new PresignedUrlResponse(presignedUrl, uploadFileUrl); + + public static PresignedUrlResponse of(String presignedUrl, String uploadFileName) { + return new PresignedUrlResponse(presignedUrl, uploadFileName); } -} \ No newline at end of file +} diff --git a/src/main/java/kr/kro/photoliner/domain/photo/infra/S3Client.java b/src/main/java/kr/kro/photoliner/domain/photo/infra/S3Client.java index b6e370f..51729a4 100644 --- a/src/main/java/kr/kro/photoliner/domain/photo/infra/S3Client.java +++ b/src/main/java/kr/kro/photoliner/domain/photo/infra/S3Client.java @@ -24,9 +24,6 @@ public class S3Client { @Value("${cloud.aws.s3.bucket}") private String bucketName; - @Value("${cloud.aws.cdn.base-url}") - private String cdnBaseUrl; - private static final Duration PRESIGNED_URL_EXPIRATION = Duration.ofMinutes(10); public List generatePresignedUrls(List requests) { @@ -50,17 +47,15 @@ private PresignedUrlResponse generatePresignedUrl(PresignedUrlRequest request) { .build(); PresignedPutObjectRequest presignedRequest = s3Presigner.presignPutObject(presignRequest); - String presignedUrl = presignedRequest.url().toString(); - String uploadFileUrl = cdnBaseUrl + "/" + objectKey; - return PresignedUrlResponse.of(presignedUrl, uploadFileUrl); + return PresignedUrlResponse.of(presignedUrl, objectKey); } private String generateObjectKey(String originalFileName) { String extension = extractExtension(originalFileName); String uuid = UUID.randomUUID().toString(); - return "images/original/" + uuid + extension; + return uuid + extension; } private String extractExtension(String fileName) { @@ -79,4 +74,4 @@ public void delete(String objectKey) { s3Client.deleteObject(deleteRequest); } -} \ No newline at end of file +} diff --git a/src/main/java/kr/kro/photoliner/domain/photo/model/Photo.java b/src/main/java/kr/kro/photoliner/domain/photo/model/Photo.java index 8ba6830..bdfc7a3 100644 --- a/src/main/java/kr/kro/photoliner/domain/photo/model/Photo.java +++ b/src/main/java/kr/kro/photoliner/domain/photo/model/Photo.java @@ -2,18 +2,14 @@ import jakarta.persistence.Column; import jakarta.persistence.Entity; -import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; -import jakarta.persistence.JoinColumn; -import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; import jakarta.validation.constraints.NotNull; import java.time.LocalDateTime; import java.util.Objects; import kr.kro.photoliner.common.model.BaseEntity; -import kr.kro.photoliner.domain.user.model.User; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; @@ -51,9 +47,8 @@ public class Photo extends BaseEntity { @Column(name = "location") private Point location; - @ManyToOne(fetch = FetchType.LAZY, optional = false) - @JoinColumn(name = "user_id", referencedColumnName = "id") - private User user; + @Column(name = "user_id") + private Long userId; public void updateCapturedDate(LocalDateTime capturedDt) { this.capturedDt = capturedDt; diff --git a/src/main/java/kr/kro/photoliner/domain/photo/service/PhotoService.java b/src/main/java/kr/kro/photoliner/domain/photo/service/PhotoService.java index 6447901..95551a4 100644 --- a/src/main/java/kr/kro/photoliner/domain/photo/service/PhotoService.java +++ b/src/main/java/kr/kro/photoliner/domain/photo/service/PhotoService.java @@ -3,6 +3,7 @@ import java.util.List; import kr.kro.photoliner.domain.album.model.view.AlbumPhotoViews; import kr.kro.photoliner.domain.album.repository.AlbumPhotoRepository; +import kr.kro.photoliner.domain.photo.dto.request.CreatePhotosRequest; import kr.kro.photoliner.domain.photo.dto.request.DeletePhotosRequest; import kr.kro.photoliner.domain.photo.dto.request.MapMarkersRequest; import kr.kro.photoliner.domain.photo.dto.request.PhotoCapturedDateUpdateRequest; @@ -31,6 +32,9 @@ public class PhotoService { private final GeometryFactory geometryFactory; private final S3Client s3Client; + private static final String ORIGINAL_BASE_PATH = "/images/original/"; + private static final String THUMBNAIL_BASE_PATH = "/images/thumb/"; + @Transactional(readOnly = true) public PhotosResponse getPhotosByIds(Long userId, Pageable pageable) { return PhotosResponse.from(photoRepository.findByUserId(userId, pageable)); @@ -49,6 +53,21 @@ public MapMarkersResponse getMarkersInViewport(MapMarkersRequest request) { ); } + @Transactional + public void createPhotos(CreatePhotosRequest request) { + List photos = request.photos().stream() + .map(photo -> Photo.builder() + .userId(request.userId()) + .fileName(photo.fileName()) + .filePath(ORIGINAL_BASE_PATH + photo.uploadFileName()) + .thumbnailPath(THUMBNAIL_BASE_PATH + photo.uploadFileName()) + .capturedDt(photo.capturedDate()) + .location(geometryFactory.createPoint(photo.convertToGeo())) + .build() + ).toList(); + photoRepository.saveAll(photos); + } + @Transactional public void updatePhotoCapturedDate(Long photoId, PhotoCapturedDateUpdateRequest request) { Photo photo = photoRepository.findById(photoId) From 8e0dfb80b5d7b59b04ebcaa6d27b5f5b7a43ac01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=9D=B8=ED=99=94?= Date: Sun, 23 Nov 2025 03:55:31 +0900 Subject: [PATCH 05/11] =?UTF-8?q?feat:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=ED=8C=8C=EC=9D=BC=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../photo/controller/PhotoController.java | 6 +- .../photoliner/domain/photo/dto/ExifData.java | 11 -- .../domain/photo/infra/ExifExtractor.java | 83 ------------ .../domain/photo/infra/FileStorage.java | 124 ------------------ .../{S3Client.java => S3CustomClient.java} | 9 +- .../domain/photo/service/PhotoService.java | 8 +- .../photo/service/PhotoUploadService.java | 57 -------- 7 files changed, 12 insertions(+), 286 deletions(-) delete mode 100644 src/main/java/kr/kro/photoliner/domain/photo/dto/ExifData.java delete mode 100644 src/main/java/kr/kro/photoliner/domain/photo/infra/ExifExtractor.java delete mode 100644 src/main/java/kr/kro/photoliner/domain/photo/infra/FileStorage.java rename src/main/java/kr/kro/photoliner/domain/photo/infra/{S3Client.java => S3CustomClient.java} (93%) delete mode 100644 src/main/java/kr/kro/photoliner/domain/photo/service/PhotoUploadService.java diff --git a/src/main/java/kr/kro/photoliner/domain/photo/controller/PhotoController.java b/src/main/java/kr/kro/photoliner/domain/photo/controller/PhotoController.java index 4b4461e..e058e40 100644 --- a/src/main/java/kr/kro/photoliner/domain/photo/controller/PhotoController.java +++ b/src/main/java/kr/kro/photoliner/domain/photo/controller/PhotoController.java @@ -11,7 +11,7 @@ import kr.kro.photoliner.domain.photo.dto.response.MapMarkersResponse; import kr.kro.photoliner.domain.photo.dto.response.PhotosResponse; import kr.kro.photoliner.domain.photo.dto.response.PresignedUrlResponse; -import kr.kro.photoliner.domain.photo.infra.S3Client; +import kr.kro.photoliner.domain.photo.infra.S3CustomClient; import kr.kro.photoliner.domain.photo.service.PhotoService; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Pageable; @@ -36,7 +36,7 @@ public class PhotoController { private final PhotoService photoService; - private final S3Client s3Client; + private final S3CustomClient s3CustomClient; @GetMapping public ResponseEntity getPhotos( @@ -55,7 +55,7 @@ public ResponseEntity getMarkersInViewport(@Valid MapMarkers public ResponseEntity> getPresignedUrls( @Valid @RequestBody List requests ) { - List responses = s3Client.generatePresignedUrls(requests); + List responses = s3CustomClient.generatePresignedUrls(requests); return ResponseEntity.ok(responses); } diff --git a/src/main/java/kr/kro/photoliner/domain/photo/dto/ExifData.java b/src/main/java/kr/kro/photoliner/domain/photo/dto/ExifData.java deleted file mode 100644 index e778361..0000000 --- a/src/main/java/kr/kro/photoliner/domain/photo/dto/ExifData.java +++ /dev/null @@ -1,11 +0,0 @@ -package kr.kro.photoliner.domain.photo.dto; - -import java.time.LocalDateTime; -import org.locationtech.jts.geom.Point; - -public record ExifData( - LocalDateTime capturedDt, - Point location -) { - -} diff --git a/src/main/java/kr/kro/photoliner/domain/photo/infra/ExifExtractor.java b/src/main/java/kr/kro/photoliner/domain/photo/infra/ExifExtractor.java deleted file mode 100644 index ea3a005..0000000 --- a/src/main/java/kr/kro/photoliner/domain/photo/infra/ExifExtractor.java +++ /dev/null @@ -1,83 +0,0 @@ -package kr.kro.photoliner.domain.photo.infra; - -import com.drew.imaging.ImageMetadataReader; -import com.drew.imaging.ImageProcessingException; -import com.drew.lang.GeoLocation; -import com.drew.metadata.Metadata; -import com.drew.metadata.exif.ExifSubIFDDirectory; -import com.drew.metadata.exif.GpsDirectory; -import java.io.IOException; -import java.io.InputStream; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.util.Date; -import java.util.Optional; -import kr.kro.photoliner.domain.photo.dto.ExifData; -import kr.kro.photoliner.global.code.ApiResponseCode; -import kr.kro.photoliner.global.exception.CustomException; -import lombok.RequiredArgsConstructor; -import org.locationtech.jts.geom.Coordinate; -import org.locationtech.jts.geom.GeometryFactory; -import org.locationtech.jts.geom.Point; -import org.springframework.stereotype.Component; -import org.springframework.web.multipart.MultipartFile; - -@Component -@RequiredArgsConstructor -public class ExifExtractor { - - private final GeometryFactory geometryFactory; - - public ExifData extract(MultipartFile file) { - try (InputStream inputStream = file.getInputStream()) { - Metadata metadata = ImageMetadataReader.readMetadata(inputStream); - LocalDateTime capturedDt = extractCapturedDateTime(metadata); - Point location = extractGpsLocation(metadata); - return new ExifData(capturedDt, location); - } catch (ImageProcessingException | IOException e) { - throw CustomException.of(ApiResponseCode.FILE_PROCESSING_ERROR, "file title: " + file.getOriginalFilename(), - e); - } - } - - private LocalDateTime extractCapturedDateTime(Metadata metadata) { - return Optional.ofNullable(metadata.getFirstDirectoryOfType(ExifSubIFDDirectory.class)) - .map(this::extractCapturedDate) - .map(this::normalize) - .orElse(null); - } - - private Point extractGpsLocation(Metadata metadata) { - return Optional.ofNullable(metadata) - .map(m -> m.getFirstDirectoryOfType(GpsDirectory.class)) - .map(GpsDirectory::getGeoLocation) - .filter(this::filterInvalidLocation) - .map(this::toPoint) - .orElse(null); - } - - private LocalDateTime normalize(Date date) { - return date.toInstant() - .atZone(ZoneId.systemDefault()) - .toLocalDateTime(); - } - - private Date extractCapturedDate(ExifSubIFDDirectory dir) { - return Optional.ofNullable(dir.getDate(ExifSubIFDDirectory.TAG_DATETIME_ORIGINAL)) - .orElseGet(() -> dir.getDate(ExifSubIFDDirectory.TAG_DATETIME)); - } - - private boolean filterInvalidLocation(GeoLocation loc) { - double lat = loc.getLatitude(); - double lon = loc.getLongitude(); - return Double.isFinite(lat) && Double.isFinite(lon) && isValidCoordinate(lat, lon); - } - - private Point toPoint(GeoLocation loc) { - return geometryFactory.createPoint(new Coordinate(loc.getLongitude(), loc.getLatitude())); - } - - private boolean isValidCoordinate(double latitude, double longitude) { - return latitude >= -90 && latitude <= 90 && longitude >= -180 && longitude <= 180; - } -} diff --git a/src/main/java/kr/kro/photoliner/domain/photo/infra/FileStorage.java b/src/main/java/kr/kro/photoliner/domain/photo/infra/FileStorage.java deleted file mode 100644 index 4a6b346..0000000 --- a/src/main/java/kr/kro/photoliner/domain/photo/infra/FileStorage.java +++ /dev/null @@ -1,124 +0,0 @@ -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; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.StandardCopyOption; -import java.util.Objects; -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 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"; - - private static final int THUMBNAIL_WIDTH = 300; - private static final int THUMBNAIL_HEIGHT = 300; - - public FileStorage(@Value("${photo.upload.base-dir}") String baseDir) { - Path rootLocation = Paths.get(baseDir).toAbsolutePath().normalize(); - this.originalLocation = rootLocation.resolve(ORIGINAL_DIR); - this.thumbnailLocation = rootLocation.resolve(THUMBNAIL_DIR); - - try { - Files.createDirectories(this.originalLocation); - Files.createDirectories(this.thumbnailLocation); - } catch (IOException e) { - throw CustomException.of(DIRECTORY_CREATION_FAILED); - } - } - - public String store(MultipartFile file) { - validateFile(file); - String fileName = generateFileName(file); - 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); - } - } - - private void validateFile(MultipartFile file) { - if (file.isEmpty()) { - throw CustomException.of(ApiResponseCode.INVALID_FILE); - } - - String originalFilename = file.getOriginalFilename(); - if (originalFilename == null || originalFilename.contains("..")) { - throw CustomException.of(ApiResponseCode.INVALID_FILE_NAME, "file title: " + originalFilename); - } - } - - private String generateFileName(MultipartFile file) { - String extension = getExtension(Objects.requireNonNull(file.getOriginalFilename())); - return UUID.randomUUID() + "." + extension; - } - - private void saveFile(MultipartFile file, Path directory, String fileName) { - try { - Path targetLocation = directory.resolve(fileName); - try (InputStream inputStream = file.getInputStream()) { - Files.copy(inputStream, targetLocation, StandardCopyOption.REPLACE_EXISTING); - } - } catch (IOException e) { - throw CustomException.of(ApiResponseCode.FILE_STORE_ERROR, "file title: " + file.getOriginalFilename(), e); - } - } - - private String getExtension(String filename) { - int lastDotIndex = filename.lastIndexOf('.'); - if (lastDotIndex == -1) { - return ""; - } - return filename.substring(lastDotIndex + 1).toLowerCase(); - } -} diff --git a/src/main/java/kr/kro/photoliner/domain/photo/infra/S3Client.java b/src/main/java/kr/kro/photoliner/domain/photo/infra/S3CustomClient.java similarity index 93% rename from src/main/java/kr/kro/photoliner/domain/photo/infra/S3Client.java rename to src/main/java/kr/kro/photoliner/domain/photo/infra/S3CustomClient.java index 51729a4..17fce0a 100644 --- a/src/main/java/kr/kro/photoliner/domain/photo/infra/S3Client.java +++ b/src/main/java/kr/kro/photoliner/domain/photo/infra/S3CustomClient.java @@ -7,19 +7,20 @@ import kr.kro.photoliner.domain.photo.dto.response.PresignedUrlResponse; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; +import org.springframework.stereotype.Component; +import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; import software.amazon.awssdk.services.s3.model.PutObjectRequest; import software.amazon.awssdk.services.s3.presigner.S3Presigner; import software.amazon.awssdk.services.s3.presigner.model.PresignedPutObjectRequest; import software.amazon.awssdk.services.s3.presigner.model.PutObjectPresignRequest; -@Service +@Component @RequiredArgsConstructor -public class S3Client { +public class S3CustomClient { private final S3Presigner s3Presigner; - private final software.amazon.awssdk.services.s3.S3Client s3Client; + private final S3Client s3Client; @Value("${cloud.aws.s3.bucket}") private String bucketName; diff --git a/src/main/java/kr/kro/photoliner/domain/photo/service/PhotoService.java b/src/main/java/kr/kro/photoliner/domain/photo/service/PhotoService.java index 95551a4..71b4cdc 100644 --- a/src/main/java/kr/kro/photoliner/domain/photo/service/PhotoService.java +++ b/src/main/java/kr/kro/photoliner/domain/photo/service/PhotoService.java @@ -10,7 +10,7 @@ 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.S3Client; +import kr.kro.photoliner.domain.photo.infra.S3CustomClient; import kr.kro.photoliner.domain.photo.model.Photo; import kr.kro.photoliner.domain.photo.repository.PhotoRepository; import kr.kro.photoliner.global.code.ApiResponseCode; @@ -30,7 +30,7 @@ public class PhotoService { private final PhotoRepository photoRepository; private final AlbumPhotoRepository albumPhotoRepository; private final GeometryFactory geometryFactory; - private final S3Client s3Client; + private final S3CustomClient s3CustomClient; private static final String ORIGINAL_BASE_PATH = "/images/original/"; private static final String THUMBNAIL_BASE_PATH = "/images/thumb/"; @@ -88,8 +88,8 @@ public void updatePhotoLocation(Long photoId, PhotoLocationUpdateRequest request @Transactional public void deletePhotos(DeletePhotosRequest request) { List photos = photoRepository.findAllById(request.ids()); - photos.forEach(photo -> s3Client.delete(photo.getFilePath())); - photos.forEach(photo -> s3Client.delete(photo.getThumbnailPath())); + photos.forEach(photo -> s3CustomClient.delete(photo.getFilePath())); + photos.forEach(photo -> s3CustomClient.delete(photo.getThumbnailPath())); photoRepository.deleteAllByIdInBatch(request.ids()); } } diff --git a/src/main/java/kr/kro/photoliner/domain/photo/service/PhotoUploadService.java b/src/main/java/kr/kro/photoliner/domain/photo/service/PhotoUploadService.java deleted file mode 100644 index c83606d..0000000 --- a/src/main/java/kr/kro/photoliner/domain/photo/service/PhotoUploadService.java +++ /dev/null @@ -1,57 +0,0 @@ -package kr.kro.photoliner.domain.photo.service; - -import java.util.List; -import kr.kro.photoliner.domain.photo.dto.ExifData; -import kr.kro.photoliner.domain.photo.dto.response.PhotoUploadResponse; -import kr.kro.photoliner.domain.photo.dto.response.PhotoUploadResponse.InnerUploadedPhotoInfo; -import kr.kro.photoliner.domain.photo.infra.ExifExtractor; -import kr.kro.photoliner.domain.photo.infra.FileStorage; -import kr.kro.photoliner.domain.photo.model.Photo; -import kr.kro.photoliner.domain.photo.repository.PhotoRepository; -import kr.kro.photoliner.domain.user.model.User; -import kr.kro.photoliner.domain.user.repository.UserRepository; -import kr.kro.photoliner.global.code.ApiResponseCode; -import kr.kro.photoliner.global.exception.CustomException; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.multipart.MultipartFile; - -@Service -@RequiredArgsConstructor -public class PhotoUploadService { - - private final FileStorage fileStorage; - private final ExifExtractor exifExtractor; - private final PhotoRepository photoRepository; - private final UserRepository userRepository; - - @Transactional - public PhotoUploadResponse uploadPhotos(Long userId, List files) { - User user = userRepository.findUserById(userId).orElseThrow( - () -> CustomException.of(ApiResponseCode.NOT_FOUND_USER, "user id: " + userId)); - List uploadedPhotos = files.stream() - .map(file -> { - ExifData exifData = exifExtractor.extract(file); - String filePath = fileStorage.store(file); - String thumbnailPath = fileStorage.storeThumbnail(filePath); - String fileName = file.getOriginalFilename(); - Photo photo = createPhoto(user, exifData, fileName, filePath, thumbnailPath); - Photo savedPhoto = photoRepository.save(photo); - return InnerUploadedPhotoInfo.from(savedPhoto); - }) - .toList(); - return PhotoUploadResponse.from(uploadedPhotos); - } - - private Photo createPhoto(User user, ExifData exifData, String fileName, String filePath, String thumbnailPath) { - return Photo.builder() - .fileName(fileName) - .filePath(filePath) - .thumbnailPath(thumbnailPath) - .capturedDt(exifData.capturedDt()) - .location(exifData.location()) - .user(user) - .build(); - } -} From a91f86220113d66887e63cca912be459a8c67ac3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=9D=B8=ED=99=94?= Date: Sun, 23 Nov 2025 04:09:09 +0900 Subject: [PATCH 06/11] =?UTF-8?q?feat:=20=EC=84=A4=EC=A0=95=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../photoliner/domain/photo/service/PhotoService.java | 8 ++++++-- src/main/resources/application-dev.yml | 11 +++++++++++ src/main/resources/application-local-example.yml | 10 +++++++++- 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/src/main/java/kr/kro/photoliner/domain/photo/service/PhotoService.java b/src/main/java/kr/kro/photoliner/domain/photo/service/PhotoService.java index 71b4cdc..e0742f1 100644 --- a/src/main/java/kr/kro/photoliner/domain/photo/service/PhotoService.java +++ b/src/main/java/kr/kro/photoliner/domain/photo/service/PhotoService.java @@ -19,6 +19,7 @@ import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.GeometryFactory; import org.locationtech.jts.geom.Point; +import org.springframework.beans.factory.annotation.Value; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -32,6 +33,9 @@ public class PhotoService { private final GeometryFactory geometryFactory; private final S3CustomClient s3CustomClient; + @Value("${cloud.aws.cdn.base-url}") + private String cdnURL; + private static final String ORIGINAL_BASE_PATH = "/images/original/"; private static final String THUMBNAIL_BASE_PATH = "/images/thumb/"; @@ -59,8 +63,8 @@ public void createPhotos(CreatePhotosRequest request) { .map(photo -> Photo.builder() .userId(request.userId()) .fileName(photo.fileName()) - .filePath(ORIGINAL_BASE_PATH + photo.uploadFileName()) - .thumbnailPath(THUMBNAIL_BASE_PATH + photo.uploadFileName()) + .filePath(cdnURL + ORIGINAL_BASE_PATH + photo.uploadFileName()) + .thumbnailPath(cdnURL + THUMBNAIL_BASE_PATH + photo.uploadFileName()) .capturedDt(photo.capturedDate()) .location(geometryFactory.createPoint(photo.convertToGeo())) .build() diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 87aa5ba..dcf4b57 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -3,6 +3,8 @@ spring: import: - "aws-parameterstore:/PHOTO_LINER_API/rds/dev/" - "aws-parameterstore:/PHOTO_LINER_API/ec2/dev/" + - "aws-parameterstore:/PHOTO_LINER_API/cdn/dev/" + - "aws-parameterstore:/PHOTO_LINER_API/s3/dev/" cloud: aws: @@ -20,3 +22,12 @@ photo: cors: allow-url: ${CORS_ALLOW_URL} + +cloud: + aws: + region: + static: ap-northeast-2 + s3: + bucket: ${BUCKET-NAME} + cdn: + base-url: ${CDN-URL} diff --git a/src/main/resources/application-local-example.yml b/src/main/resources/application-local-example.yml index cad07ec..a724c0a 100644 --- a/src/main/resources/application-local-example.yml +++ b/src/main/resources/application-local-example.yml @@ -1,5 +1,4 @@ spring: - datasource: url: url username: username @@ -11,3 +10,12 @@ photo: cors: allow-url: url + +cloud: + aws: + region: + static: ap-northeast-2 + s3: + bucket: name + cdn: + base-url: url \ No newline at end of file From b07087c6a80f3493ab84cc82119b25cf3bd7a642 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=9D=B8=ED=99=94?= Date: Sun, 23 Nov 2025 04:17:32 +0900 Subject: [PATCH 07/11] =?UTF-8?q?fix:=20=EB=B9=8C=EB=93=9C=20=EC=8B=A4?= =?UTF-8?q?=ED=8C=A8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../photoliner/domain/photo/controller/PhotoController.java | 4 ++-- .../photoliner/domain/photo/repository/PhotoRepository.java | 2 +- .../kr/kro/photoliner/domain/photo/service/PhotoService.java | 3 --- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/main/java/kr/kro/photoliner/domain/photo/controller/PhotoController.java b/src/main/java/kr/kro/photoliner/domain/photo/controller/PhotoController.java index 31c2a2a..e153778 100644 --- a/src/main/java/kr/kro/photoliner/domain/photo/controller/PhotoController.java +++ b/src/main/java/kr/kro/photoliner/domain/photo/controller/PhotoController.java @@ -4,11 +4,11 @@ import java.util.List; import kr.kro.photoliner.domain.photo.dto.request.CreatePhotosRequest; import kr.kro.photoliner.domain.photo.dto.request.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.request.PhotoMarkersRequest; import kr.kro.photoliner.domain.photo.dto.request.PresignedUrlRequest; -import kr.kro.photoliner.domain.photo.dto.response.MapMarkersResponse; +import kr.kro.photoliner.domain.photo.dto.response.PhotoMarkersResponse; import kr.kro.photoliner.domain.photo.dto.response.PhotosResponse; import kr.kro.photoliner.domain.photo.dto.response.PresignedUrlResponse; import kr.kro.photoliner.domain.photo.infra.S3CustomClient; diff --git a/src/main/java/kr/kro/photoliner/domain/photo/repository/PhotoRepository.java b/src/main/java/kr/kro/photoliner/domain/photo/repository/PhotoRepository.java index 0d0c62c..7b82616 100644 --- a/src/main/java/kr/kro/photoliner/domain/photo/repository/PhotoRepository.java +++ b/src/main/java/kr/kro/photoliner/domain/photo/repository/PhotoRepository.java @@ -21,7 +21,7 @@ Page findByUserId( select p from Photo p left outer join PhotoItem pi on p.id = pi.photoId - where p.user.id = :userId + where p.userId = :userId and function('st_x', p.location) between function('st_x', :sw) and function('st_x', :ne) and function('st_y', p.location) between function('st_y', :sw) and function('st_y', :ne) order by p.capturedDt diff --git a/src/main/java/kr/kro/photoliner/domain/photo/service/PhotoService.java b/src/main/java/kr/kro/photoliner/domain/photo/service/PhotoService.java index 1fcbb45..a9db269 100644 --- a/src/main/java/kr/kro/photoliner/domain/photo/service/PhotoService.java +++ b/src/main/java/kr/kro/photoliner/domain/photo/service/PhotoService.java @@ -1,10 +1,8 @@ package kr.kro.photoliner.domain.photo.service; import java.util.List; -import kr.kro.photoliner.domain.album.repository.AlbumPhotoRepository; import kr.kro.photoliner.domain.photo.dto.request.CreatePhotosRequest; import kr.kro.photoliner.domain.photo.dto.request.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.request.PhotoMarkersRequest; @@ -30,7 +28,6 @@ public class PhotoService { private final PhotoRepository photoRepository; - private final AlbumPhotoRepository albumPhotoRepository; private final GeometryFactory geometryFactory; private final S3CustomClient s3CustomClient; From 36a290ee6412ec2ffd25133f39a49597e3cbe7ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=9D=B8=ED=99=94?= Date: Sun, 23 Nov 2025 14:01:06 +0900 Subject: [PATCH 08/11] =?UTF-8?q?fix:=20=EB=AF=B8=EB=94=94=EC=96=B4=20?= =?UTF-8?q?=ED=83=80=EC=9E=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../photoliner/domain/photo/controller/PhotoController.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/kr/kro/photoliner/domain/photo/controller/PhotoController.java b/src/main/java/kr/kro/photoliner/domain/photo/controller/PhotoController.java index e153778..8727e31 100644 --- a/src/main/java/kr/kro/photoliner/domain/photo/controller/PhotoController.java +++ b/src/main/java/kr/kro/photoliner/domain/photo/controller/PhotoController.java @@ -18,7 +18,6 @@ import org.springframework.data.domain.Sort; import org.springframework.data.web.PageableDefault; import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; @@ -59,7 +58,7 @@ public ResponseEntity> getPresignedUrls( return ResponseEntity.ok(responses); } - @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @PostMapping public ResponseEntity createPhotos( @Valid @RequestBody CreatePhotosRequest request ) { From b57058e298ef223892a546c59268a4af44e11d4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=9D=B8=ED=99=94?= Date: Sun, 23 Nov 2025 14:32:22 +0900 Subject: [PATCH 09/11] =?UTF-8?q?fix:=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=20=EA=B2=BD=EB=A1=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/photo/dto/request/CreatePhotosRequest.java | 1 + .../photoliner/domain/photo/infra/S3CustomClient.java | 3 ++- .../photoliner/domain/photo/service/PhotoService.java | 9 ++++++++- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/main/java/kr/kro/photoliner/domain/photo/dto/request/CreatePhotosRequest.java b/src/main/java/kr/kro/photoliner/domain/photo/dto/request/CreatePhotosRequest.java index 955f8fd..d9367dd 100644 --- a/src/main/java/kr/kro/photoliner/domain/photo/dto/request/CreatePhotosRequest.java +++ b/src/main/java/kr/kro/photoliner/domain/photo/dto/request/CreatePhotosRequest.java @@ -33,6 +33,7 @@ public record InnerPhoto( @Nullable Integer longitude ) { + public Coordinate convertToGeo() { if (Objects.isNull(latitude) || Objects.isNull(longitude)) { return null; diff --git a/src/main/java/kr/kro/photoliner/domain/photo/infra/S3CustomClient.java b/src/main/java/kr/kro/photoliner/domain/photo/infra/S3CustomClient.java index 17fce0a..131d2f8 100644 --- a/src/main/java/kr/kro/photoliner/domain/photo/infra/S3CustomClient.java +++ b/src/main/java/kr/kro/photoliner/domain/photo/infra/S3CustomClient.java @@ -26,6 +26,7 @@ public class S3CustomClient { private String bucketName; private static final Duration PRESIGNED_URL_EXPIRATION = Duration.ofMinutes(10); + private static final String BASE_URL = "images/original/"; public List generatePresignedUrls(List requests) { return requests.stream() @@ -38,7 +39,7 @@ private PresignedUrlResponse generatePresignedUrl(PresignedUrlRequest request) { PutObjectRequest putObjectRequest = PutObjectRequest.builder() .bucket(bucketName) - .key(objectKey) + .key(BASE_URL + objectKey) .contentType(request.contentType()) .build(); diff --git a/src/main/java/kr/kro/photoliner/domain/photo/service/PhotoService.java b/src/main/java/kr/kro/photoliner/domain/photo/service/PhotoService.java index a9db269..9414be8 100644 --- a/src/main/java/kr/kro/photoliner/domain/photo/service/PhotoService.java +++ b/src/main/java/kr/kro/photoliner/domain/photo/service/PhotoService.java @@ -61,7 +61,7 @@ public void createPhotos(CreatePhotosRequest request) { .filePath(cdnURL + ORIGINAL_BASE_PATH + photo.uploadFileName()) .thumbnailPath(cdnURL + THUMBNAIL_BASE_PATH + photo.uploadFileName()) .capturedDt(photo.capturedDate()) - .location(geometryFactory.createPoint(photo.convertToGeo())) + .location(getPointOrNull(photo.convertToGeo())) .build() ).toList(); photoRepository.saveAll(photos); @@ -91,4 +91,11 @@ public void deletePhotos(DeletePhotosRequest request) { photos.forEach(photo -> s3CustomClient.delete(photo.getThumbnailPath())); photoRepository.deleteAllByIdInBatch(request.ids()); } + + private Point getPointOrNull(Coordinate coordinate) { + if (coordinate == null) { + return null; + } + return geometryFactory.createPoint(coordinate); + } } From 3ac8272c246c1a8135fc3fef10695f572b7a61f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=9D=B8=ED=99=94?= Date: Sun, 23 Nov 2025 14:33:47 +0900 Subject: [PATCH 10/11] =?UTF-8?q?fix:=20=EC=9C=84=EB=8F=84=20=EA=B2=BD?= =?UTF-8?q?=EB=8F=84=20=EC=88=9C=EC=84=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/photo/dto/request/CreatePhotosRequest.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/kr/kro/photoliner/domain/photo/dto/request/CreatePhotosRequest.java b/src/main/java/kr/kro/photoliner/domain/photo/dto/request/CreatePhotosRequest.java index d9367dd..38deda0 100644 --- a/src/main/java/kr/kro/photoliner/domain/photo/dto/request/CreatePhotosRequest.java +++ b/src/main/java/kr/kro/photoliner/domain/photo/dto/request/CreatePhotosRequest.java @@ -1,6 +1,8 @@ package kr.kro.photoliner.domain.photo.dto.request; import jakarta.annotation.Nullable; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import java.time.LocalDateTime; @@ -28,9 +30,13 @@ public record InnerPhoto( LocalDateTime capturedDate, @Nullable + @Min(0) + @Max(90) Integer latitude, @Nullable + @Min(0) + @Max(180) Integer longitude ) { @@ -38,7 +44,7 @@ public Coordinate convertToGeo() { if (Objects.isNull(latitude) || Objects.isNull(longitude)) { return null; } - return new Coordinate(latitude, longitude); + return new Coordinate(longitude, latitude); } } } From 24b74fa5efc82ee83530c1a6ebbbdf80b05984be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EC=9D=B8=ED=99=94?= Date: Sun, 23 Nov 2025 14:51:28 +0900 Subject: [PATCH 11/11] =?UTF-8?q?fix:=20=EC=9C=84=EB=8F=84=20=EA=B2=BD?= =?UTF-8?q?=EB=8F=84=20=EC=9E=90=EB=A3=8C=ED=98=95=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/photo/dto/request/CreatePhotosRequest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/kr/kro/photoliner/domain/photo/dto/request/CreatePhotosRequest.java b/src/main/java/kr/kro/photoliner/domain/photo/dto/request/CreatePhotosRequest.java index 38deda0..06e3a43 100644 --- a/src/main/java/kr/kro/photoliner/domain/photo/dto/request/CreatePhotosRequest.java +++ b/src/main/java/kr/kro/photoliner/domain/photo/dto/request/CreatePhotosRequest.java @@ -32,12 +32,12 @@ public record InnerPhoto( @Nullable @Min(0) @Max(90) - Integer latitude, + Double latitude, @Nullable @Min(0) @Max(180) - Integer longitude + Double longitude ) { public Coordinate convertToGeo() {