diff --git a/src/main/java/com/somemore/global/imageupload/service/ImageUploadService.java b/src/main/java/com/somemore/global/imageupload/service/ImageUploadService.java index cc0deb65d..3f9b524be 100644 --- a/src/main/java/com/somemore/global/imageupload/service/ImageUploadService.java +++ b/src/main/java/com/somemore/global/imageupload/service/ImageUploadService.java @@ -5,7 +5,6 @@ import com.somemore.global.imageupload.usecase.ImageUploadUseCase; import com.somemore.global.imageupload.util.ImageUploadUtils; import com.somemore.global.imageupload.validator.ImageUploadValidator; -import jakarta.annotation.PostConstruct; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; @@ -13,8 +12,11 @@ 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.presigner.S3Presigner; +import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest; import java.io.IOException; +import java.time.Duration; import static com.somemore.global.exception.ExceptionMessage.UPLOAD_FAILED; @@ -23,6 +25,7 @@ public class ImageUploadService implements ImageUploadUseCase { private final S3Client s3Client; + private final S3Presigner s3Presigner; private final ImageUploadValidator imageUploadValidator; @Value("${cloud.aws.s3.bucket}") @@ -34,11 +37,28 @@ public class ImageUploadService implements ImageUploadUseCase { @Value("${default.image.url}") private String defaultImageUrl; - public static String DEFAULT_IMAGE_URL; + public static final String DEFAULT_IMAGE_URL = ""; + private static final Duration GET_URL_EXPIRATION_DURATION = Duration.ofMinutes(3); - @PostConstruct - private void init() { - DEFAULT_IMAGE_URL = defaultImageUrl; + + @Override + public String getPresignedUrl(String filename) { + if(imageUploadValidator.isEmptyFileName(filename)) { + return null; + } + + String uniqueFilename = ImageUploadUtils.generateUniqueFileName(filename); + + GetObjectPresignRequest getObjectPresignRequest = GetObjectPresignRequest.builder() + .signatureDuration(GET_URL_EXPIRATION_DURATION) + .getObjectRequest(builder -> builder + .bucket(bucket) + .key(uniqueFilename)) + .build(); + + return s3Presigner.presignGetObject(getObjectPresignRequest) + .url() + .toString(); } @Override diff --git a/src/main/java/com/somemore/global/imageupload/usecase/ImageUploadUseCase.java b/src/main/java/com/somemore/global/imageupload/usecase/ImageUploadUseCase.java index 4b1ee30c9..1d811a100 100644 --- a/src/main/java/com/somemore/global/imageupload/usecase/ImageUploadUseCase.java +++ b/src/main/java/com/somemore/global/imageupload/usecase/ImageUploadUseCase.java @@ -4,4 +4,5 @@ public interface ImageUploadUseCase { String uploadImage(ImageUploadRequestDto requestDto); + String getPresignedUrl(String filename); } diff --git a/src/main/java/com/somemore/global/imageupload/util/ImageUploadUtils.java b/src/main/java/com/somemore/global/imageupload/util/ImageUploadUtils.java index 21feb53f8..e0d23fbc9 100644 --- a/src/main/java/com/somemore/global/imageupload/util/ImageUploadUtils.java +++ b/src/main/java/com/somemore/global/imageupload/util/ImageUploadUtils.java @@ -11,9 +11,14 @@ private ImageUploadUtils() { } public static String generateUniqueFileName(String originalFileName) { + String uuid = UUID.randomUUID().toString(); + String fileExtension = extractFileExtension(originalFileName); - return uuid + fileExtension; + + String fileNameWithoutExtension = originalFileName.substring(0, originalFileName.lastIndexOf(".")); + + return uuid + "_" + fileNameWithoutExtension + fileExtension; } private static String extractFileExtension(String fileName) { diff --git a/src/main/java/com/somemore/global/imageupload/validator/DefaultImageUploadValidator.java b/src/main/java/com/somemore/global/imageupload/validator/DefaultImageUploadValidator.java index 6e623d672..1a0d6bddf 100644 --- a/src/main/java/com/somemore/global/imageupload/validator/DefaultImageUploadValidator.java +++ b/src/main/java/com/somemore/global/imageupload/validator/DefaultImageUploadValidator.java @@ -25,6 +25,11 @@ public void validateFileType(MultipartFile file) { } } + @Override + public boolean isEmptyFileName(String fileName) { + return fileName == null || fileName.isEmpty(); + } + @Override public boolean isEmptyFile(MultipartFile file) { return file == null || file.isEmpty(); diff --git a/src/main/java/com/somemore/global/imageupload/validator/ImageUploadValidator.java b/src/main/java/com/somemore/global/imageupload/validator/ImageUploadValidator.java index a8f9f8742..e065fc11f 100644 --- a/src/main/java/com/somemore/global/imageupload/validator/ImageUploadValidator.java +++ b/src/main/java/com/somemore/global/imageupload/validator/ImageUploadValidator.java @@ -9,4 +9,6 @@ public interface ImageUploadValidator { void validateFileType(MultipartFile file); boolean isEmptyFile(MultipartFile file); + + boolean isEmptyFileName(String fileName); } diff --git a/src/test/java/com/somemore/global/imageupload/service/ImageUploadServiceTest.java b/src/test/java/com/somemore/global/imageupload/service/ImageUploadServiceTest.java index 0e3412cfb..766371345 100644 --- a/src/test/java/com/somemore/global/imageupload/service/ImageUploadServiceTest.java +++ b/src/test/java/com/somemore/global/imageupload/service/ImageUploadServiceTest.java @@ -1,9 +1,7 @@ package com.somemore.global.imageupload.service; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.*; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.any; import static org.mockito.Mockito.mock; @@ -17,6 +15,8 @@ import com.somemore.support.IntegrationTestSupport; import java.io.IOException; import java.io.InputStream; +import java.net.URL; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -28,6 +28,9 @@ 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.presigner.S3Presigner; +import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest; +import software.amazon.awssdk.services.s3.presigner.model.PresignedGetObjectRequest; class ImageUploadServiceTest extends IntegrationTestSupport { @@ -76,6 +79,7 @@ void testUploadImage_success() { @DisplayName("이미지 형식이 올바르지 않다면 업로드 할 수 없다.") @Test void testUploadImage_failure() throws IOException { + // given when(multipartFile.getInputStream()).thenThrow(new IOException()); @@ -88,6 +92,7 @@ void testUploadImage_failure() throws IOException { @DisplayName("이미지 파일이 없다면 기본 이미지 링크를 반환한다.") @Test void uploadImageWithEmptyFile() { + // given MultipartFile emptyFile = new MockMultipartFile("file", new byte[0]); given(imageUploadValidator.isEmptyFile(emptyFile)).willReturn(true); @@ -99,4 +104,53 @@ void uploadImageWithEmptyFile() { // then assertThat(imgUrl).isEqualTo(ImageUploadService.DEFAULT_IMAGE_URL); } + + @DisplayName("유효한 파일명으로 사전 서명된 URL을 생성할 수 있다.") + @Test + void getPresignedUrl_success() { + + // given + String filename = "testImage.jpg"; + + when(imageUploadValidator.isEmptyFileName(filename)).thenReturn(false); + + S3Presigner mockPresigner = mock(S3Presigner.class); + ReflectionTestUtils.setField(imageUploadService, "s3Presigner", mockPresigner); + + PresignedGetObjectRequest mockPresignedRequest = mock(PresignedGetObjectRequest.class); + URL mockUrl = mock(URL.class); + + when(mockUrl.toString()).thenReturn("https://test-bucket.s3.amazonaws.com/unique-test-image.jpg"); + when(mockPresignedRequest.url()).thenReturn(mockUrl); + when(mockPresigner.presignGetObject(any(GetObjectPresignRequest.class))).thenReturn(mockPresignedRequest); + + // when + String presignedUrl = imageUploadService.getPresignedUrl(filename); + + // then + assertNotNull(presignedUrl); + assertTrue(presignedUrl.startsWith("https://test-bucket.s3.amazonaws.com/")); + assertTrue(presignedUrl.endsWith(".jpg")); + + verify(imageUploadValidator, times(1)).isEmptyFileName(filename); + verify(mockPresigner, times(1)).presignGetObject(any(GetObjectPresignRequest.class)); + } + + @DisplayName("파일명 검증에 실패하면 null을 반환한다.") + @Test + void getPresignedUrl_invalidFileName() { + + // given + String filename = ""; + + when(imageUploadValidator.isEmptyFileName(filename)).thenReturn(true); + + // when + String presignedUrl = imageUploadService.getPresignedUrl(filename); + + // then + assertNull(presignedUrl); + + verify(imageUploadValidator, times(1)).isEmptyFileName(filename); + } } diff --git a/src/test/java/com/somemore/global/imageupload/validator/DefaultImageUploadValidatorTest.java b/src/test/java/com/somemore/global/imageupload/validator/DefaultImageUploadValidatorTest.java index f4f13a673..3fb2dc7e8 100644 --- a/src/test/java/com/somemore/global/imageupload/validator/DefaultImageUploadValidatorTest.java +++ b/src/test/java/com/somemore/global/imageupload/validator/DefaultImageUploadValidatorTest.java @@ -93,4 +93,43 @@ void shouldThrowExceptionWhenFileTypeIsNull() { // then assertEquals(ImageUploadException.class, exception.getClass()); } + + @Test + @DisplayName("파일 이름이 비어있는지 확인할 수 있다.") + void shouldReturnTrueWhenFileNameIsEmpty() { + // given + String emptyFileName = ""; + + // when + boolean isEmptyFileName = imageUploadValidator.isEmptyFileName(emptyFileName); + + // then + assertThat(isEmptyFileName).isTrue(); + } + + @Test + @DisplayName("파일 이름이 null이면 true를 반환한다.") + void shouldReturnTrueWhenFileNameIsNull() { + // given + String nullFileName = null; + + // when + boolean isEmptyFileName = imageUploadValidator.isEmptyFileName(nullFileName); + + // then + assertThat(isEmptyFileName).isTrue(); + } + + @Test + @DisplayName("파일 이름이 비어있지 않으면 false를 반환한다.") + void shouldReturnFalseWhenFileNameIsNotEmpty() { + // given + String validFileName = "testImage.jpg"; + + // when + boolean isEmptyFileName = imageUploadValidator.isEmptyFileName(validFileName); + + // then + assertThat(isEmptyFileName).isFalse(); + } }