diff --git a/cherrypic-api/src/main/java/org/cherrypic/domain/image/controller/ImageController.java b/cherrypic-api/src/main/java/org/cherrypic/domain/image/controller/ImageController.java index 105739a7..bf14f893 100644 --- a/cherrypic-api/src/main/java/org/cherrypic/domain/image/controller/ImageController.java +++ b/cherrypic-api/src/main/java/org/cherrypic/domain/image/controller/ImageController.java @@ -12,6 +12,7 @@ import org.cherrypic.global.pagination.SliceResponse; import org.cherrypic.global.pagination.SortDirection; import org.cherrypic.global.pagination.SortParameter; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; @@ -28,8 +29,8 @@ public class ImageController { @Operation( summary = "회원 프로필 이미지 Presigned URL 생성", description = "회원 프로필 이미지 업로드를 위한 Presigned URL을 생성합니다.") - public PresignedUrlResponse memberProfileImageUploadUrlCreate( - @Valid @RequestBody ImageUploadRequest request) { + public ImagePresignedUrlResponse memberProfileImageUploadUrlCreate( + @Valid @RequestBody ImageUploadUrlRequest request) { return imageService.createMemberProfileImageUploadUrl(request); } @@ -37,8 +38,8 @@ public PresignedUrlResponse memberProfileImageUploadUrlCreate( @Operation( summary = "앨범 커버 이미지 Presigned URL 생성", description = "앨범 커버 이미지 업로드를 위한 Presigned URL을 생성합니다.") - public PresignedUrlResponse albumCoverImageUploadUrlCreate( - @Valid @RequestBody ImageUploadRequest request) { + public ImagePresignedUrlResponse albumCoverImageUploadUrlCreate( + @Valid @RequestBody ImageUploadUrlRequest request) { return imageService.createAlbumCoverImageUploadUrl(request); } @@ -46,20 +47,38 @@ public PresignedUrlResponse albumCoverImageUploadUrlCreate( @Operation( summary = "이벤트 커버 이미지 Presigned URL 생성", description = "이벤트 커버 이미지 업로드를 위한 Presigned URL을 생성합니다.") - public PresignedUrlResponse eventCoverImageUploadUrlCreate( - @Valid @RequestBody ImageUploadRequest request) { + public ImagePresignedUrlResponse eventCoverImageUploadUrlCreate( + @Valid @RequestBody ImageUploadUrlRequest request) { return imageService.createEventCoverImageUploadUrl(request); } - @PostMapping("/albums/{albumId}/images") + @PostMapping("/images/upload-complete") + @Operation(summary = "앨범 사진 외 이미지 업로드 완료", description = "프로필, 커버 사진 등의 이미지 업로드를 완료합니다.") + public ResponseEntity nonAlbumImageUploadComplete( + @Valid @RequestBody ImageUploadCompleteRequest request) { + imageService.completeNonAlbumImageUpload(request); + return ResponseEntity.noContent().build(); + } + + @PostMapping("/albums/{albumId}/upload-url") @Operation( summary = "앨범 이미지 업로드 Presigned URL들 생성", description = "앨범 이미지 업로드를 위한 Presigned URL들을 생성합니다.") - public ImageUploadListResponse albumImageUploadUrlsCreate( - @PathVariable Long albumId, @Valid @RequestBody AlbumImageUploadRequest request) { + public AlbumImagesPresignedUrlResponse albumImageUploadUrlsCreate( + @PathVariable Long albumId, @Valid @RequestBody AlbumImagesUploadUrlRequest request) { return imageService.createAlbumImageUploadUrls(albumId, request); } + @PostMapping("/albums/{albumId}/upload-complete") + @Operation(summary = "앨범 이미지들 업로드 완료", description = "앨범 이미지들의 업로드를 완료합니다.") + public ResponseEntity albumImagesUploadComplete( + @PathVariable Long albumId, + @Valid @RequestBody AlbumImagesUploadCompleteRequest request) { + AlbumImagesUploadCompleteResponse response = + imageService.completeAlbumImagesUpload(albumId, request); + return ResponseEntity.status(HttpStatus.CREATED).body(response); + } + @GetMapping("/albums/{albumId}/images") @Operation(summary = "앨범 이미지 목록 조회", description = "앨범의 이미지 목록을 조회합니다.") public SliceResponse albumImagesGet( @@ -102,16 +121,26 @@ public ResponseEntity albumImageDelete( return ResponseEntity.noContent().build(); } - @PostMapping("temp-albums/{tempAlbumId}/images") + @PostMapping("temp-albums/{tempAlbumId}/upload-url") @Operation( - summary = "임시 앨범 이미지 업로드 Presigned URL들 생성", - description = "임시 앨범 이미지 업로드를 위한 Presigned URL들을 생성합니다.") - public TempAlbumImageUploadListResponse tempAlbumImageUploadUrlsCreate( + summary = "임시 앨범 이미지들 업로드 Presigned URL들 생성", + description = "임시 앨범 이미지들 업로드를 위한 Presigned URL들을 생성합니다.") + public TempAlbumImagesPresignedUrlResponse tempAlbumImageUploadUrlsCreate( @PathVariable Long tempAlbumId, - @Valid @RequestBody TempAlbumImageUploadRequest request) { + @Valid @RequestBody TempAlbumImagesUploadUrlRequest request) { return imageService.createTempAlbumImageUploadUrls(tempAlbumId, request); } + @PostMapping("/temp-albums/{tempAlbumId}/upload-complete") + @Operation(summary = "임시 앨범 이미지들 업로드 완료", description = "임시 앨범 이미지들의 업로드를 완료합니다.") + public ResponseEntity tempAlbumImagesUploadComplete( + @PathVariable Long tempAlbumId, + @Valid @RequestBody TempAlbumImagesUploadCompleteRequest request) { + TempAlbumImagesUploadCompleteResponse response = + imageService.completeTempAlbumImagesUpload(tempAlbumId, request); + return ResponseEntity.status(HttpStatus.CREATED).body(response); + } + @DeleteMapping("temp-albums/{tempAlbumId}/images") @Operation(summary = "임시 앨범 이미지 삭제", description = "임시 앨범의 이미지를 삭제합니다.") public ResponseEntity tempAlbumImageDelete( diff --git a/cherrypic-api/src/main/java/org/cherrypic/domain/image/dto/request/AlbumImagesUploadCompleteRequest.java b/cherrypic-api/src/main/java/org/cherrypic/domain/image/dto/request/AlbumImagesUploadCompleteRequest.java new file mode 100644 index 00000000..34ecf19f --- /dev/null +++ b/cherrypic-api/src/main/java/org/cherrypic/domain/image/dto/request/AlbumImagesUploadCompleteRequest.java @@ -0,0 +1,27 @@ +package org.cherrypic.domain.image.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; + +public record AlbumImagesUploadCompleteRequest( + @NotEmpty(message = "업로드 완료 하는 이미지들의 정보들은 비워둘 수 없습니다.") + @Valid + @Schema(description = "업로드 완료 요청 리스트") + List payloads) { + @Schema(name = "AlbumImagesUploadCompletePayload") + public record Payload( + @Schema(description = "파일이 찍힌 시간, 정보가 없다면 null을 넣어주세요.") LocalDateTime generatedAt, + @NotNull(message = "파일의 용량은 비워둘 수 없습니다.") + @Schema(description = "업로드 하는 파일의 용량(MB), 소수점 2자리 까지", example = "0.04") + BigDecimal capacityMb, + @NotBlank(message = "업로드 완료 하고자 하는 imageUrl은 비워둘 수 없습니다.") + @Valid + @Schema(description = "엄로드 완료 요청 이미지 Url") + String imageUrl) {} +} diff --git a/cherrypic-api/src/main/java/org/cherrypic/domain/image/dto/request/AlbumImageUploadRequest.java b/cherrypic-api/src/main/java/org/cherrypic/domain/image/dto/request/AlbumImagesUploadUrlRequest.java similarity index 88% rename from cherrypic-api/src/main/java/org/cherrypic/domain/image/dto/request/AlbumImageUploadRequest.java rename to cherrypic-api/src/main/java/org/cherrypic/domain/image/dto/request/AlbumImagesUploadUrlRequest.java index d5b16529..3b3f642c 100644 --- a/cherrypic-api/src/main/java/org/cherrypic/domain/image/dto/request/AlbumImageUploadRequest.java +++ b/cherrypic-api/src/main/java/org/cherrypic/domain/image/dto/request/AlbumImagesUploadUrlRequest.java @@ -6,12 +6,11 @@ import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import java.math.BigDecimal; -import java.time.LocalDateTime; import java.util.List; import org.cherrypic.global.annotation.Enum; import org.cherrypic.s3.enums.FileExtension; -public record AlbumImageUploadRequest( +public record AlbumImagesUploadUrlRequest( @NotEmpty(message = "업로드할 피일들의 정보는 비워둘 수 없습니다.") @Valid @Schema(description = "업로드 요청 리스트") List payloads) { @Schema(name = "AlbumImageUploadRequestPayload") @@ -25,7 +24,6 @@ public record Payload( @NotBlank(message = "MD5 해시값은 비워둘 수 없습니다.") @Schema(description = "S3 업로드시 파일의 변형을 확인하기 위한 md5 해시") String md5Hashes, - @Schema(description = "파일이 찍힌 시간, 정보가 없다면 null을 넣어주세요.") LocalDateTime generatedAt, @NotNull(message = "파일의 용량은 비워둘 수 없습니다.") @Schema(description = "업로드 하는 파일의 용량(MB), 소수점 2자리 까지", example = "0.04") BigDecimal capacityMb) {} diff --git a/cherrypic-api/src/main/java/org/cherrypic/domain/image/dto/request/ImageUploadCompleteRequest.java b/cherrypic-api/src/main/java/org/cherrypic/domain/image/dto/request/ImageUploadCompleteRequest.java new file mode 100644 index 00000000..ae47aea1 --- /dev/null +++ b/cherrypic-api/src/main/java/org/cherrypic/domain/image/dto/request/ImageUploadCompleteRequest.java @@ -0,0 +1,9 @@ +package org.cherrypic.domain.image.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; + +public record ImageUploadCompleteRequest( + @NotBlank(message = "이미지 url을 비워둘 수 없습니다.") + @Schema(description = "업로드 완료 하고자 하는 이미지 url", example = "https://example.jpg") + String imageUrl) {} diff --git a/cherrypic-api/src/main/java/org/cherrypic/domain/image/dto/request/ImageUploadRequest.java b/cherrypic-api/src/main/java/org/cherrypic/domain/image/dto/request/ImageUploadUrlRequest.java similarity index 95% rename from cherrypic-api/src/main/java/org/cherrypic/domain/image/dto/request/ImageUploadRequest.java rename to cherrypic-api/src/main/java/org/cherrypic/domain/image/dto/request/ImageUploadUrlRequest.java index 0d5fda12..157cbf30 100644 --- a/cherrypic-api/src/main/java/org/cherrypic/domain/image/dto/request/ImageUploadRequest.java +++ b/cherrypic-api/src/main/java/org/cherrypic/domain/image/dto/request/ImageUploadUrlRequest.java @@ -5,7 +5,7 @@ import org.cherrypic.global.annotation.Enum; import org.cherrypic.s3.enums.FileExtension; -public record ImageUploadRequest( +public record ImageUploadUrlRequest( @Enum(message = "이미지 파일의 확장자는 비워둘 수 없으며, PNG, JPG, JPEG, WEBP, HEIC, HEIF만 지원됩니다.") @Schema(description = "이미지 파일의 확장자", defaultValue = "JPEG") FileExtension fileExtension, diff --git a/cherrypic-api/src/main/java/org/cherrypic/domain/image/dto/request/TempAlbumImagesUploadCompleteRequest.java b/cherrypic-api/src/main/java/org/cherrypic/domain/image/dto/request/TempAlbumImagesUploadCompleteRequest.java new file mode 100644 index 00000000..63f3e8b5 --- /dev/null +++ b/cherrypic-api/src/main/java/org/cherrypic/domain/image/dto/request/TempAlbumImagesUploadCompleteRequest.java @@ -0,0 +1,25 @@ +package org.cherrypic.domain.image.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import java.math.BigDecimal; +import java.util.List; + +public record TempAlbumImagesUploadCompleteRequest( + @NotEmpty(message = "업로드 완료 하고자 하는 임시 앨범 이미지들의 정보들은 비워둘 수 없습니다.") + @Valid + @Schema(description = "업로드 완료 요청 리스트") + List payloads) { + @Schema(name = "TempAlbumImagesUploadCompletePayload") + public record Payload( + @NotNull(message = "파일의 용량은 비워둘 수 없습니다.") + @Schema(description = "업로드 하는 파일의 용량(MB), 소수점 2자리 까지", example = "0.04") + BigDecimal capacityMb, + @NotBlank(message = "업로드 완료 하고자 하는 tempAlbumImageUrl은 비워둘 수 없습니다.") + @Valid + @Schema(description = "업로드 완료 요청 임시 앨범 이미지 Url") + String tempAlbumImageUrl) {} +} diff --git a/cherrypic-api/src/main/java/org/cherrypic/domain/image/dto/request/TempAlbumImageUploadRequest.java b/cherrypic-api/src/main/java/org/cherrypic/domain/image/dto/request/TempAlbumImagesUploadUrlRequest.java similarity index 97% rename from cherrypic-api/src/main/java/org/cherrypic/domain/image/dto/request/TempAlbumImageUploadRequest.java rename to cherrypic-api/src/main/java/org/cherrypic/domain/image/dto/request/TempAlbumImagesUploadUrlRequest.java index 0bf06e49..d8340209 100644 --- a/cherrypic-api/src/main/java/org/cherrypic/domain/image/dto/request/TempAlbumImageUploadRequest.java +++ b/cherrypic-api/src/main/java/org/cherrypic/domain/image/dto/request/TempAlbumImagesUploadUrlRequest.java @@ -10,7 +10,7 @@ import org.cherrypic.global.annotation.Enum; import org.cherrypic.s3.enums.FileExtension; -public record TempAlbumImageUploadRequest( +public record TempAlbumImagesUploadUrlRequest( @NotEmpty(message = "업로드할 피일들의 정보는 비워둘 수 없습니다.") @Valid @Schema(description = "업로드 요청 리스트") List payloads) { @Schema(name = "TempAlbumImageUploadRequestPayload") diff --git a/cherrypic-api/src/main/java/org/cherrypic/domain/image/dto/response/AlbumImagesPresignedUrlResponse.java b/cherrypic-api/src/main/java/org/cherrypic/domain/image/dto/response/AlbumImagesPresignedUrlResponse.java new file mode 100644 index 00000000..53a62e12 --- /dev/null +++ b/cherrypic-api/src/main/java/org/cherrypic/domain/image/dto/response/AlbumImagesPresignedUrlResponse.java @@ -0,0 +1,11 @@ +package org.cherrypic.domain.image.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; + +public record AlbumImagesPresignedUrlResponse( + @Schema(description = "생성된 presigned url 리스트") List urls) { + public static AlbumImagesPresignedUrlResponse of(List urls) { + return new AlbumImagesPresignedUrlResponse(urls); + } +} diff --git a/cherrypic-api/src/main/java/org/cherrypic/domain/image/dto/response/AlbumImagesUploadCompleteResponse.java b/cherrypic-api/src/main/java/org/cherrypic/domain/image/dto/response/AlbumImagesUploadCompleteResponse.java new file mode 100644 index 00000000..0868b9d1 --- /dev/null +++ b/cherrypic-api/src/main/java/org/cherrypic/domain/image/dto/response/AlbumImagesUploadCompleteResponse.java @@ -0,0 +1,11 @@ +package org.cherrypic.domain.image.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; + +public record AlbumImagesUploadCompleteResponse( + @Schema(description = "생성된 이미지들의 ID 리스트") List imageIds) { + public static AlbumImagesUploadCompleteResponse of(List imageIds) { + return new AlbumImagesUploadCompleteResponse(imageIds); + } +} diff --git a/cherrypic-api/src/main/java/org/cherrypic/domain/image/dto/response/ImagePresignedUrlResponse.java b/cherrypic-api/src/main/java/org/cherrypic/domain/image/dto/response/ImagePresignedUrlResponse.java new file mode 100644 index 00000000..9712c0f2 --- /dev/null +++ b/cherrypic-api/src/main/java/org/cherrypic/domain/image/dto/response/ImagePresignedUrlResponse.java @@ -0,0 +1,10 @@ +package org.cherrypic.domain.image.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; + +public record ImagePresignedUrlResponse( + @Schema(description = "Presigned URL") String presignedUrl) { + public static ImagePresignedUrlResponse of(String presignedUrl) { + return new ImagePresignedUrlResponse(presignedUrl); + } +} diff --git a/cherrypic-api/src/main/java/org/cherrypic/domain/image/dto/response/ImageUploadListResponse.java b/cherrypic-api/src/main/java/org/cherrypic/domain/image/dto/response/ImageUploadListResponse.java deleted file mode 100644 index 0b020806..00000000 --- a/cherrypic-api/src/main/java/org/cherrypic/domain/image/dto/response/ImageUploadListResponse.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.cherrypic.domain.image.dto.response; - -import io.swagger.v3.oas.annotations.media.Schema; -import java.util.List; - -public record ImageUploadListResponse( - @Schema(description = "로컬 사진 삭제 허용 여부") Boolean localImageDeletion, - @Schema(description = "업로드된 이미지들의 정보 리스트") List content) { - public static ImageUploadListResponse of(List content, Boolean localImageDeletion) { - return new ImageUploadListResponse(localImageDeletion, content); - } - - @Schema(name = "ImageUploadListResponseContent") - public record Content( - @Schema(description = "생성된 이미지의 ID") Long imageId, - @Schema(description = "생성된 Presigned Url") String presignedUrl) { - public static Content of(Long imageId, String presignedUrl) { - return new Content(imageId, presignedUrl); - } - } -} diff --git a/cherrypic-api/src/main/java/org/cherrypic/domain/image/dto/response/PresignedUrlResponse.java b/cherrypic-api/src/main/java/org/cherrypic/domain/image/dto/response/PresignedUrlResponse.java deleted file mode 100644 index 2ae4a804..00000000 --- a/cherrypic-api/src/main/java/org/cherrypic/domain/image/dto/response/PresignedUrlResponse.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.cherrypic.domain.image.dto.response; - -import io.swagger.v3.oas.annotations.media.Schema; - -public record PresignedUrlResponse(@Schema(description = "Presigned URL") String presignedUrl) { - public static PresignedUrlResponse of(String presignedUrl) { - return new PresignedUrlResponse(presignedUrl); - } -} diff --git a/cherrypic-api/src/main/java/org/cherrypic/domain/image/dto/response/TempAlbumImageUploadListResponse.java b/cherrypic-api/src/main/java/org/cherrypic/domain/image/dto/response/TempAlbumImageUploadListResponse.java deleted file mode 100644 index dea92cc6..00000000 --- a/cherrypic-api/src/main/java/org/cherrypic/domain/image/dto/response/TempAlbumImageUploadListResponse.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.cherrypic.domain.image.dto.response; - -import io.swagger.v3.oas.annotations.media.Schema; -import java.util.List; - -public record TempAlbumImageUploadListResponse( - @Schema(description = "업로드된 임시 앨범 이미지들의 정보 리스트") List content) { - public static TempAlbumImageUploadListResponse of(List content) { - return new TempAlbumImageUploadListResponse(content); - } - - @Schema(name = "TempAlbumImageUploadResponseContent") - public record Content( - @Schema(description = "생성된 임시 앨범 이미지의 ID") Long tempAlbumImageId, - @Schema(description = "생성된 Presigned Url") String presignedUrl) { - public static Content of(Long imageId, String presignedUrl) { - return new Content(imageId, presignedUrl); - } - } -} diff --git a/cherrypic-api/src/main/java/org/cherrypic/domain/image/dto/response/TempAlbumImagesPresignedUrlResponse.java b/cherrypic-api/src/main/java/org/cherrypic/domain/image/dto/response/TempAlbumImagesPresignedUrlResponse.java new file mode 100644 index 00000000..11ff0e05 --- /dev/null +++ b/cherrypic-api/src/main/java/org/cherrypic/domain/image/dto/response/TempAlbumImagesPresignedUrlResponse.java @@ -0,0 +1,11 @@ +package org.cherrypic.domain.image.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; + +public record TempAlbumImagesPresignedUrlResponse( + @Schema(description = "생성된 presigned url 리스트") List urls) { + public static TempAlbumImagesPresignedUrlResponse of(List urls) { + return new TempAlbumImagesPresignedUrlResponse(urls); + } +} diff --git a/cherrypic-api/src/main/java/org/cherrypic/domain/image/dto/response/TempAlbumImagesUploadCompleteResponse.java b/cherrypic-api/src/main/java/org/cherrypic/domain/image/dto/response/TempAlbumImagesUploadCompleteResponse.java new file mode 100644 index 00000000..eda6c3e5 --- /dev/null +++ b/cherrypic-api/src/main/java/org/cherrypic/domain/image/dto/response/TempAlbumImagesUploadCompleteResponse.java @@ -0,0 +1,11 @@ +package org.cherrypic.domain.image.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; + +public record TempAlbumImagesUploadCompleteResponse( + @Schema(description = "생성된 임시 앨범 이미지들의 ID 리스트") List tempAlbumImageIds) { + public static TempAlbumImagesUploadCompleteResponse of(List tempAlbumImageIds) { + return new TempAlbumImagesUploadCompleteResponse(tempAlbumImageIds); + } +} diff --git a/cherrypic-api/src/main/java/org/cherrypic/domain/image/exception/ImageErrorCode.java b/cherrypic-api/src/main/java/org/cherrypic/domain/image/exception/ImageErrorCode.java index 1166ff8a..6f905c17 100644 --- a/cherrypic-api/src/main/java/org/cherrypic/domain/image/exception/ImageErrorCode.java +++ b/cherrypic-api/src/main/java/org/cherrypic/domain/image/exception/ImageErrorCode.java @@ -11,6 +11,7 @@ public enum ImageErrorCode implements BaseErrorCode { IMAGE_DELETED(409, "이미지 관련 작업 중 이미지가 삭제되었습니다."), IMAGE_CONFLICT(409, "예상치 못한 이미지 무결성 오류"), NOT_IMAGE_EXTENSION(400, "프로필과 커버에는 이미지 파일만 업로드 가능합니다."), + IMAGE_UPLOAD_FAIL(400, "이미지가 성공적으로 업로드 되지 못했습니다."), PRESIGNED_IMAGES_NOT_MINE(403, "본인이 업로드하지 않은 Presigned Image는 삭제할 수 없습니다."), DUPLICATE_HASHES(400, "중복되는 md5 해시값이 존재합니다."); diff --git a/cherrypic-api/src/main/java/org/cherrypic/domain/image/service/ImageService.java b/cherrypic-api/src/main/java/org/cherrypic/domain/image/service/ImageService.java index 9cc9abd3..38b9a7dc 100644 --- a/cherrypic-api/src/main/java/org/cherrypic/domain/image/service/ImageService.java +++ b/cherrypic-api/src/main/java/org/cherrypic/domain/image/service/ImageService.java @@ -7,14 +7,14 @@ import org.cherrypic.global.pagination.SortParameter; public interface ImageService { - PresignedUrlResponse createMemberProfileImageUploadUrl(ImageUploadRequest request); + ImagePresignedUrlResponse createMemberProfileImageUploadUrl(ImageUploadUrlRequest request); - PresignedUrlResponse createAlbumCoverImageUploadUrl(ImageUploadRequest request); + ImagePresignedUrlResponse createAlbumCoverImageUploadUrl(ImageUploadUrlRequest request); - PresignedUrlResponse createEventCoverImageUploadUrl(ImageUploadRequest request); + ImagePresignedUrlResponse createEventCoverImageUploadUrl(ImageUploadUrlRequest request); - ImageUploadListResponse createAlbumImageUploadUrls( - Long albumId, AlbumImageUploadRequest request); + AlbumImagesPresignedUrlResponse createAlbumImageUploadUrls( + Long albumId, AlbumImagesUploadUrlRequest request); SliceResponse getAlbumImages( Long albumId, @@ -32,8 +32,16 @@ SliceResponse getEventImages( void deleteAlbumImage(Long albumId, AlbumImageDeleteRequest request); - TempAlbumImageUploadListResponse createTempAlbumImageUploadUrls( - Long tempAlbumId, TempAlbumImageUploadRequest request); + TempAlbumImagesPresignedUrlResponse createTempAlbumImageUploadUrls( + Long tempAlbumId, TempAlbumImagesUploadUrlRequest request); void deleteTempAlbumImage(Long tempAlbumId, TempAlbumImageDeleteRequest request); + + void completeNonAlbumImageUpload(ImageUploadCompleteRequest request); + + AlbumImagesUploadCompleteResponse completeAlbumImagesUpload( + Long albumId, AlbumImagesUploadCompleteRequest request); + + TempAlbumImagesUploadCompleteResponse completeTempAlbumImagesUpload( + Long tempAlbumId, TempAlbumImagesUploadCompleteRequest request); } diff --git a/cherrypic-api/src/main/java/org/cherrypic/domain/image/service/ImageServiceImpl.java b/cherrypic-api/src/main/java/org/cherrypic/domain/image/service/ImageServiceImpl.java index 98e59e10..d7efd006 100644 --- a/cherrypic-api/src/main/java/org/cherrypic/domain/image/service/ImageServiceImpl.java +++ b/cherrypic-api/src/main/java/org/cherrypic/domain/image/service/ImageServiceImpl.java @@ -66,7 +66,8 @@ public class ImageServiceImpl implements ImageService { private final ApplicationEventPublisher eventPublisher; @Override - public PresignedUrlResponse createMemberProfileImageUploadUrl(ImageUploadRequest request) { + public ImagePresignedUrlResponse createMemberProfileImageUploadUrl( + ImageUploadUrlRequest request) { final Member currentMember = memberUtil.getCurrentMember(); validateImageExtension(request.fileExtension()); @@ -78,11 +79,11 @@ public PresignedUrlResponse createMemberProfileImageUploadUrl(ImageUploadRequest request.fileExtension(), request.md5Hash()); - return PresignedUrlResponse.of(presignedUrl); + return ImagePresignedUrlResponse.of(presignedUrl); } @Override - public PresignedUrlResponse createAlbumCoverImageUploadUrl(ImageUploadRequest request) { + public ImagePresignedUrlResponse createAlbumCoverImageUploadUrl(ImageUploadUrlRequest request) { final Member currentMember = memberUtil.getCurrentMember(); validateImageExtension(request.fileExtension()); @@ -94,11 +95,11 @@ public PresignedUrlResponse createAlbumCoverImageUploadUrl(ImageUploadRequest re request.fileExtension(), request.md5Hash()); - return PresignedUrlResponse.of(presignedUrl); + return ImagePresignedUrlResponse.of(presignedUrl); } @Override - public PresignedUrlResponse createEventCoverImageUploadUrl(ImageUploadRequest request) { + public ImagePresignedUrlResponse createEventCoverImageUploadUrl(ImageUploadUrlRequest request) { final Member currentMember = memberUtil.getCurrentMember(); validateImageExtension(request.fileExtension()); @@ -110,12 +111,12 @@ public PresignedUrlResponse createEventCoverImageUploadUrl(ImageUploadRequest re request.fileExtension(), request.md5Hash()); - return PresignedUrlResponse.of(presignedUrl); + return ImagePresignedUrlResponse.of(presignedUrl); } @Override - public ImageUploadListResponse createAlbumImageUploadUrls( - Long albumId, AlbumImageUploadRequest request) { + public AlbumImagesPresignedUrlResponse createAlbumImageUploadUrls( + Long albumId, AlbumImagesUploadUrlRequest request) { final Member currentMember = memberUtil.getCurrentMember(); final Album album = getAlbumByIdWithLock(albumId); @@ -124,14 +125,12 @@ public ImageUploadListResponse createAlbumImageUploadUrls( BigDecimal uploadCapacityMb = request.payloads().stream() - .map(AlbumImageUploadRequest.Payload::capacityMb) + .map(AlbumImagesUploadUrlRequest.Payload::capacityMb) .reduce(BigDecimal.ZERO, BigDecimal::add); validateAlbumCapacity(album, uploadCapacityMb); validateDistinctHashes(request); - album.increaseCapacity(uploadCapacityMb); - List presignedUrls = request.payloads().stream() .map( @@ -143,42 +142,7 @@ public ImageUploadListResponse createAlbumImageUploadUrls( req.md5Hashes())) .toList(); - List images = - IntStream.range(0, request.payloads().size()) - .mapToObj( - i -> { - AlbumImageUploadRequest.Payload req = request.payloads().get(i); - String presignedUrl = presignedUrls.get(i); - - String objectUrl = - presignedUrl.substring(0, presignedUrl.indexOf("?")); - - return Image.createImage( - album, - currentMember.getId(), - objectUrl, - req.generatedAt() != null - ? req.generatedAt() - : LocalDateTime.now(), - req.capacityMb()); - }) - .toList(); - - imageRepository.bulkInsertImages(images); - - List imageIds = - imageRepository.findImageIdsByUrlsInOrder( - images.stream().map(Image::getUrl).toList()); - - List content = - IntStream.range(0, images.size()) - .mapToObj( - i -> - ImageUploadListResponse.Content.of( - imageIds.get(i), presignedUrls.get(i))) - .toList(); - - return ImageUploadListResponse.of(content, currentMember.getLocalImageDeletion()); + return AlbumImagesPresignedUrlResponse.of(presignedUrls); } @Override @@ -244,8 +208,8 @@ public void deleteAlbumImage(Long albumId, AlbumImageDeleteRequest request) { @Override @Transactional - public TempAlbumImageUploadListResponse createTempAlbumImageUploadUrls( - Long tempAlbumId, TempAlbumImageUploadRequest request) { + public TempAlbumImagesPresignedUrlResponse createTempAlbumImageUploadUrls( + Long tempAlbumId, TempAlbumImagesUploadUrlRequest request) { final Member currentMember = memberUtil.getCurrentMember(); final TempAlbum tempAlbum = getTempAlbumById(tempAlbumId); @@ -253,14 +217,12 @@ public TempAlbumImageUploadListResponse createTempAlbumImageUploadUrls( BigDecimal uploadCapacityMb = request.payloads().stream() - .map(TempAlbumImageUploadRequest.Payload::capacityMb) + .map(TempAlbumImagesUploadUrlRequest.Payload::capacityMb) .reduce(BigDecimal.ZERO, BigDecimal::add); validateTempAlbumCapacity(tempAlbum, uploadCapacityMb); validateDistinctHashes(request); - tempAlbum.increaseCapacity(uploadCapacityMb); - List presignedUrls = request.payloads().stream() .map( @@ -272,37 +234,7 @@ public TempAlbumImageUploadListResponse createTempAlbumImageUploadUrls( req.md5Hashes())) .toList(); - List tempAlbumImages = - IntStream.range(0, request.payloads().size()) - .mapToObj( - i -> { - TempAlbumImageUploadRequest.Payload req = - request.payloads().get(i); - String presignedUrl = presignedUrls.get(i); - - String objectUrl = - presignedUrl.substring(0, presignedUrl.indexOf("?")); - - return TempAlbumImage.createTempAlbumImage( - tempAlbum, objectUrl, req.capacityMb()); - }) - .toList(); - - imageRepository.bulkInsertTempAlbumImages(tempAlbumImages); - - List tempAlbumImageIds = - imageRepository.findTempImageIdsByUrlsInOrder( - tempAlbumImages.stream().map(TempAlbumImage::getUrl).toList()); - - List content = - IntStream.range(0, tempAlbumImageIds.size()) - .mapToObj( - i -> - TempAlbumImageUploadListResponse.Content.of( - tempAlbumImageIds.get(i), presignedUrls.get(i))) - .toList(); - - return TempAlbumImageUploadListResponse.of(content); + return TempAlbumImagesPresignedUrlResponse.of(presignedUrls); } @Override @@ -333,6 +265,105 @@ public void deleteTempAlbumImage(Long tempAlbumId, TempAlbumImageDeleteRequest r tempAlbumImageRepository.deleteAllInBatch(tempAlbumImages); } + @Override + public void completeNonAlbumImageUpload(ImageUploadCompleteRequest request) { + validateImageUpload(request.imageUrl()); + + s3Util.updateTagToCompleteByUrl(request.imageUrl()); + } + + @Override + public AlbumImagesUploadCompleteResponse completeAlbumImagesUpload( + Long albumId, AlbumImagesUploadCompleteRequest request) { + final Member currentMember = memberUtil.getCurrentMember(); + final Album album = getAlbumById(albumId); + + List imageUrls = + request.payloads().stream() + .map(AlbumImagesUploadCompleteRequest.Payload::imageUrl) + .toList(); + + validateImagesUpload(imageUrls); + + s3Util.updateTagsToCompleteByUrls(imageUrls); + + BigDecimal uploadCapacityMb = + request.payloads().stream() + .map(AlbumImagesUploadCompleteRequest.Payload::capacityMb) + .reduce(BigDecimal.ZERO, BigDecimal::add); + album.increaseCapacity(uploadCapacityMb); + + List images = + IntStream.range(0, request.payloads().size()) + .mapToObj( + i -> { + AlbumImagesUploadCompleteRequest.Payload req = + request.payloads().get(i); + String imageUrl = imageUrls.get(i); + + return Image.createImage( + album, + currentMember.getId(), + imageUrl, + req.generatedAt() != null + ? req.generatedAt() + : LocalDateTime.now(), + req.capacityMb()); + }) + .toList(); + + imageRepository.bulkInsertImages(images); + + List imageIds = + imageRepository.findImageIdsByUrlsInOrder( + images.stream().map(Image::getUrl).toList()); + + return AlbumImagesUploadCompleteResponse.of(imageIds); + } + + @Override + public TempAlbumImagesUploadCompleteResponse completeTempAlbumImagesUpload( + Long tempAlbumId, TempAlbumImagesUploadCompleteRequest request) { + final TempAlbum tempAlbum = getTempAlbumById(tempAlbumId); + + List imageUrls = + request.payloads().stream() + .map(TempAlbumImagesUploadCompleteRequest.Payload::tempAlbumImageUrl) + .toList(); + + validateImagesUpload(imageUrls); + + s3Util.updateTagsToCompleteByUrls(imageUrls); + + BigDecimal uploadCapacityMb = + request.payloads().stream() + .map(TempAlbumImagesUploadCompleteRequest.Payload::capacityMb) + .reduce(BigDecimal.ZERO, BigDecimal::add); + + tempAlbum.increaseCapacity(uploadCapacityMb); + + List tempAlbumImages = + IntStream.range(0, request.payloads().size()) + .mapToObj( + i -> { + TempAlbumImagesUploadCompleteRequest.Payload req = + request.payloads().get(i); + String imageUrl = imageUrls.get(i); + + return TempAlbumImage.createTempAlbumImage( + tempAlbum, imageUrl, req.capacityMb()); + }) + .toList(); + + imageRepository.bulkInsertTempAlbumImages(tempAlbumImages); + + List tempAlbumImageIds = + imageRepository.findTempImageIdsByUrlsInOrder( + tempAlbumImages.stream().map(TempAlbumImage::getUrl).toList()); + + return TempAlbumImagesUploadCompleteResponse.of(tempAlbumImageIds); + } + private Album getAlbumById(Long albumId) { return albumRepository .findById(albumId) @@ -404,10 +435,10 @@ private void validateTempAlbumCapacity(TempAlbum tempAlbum, BigDecimal uploadCap } } - private void validateDistinctHashes(AlbumImageUploadRequest request) { + private void validateDistinctHashes(AlbumImagesUploadUrlRequest request) { List hashes = request.payloads().stream() - .map(AlbumImageUploadRequest.Payload::md5Hashes) + .map(AlbumImagesUploadUrlRequest.Payload::md5Hashes) .toList(); if (hashes.stream().distinct().count() != hashes.size()) { @@ -415,10 +446,10 @@ private void validateDistinctHashes(AlbumImageUploadRequest request) { } } - private void validateDistinctHashes(TempAlbumImageUploadRequest request) { + private void validateDistinctHashes(TempAlbumImagesUploadUrlRequest request) { List hashes = request.payloads().stream() - .map(TempAlbumImageUploadRequest.Payload::md5Hashes) + .map(TempAlbumImagesUploadUrlRequest.Payload::md5Hashes) .toList(); if (hashes.stream().distinct().count() != hashes.size()) { @@ -460,4 +491,16 @@ private void validateSubscriptionNotExpired(Album album) { throw new CustomException(AlbumErrorCode.EXPIRED_SUBSCRIPTION); } } + + private void validateImageUpload(String imageUrl) { + if (!s3Util.doesFileExistByUrl(imageUrl)) { + throw new CustomException(ImageErrorCode.IMAGE_UPLOAD_FAIL); + } + } + + private void validateImagesUpload(List imageUrls) { + if (!s3Util.doAllFilesExistByUrls(imageUrls)) { + throw new CustomException(ImageErrorCode.IMAGE_UPLOAD_FAIL); + } + } } diff --git a/cherrypic-api/src/test/java/org/cherrypic/image/controller/ImageControllerTest.java b/cherrypic-api/src/test/java/org/cherrypic/image/controller/ImageControllerTest.java index c943ba35..34849112 100644 --- a/cherrypic-api/src/test/java/org/cherrypic/image/controller/ImageControllerTest.java +++ b/cherrypic-api/src/test/java/org/cherrypic/image/controller/ImageControllerTest.java @@ -23,6 +23,7 @@ import org.cherrypic.global.pagination.SortParameter; import org.cherrypic.s3.enums.FileExtension; import org.cherrypic.tempalbum.enums.TempAlbumType; +import org.hamcrest.Matchers; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -53,9 +54,10 @@ class 프로필용_Presigned_URL_생성_요청_시 { @Test void 유효한_요청이면_회원_프로필_이미지용_Presigned_URL을_반환한다() throws Exception { // given - ImageUploadRequest request = new ImageUploadRequest(FileExtension.JPEG, "testMd5Hash"); + ImageUploadUrlRequest request = + new ImageUploadUrlRequest(FileExtension.JPEG, "testMd5Hash"); - PresignedUrlResponse response = new PresignedUrlResponse("testPresignedUrl"); + ImagePresignedUrlResponse response = new ImagePresignedUrlResponse("testPresignedUrl"); given(imageService.createMemberProfileImageUploadUrl(request)).willReturn(response); @@ -75,7 +77,8 @@ class 프로필용_Presigned_URL_생성_요청_시 { @Test void 동영상_확장자를_입력할_경우_예외가_발생한다() throws Exception { // given - ImageUploadRequest request = new ImageUploadRequest(FileExtension.MKV, "testMd5Hash"); + ImageUploadUrlRequest request = + new ImageUploadUrlRequest(FileExtension.MKV, "testMd5Hash"); given(imageService.createMemberProfileImageUploadUrl(request)) .willThrow(new CustomException(ImageErrorCode.NOT_IMAGE_EXTENSION)); @@ -100,8 +103,8 @@ class 프로필용_Presigned_URL_생성_요청_시 { @ValueSource(strings = {"JPEG1", "PDF", "TXT"}) void 이미지_파일_확장자가_null_또는_지원하지_않는_형식이면_예외가_발생한다(String extension) throws Exception { // given - ImageUploadRequest request = - new ImageUploadRequest(FileExtension.from(extension), "testMd5Hash"); + ImageUploadUrlRequest request = + new ImageUploadUrlRequest(FileExtension.from(extension), "testMd5Hash"); // when & then ResultActions perform = @@ -126,7 +129,7 @@ class 프로필용_Presigned_URL_생성_요청_시 { @ValueSource(strings = {" "}) void MD5_해시를_비워두면_예외가_발생한다(String md5Hash) throws Exception { // given - ImageUploadRequest request = new ImageUploadRequest(FileExtension.JPG, md5Hash); + ImageUploadUrlRequest request = new ImageUploadUrlRequest(FileExtension.JPG, md5Hash); // when & then ResultActions perform = @@ -149,9 +152,10 @@ class 앨범_커버용_Presigned_URL_생성_요청_시 { @Test void 유효한_요청이면_앨범_커버_이미지용_Presigned_URL을_반환한다() throws Exception { // given - ImageUploadRequest request = new ImageUploadRequest(FileExtension.JPEG, "testMd5Hash"); + ImageUploadUrlRequest request = + new ImageUploadUrlRequest(FileExtension.JPEG, "testMd5Hash"); - PresignedUrlResponse response = new PresignedUrlResponse("testPresignedUrl"); + ImagePresignedUrlResponse response = new ImagePresignedUrlResponse("testPresignedUrl"); given(imageService.createAlbumCoverImageUploadUrl(request)).willReturn(response); @@ -171,7 +175,8 @@ class 앨범_커버용_Presigned_URL_생성_요청_시 { @Test void 동영상_확장자를_입력할_경우_예외가_발생한다() throws Exception { // given - ImageUploadRequest request = new ImageUploadRequest(FileExtension.MKV, "testMd5Hash"); + ImageUploadUrlRequest request = + new ImageUploadUrlRequest(FileExtension.MKV, "testMd5Hash"); given(imageService.createAlbumCoverImageUploadUrl(request)) .willThrow(new CustomException(ImageErrorCode.NOT_IMAGE_EXTENSION)); @@ -196,8 +201,8 @@ class 앨범_커버용_Presigned_URL_생성_요청_시 { @ValueSource(strings = {"JPEG1", "PDF", "TXT"}) void 이미지_파일_확장자가_null_또는_지원하지_않는_형식이면_예외가_발생한다(String extension) throws Exception { // given - ImageUploadRequest request = - new ImageUploadRequest(FileExtension.from(extension), "testMd5Hash"); + ImageUploadUrlRequest request = + new ImageUploadUrlRequest(FileExtension.from(extension), "testMd5Hash"); // when & then ResultActions perform = @@ -222,7 +227,7 @@ class 앨범_커버용_Presigned_URL_생성_요청_시 { @ValueSource(strings = {" "}) void MD5_해시를_비워두면_예외가_발생한다(String md5Hash) throws Exception { // given - ImageUploadRequest request = new ImageUploadRequest(FileExtension.JPG, md5Hash); + ImageUploadUrlRequest request = new ImageUploadUrlRequest(FileExtension.JPG, md5Hash); // when & then ResultActions perform = @@ -245,9 +250,10 @@ class 이벤트_커버용_Presigned_URL_생성_요청_시 { @Test void 유효한_요청이면_이벤트_커버_이미지용_Presigned_URL을_반환한다() throws Exception { // given - ImageUploadRequest request = new ImageUploadRequest(FileExtension.JPEG, "testMd5Hash"); + ImageUploadUrlRequest request = + new ImageUploadUrlRequest(FileExtension.JPEG, "testMd5Hash"); - PresignedUrlResponse response = new PresignedUrlResponse("testPresignedUrl"); + ImagePresignedUrlResponse response = new ImagePresignedUrlResponse("testPresignedUrl"); given(imageService.createEventCoverImageUploadUrl(request)).willReturn(response); @@ -267,7 +273,8 @@ class 이벤트_커버용_Presigned_URL_생성_요청_시 { @Test void 동영상_확장자를_입력할_경우_예외가_발생한다() throws Exception { // given - ImageUploadRequest request = new ImageUploadRequest(FileExtension.MKV, "testMd5Hash"); + ImageUploadUrlRequest request = + new ImageUploadUrlRequest(FileExtension.MKV, "testMd5Hash"); given(imageService.createEventCoverImageUploadUrl(request)) .willThrow(new CustomException(ImageErrorCode.NOT_IMAGE_EXTENSION)); @@ -292,8 +299,8 @@ class 이벤트_커버용_Presigned_URL_생성_요청_시 { @ValueSource(strings = {"JPEG1", "PDF", "TXT"}) void 이미지_파일_확장자가_null_또는_지원하지_않는_형식이면_예외가_발생한다(String extension) throws Exception { // given - ImageUploadRequest request = - new ImageUploadRequest(FileExtension.from(extension), "testMd5Hash"); + ImageUploadUrlRequest request = + new ImageUploadUrlRequest(FileExtension.from(extension), "testMd5Hash"); // when & then ResultActions perform = @@ -318,7 +325,7 @@ class 이벤트_커버용_Presigned_URL_생성_요청_시 { @ValueSource(strings = {" "}) void MD5_해시를_비워두면_예외가_발생한다(String md5Hash) throws Exception { // given - ImageUploadRequest request = new ImageUploadRequest(FileExtension.JPG, md5Hash); + ImageUploadUrlRequest request = new ImageUploadUrlRequest(FileExtension.JPG, md5Hash); // when & then ResultActions perform = @@ -341,59 +348,43 @@ class 앨범_이미지_업로드_Presigned_URL을_생성_요청_시 { @Test void 유효한_요청이면_이미지_업로드_Presigned_URL들을_반환한다() throws Exception { // given - AlbumImageUploadRequest request = - new AlbumImageUploadRequest( + AlbumImagesUploadUrlRequest request = + new AlbumImagesUploadUrlRequest( List.of( - new AlbumImageUploadRequest.Payload( - FileExtension.JPEG, - "testMd5Hash1", - LocalDateTime.now(), - BigDecimal.ONE), - new AlbumImageUploadRequest.Payload( - FileExtension.JPEG, - "testMd5Hash2", - LocalDateTime.now(), - BigDecimal.ONE))); + new AlbumImagesUploadUrlRequest.Payload( + FileExtension.JPEG, "testMd5Hash1", BigDecimal.ONE), + new AlbumImagesUploadUrlRequest.Payload( + FileExtension.JPEG, "testMd5Hash2", BigDecimal.ONE))); - ImageUploadListResponse response = - new ImageUploadListResponse( - false, - List.of( - new ImageUploadListResponse.Content(1L, "testPresignedUrl1"), - new ImageUploadListResponse.Content(2L, "testPresignedUrl2"))); + AlbumImagesPresignedUrlResponse response = + new AlbumImagesPresignedUrlResponse( + List.of("testPresignedUrl1", "testPresignedUrl2")); given(imageService.createAlbumImageUploadUrls(1L, request)).willReturn(response); // when & then ResultActions perform = mockMvc.perform( - post("/albums/1/images") + post("/albums/1/upload-url") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))); perform.andExpect(status().isOk()) .andExpect(jsonPath("$.success").value(true)) .andExpect(jsonPath("$.status").value(HttpStatus.OK.value())) - .andExpect(jsonPath("$.data.content").isNotEmpty()) - .andExpect(jsonPath("$.data.localImageDeletion").value(false)); + .andExpect(jsonPath("$.data.urls").isNotEmpty()); } @Test void 앨범이_존재하지_않는_경우_예외가_발생한다() throws Exception { // given - AlbumImageUploadRequest request = - new AlbumImageUploadRequest( + AlbumImagesUploadUrlRequest request = + new AlbumImagesUploadUrlRequest( List.of( - new AlbumImageUploadRequest.Payload( - FileExtension.JPEG, - "testMd5Hash1", - LocalDateTime.now(), - BigDecimal.ONE), - new AlbumImageUploadRequest.Payload( - FileExtension.JPEG, - "testMd5Hash2", - LocalDateTime.now(), - BigDecimal.ONE))); + new AlbumImagesUploadUrlRequest.Payload( + FileExtension.JPEG, "testMd5Hash1", BigDecimal.ONE), + new AlbumImagesUploadUrlRequest.Payload( + FileExtension.JPEG, "testMd5Hash2", BigDecimal.ONE))); given(imageService.createAlbumImageUploadUrls(1L, request)) .willThrow(new CustomException(AlbumErrorCode.ALBUM_NOT_FOUND)); @@ -401,7 +392,7 @@ class 앨범_이미지_업로드_Presigned_URL을_생성_요청_시 { // when & then ResultActions perform = mockMvc.perform( - post("/albums/1/images") + post("/albums/1/upload-url") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))); @@ -415,19 +406,13 @@ class 앨범_이미지_업로드_Presigned_URL을_생성_요청_시 { @Test void 앨범에_속하지_않은_사용자가_앨범_이미지_업로드_URL을_요청하면_예외가_발생한다() throws Exception { // given - AlbumImageUploadRequest request = - new AlbumImageUploadRequest( + AlbumImagesUploadUrlRequest request = + new AlbumImagesUploadUrlRequest( List.of( - new AlbumImageUploadRequest.Payload( - FileExtension.JPEG, - "testMd5Hash1", - LocalDateTime.now(), - BigDecimal.ONE), - new AlbumImageUploadRequest.Payload( - FileExtension.JPEG, - "testMd5Hash2", - LocalDateTime.now(), - BigDecimal.ONE))); + new AlbumImagesUploadUrlRequest.Payload( + FileExtension.JPEG, "testMd5Hash1", BigDecimal.ONE), + new AlbumImagesUploadUrlRequest.Payload( + FileExtension.JPEG, "testMd5Hash2", BigDecimal.ONE))); given(imageService.createAlbumImageUploadUrls(1L, request)) .willThrow(new CustomException(AlbumErrorCode.NOT_ALBUM_PARTICIPANT)); @@ -435,7 +420,7 @@ class 앨범_이미지_업로드_Presigned_URL을_생성_요청_시 { // when & then ResultActions perform = mockMvc.perform( - post("/albums/1/images") + post("/albums/1/upload-url") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))); @@ -449,19 +434,13 @@ class 앨범_이미지_업로드_Presigned_URL을_생성_요청_시 { @Test void LIMITED_권한의_사용자가_앨범_이미지_업로드_URL을_요청하면_예외가_발생한다() throws Exception { // given - AlbumImageUploadRequest request = - new AlbumImageUploadRequest( + AlbumImagesUploadUrlRequest request = + new AlbumImagesUploadUrlRequest( List.of( - new AlbumImageUploadRequest.Payload( - FileExtension.JPEG, - "testMd5Hash1", - LocalDateTime.now(), - BigDecimal.ONE), - new AlbumImageUploadRequest.Payload( - FileExtension.JPEG, - "testMd5Hash2", - LocalDateTime.now(), - BigDecimal.ONE))); + new AlbumImagesUploadUrlRequest.Payload( + FileExtension.JPEG, "testMd5Hash1", BigDecimal.ONE), + new AlbumImagesUploadUrlRequest.Payload( + FileExtension.JPEG, "testMd5Hash2", BigDecimal.ONE))); given(imageService.createAlbumImageUploadUrls(1L, request)) .willThrow(new CustomException(AlbumErrorCode.LIMITED_AUTHORITY)); @@ -469,7 +448,7 @@ class 앨범_이미지_업로드_Presigned_URL을_생성_요청_시 { // when & then ResultActions perform = mockMvc.perform( - post("/albums/1/images") + post("/albums/1/upload-url") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))); @@ -483,19 +462,13 @@ class 앨범_이미지_업로드_Presigned_URL을_생성_요청_시 { @Test void 구독이_만료된_앨범인_경우_예외가_발생한다() throws Exception { // given - AlbumImageUploadRequest request = - new AlbumImageUploadRequest( + AlbumImagesUploadUrlRequest request = + new AlbumImagesUploadUrlRequest( List.of( - new AlbumImageUploadRequest.Payload( - FileExtension.JPEG, - "testMd5Hash1", - LocalDateTime.now(), - BigDecimal.ONE), - new AlbumImageUploadRequest.Payload( - FileExtension.JPEG, - "testMd5Hash2", - LocalDateTime.now(), - BigDecimal.ONE))); + new AlbumImagesUploadUrlRequest.Payload( + FileExtension.JPEG, "testMd5Hash1", BigDecimal.ONE), + new AlbumImagesUploadUrlRequest.Payload( + FileExtension.JPEG, "testMd5Hash2", BigDecimal.ONE))); given(imageService.createAlbumImageUploadUrls(1L, request)) .willThrow(new CustomException(AlbumErrorCode.EXPIRED_SUBSCRIPTION)); @@ -504,7 +477,7 @@ class 앨범_이미지_업로드_Presigned_URL을_생성_요청_시 { // when & then ResultActions perform = mockMvc.perform( - post("/albums/1/images") + post("/albums/1/upload-url") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))); @@ -518,19 +491,13 @@ class 앨범_이미지_업로드_Presigned_URL을_생성_요청_시 { @Test void 앨범의_남은_용량을_초과해서_요청하면_예외가_발생한다() throws Exception { // given - AlbumImageUploadRequest request = - new AlbumImageUploadRequest( + AlbumImagesUploadUrlRequest request = + new AlbumImagesUploadUrlRequest( List.of( - new AlbumImageUploadRequest.Payload( - FileExtension.JPEG, - "testMd5Hash1", - LocalDateTime.now(), - BigDecimal.ONE), - new AlbumImageUploadRequest.Payload( - FileExtension.JPEG, - "testMd5Hash2", - LocalDateTime.now(), - BigDecimal.ONE))); + new AlbumImagesUploadUrlRequest.Payload( + FileExtension.JPEG, "testMd5Hash1", BigDecimal.ONE), + new AlbumImagesUploadUrlRequest.Payload( + FileExtension.JPEG, "testMd5Hash2", BigDecimal.ONE))); given(imageService.createAlbumImageUploadUrls(1L, request)) .willThrow(new CustomException(AlbumErrorCode.ALBUM_CAPACITY_EXCEEDED)); @@ -538,7 +505,7 @@ class 앨범_이미지_업로드_Presigned_URL을_생성_요청_시 { // when & then ResultActions perform = mockMvc.perform( - post("/albums/1/images") + post("/albums/1/upload-url") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))); @@ -552,19 +519,13 @@ class 앨범_이미지_업로드_Presigned_URL을_생성_요청_시 { @Test void MD5_해시에_중복된_값이_존재하면_예외가_발생한다() throws Exception { // given - AlbumImageUploadRequest request = - new AlbumImageUploadRequest( + AlbumImagesUploadUrlRequest request = + new AlbumImagesUploadUrlRequest( List.of( - new AlbumImageUploadRequest.Payload( - FileExtension.JPEG, - "testMd5Hash", - LocalDateTime.now(), - BigDecimal.ONE), - new AlbumImageUploadRequest.Payload( - FileExtension.JPEG, - "testMd5Hash", - LocalDateTime.now(), - BigDecimal.ONE))); + new AlbumImagesUploadUrlRequest.Payload( + FileExtension.JPEG, "testMd5Hash", BigDecimal.ONE), + new AlbumImagesUploadUrlRequest.Payload( + FileExtension.JPEG, "testMd5Hash", BigDecimal.ONE))); given(imageService.createAlbumImageUploadUrls(1L, request)) .willThrow(new CustomException(ImageErrorCode.DUPLICATE_HASHES)); @@ -572,7 +533,7 @@ class 앨범_이미지_업로드_Presigned_URL을_생성_요청_시 { // when & then ResultActions perform = mockMvc.perform( - post("/albums/1/images") + post("/albums/1/upload-url") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))); @@ -589,19 +550,18 @@ class 앨범_이미지_업로드_Presigned_URL을_생성_요청_시 { @ValueSource(strings = {"JPEG1", "PDF", "TXT"}) void 파일_확장자가_null_또는_지원하지_않는_형식이면_예외가_발생한다(String extension) throws Exception { // given - AlbumImageUploadRequest request = - new AlbumImageUploadRequest( + AlbumImagesUploadUrlRequest request = + new AlbumImagesUploadUrlRequest( List.of( - new AlbumImageUploadRequest.Payload( + new AlbumImagesUploadUrlRequest.Payload( FileExtension.from(extension), "testMd5Hash1", - LocalDateTime.now(), BigDecimal.ONE))); // when & then ResultActions perform = mockMvc.perform( - post("/albums/1/images") + post("/albums/1/upload-url") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))); @@ -618,19 +578,16 @@ class 앨범_이미지_업로드_Presigned_URL을_생성_요청_시 { @Test void 이미지_용량을_비워두면_예외가_발생한다() throws Exception { // given - AlbumImageUploadRequest request = - new AlbumImageUploadRequest( + AlbumImagesUploadUrlRequest request = + new AlbumImagesUploadUrlRequest( List.of( - new AlbumImageUploadRequest.Payload( - FileExtension.JPEG, - "testMd5Hash", - LocalDateTime.now(), - null))); + new AlbumImagesUploadUrlRequest.Payload( + FileExtension.JPEG, "testMd5Hash", null))); // when & then ResultActions perform = mockMvc.perform( - post("/albums/1/images") + post("/albums/1/upload-url") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))); @@ -647,19 +604,16 @@ class 앨범_이미지_업로드_Presigned_URL을_생성_요청_시 { @ValueSource(strings = {" "}) void MD5_해시가_null_또는_공백이면_예외가_발생한다(String md5Hash) throws Exception { // given - AlbumImageUploadRequest request = - new AlbumImageUploadRequest( + AlbumImagesUploadUrlRequest request = + new AlbumImagesUploadUrlRequest( List.of( - new AlbumImageUploadRequest.Payload( - FileExtension.JPEG, - md5Hash, - LocalDateTime.now(), - BigDecimal.ONE))); + new AlbumImagesUploadUrlRequest.Payload( + FileExtension.JPEG, md5Hash, BigDecimal.ONE))); // when & then ResultActions perform = mockMvc.perform( - post("/albums/1/images") + post("/albums/1/upload-url") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))); @@ -673,12 +627,12 @@ class 앨범_이미지_업로드_Presigned_URL을_생성_요청_시 { @Test void 업로드_요청_정보를_비워두면_예외가_발생한다() throws Exception { // given - AlbumImageUploadRequest request = new AlbumImageUploadRequest(List.of()); + AlbumImagesUploadUrlRequest request = new AlbumImagesUploadUrlRequest(List.of()); // when & then ResultActions perform = mockMvc.perform( - post("/albums/1/images") + post("/albums/1/upload-url") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))); @@ -1389,52 +1343,48 @@ class 임시_앨범_이미지_업로드_Presigned_URL을_생성_요청_시 { @Test void 유효한_요청이면_임시_앨범_이미지_업로드_Presigned_URL들을_반환한다() throws Exception { // given - TempAlbumImageUploadRequest request = - new TempAlbumImageUploadRequest( + TempAlbumImagesUploadUrlRequest request = + new TempAlbumImagesUploadUrlRequest( List.of( - new TempAlbumImageUploadRequest.Payload( + new TempAlbumImagesUploadUrlRequest.Payload( FileExtension.JPEG, "testMd5Hash1", new BigDecimal("0.3")), - new TempAlbumImageUploadRequest.Payload( + new TempAlbumImagesUploadUrlRequest.Payload( FileExtension.JPEG, "testMd5Hash2", new BigDecimal("0.3")))); - TempAlbumImageUploadListResponse response = - new TempAlbumImageUploadListResponse( - List.of( - new TempAlbumImageUploadListResponse.Content( - 1L, "testPresignedUrl1"), - new TempAlbumImageUploadListResponse.Content( - 2L, "testPresignedUrl2"))); + TempAlbumImagesPresignedUrlResponse response = + new TempAlbumImagesPresignedUrlResponse( + List.of("testPresignedUrl1", "testPresignedUrl2")); given(imageService.createTempAlbumImageUploadUrls(1L, request)).willReturn(response); // when & then ResultActions perform = mockMvc.perform( - post("/temp-albums/1/images") + post("/temp-albums/1/upload-url") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))); perform.andExpect(status().isOk()) .andExpect(jsonPath("$.success").value(true)) .andExpect(jsonPath("$.status").value(HttpStatus.OK.value())) - .andExpect(jsonPath("$.data.content").isNotEmpty()); + .andExpect(jsonPath("$.data.urls").isNotEmpty()); } @Test void 임시_앨범이_존재하지_않는_경우_예외가_발생한다() throws Exception { // given - TempAlbumImageUploadRequest request = - new TempAlbumImageUploadRequest( + TempAlbumImagesUploadUrlRequest request = + new TempAlbumImagesUploadUrlRequest( List.of( - new TempAlbumImageUploadRequest.Payload( + new TempAlbumImagesUploadUrlRequest.Payload( FileExtension.JPEG, "testMd5Hash1", new BigDecimal("0.3")), - new TempAlbumImageUploadRequest.Payload( + new TempAlbumImagesUploadUrlRequest.Payload( FileExtension.JPEG, "testMd5Hash2", new BigDecimal("0.3")))); @@ -1445,7 +1395,7 @@ class 임시_앨범_이미지_업로드_Presigned_URL을_생성_요청_시 { // when & then ResultActions perform = mockMvc.perform( - post("/temp-albums/1/images") + post("/temp-albums/1/upload-url") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))); @@ -1459,14 +1409,14 @@ class 임시_앨범_이미지_업로드_Presigned_URL을_생성_요청_시 { @Test void 임시_앨범_소유자가_아닌_사용자가_앨범_이미지_업로드_URL을_요청하면_예외가_발생한다() throws Exception { // given - TempAlbumImageUploadRequest request = - new TempAlbumImageUploadRequest( + TempAlbumImagesUploadUrlRequest request = + new TempAlbumImagesUploadUrlRequest( List.of( - new TempAlbumImageUploadRequest.Payload( + new TempAlbumImagesUploadUrlRequest.Payload( FileExtension.JPEG, "testMd5Hash1", new BigDecimal("0.3")), - new TempAlbumImageUploadRequest.Payload( + new TempAlbumImagesUploadUrlRequest.Payload( FileExtension.JPEG, "testMd5Hash2", new BigDecimal("0.3")))); @@ -1477,7 +1427,7 @@ class 임시_앨범_이미지_업로드_Presigned_URL을_생성_요청_시 { // when & then ResultActions perform = mockMvc.perform( - post("/temp-albums/1/images") + post("/temp-albums/1/upload-url") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))); @@ -1491,14 +1441,14 @@ class 임시_앨범_이미지_업로드_Presigned_URL을_생성_요청_시 { @Test void 임시_앨범의_남은_용량을_초과해서_요청하면_예외가_발생한다() throws Exception { // given - TempAlbumImageUploadRequest request = - new TempAlbumImageUploadRequest( + TempAlbumImagesUploadUrlRequest request = + new TempAlbumImagesUploadUrlRequest( List.of( - new TempAlbumImageUploadRequest.Payload( + new TempAlbumImagesUploadUrlRequest.Payload( FileExtension.JPEG, "testMd5Hash1", TempAlbumType.DEFAULT.getCapacityMb()), - new TempAlbumImageUploadRequest.Payload( + new TempAlbumImagesUploadUrlRequest.Payload( FileExtension.JPEG, "testMd5Hash2", BigDecimal.ONE))); given(imageService.createTempAlbumImageUploadUrls(1L, request)) @@ -1508,7 +1458,7 @@ class 임시_앨범_이미지_업로드_Presigned_URL을_생성_요청_시 { // when & then ResultActions perform = mockMvc.perform( - post("/temp-albums/1/images") + post("/temp-albums/1/upload-url") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))); @@ -1522,14 +1472,14 @@ class 임시_앨범_이미지_업로드_Presigned_URL을_생성_요청_시 { @Test void MD5_해시에_중복된_값이_존재하면_예외가_발생한다() throws Exception { // given - TempAlbumImageUploadRequest request = - new TempAlbumImageUploadRequest( + TempAlbumImagesUploadUrlRequest request = + new TempAlbumImagesUploadUrlRequest( List.of( - new TempAlbumImageUploadRequest.Payload( + new TempAlbumImagesUploadUrlRequest.Payload( FileExtension.JPEG, "testMd5Hash1", new BigDecimal("0.3")), - new TempAlbumImageUploadRequest.Payload( + new TempAlbumImagesUploadUrlRequest.Payload( FileExtension.JPEG, "testMd5Hash2", new BigDecimal("0.3")))); @@ -1540,7 +1490,7 @@ class 임시_앨범_이미지_업로드_Presigned_URL을_생성_요청_시 { // when & then ResultActions perform = mockMvc.perform( - post("/temp-albums/1/images") + post("/temp-albums/1/upload-url") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))); @@ -1557,10 +1507,10 @@ class 임시_앨범_이미지_업로드_Presigned_URL을_생성_요청_시 { @ValueSource(strings = {"JPEG1", "PDF", "TXT"}) void 파일_확장자가_null_또는_지원하지_않는_형식이면_예외가_발생한다(String extension) throws Exception { // given - TempAlbumImageUploadRequest request = - new TempAlbumImageUploadRequest( + TempAlbumImagesUploadUrlRequest request = + new TempAlbumImagesUploadUrlRequest( List.of( - new TempAlbumImageUploadRequest.Payload( + new TempAlbumImagesUploadUrlRequest.Payload( FileExtension.from(extension), "testMd5Hash1", BigDecimal.ONE))); @@ -1568,7 +1518,7 @@ class 임시_앨범_이미지_업로드_Presigned_URL을_생성_요청_시 { // when & then ResultActions perform = mockMvc.perform( - post("/temp-albums/1/images") + post("/temp-albums/1/upload-url") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))); @@ -1585,16 +1535,16 @@ class 임시_앨범_이미지_업로드_Presigned_URL을_생성_요청_시 { @Test void 이미지_용량을_비워두면_예외가_발생한다() throws Exception { // given - TempAlbumImageUploadRequest request = - new TempAlbumImageUploadRequest( + TempAlbumImagesUploadUrlRequest request = + new TempAlbumImagesUploadUrlRequest( List.of( - new TempAlbumImageUploadRequest.Payload( + new TempAlbumImagesUploadUrlRequest.Payload( FileExtension.JPEG, "testMd5Hash", null))); // when & then ResultActions perform = mockMvc.perform( - post("/temp-albums/1/images") + post("/temp-albums/1/upload-url") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))); @@ -1611,16 +1561,16 @@ class 임시_앨범_이미지_업로드_Presigned_URL을_생성_요청_시 { @ValueSource(strings = {" "}) void MD5_해시가_null_또는_공백이면_예외가_발생한다(String md5Hash) throws Exception { // given - TempAlbumImageUploadRequest request = - new TempAlbumImageUploadRequest( + TempAlbumImagesUploadUrlRequest request = + new TempAlbumImagesUploadUrlRequest( List.of( - new TempAlbumImageUploadRequest.Payload( + new TempAlbumImagesUploadUrlRequest.Payload( FileExtension.JPEG, md5Hash, BigDecimal.ONE))); // when & then ResultActions perform = mockMvc.perform( - post("/temp-albums/1/images") + post("/temp-albums/1/upload-url") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))); @@ -1634,12 +1584,13 @@ class 임시_앨범_이미지_업로드_Presigned_URL을_생성_요청_시 { @Test void 업로드_요청_정보를_비워두면_예외가_발생한다() throws Exception { // given - TempAlbumImageUploadRequest request = new TempAlbumImageUploadRequest(List.of()); + TempAlbumImagesUploadUrlRequest request = + new TempAlbumImagesUploadUrlRequest(List.of()); // when & then ResultActions perform = mockMvc.perform( - post("/temp-albums/1/images") + post("/temp-albums/1/upload-url") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(request))); @@ -1763,4 +1714,395 @@ class 임시_앨범_이미지_삭제_요청_시 { .value("삭제하고자 하는 임시 앨범 이미지 ID들은 비워둘 수 없습니다.")); } } + + @Nested + class 앨범_외_이미지_업로드_완료_요청_시 { + + @Test + void 유효한_요청이면_NO_CONTENT를_반환한다() throws Exception { + // given + ImageUploadCompleteRequest request = new ImageUploadCompleteRequest("testImageUrl"); + + willDoNothing().given(imageService).completeNonAlbumImageUpload(request); + + // when & then + ResultActions perform = + mockMvc.perform( + post("/images/upload-complete") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))); + + perform.andExpect(status().isNoContent()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.status").value(HttpStatus.NO_CONTENT.value())); + } + + @Test + void 이미지_업로드_실패_시_예외가_발생한다() throws Exception { + // given + ImageUploadCompleteRequest request = new ImageUploadCompleteRequest("testImageUrl"); + + willThrow(new CustomException(ImageErrorCode.IMAGE_UPLOAD_FAIL)) + .given(imageService) + .completeNonAlbumImageUpload(request); + + // when & then + ResultActions perform = + mockMvc.perform( + post("/images/upload-complete") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))); + + perform.andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(HttpStatus.BAD_REQUEST.value())) + .andExpect(jsonPath("$.data.code").value("IMAGE_UPLOAD_FAIL")) + .andExpect(jsonPath("$.data.message").value("이미지가 성공적으로 업로드 되지 못했습니다.")); + } + + @ParameterizedTest + @NullSource + @EmptySource + @ValueSource(strings = {" "}) + void 이미지_url을_비워두면_예외가_발생한다(String imageUrl) throws Exception { + // given + ImageUploadCompleteRequest request = new ImageUploadCompleteRequest(imageUrl); + + // when & then + ResultActions perform = + mockMvc.perform( + post("/images/upload-complete") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))); + + perform.andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(HttpStatus.BAD_REQUEST.value())) + .andExpect(jsonPath("$.data.code").value("MethodArgumentNotValidException")) + .andExpect(jsonPath("$.data.message").value("이미지 url을 비워둘 수 없습니다.")); + } + } + + @Nested + class 앨범_이미지_업로드_요청_시 { + + @Test + void 유효한_요청이면_생성된_이미지_ID와_로컬_이미지_삭제_여부를_반환한다() throws Exception { + // given + AlbumImagesUploadCompleteRequest request = + new AlbumImagesUploadCompleteRequest( + List.of( + new AlbumImagesUploadCompleteRequest.Payload( + LocalDateTime.now(), BigDecimal.ONE, "testImageUrl1"), + new AlbumImagesUploadCompleteRequest.Payload( + LocalDateTime.now(), BigDecimal.ONE, "testImageUrl2"))); + + AlbumImagesUploadCompleteResponse response = + new AlbumImagesUploadCompleteResponse(List.of(1L, 2L)); + + given(imageService.completeAlbumImagesUpload(1L, request)).willReturn(response); + + // when & then + ResultActions perform = + mockMvc.perform( + post("/albums/1/upload-complete") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))); + + perform.andExpect(status().isCreated()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.status").value(HttpStatus.CREATED.value())) + .andExpect(jsonPath("$.data.imageIds").value(Matchers.contains(1, 2))); + } + + @Test + void 앨범이_존재하지_않는_경우_예외가_발생한다() throws Exception { + // given + AlbumImagesUploadCompleteRequest request = + new AlbumImagesUploadCompleteRequest( + List.of( + new AlbumImagesUploadCompleteRequest.Payload( + LocalDateTime.now(), BigDecimal.ONE, "testImageUrl1"), + new AlbumImagesUploadCompleteRequest.Payload( + LocalDateTime.now(), BigDecimal.ONE, "testImageUrl2"))); + + given(imageService.completeAlbumImagesUpload(1L, request)) + .willThrow(new CustomException(AlbumErrorCode.ALBUM_NOT_FOUND)); + + // when & then + ResultActions perform = + mockMvc.perform( + post("/albums/1/upload-complete") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))); + + perform.andExpect(status().isNotFound()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(HttpStatus.NOT_FOUND.value())) + .andExpect(jsonPath("$.data.code").value("ALBUM_NOT_FOUND")) + .andExpect(jsonPath("$.data.message").value("앨범이 존재하지 않습니다.")); + } + + @Test + void 모든_이미지를_업로드_성공하지_않았을_경우_예외가_발생한다() throws Exception { + // given + AlbumImagesUploadCompleteRequest request = + new AlbumImagesUploadCompleteRequest( + List.of( + new AlbumImagesUploadCompleteRequest.Payload( + LocalDateTime.now(), BigDecimal.ONE, "testImageUrl1"), + new AlbumImagesUploadCompleteRequest.Payload( + LocalDateTime.now(), BigDecimal.ONE, "testImageUrl2"))); + + given(imageService.completeAlbumImagesUpload(1L, request)) + .willThrow(new CustomException(ImageErrorCode.IMAGE_UPLOAD_FAIL)); + + // when & then + ResultActions perform = + mockMvc.perform( + post("/albums/1/upload-complete") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))); + + perform.andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(HttpStatus.BAD_REQUEST.value())) + .andExpect(jsonPath("$.data.code").value("IMAGE_UPLOAD_FAIL")) + .andExpect(jsonPath("$.data.message").value("이미지가 성공적으로 업로드 되지 못했습니다.")); + } + + @Test + void 업로드_완료_하고자_하는_이미지들의_정보를_비워두면_예외가_발생한다() throws Exception { + // given + AlbumImagesUploadCompleteRequest request = + new AlbumImagesUploadCompleteRequest(List.of()); + + // when & then + ResultActions perform = + mockMvc.perform( + post("/albums/1/upload-complete") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))); + + perform.andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(HttpStatus.BAD_REQUEST.value())) + .andExpect(jsonPath("$.data.code").value("MethodArgumentNotValidException")) + .andExpect( + jsonPath("$.data.message").value("업로드 완료 하는 이미지들의 정보들은 비워둘 수 없습니다.")); + } + + @Test + void 업로드_하는_파일의_용량을_비워두면_예외가_발생한다() throws Exception { + // given + AlbumImagesUploadCompleteRequest request = + new AlbumImagesUploadCompleteRequest( + List.of( + new AlbumImagesUploadCompleteRequest.Payload( + LocalDateTime.now(), null, "testImageUrl1"))); + + // when & then + ResultActions perform = + mockMvc.perform( + post("/albums/1/upload-complete") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))); + + perform.andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(HttpStatus.BAD_REQUEST.value())) + .andExpect(jsonPath("$.data.code").value("MethodArgumentNotValidException")) + .andExpect(jsonPath("$.data.message").value("파일의 용량은 비워둘 수 없습니다.")); + } + + @ParameterizedTest + @NullSource + @EmptySource + @ValueSource(strings = {" "}) + void 업로드_완료_요청_이미지_url을_비워두면_예외가_발생한다(String imageUrl) throws Exception { + // given + AlbumImagesUploadCompleteRequest request = + new AlbumImagesUploadCompleteRequest( + List.of( + new AlbumImagesUploadCompleteRequest.Payload( + LocalDateTime.now(), BigDecimal.ONE, imageUrl))); + + // when & then + ResultActions perform = + mockMvc.perform( + post("/albums/1/upload-complete") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))); + + perform.andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(HttpStatus.BAD_REQUEST.value())) + .andExpect(jsonPath("$.data.code").value("MethodArgumentNotValidException")) + .andExpect( + jsonPath("$.data.message") + .value("업로드 완료 하고자 하는 imageUrl은 비워둘 수 없습니다.")); + } + } + + @Nested + class 임시_앨범_이미지_업로드_요청_시 { + + @Test + void 유효한_요청이면_생성된_임시_앨범_이미지_ID들을_반환한다() throws Exception { + // given + TempAlbumImagesUploadCompleteRequest request = + new TempAlbumImagesUploadCompleteRequest( + List.of( + new TempAlbumImagesUploadCompleteRequest.Payload( + BigDecimal.ONE, "testImageUrl1"), + new TempAlbumImagesUploadCompleteRequest.Payload( + BigDecimal.ONE, "testImageUrl2"))); + + TempAlbumImagesUploadCompleteResponse response = + new TempAlbumImagesUploadCompleteResponse(List.of(1L, 2L)); + + given(imageService.completeTempAlbumImagesUpload(1L, request)).willReturn(response); + + // when & then + ResultActions perform = + mockMvc.perform( + post("/temp-albums/1/upload-complete") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))); + + perform.andExpect(status().isCreated()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.status").value(HttpStatus.CREATED.value())) + .andExpect(jsonPath("$.data.tempAlbumImageIds").value(Matchers.contains(1, 2))); + } + + @Test + void 임시_앨범이_존재하지_않는_경우_예외가_발생한다() throws Exception { + // given + TempAlbumImagesUploadCompleteRequest request = + new TempAlbumImagesUploadCompleteRequest( + List.of( + new TempAlbumImagesUploadCompleteRequest.Payload( + BigDecimal.ONE, "testImageUrl1"), + new TempAlbumImagesUploadCompleteRequest.Payload( + BigDecimal.ONE, "testImageUrl2"))); + + given(imageService.completeTempAlbumImagesUpload(1L, request)) + .willThrow(new CustomException(TempAlbumErrorCode.TEMP_ALBUM_NOT_FOUND)); + + // when & then + ResultActions perform = + mockMvc.perform( + post("/temp-albums/1/upload-complete") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))); + + perform.andExpect(status().isNotFound()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(HttpStatus.NOT_FOUND.value())) + .andExpect(jsonPath("$.data.code").value("TEMP_ALBUM_NOT_FOUND")) + .andExpect(jsonPath("$.data.message").value("임시 앨범이 존재하지 않습니다.")); + } + + @Test + void 모든_이미지를_업로드_성공하지_않았을_경우_예외가_발생한다() throws Exception { + // given + TempAlbumImagesUploadCompleteRequest request = + new TempAlbumImagesUploadCompleteRequest( + List.of( + new TempAlbumImagesUploadCompleteRequest.Payload( + BigDecimal.ONE, "testImageUrl1"), + new TempAlbumImagesUploadCompleteRequest.Payload( + BigDecimal.ONE, "testImageUrl2"))); + + given(imageService.completeTempAlbumImagesUpload(1L, request)) + .willThrow(new CustomException(ImageErrorCode.IMAGE_UPLOAD_FAIL)); + + // when & then + ResultActions perform = + mockMvc.perform( + post("/temp-albums/1/upload-complete") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))); + + perform.andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(HttpStatus.BAD_REQUEST.value())) + .andExpect(jsonPath("$.data.code").value("IMAGE_UPLOAD_FAIL")) + .andExpect(jsonPath("$.data.message").value("이미지가 성공적으로 업로드 되지 못했습니다.")); + } + + @Test + void 업로드_완료_하고자_하는_이미지들의_정보를_비워두면_예외가_발생한다() throws Exception { + // given + TempAlbumImagesUploadCompleteRequest request = + new TempAlbumImagesUploadCompleteRequest(List.of()); + + // when & then + ResultActions perform = + mockMvc.perform( + post("/temp-albums/1/upload-complete") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))); + + perform.andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(HttpStatus.BAD_REQUEST.value())) + .andExpect(jsonPath("$.data.code").value("MethodArgumentNotValidException")) + .andExpect( + jsonPath("$.data.message") + .value("업로드 완료 하고자 하는 임시 앨범 이미지들의 정보들은 비워둘 수 없습니다.")); + } + + @Test + void 업로드_하는_파일의_용량을_비워두면_예외가_발생한다() throws Exception { + // given + TempAlbumImagesUploadCompleteRequest request = + new TempAlbumImagesUploadCompleteRequest( + List.of( + new TempAlbumImagesUploadCompleteRequest.Payload( + null, "testImageUrl1"))); + + // when & then + ResultActions perform = + mockMvc.perform( + post("/temp-albums/1/upload-complete") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))); + + perform.andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(HttpStatus.BAD_REQUEST.value())) + .andExpect(jsonPath("$.data.code").value("MethodArgumentNotValidException")) + .andExpect(jsonPath("$.data.message").value("파일의 용량은 비워둘 수 없습니다.")); + } + + @ParameterizedTest + @NullSource + @EmptySource + @ValueSource(strings = {" "}) + void 업로드_완료_요청_이미지_url을_비워두면_예외가_발생한다(String imageUrl) throws Exception { + // given + TempAlbumImagesUploadCompleteRequest request = + new TempAlbumImagesUploadCompleteRequest( + List.of( + new TempAlbumImagesUploadCompleteRequest.Payload( + BigDecimal.ONE, imageUrl))); + + // when & then + ResultActions perform = + mockMvc.perform( + post("/temp-albums/1/upload-complete") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))); + + perform.andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.status").value(HttpStatus.BAD_REQUEST.value())) + .andExpect(jsonPath("$.data.code").value("MethodArgumentNotValidException")) + .andExpect( + jsonPath("$.data.message") + .value("업로드 완료 하고자 하는 tempAlbumImageUrl은 비워둘 수 없습니다.")); + } + } } diff --git a/cherrypic-api/src/test/java/org/cherrypic/image/service/ImageServiceTest.java b/cherrypic-api/src/test/java/org/cherrypic/image/service/ImageServiceTest.java index 72f0e216..55968108 100644 --- a/cherrypic-api/src/test/java/org/cherrypic/image/service/ImageServiceTest.java +++ b/cherrypic-api/src/test/java/org/cherrypic/image/service/ImageServiceTest.java @@ -1,8 +1,10 @@ package org.cherrypic.image.service; import static org.assertj.core.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.mockito.ArgumentMatchers.*; import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willDoNothing; import java.math.BigDecimal; import java.time.LocalDateTime; @@ -93,7 +95,8 @@ void setUp() { @Test void 유효한_요청이면_회원_프로필_이미지용_Presigned_URL을_생성한다() { // given - ImageUploadRequest request = new ImageUploadRequest(FileExtension.JPEG, "testMd5Hash"); + ImageUploadUrlRequest request = + new ImageUploadUrlRequest(FileExtension.JPEG, "testMd5Hash"); given( s3Util.createPresignedUrl( ImageType.MEMBER_PROFILE, @@ -112,7 +115,8 @@ void setUp() { + " + \"&Content-MD5=testMd5Hash\""); // when - PresignedUrlResponse response = imageService.createMemberProfileImageUploadUrl(request); + ImagePresignedUrlResponse response = + imageService.createMemberProfileImageUploadUrl(request); // then assertThat(response.presignedUrl()) @@ -124,7 +128,8 @@ void setUp() { @Test void 동영상_확장자를_입력할_경우_예외가_발생한다() { // given - ImageUploadRequest request = new ImageUploadRequest(FileExtension.MKV, "testMd5Hash"); + ImageUploadUrlRequest request = + new ImageUploadUrlRequest(FileExtension.MKV, "testMd5Hash"); // when & then assertThatThrownBy(() -> imageService.createMemberProfileImageUploadUrl(request)) @@ -161,7 +166,8 @@ void setUp() { @Test void 유효한_요청이면_앨범_커버_이미지용_Presigned_URL을_생성한다() { // given - ImageUploadRequest request = new ImageUploadRequest(FileExtension.JPEG, "testMd5Hash"); + ImageUploadUrlRequest request = + new ImageUploadUrlRequest(FileExtension.JPEG, "testMd5Hash"); given( s3Util.createPresignedUrl( ImageType.ALBUM_COVER, 1L, FileExtension.JPEG, "testMd5Hash")) @@ -177,7 +183,8 @@ void setUp() { + "&Content-MD5=testMd5Hash"); // when - PresignedUrlResponse response = imageService.createAlbumCoverImageUploadUrl(request); + ImagePresignedUrlResponse response = + imageService.createAlbumCoverImageUploadUrl(request); // then assertThat(response.presignedUrl()) @@ -188,7 +195,8 @@ void setUp() { @Test void 동영상_확장자를_입력할_경우_예외가_발생한다() { // given - ImageUploadRequest request = new ImageUploadRequest(FileExtension.MKV, "testMd5Hash"); + ImageUploadUrlRequest request = + new ImageUploadUrlRequest(FileExtension.MKV, "testMd5Hash"); // when & then assertThatThrownBy(() -> imageService.createAlbumCoverImageUploadUrl(request)) @@ -230,7 +238,8 @@ void setUp() { @Test void 유효한_요청이면_이벤트_커버_이미지용_Presigned_URL을_생성한다() { // given - ImageUploadRequest request = new ImageUploadRequest(FileExtension.JPEG, "testMd5Hash"); + ImageUploadUrlRequest request = + new ImageUploadUrlRequest(FileExtension.JPEG, "testMd5Hash"); given( s3Util.createPresignedUrl( ImageType.EVENT_COVER, 1L, FileExtension.JPEG, "testMd5Hash")) @@ -246,7 +255,8 @@ void setUp() { + "&Content-MD5=testMd5Hash"); // when - PresignedUrlResponse response = imageService.createEventCoverImageUploadUrl(request); + ImagePresignedUrlResponse response = + imageService.createEventCoverImageUploadUrl(request); // then assertThat(response.presignedUrl()) @@ -257,7 +267,8 @@ void setUp() { @Test void 동영상_확장자를_입력할_경우_예외가_발생한다() { // given - ImageUploadRequest request = new ImageUploadRequest(FileExtension.MKV, "testMd5Hash"); + ImageUploadUrlRequest request = + new ImageUploadUrlRequest(FileExtension.MKV, "testMd5Hash"); // when & then assertThatThrownBy(() -> imageService.createEventCoverImageUploadUrl(request)) @@ -301,21 +312,15 @@ void setUp() { } @Test - void 유효한_요청이면_이미지를_저장하고_Presigned_URL들을_반환한다() { + void 유효한_요청이면_Presigned_URL들을_반환한다() { // given - AlbumImageUploadRequest request = - new AlbumImageUploadRequest( + AlbumImagesUploadUrlRequest request = + new AlbumImagesUploadUrlRequest( List.of( - new AlbumImageUploadRequest.Payload( - FileExtension.JPEG, - "testMd5Hash1", - LocalDateTime.now(), - BigDecimal.ONE), - new AlbumImageUploadRequest.Payload( - FileExtension.JPEG, - "testMd5Hash2", - LocalDateTime.now(), - BigDecimal.ONE))); + new AlbumImagesUploadUrlRequest.Payload( + FileExtension.JPEG, "testMd5Hash1", BigDecimal.ONE), + new AlbumImagesUploadUrlRequest.Payload( + FileExtension.JPEG, "testMd5Hash2", BigDecimal.ONE))); given( s3Util.createPresignedUrl( eq(ImageType.ALBUM_IMAGE), @@ -343,57 +348,35 @@ void setUp() { + "&Content-MD5=testMd5Hash2"); // when - ImageUploadListResponse response = imageService.createAlbumImageUploadUrls(1L, request); + AlbumImagesPresignedUrlResponse response = + imageService.createAlbumImageUploadUrls(1L, request); // then - assertThat(response.content()) + assertThat(response.urls()) .hasSize(2) .satisfiesExactly( payload1 -> { - assertThat(payload1.imageId()).isEqualTo(1L); - assertThat(payload1.presignedUrl()) + assertThat(payload1) .containsPattern( ".*/local/album-image/1/[\\w\\-]+\\.(jpg|jpeg)\\?.+"); }, payload2 -> { - assertThat(payload2.imageId()).isEqualTo(2L); - assertThat(payload2.presignedUrl()) + assertThat(payload2) .containsPattern( ".*/local/album-image/1/[\\w\\-]+\\.(jpg|jpeg)\\?.+"); }); - assertThat(response.localImageDeletion()).isFalse(); - - List images = imageRepository.findAll(); - assertThat(images) - .hasSize(2) - .allSatisfy( - image -> { - assertThat(image.getAlbum().getId()).isEqualTo(1L); - assertThat(image.getMemberId()).isEqualTo(1L); - assertThat(image.getUrl()) - .containsPattern( - String.format( - "/%s/%s/%d/[\\w\\-]+\\.(jpg|jpeg)", - "local", "album-image", 1)); - }); } @Test void 앨범이_존재하지_않는_경우_예외가_발생한다() { // given - AlbumImageUploadRequest request = - new AlbumImageUploadRequest( + AlbumImagesUploadUrlRequest request = + new AlbumImagesUploadUrlRequest( List.of( - new AlbumImageUploadRequest.Payload( - FileExtension.JPEG, - "testMd5Hash1", - LocalDateTime.now(), - BigDecimal.ZERO), - new AlbumImageUploadRequest.Payload( - FileExtension.JPEG, - "testMd5Hash2", - LocalDateTime.now(), - BigDecimal.ZERO))); + new AlbumImagesUploadUrlRequest.Payload( + FileExtension.JPEG, "testMd5Hash1", BigDecimal.ONE), + new AlbumImagesUploadUrlRequest.Payload( + FileExtension.JPEG, "testMd5Hash2", BigDecimal.ONE))); // when & then assertThatThrownBy(() -> imageService.createAlbumImageUploadUrls(999L, request)) @@ -404,19 +387,13 @@ void setUp() { @Test void 앨범에_속하지_않은_사용자가_앨범_이미지_업로드_URL을_요청하면_예외가_발생한다() { // given - AlbumImageUploadRequest request = - new AlbumImageUploadRequest( + AlbumImagesUploadUrlRequest request = + new AlbumImagesUploadUrlRequest( List.of( - new AlbumImageUploadRequest.Payload( - FileExtension.JPEG, - "testMd5Hash1", - LocalDateTime.now(), - BigDecimal.ZERO), - new AlbumImageUploadRequest.Payload( - FileExtension.JPEG, - "testMd5Hash2", - LocalDateTime.now(), - BigDecimal.ZERO))); + new AlbumImagesUploadUrlRequest.Payload( + FileExtension.JPEG, "testMd5Hash1", BigDecimal.ONE), + new AlbumImagesUploadUrlRequest.Payload( + FileExtension.JPEG, "testMd5Hash2", BigDecimal.ONE))); // when & then assertThatThrownBy(() -> imageService.createAlbumImageUploadUrls(3L, request)) @@ -427,19 +404,13 @@ void setUp() { @Test void LIMITED_권한의_사용자가_앨범_이미지_업로드_URL을_요청하면_예외가_발생한다() { // given - AlbumImageUploadRequest request = - new AlbumImageUploadRequest( + AlbumImagesUploadUrlRequest request = + new AlbumImagesUploadUrlRequest( List.of( - new AlbumImageUploadRequest.Payload( - FileExtension.JPEG, - "testMd5Hash1", - LocalDateTime.now(), - BigDecimal.ZERO), - new AlbumImageUploadRequest.Payload( - FileExtension.JPEG, - "testMd5Hash2", - LocalDateTime.now(), - BigDecimal.ZERO))); + new AlbumImagesUploadUrlRequest.Payload( + FileExtension.JPEG, "testMd5Hash1", BigDecimal.ONE), + new AlbumImagesUploadUrlRequest.Payload( + FileExtension.JPEG, "testMd5Hash2", BigDecimal.ONE))); // when & then assertThatThrownBy(() -> imageService.createAlbumImageUploadUrls(2L, request)) @@ -450,19 +421,13 @@ void setUp() { @Test void 구독이_만료된_앨범인_경우_예외가_발생한다() { // given - AlbumImageUploadRequest request = - new AlbumImageUploadRequest( + AlbumImagesUploadUrlRequest request = + new AlbumImagesUploadUrlRequest( List.of( - new AlbumImageUploadRequest.Payload( - FileExtension.JPEG, - "testMd5Hash1", - LocalDateTime.now(), - BigDecimal.ZERO), - new AlbumImageUploadRequest.Payload( - FileExtension.JPEG, - "testMd5Hash2", - LocalDateTime.now(), - BigDecimal.ZERO))); + new AlbumImagesUploadUrlRequest.Payload( + FileExtension.JPEG, "testMd5Hash1", BigDecimal.ONE), + new AlbumImagesUploadUrlRequest.Payload( + FileExtension.JPEG, "testMd5Hash2", BigDecimal.ONE))); // when & then assertThatThrownBy(() -> imageService.createAlbumImageUploadUrls(4L, request)) @@ -473,19 +438,15 @@ void setUp() { @Test void 앨범의_남은_용량을_초과해서_요청하면_예외가_발생한다() { /// given - AlbumImageUploadRequest request = - new AlbumImageUploadRequest( + AlbumImagesUploadUrlRequest request = + new AlbumImagesUploadUrlRequest( List.of( - new AlbumImageUploadRequest.Payload( + new AlbumImagesUploadUrlRequest.Payload( FileExtension.JPEG, "testMd5Hash1", - LocalDateTime.now(), AlbumType.BASIC.getCapacityMb()), - new AlbumImageUploadRequest.Payload( - FileExtension.JPEG, - "testMd5Hash2", - LocalDateTime.now(), - BigDecimal.ONE))); + new AlbumImagesUploadUrlRequest.Payload( + FileExtension.JPEG, "testMd5Hash2", BigDecimal.ONE))); // when & then assertThatThrownBy(() -> imageService.createAlbumImageUploadUrls(1L, request)) @@ -496,19 +457,13 @@ void setUp() { @Test void 해시값에_중복이_존재하면_예외가_발생한다() { // given - AlbumImageUploadRequest request = - new AlbumImageUploadRequest( + AlbumImagesUploadUrlRequest request = + new AlbumImagesUploadUrlRequest( List.of( - new AlbumImageUploadRequest.Payload( - FileExtension.JPEG, - "testMd5Hash1", - LocalDateTime.now(), - BigDecimal.ZERO), - new AlbumImageUploadRequest.Payload( - FileExtension.JPEG, - "testMd5Hash1", - LocalDateTime.now(), - BigDecimal.ZERO))); + new AlbumImagesUploadUrlRequest.Payload( + FileExtension.JPEG, "testMd5Hash1", BigDecimal.ZERO), + new AlbumImagesUploadUrlRequest.Payload( + FileExtension.JPEG, "testMd5Hash1", BigDecimal.ZERO))); // when & then assertThatThrownBy(() -> imageService.createAlbumImageUploadUrls(1L, request)) .isInstanceOf(CustomException.class) @@ -1009,16 +964,16 @@ void setUp() { } @Test - void 유효한_요청이면_임시_앨범_이미지를_저장하고_Presigned_URL들을_반환한다() { + void 유효한_요청이면_Presigned_URL들을_반환한다() { // given - TempAlbumImageUploadRequest request = - new TempAlbumImageUploadRequest( + TempAlbumImagesUploadUrlRequest request = + new TempAlbumImagesUploadUrlRequest( List.of( - new TempAlbumImageUploadRequest.Payload( + new TempAlbumImagesUploadUrlRequest.Payload( FileExtension.JPEG, "testMd5Hash1", new BigDecimal("0.3")), - new TempAlbumImageUploadRequest.Payload( + new TempAlbumImagesUploadUrlRequest.Payload( FileExtension.JPEG, "testMd5Hash2", new BigDecimal("0.3")))); @@ -1049,49 +1004,34 @@ void setUp() { + "&Content-MD5=testMd5Hash2"); // when - TempAlbumImageUploadListResponse response = + TempAlbumImagesPresignedUrlResponse response = imageService.createTempAlbumImageUploadUrls(1L, request); // then - assertThat(response.content()) + assertThat(response.urls()) .hasSize(2) .satisfiesExactly( payload1 -> { - assertThat(payload1.tempAlbumImageId()).isEqualTo(1L); - assertThat(payload1.presignedUrl()) + assertThat(payload1) .containsPattern( ".*/local/temp-album-image/1/[\\w\\-]+\\.(jpg|jpeg)\\?.+"); }, payload2 -> { - assertThat(payload2.tempAlbumImageId()).isEqualTo(2L); - assertThat(payload2.presignedUrl()) + assertThat(payload2) .containsPattern( ".*/local/temp-album-image/1/[\\w\\-]+\\.(jpg|jpeg)\\?.+"); }); - - List tempAlbumImages = tempAlbumImageRepository.findAll(); - assertThat(tempAlbumImages) - .hasSize(2) - .allSatisfy( - tempAlbumImage -> { - assertThat(tempAlbumImage.getTempAlbum().getId()).isEqualTo(1L); - assertThat(tempAlbumImage.getUrl()) - .containsPattern( - String.format( - "/%s/%s/%d/[\\w\\-]+\\.(jpg|jpeg)", - "local", "temp-album-image", 1)); - }); } @Test void 임시_앨범이_존재하지_않는_경우_예외가_발생한다() { // given - TempAlbumImageUploadRequest request = - new TempAlbumImageUploadRequest( + TempAlbumImagesUploadUrlRequest request = + new TempAlbumImagesUploadUrlRequest( List.of( - new TempAlbumImageUploadRequest.Payload( + new TempAlbumImagesUploadUrlRequest.Payload( FileExtension.JPEG, "testMd5Hash1", BigDecimal.ZERO), - new TempAlbumImageUploadRequest.Payload( + new TempAlbumImagesUploadUrlRequest.Payload( FileExtension.JPEG, "testMd5Hash2", BigDecimal.ZERO))); // when & then @@ -1103,12 +1043,12 @@ void setUp() { @Test void 임시_앨범의_소유자가_아닌_사람이_이미지_업로드를_시도하면_예외가_발생한다() { // given - TempAlbumImageUploadRequest request = - new TempAlbumImageUploadRequest( + TempAlbumImagesUploadUrlRequest request = + new TempAlbumImagesUploadUrlRequest( List.of( - new TempAlbumImageUploadRequest.Payload( + new TempAlbumImagesUploadUrlRequest.Payload( FileExtension.JPEG, "testMd5Hash1", BigDecimal.ZERO), - new TempAlbumImageUploadRequest.Payload( + new TempAlbumImagesUploadUrlRequest.Payload( FileExtension.JPEG, "testMd5Hash2", BigDecimal.ZERO))); // when & then @@ -1120,14 +1060,14 @@ void setUp() { @Test void 임시_앨범의_남은_용량을_초과해서_요청하면_예외가_발생한다() { // given - TempAlbumImageUploadRequest request = - new TempAlbumImageUploadRequest( + TempAlbumImagesUploadUrlRequest request = + new TempAlbumImagesUploadUrlRequest( List.of( - new TempAlbumImageUploadRequest.Payload( + new TempAlbumImagesUploadUrlRequest.Payload( FileExtension.JPEG, "testMd5Hash1", TempAlbumType.DEFAULT.getCapacityMb()), - new TempAlbumImageUploadRequest.Payload( + new TempAlbumImagesUploadUrlRequest.Payload( FileExtension.JPEG, "testMd5Hash2", BigDecimal.ONE))); // when & then @@ -1139,12 +1079,12 @@ void setUp() { @Test void 해시값에_중복이_존재하면_예외가_발생한다() { // given - TempAlbumImageUploadRequest request = - new TempAlbumImageUploadRequest( + TempAlbumImagesUploadUrlRequest request = + new TempAlbumImagesUploadUrlRequest( List.of( - new TempAlbumImageUploadRequest.Payload( + new TempAlbumImagesUploadUrlRequest.Payload( FileExtension.JPEG, "testMd5Hash1", BigDecimal.ZERO), - new TempAlbumImageUploadRequest.Payload( + new TempAlbumImagesUploadUrlRequest.Payload( FileExtension.JPEG, "testMd5Hash1", BigDecimal.ZERO))); // when & then assertThatThrownBy(() -> imageService.createTempAlbumImageUploadUrls(1L, request)) @@ -1251,4 +1191,198 @@ void setUp() { .hasMessage(TempAlbumErrorCode.IMAGES_NOT_IN_TEMP_ALBUM.getMessage()); } } + + @Nested + class 앨범_외_이미지_업로드_완료_요청할_때 { + + @Test + void 업로드_성공_시_예외가_발생하지_않는다() { + // given + ImageUploadCompleteRequest request = new ImageUploadCompleteRequest("testImageUrl"); + given(s3Util.doesFileExistByUrl("testImageUrl")).willReturn(true); + + // when & then + assertDoesNotThrow(() -> imageService.completeNonAlbumImageUpload(request)); + } + + @Test + void 업로드_실패_시_예외가_발생한다() { + // given + ImageUploadCompleteRequest request = new ImageUploadCompleteRequest("testImageUrl"); + given(s3Util.doesFileExistByUrl("testImageUrl")).willReturn(false); + + // when & then + assertThatThrownBy(() -> imageService.completeNonAlbumImageUpload(request)) + .isInstanceOf(CustomException.class) + .hasMessage(ImageErrorCode.IMAGE_UPLOAD_FAIL.getMessage()); + } + } + + @Nested + class 앨범_이미지_업로드_완료_요청할_때 { + + @BeforeEach + void setUp() { + Member member = + Member.createMember( + OauthInfo.createOauthInfo("testOauthId", "testOauthProvider"), + "testNickname", + "testProfileImageUrl"); + memberRepository.save(member); + given(memberUtil.getCurrentMember()).willReturn(member); + + Album album = Album.createAlbum("testTitle1", "testCoverUrl1", AlbumType.BASIC, false); + album.increaseCapacity(BigDecimal.ONE); + albumRepository.save(album); + } + + @Test + void 유효한_요청이면_이미지를_생성한다() { + // given + AlbumImagesUploadCompleteRequest request = + new AlbumImagesUploadCompleteRequest( + List.of( + new AlbumImagesUploadCompleteRequest.Payload( + LocalDateTime.now(), BigDecimal.ONE, "testImageUrl1"), + new AlbumImagesUploadCompleteRequest.Payload( + LocalDateTime.now(), BigDecimal.ONE, "testImageUrl2"))); + + given(s3Util.doAllFilesExistByUrls(List.of("testImageUrl1", "testImageUrl2"))) + .willReturn(true); + willDoNothing() + .given(s3Util) + .updateTagsToCompleteByUrls(List.of("testImageUrl1", "testImageUrl2")); + + // when + AlbumImagesUploadCompleteResponse response = + imageService.completeAlbumImagesUpload(1L, request); + + // then + Assertions.assertAll( + () -> assertThat(response.imageIds()).isEqualTo(List.of(1L, 2L)), + () -> + assertThat(imageRepository.findAllById(List.of(1L, 2L)).size()) + .isEqualTo(2)); + } + + @Test + void 앨범이_존재하지_않는_경우_예외가_발생한다() { + // given + AlbumImagesUploadCompleteRequest request = + new AlbumImagesUploadCompleteRequest( + List.of( + new AlbumImagesUploadCompleteRequest.Payload( + LocalDateTime.now(), BigDecimal.ONE, "testImageUrl1"), + new AlbumImagesUploadCompleteRequest.Payload( + LocalDateTime.now(), BigDecimal.ONE, "testImageUrl2"))); + + // when & then + assertThatThrownBy(() -> imageService.completeAlbumImagesUpload(999L, request)) + .isInstanceOf(CustomException.class) + .hasMessage(AlbumErrorCode.ALBUM_NOT_FOUND.getMessage()); + } + + @Test + void 모든_이미지를_업로드_성공하지_않았을_경우_예외가_발생한다() { + // given + AlbumImagesUploadCompleteRequest request = + new AlbumImagesUploadCompleteRequest( + List.of( + new AlbumImagesUploadCompleteRequest.Payload( + LocalDateTime.now(), BigDecimal.ONE, "testImageUrl1"), + new AlbumImagesUploadCompleteRequest.Payload( + LocalDateTime.now(), BigDecimal.ONE, "testImageUrl2"))); + given(s3Util.doAllFilesExistByUrls(List.of("testImageUrl1", "testImageUrl2"))) + .willReturn(false); + + // when & then + assertThatThrownBy(() -> imageService.completeAlbumImagesUpload(1L, request)) + .isInstanceOf(CustomException.class) + .hasMessage(ImageErrorCode.IMAGE_UPLOAD_FAIL.getMessage()); + } + } + + @Nested + class 임시_앨범_이미지_업로드_완료_요청할_때 { + + @BeforeEach + void setUp() { + Member member = + Member.createMember( + OauthInfo.createOauthInfo("testOauthId", "testOauthProvider"), + "testNickname1", + "testProfileImageUrl1"); + memberRepository.save(member); + given(memberUtil.getCurrentMember()).willReturn(member); + + TempAlbum tempAlbum = TempAlbum.createTempAlbum(member, "testTitle1"); + tempAlbumRepository.save(tempAlbum); + } + + @Test + void 유효한_요청이면_임시_앨범_이미지를_생성한다() { + // given + TempAlbumImagesUploadCompleteRequest request = + new TempAlbumImagesUploadCompleteRequest( + List.of( + new TempAlbumImagesUploadCompleteRequest.Payload( + BigDecimal.ONE, "testImageUrl1"), + new TempAlbumImagesUploadCompleteRequest.Payload( + BigDecimal.ONE, "testImageUrl2"))); + + given(s3Util.doAllFilesExistByUrls(List.of("testImageUrl1", "testImageUrl2"))) + .willReturn(true); + willDoNothing() + .given(s3Util) + .updateTagsToCompleteByUrls(List.of("testImageUrl1", "testImageUrl2")); + + // when + TempAlbumImagesUploadCompleteResponse response = + imageService.completeTempAlbumImagesUpload(1L, request); + + // then + Assertions.assertAll( + () -> assertThat(response.tempAlbumImageIds()).isEqualTo(List.of(1L, 2L)), + () -> + assertThat(tempAlbumImageRepository.findAllById(List.of(1L, 2L)).size()) + .isEqualTo(2)); + } + + @Test + void 임시_앨범이_존재하지_않는_경우_예외가_발생한다() { + // given + TempAlbumImagesUploadCompleteRequest request = + new TempAlbumImagesUploadCompleteRequest( + List.of( + new TempAlbumImagesUploadCompleteRequest.Payload( + BigDecimal.ONE, "testImageUrl1"), + new TempAlbumImagesUploadCompleteRequest.Payload( + BigDecimal.ONE, "testImageUrl2"))); + + // when & then + assertThatThrownBy(() -> imageService.completeTempAlbumImagesUpload(999L, request)) + .isInstanceOf(CustomException.class) + .hasMessage(TempAlbumErrorCode.TEMP_ALBUM_NOT_FOUND.getMessage()); + } + + @Test + void 모든_이미지를_업로드_성공하지_않았을_경우_예외가_발생한다() { + // given + TempAlbumImagesUploadCompleteRequest request = + new TempAlbumImagesUploadCompleteRequest( + List.of( + new TempAlbumImagesUploadCompleteRequest.Payload( + BigDecimal.ONE, "testImageUrl1"), + new TempAlbumImagesUploadCompleteRequest.Payload( + BigDecimal.ONE, "testImageUrl2"))); + + given(s3Util.doAllFilesExistByUrls(List.of("testImageUrl1", "testImageUrl2"))) + .willReturn(false); + + // when & then + assertThatThrownBy(() -> imageService.completeTempAlbumImagesUpload(1L, request)) + .isInstanceOf(CustomException.class) + .hasMessage(ImageErrorCode.IMAGE_UPLOAD_FAIL.getMessage()); + } + } } diff --git a/cherrypic-infrastructure/src/main/java/org/cherrypic/s3/S3Util.java b/cherrypic-infrastructure/src/main/java/org/cherrypic/s3/S3Util.java index 1a0b4fb7..3b7f4779 100644 --- a/cherrypic-infrastructure/src/main/java/org/cherrypic/s3/S3Util.java +++ b/cherrypic-infrastructure/src/main/java/org/cherrypic/s3/S3Util.java @@ -1,6 +1,7 @@ package org.cherrypic.s3; import com.amazonaws.HttpMethod; +import com.amazonaws.SdkClientException; import com.amazonaws.services.s3.AmazonS3; import com.amazonaws.services.s3.Headers; import com.amazonaws.services.s3.model.*; @@ -61,11 +62,59 @@ private GeneratePresignedUrlRequest generatePresignedUrlRequest( generatePresignedUrlRequest.addRequestParameter( Headers.S3_CANNED_ACL, CannedAccessControlList.PublicRead.toString()); + generatePresignedUrlRequest.addRequestParameter("x-amz-tagging", "status=pending"); + generatePresignedUrlRequest.setContentMd5(base64Md5); return generatePresignedUrlRequest; } + public void updateTagToCompleteByUrl(String url) { + String bucket = s3Properties.bucket(); + String key = extractObjectKey(url); + + List tags = List.of(new Tag("status", "complete")); + ObjectTagging tagging = new ObjectTagging(tags); + + SetObjectTaggingRequest request = new SetObjectTaggingRequest(bucket, key, tagging); + amazonS3.setObjectTagging(request); + } + + public void updateTagsToCompleteByUrls(List urls) { + for (String url : urls) { + updateTagToCompleteByUrl(url); + } + } + + public boolean doesFileExistByUrl(String url) { + String bucket = s3Properties.bucket(); + String key = extractObjectKey(url); + try { + return amazonS3.doesObjectExist(bucket, key); + } catch (AmazonS3Exception e) { + if (e.getStatusCode() == 403) { + log.warn("Access denied for key={}, treating as non-existent", key); + return false; + } + log.error("S3 error while checking existence: {}", e.getErrorMessage(), e); + return false; + } catch (SdkClientException e) { + log.error("Network error while connecting to S3: {}", e.getMessage()); + return false; + } + } + + public boolean doAllFilesExistByUrls(List urls) { + for (String url : urls) { + if (!doesFileExistByUrl(url)) { + log.warn("File not found or inaccessible: {}", url); + return false; + } + } + + return true; + } + public void deleteAllByUrls(List urls) { if (urls == null || urls.isEmpty()) { log.info("deleteAllByUrls skipped: received null or empty urls");