Skip to content

Commit

Permalink
feat: 이벤트당 이미지 업로드 수량 제한 (#819)
Browse files Browse the repository at this point in the history
* feat: 행사 이미지 업로드 수량 제한 기능 추가

* refactor: CompletableFuture 이미지 업로드 예외 처리 추가
  • Loading branch information
Arachneee committed Nov 13, 2024
1 parent a093707 commit f41745f
Show file tree
Hide file tree
Showing 10 changed files with 128 additions and 16 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package server.haengdong.application;

import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

@RequiredArgsConstructor
@Service
public class EventImageFacadeService {

private final EventService eventService;
private final ImageService imageService;

public void uploadImages(String token, List<MultipartFile> images) {
eventService.validateImageCount(token, images.size());
List<String> imageNames = imageService.uploadImages(images);
eventService.saveImages(token, imageNames);
}

public void deleteImage(String token, Long imageId) {
String imageName = eventService.deleteImage(token, imageId);
imageService.deleteImage(imageName);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import server.haengdong.application.request.EventAppRequest;
import server.haengdong.application.request.EventLoginAppRequest;
Expand All @@ -31,6 +32,8 @@
@Service
public class EventService {

private static final int MAX_IMAGE_COUNT = 10;

private final EventRepository eventRepository;
private final EventTokenProvider eventTokenProvider;
private final BillRepository billRepository;
Expand Down Expand Up @@ -137,4 +140,14 @@ public String deleteImage(String token, Long imageId) {
eventImageRepository.delete(eventImage);
return eventImage.getName();
}

public void validateImageCount(String token, int uploadImageCount) {
Event event = getEvent(token);
Long imageCount = eventImageRepository.countByEvent(event);
Long totalImageCount = imageCount + uploadImageCount;

if (totalImageCount > MAX_IMAGE_COUNT) {
throw new HaengdongException(HaengdongErrorCode.IMAGE_COUNT_INVALID, totalImageCount);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import java.util.List;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import lombok.RequiredArgsConstructor;
Expand Down Expand Up @@ -39,12 +40,12 @@ public List<String> uploadImages(List<MultipartFile> images) {
.map(image -> CompletableFuture.supplyAsync(() -> uploadImage(image), executorService))
.toList();

CompletableFuture<List<String>> result = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
.thenApply(v -> futures.stream()
.map(this::getFuture)
.toList());

return result.join();
try {
CompletableFuture<List<String>> result = collectUploadResults(futures);
return result.join();
} catch (CompletionException e) {
throw new HaengdongException(HaengdongErrorCode.IMAGE_UPLOAD_FAIL, e);
}
}

private String uploadImage(MultipartFile image) {
Expand All @@ -71,7 +72,14 @@ private String uploadImageToStorage(InputStream inputStream, MultipartFile image
return imageName;
}

private String getFuture(CompletableFuture<String> future) {
private CompletableFuture<List<String>> collectUploadResults(List<CompletableFuture<String>> futures) {
return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
.thenApply(v -> futures.stream()
.map(this::resolveFuture)
.toList());
}

private String resolveFuture(CompletableFuture<String> future) {
try {
return future.get();
} catch (InterruptedException | ExecutionException e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,6 @@
public interface EventImageRepository extends JpaRepository<EventImage, Long> {

List<EventImage> findAllByEvent(Event event);

Long countByEvent(Event event);
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public enum HaengdongErrorCode {
DIFFERENT_STEP_MEMBERS("참여자 목록이 일치하지 않습니다."),

IMAGE_NOT_FOUND("존재하지 않는 이미지 입니다."),
IMAGE_COUNT_INVALID("이미지 수량은 %d개 까지 업로드할 수 있습니다."),

/* Authentication */

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import server.haengdong.application.EventImageFacadeService;
import server.haengdong.application.EventService;
import server.haengdong.application.ImageService;
import server.haengdong.application.response.ImageNameAppResponse;
Expand All @@ -24,7 +25,7 @@
public class AdminEventController {

private final EventService eventService;
private final ImageService imageUploadService;
private final EventImageFacadeService eventImageFacadeService;

@PostMapping("/api/admin/events/{eventId}/auth")
public ResponseEntity<Void> authenticate() {
Expand All @@ -46,8 +47,7 @@ public ResponseEntity<Void> uploadImages(
@PathVariable("eventId") String token,
@RequestPart("images") List<MultipartFile> images
) {
List<String> imageNames = imageUploadService.uploadImages(images);
eventService.saveImages(token, imageNames);
eventImageFacadeService.uploadImages(token, images);

return ResponseEntity.ok().build();
}
Expand All @@ -57,8 +57,7 @@ public ResponseEntity<Void> deleteImage(
@PathVariable("eventId") String token,
@PathVariable("imageId") Long imageId
) {
String imageName = eventService.deleteImage(token, imageId);
imageUploadService.deleteImage(imageName);
eventImageFacadeService.deleteImage(token, imageId);

return ResponseEntity.ok().build();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@


import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.AssertionsForClassTypes.tuple;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.mockito.BDDMockito.given;
Expand All @@ -28,6 +29,7 @@
import server.haengdong.domain.event.Event;
import server.haengdong.domain.event.EventRepository;
import server.haengdong.domain.event.EventTokenProvider;
import server.haengdong.exception.HaengdongException;
import server.haengdong.support.fixture.Fixture;

class EventServiceTest extends ServiceTestSupport {
Expand Down Expand Up @@ -228,4 +230,17 @@ void deleteImage() {
assertThat(eventImageRepository.findById(eventImage.getId()))
.isEmpty();
}

@DisplayName("행사 1개당 이미지는 10개까지 업로드 가능하다.")
@Test
void validateImageCount() {
Event event = Fixture.EVENT1;
eventRepository.save(event);
List<String> imageNames = List.of("image1.jpg", "image2.jpg");
String token = event.getToken();
eventService.saveImages(token, imageNames);

assertThatThrownBy(() -> eventService.validateImageCount(token, 9))
.isInstanceOf(HaengdongException.class);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package server.haengdong.application;

import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.awaitility.Awaitility.given;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.when;

import java.util.List;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.web.multipart.MultipartFile;
import server.haengdong.exception.HaengdongException;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
import software.amazon.awssdk.services.s3.model.S3Exception;


class ImageServiceTest extends ServiceTestSupport {

@Autowired
private ImageService imageService;

@MockBean
private S3Client s3Client;

@DisplayName("이미지 업로드 도중 실패하면 예외가 커스텀 예외가 발생한다.")
@Test
void uploadImages() {
MultipartFile multipartFile1 = new MockMultipartFile("file1", "test1.txt", "text/plain", "hello1".getBytes());
MultipartFile multipartFile2 = new MockMultipartFile("file2", "test2.txt", "text/plain", "hello2".getBytes());
MultipartFile multipartFile3 = new MockMultipartFile("file3", "test3.txt", "text/plain", "hello3".getBytes());
MultipartFile multipartFile4 = new MockMultipartFile("file4", "test4.txt", "text/plain", "hello4".getBytes());
MultipartFile multipartFile5 = new MockMultipartFile("file5", "test5.txt", "text/plain", "hello5".getBytes());

doThrow(new RuntimeException("S3 upload failed"))
.when(s3Client).putObject(any(PutObjectRequest.class), any(RequestBody.class));

assertThatThrownBy(() -> imageService.uploadImages(List.of(multipartFile1, multipartFile2, multipartFile3, multipartFile4, multipartFile5)))
.isInstanceOf(HaengdongException.class);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,19 @@
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.restdocs.payload.JsonFieldType;
import server.haengdong.application.EventImageFacadeService;
import server.haengdong.application.EventService;
import server.haengdong.application.ImageService;
import server.haengdong.presentation.admin.AdminEventController;
import server.haengdong.presentation.request.EventUpdateRequest;

class AdminEventControllerDocsTest extends RestDocsSupport {

private final EventService eventService = mock(EventService.class);
private final ImageService imageUploadService = mock(ImageService.class);
private final EventImageFacadeService eventImageFacadeService = mock(EventImageFacadeService.class);

@Override
protected Object initController() {
return new AdminEventController(eventService, imageUploadService);
return new AdminEventController(eventService, eventImageFacadeService);
}

@DisplayName("행사 어드민 권한을 확인한다.")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import server.haengdong.application.AuthService;
import server.haengdong.application.BillService;
import server.haengdong.application.EventImageFacadeService;
import server.haengdong.application.EventService;
import server.haengdong.application.ImageService;
import server.haengdong.application.MemberService;
Expand Down Expand Up @@ -49,5 +50,5 @@ public abstract class ControllerTestSupport {
protected BillService billService;

@MockBean
protected ImageService imageUploadService;
protected EventImageFacadeService eventImageFacadeService;
}

0 comments on commit f41745f

Please sign in to comment.