Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,10 @@
import org.cherrypic.domain.image.dto.request.AlbumFileUploadRequest;
import org.cherrypic.domain.image.dto.request.AlbumImageDeleteRequest;
import org.cherrypic.domain.image.dto.request.ImageUploadRequest;
import org.cherrypic.domain.image.dto.request.UploadFailedFileDeleteRequest;
import org.cherrypic.domain.image.dto.response.AlbumImageListResponse;
import org.cherrypic.domain.image.dto.response.EventImageListResponse;
import org.cherrypic.domain.image.dto.response.PresignedUrlResponse;
import org.cherrypic.domain.image.dto.response.PresignedUrlsResponse;
import org.cherrypic.domain.image.dto.response.UploadFileListResponse;
import org.cherrypic.domain.image.service.ImageService;
import org.cherrypic.global.annotation.PageSize;
import org.cherrypic.global.pagination.SliceResponse;
Expand Down Expand Up @@ -60,21 +59,11 @@ public PresignedUrlResponse eventCoverImageUploadUrlCreate(
@Operation(
summary = "앨범 이미지 업로드 Presigned URL들 생성",
description = "앨범 이미지 업로드를 위한 Presigned URL들을 생성합니다.")
public PresignedUrlsResponse albumFileUploadUrlsCreate(
public UploadFileListResponse albumFileUploadUrlsCreate(
@PathVariable Long albumId, @Valid @RequestBody AlbumFileUploadRequest request) {
return imageService.createAlbumFileUploadUrls(albumId, request);
}

@DeleteMapping("/images")
@Operation(
summary = "업로드 실패한 파일 삭제",
description = "업로드를 실패한 Presigned URL을 기반으로 동영상 & 이미지를 삭제합니다.")
public ResponseEntity<Void> uploadFailedFileDelete(
@Valid @RequestBody UploadFailedFileDeleteRequest request) {
imageService.deleteUploadFailedFile(request);
return ResponseEntity.noContent().build();
}

@GetMapping("/albums/{albumId}/images")
@Operation(summary = "앨범 이미지 목록 조회", description = "앨범의 이미지 목록을 조회합니다.")
public SliceResponse<AlbumImageListResponse> albumImagesGet(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,6 @@
import org.cherrypic.global.annotation.Enum;

public record AlbumFileUploadRequest(
@NotNull(message = "파일들의 용량은 비워둘 수 없습니다.")
@Schema(description = "업로드 하는 파일들의 용량 총합(GB)", example = "1.23")
BigDecimal capacity,
@NotEmpty(message = "업로드할 피일들의 정보는 비워둘 수 없습니다.") @Valid @Schema(description = "업로드 요청 리스트")
List<Payload> payloads) {
public record Payload(
Expand All @@ -27,5 +24,8 @@ public record Payload(
@NotBlank(message = "MD5 해시값은 비워둘 수 없습니다.")
@Schema(description = "S3 업로드시 파일의 변형을 확인하기 위한 md5 해시")
String md5Hashes,
@Schema(description = "파일이 찍힌 시간, 정보가 없다면 null을 넣어주세요.") LocalDateTime generatedAt) {}
@Schema(description = "파일이 찍힌 시간, 정보가 없다면 null을 넣어주세요.") LocalDateTime generatedAt,
@NotNull(message = "파일의 용량은 비워둘 수 없습니다.")
@Schema(description = "업로드 하는 파일의 용량(GB)", example = "0.04")
BigDecimal capacity) {}
}

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package org.cherrypic.domain.image.dto.response;

import io.swagger.v3.oas.annotations.media.Schema;
import java.util.List;

public record UploadFileListResponse(
@Schema(description = "업로드된 파일들의 정보 리스트") List<Payload> payloads) {
public static UploadFileListResponse of(List<Payload> payloads) {
return new UploadFileListResponse(payloads);
}

public record Payload(
@Schema(description = "생성된 이미지의 ID") Long imageId,
@Schema(description = "생성된 Presigned Url") String presignedUrl) {
public static Payload of(Long imageId, String presignedUrl) {
return new Payload(imageId, presignedUrl);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,4 @@ public interface ImageRepository extends JpaRepository<Image, Long>, ImageReposi
long countByIdIn(List<Long> ids);

long countByIdInAndAlbumId(List<Long> imageIds, Long albumId);

List<Image> findByUrlIn(List<String> urls);
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,6 @@ Slice<AlbumImageListResponse> findAllByAlbumId(
List<Image> findAllUnmappedToEvent(Long eventId, List<Long> imageIds);

void bulkInsertImages(List<Image> images);

List<Long> findIdsByUrlsInOrder(List<String> urls);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

import com.querydsl.core.types.Projections;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.core.types.dsl.CaseBuilder;
import com.querydsl.core.types.dsl.NumberExpression;
import com.querydsl.jpa.impl.JPAQueryFactory;
import java.util.List;
import lombok.RequiredArgsConstructor;
Expand Down Expand Up @@ -80,8 +82,8 @@ public List<Image> findAllUnmappedToEvent(Long eventId, List<Long> imageIds) {
@Override
public void bulkInsertImages(List<Image> images) {
String sql =
"INSERT INTO image (album_id, member_id, url, generated_at, created_at, updated_at) "
+ "VALUES (?, ?, ?, ?, NOW(), NOW())";
"INSERT INTO image (album_id, member_id, url, capacity_gb, generated_at, created_at, updated_at) "
+ "VALUES (?, ?, ?, ?, ?, NOW(), NOW())";

jdbcTemplate.batchUpdate(
sql,
Expand All @@ -91,10 +93,32 @@ public void bulkInsertImages(List<Image> images) {
ps.setLong(1, image.getAlbum().getId());
ps.setLong(2, image.getMemberId());
ps.setString(3, image.getUrl());
ps.setObject(4, image.getGeneratedAt());
ps.setBigDecimal(4, image.getCapacityGb()); // capacity_gb 추가
ps.setObject(5, image.getGeneratedAt());
});
}

@Override
public List<Long> findIdsByUrlsInOrder(List<String> urls) {
if (urls == null || urls.isEmpty()) {
return List.of();
}

var cases = new CaseBuilder().when(image.url.eq(urls.get(0))).then(0);

for (int i = 1; i < urls.size(); i++) {
cases = cases.when(image.url.eq(urls.get(i))).then(i);
}
NumberExpression<Integer> orderExpr = cases.otherwise(999_999);

return queryFactory
.select(image.id)
.from(image)
.where(image.url.in(urls))
.orderBy(orderExpr.asc())
.fetch();
}

private BooleanExpression lastImageIdCondition(Long imageId, SortDirection direction) {
if (imageId == null) {
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,10 @@
import org.cherrypic.domain.image.dto.request.AlbumFileUploadRequest;
import org.cherrypic.domain.image.dto.request.AlbumImageDeleteRequest;
import org.cherrypic.domain.image.dto.request.ImageUploadRequest;
import org.cherrypic.domain.image.dto.request.UploadFailedFileDeleteRequest;
import org.cherrypic.domain.image.dto.response.AlbumImageListResponse;
import org.cherrypic.domain.image.dto.response.EventImageListResponse;
import org.cherrypic.domain.image.dto.response.PresignedUrlResponse;
import org.cherrypic.domain.image.dto.response.PresignedUrlsResponse;
import org.cherrypic.domain.image.dto.response.UploadFileListResponse;
import org.cherrypic.global.pagination.SliceResponse;
import org.cherrypic.global.pagination.SortDirection;

Expand All @@ -18,15 +17,13 @@ public interface ImageService {

PresignedUrlResponse createEventCoverImageUploadUrl(ImageUploadRequest request);

PresignedUrlsResponse createAlbumFileUploadUrls(Long albumId, AlbumFileUploadRequest request);
UploadFileListResponse createAlbumFileUploadUrls(Long albumId, AlbumFileUploadRequest request);

SliceResponse<AlbumImageListResponse> getAlbumImages(
Long albumId, Long lastImageId, int size, SortDirection direction);

SliceResponse<EventImageListResponse> getEventImages(
Long eventId, Long lastImageId, int size, SortDirection direction);

void deleteUploadFailedFile(UploadFailedFileDeleteRequest request);

void deleteAlbumImage(Long albumId, AlbumImageDeleteRequest request);
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,10 @@
import org.cherrypic.domain.image.dto.request.AlbumFileUploadRequest;
import org.cherrypic.domain.image.dto.request.AlbumImageDeleteRequest;
import org.cherrypic.domain.image.dto.request.ImageUploadRequest;
import org.cherrypic.domain.image.dto.request.UploadFailedFileDeleteRequest;
import org.cherrypic.domain.image.dto.response.AlbumImageListResponse;
import org.cherrypic.domain.image.dto.response.EventImageListResponse;
import org.cherrypic.domain.image.dto.response.PresignedUrlResponse;
import org.cherrypic.domain.image.dto.response.PresignedUrlsResponse;
import org.cherrypic.domain.image.dto.response.UploadFileListResponse;
import org.cherrypic.domain.image.enums.FileExtension;
import org.cherrypic.domain.image.enums.ImageType;
import org.cherrypic.domain.image.event.ImagesDeleteEvent;
Expand Down Expand Up @@ -103,16 +102,22 @@ public PresignedUrlResponse createEventCoverImageUploadUrl(ImageUploadRequest re
}

@Override
public PresignedUrlsResponse createAlbumFileUploadUrls(
public UploadFileListResponse createAlbumFileUploadUrls(
Long albumId, AlbumFileUploadRequest request) {
final Member currentMember = memberUtil.getCurrentMember();
final Album album = getAlbumByIdWithLock(albumId);

validateParticipantAuthority(currentMember.getId(), album.getId());
validateAlbumCapacity(album, request.capacity());

BigDecimal uploadCapacity =
request.payloads().stream()
.map(AlbumFileUploadRequest.Payload::capacity)
.reduce(BigDecimal.ZERO, BigDecimal::add);
Comment on lines +112 to +115
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

용량 계산 전에 앨범 참가자 검증을 먼저 해도 괜찮을 것 같아요

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

반영했습니다!


validateAlbumCapacity(album, uploadCapacity);
validateDistinctHashes(request);

album.increaseCapacity(request.capacity());
album.increaseCapacity(uploadCapacity);

List<String> presignedUrls =
request.payloads().stream()
Expand Down Expand Up @@ -141,13 +146,25 @@ public PresignedUrlsResponse createAlbumFileUploadUrls(
objectUrl,
req.generatedAt() != null
? req.generatedAt()
: LocalDateTime.now());
: LocalDateTime.now(),
req.capacity());
})
.toList();

imageRepository.bulkInsertImages(images);

return PresignedUrlsResponse.of(presignedUrls);
List<Long> imageIds =
imageRepository.findIdsByUrlsInOrder(images.stream().map(Image::getUrl).toList());

List<UploadFileListResponse.Payload> payloads =
IntStream.range(0, images.size())
.mapToObj(
i ->
UploadFileListResponse.Payload.of(
imageIds.get(i), presignedUrls.get(i)))
.toList();

return UploadFileListResponse.of(payloads);
}

@Override
Expand Down Expand Up @@ -177,16 +194,6 @@ public SliceResponse<EventImageListResponse> getEventImages(
return SliceResponse.from(result);
}

@Override
public void deleteUploadFailedFile(UploadFailedFileDeleteRequest request) {
final Member currentMember = memberUtil.getCurrentMember();
final List<Image> images = imageRepository.findByUrlIn(request.presignedUrls());

validatePresignedImageOwnership(currentMember, images);

imageRepository.deleteAllInBatch(images);
}

@Override
public void deleteAlbumImage(Long albumId, AlbumImageDeleteRequest request) {
final Member currentMember = memberUtil.getCurrentMember();
Expand All @@ -200,6 +207,12 @@ public void deleteAlbumImage(Long albumId, AlbumImageDeleteRequest request) {

validateImagesInAlbum(images, album);

album.decreaseCapacity(
images.stream()
.map(Image::getCapacityGb)
.filter(Objects::nonNull)
.reduce(BigDecimal.ZERO, BigDecimal::add));

eventPublisher.publishEvent(
ImagesDeleteEvent.of(images.stream().map(Image::getUrl).toList()));
imageRepository.deleteAllInBatch(images);
Expand Down Expand Up @@ -237,21 +250,11 @@ private void validateParticipantAuthority(Long memberId, Long albumId) {
}
}

private void validatePresignedImageOwnership(Member member, List<Image> images) {
Long memberId = member.getId();

boolean hasInvalidImage =
images.stream().anyMatch(image -> !image.getMemberId().equals(memberId));

if (hasInvalidImage) {
throw new CustomException(ImageErrorCode.PRESIGNED_IMAGES_NOT_MINE);
}
}
private void validateAlbumCapacity(Album album, BigDecimal uploadCapacity) {

private void validateAlbumCapacity(Album album, BigDecimal additionalUpload) {
BigDecimal maxCapacity = album.getType().getCapacityGb();
BigDecimal current = album.getCapacityGb();
BigDecimal afterUpload = current.add(additionalUpload);
BigDecimal afterUpload = current.add(uploadCapacity);

if (afterUpload.compareTo(maxCapacity) > 0) {
throw new CustomException(AlbumErrorCode.ALBUM_CAPACITY_EXCEEDED);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1087,7 +1087,8 @@ void setUp() {
Album album5 = Album.createAlbum("testAlbum5", "testURL5", AlbumType.PRO, false);
albumRepository.saveAll(List.of(album1, album2, album3, album4, album5));

Image image = Image.createImage(album1, 1L, "testUrl", LocalDateTime.now());
Image image =
Image.createImage(album1, 1L, "testUrl", LocalDateTime.now(), BigDecimal.ONE);
imageRepository.save(image);

subscriptionRepository.save(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import static org.mockito.Mockito.*;

import jakarta.persistence.EntityManager;
import java.math.BigDecimal;
import java.sql.SQLIntegrityConstraintViolationException;
import java.time.LocalDateTime;
import java.util.List;
Expand Down Expand Up @@ -359,8 +360,10 @@ void setUp() {
Event event2 = Event.createEvent(album1, "testTitle2", "testCoverUrl2");
eventRepository.saveAll(List.of(event1, event2));

Image image1 = Image.createImage(album1, 1L, "testUrl", LocalDateTime.now());
Image image2 = Image.createImage(album1, 1L, "testUrl2", LocalDateTime.now());
Image image1 =
Image.createImage(album1, 1L, "testUrl", LocalDateTime.now(), BigDecimal.ZERO);
Image image2 =
Image.createImage(album1, 1L, "testUrl2", LocalDateTime.now(), BigDecimal.ZERO);
imageRepository.saveAll(List.of(image1, image2));

EventImage eventImage1 = EventImage.createEventImage(event1, image1);
Expand Down Expand Up @@ -479,11 +482,16 @@ void setUp() {
Event event3 = Event.createEvent(album3, "testTitle3", "testCoverUrl3");
eventRepository.saveAll(List.of(event1, event2, event3));

Image image1 = Image.createImage(album1, 1L, "testUrl", LocalDateTime.now());
Image image2 = Image.createImage(album1, 1L, "testUrl2", LocalDateTime.now());
Image image3 = Image.createImage(album2, 1L, "testUrl3", LocalDateTime.now());
Image image4 = Image.createImage(album1, 1L, "testUrl4", LocalDateTime.now());
Image image5 = Image.createImage(album3, 1L, "testUrl5", LocalDateTime.now());
Image image1 =
Image.createImage(album1, 1L, "testUrl", LocalDateTime.now(), BigDecimal.ZERO);
Image image2 =
Image.createImage(album1, 1L, "testUrl2", LocalDateTime.now(), BigDecimal.ZERO);
Image image3 =
Image.createImage(album2, 1L, "testUrl3", LocalDateTime.now(), BigDecimal.ZERO);
Image image4 =
Image.createImage(album1, 1L, "testUrl4", LocalDateTime.now(), BigDecimal.ZERO);
Image image5 =
Image.createImage(album3, 1L, "testUrl5", LocalDateTime.now(), BigDecimal.ZERO);
imageRepository.saveAll(List.of(image1, image2, image3, image4, image5));

EventImage eventImage = EventImage.createEventImage(event1, image2);
Expand Down Expand Up @@ -715,9 +723,12 @@ void setUp() {
Event event4 = Event.createEvent(album1, "testTitle4", "testCoverUrl4");
eventRepository.saveAll(List.of(event1, event2, event3, event4));

Image image1 = Image.createImage(album1, 1L, "testUrl", LocalDateTime.now());
Image image2 = Image.createImage(album1, 1L, "testUrl2", LocalDateTime.now());
Image image3 = Image.createImage(album2, 1L, "testUrl3", LocalDateTime.now());
Image image1 =
Image.createImage(album1, 1L, "testUrl", LocalDateTime.now(), BigDecimal.ZERO);
Image image2 =
Image.createImage(album1, 1L, "testUrl2", LocalDateTime.now(), BigDecimal.ZERO);
Image image3 =
Image.createImage(album2, 1L, "testUrl3", LocalDateTime.now(), BigDecimal.ZERO);
imageRepository.saveAll(List.of(image1, image2, image3));

EventImage eventImage1 = EventImage.createEventImage(event1, image1);
Expand Down
Loading