From f282a280b045c8fb0c31cb74c530874583e9d200 Mon Sep 17 00:00:00 2001 From: na-yk Date: Thu, 30 Nov 2023 16:40:50 +0900 Subject: [PATCH 1/3] =?UTF-8?q?:recycle:=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?=EC=97=85=EB=A1=9C=EB=93=9C=20url=20=EC=9A=94=EC=B2=AD=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20storage=20=ED=8C=A8=ED=82=A4=EC=A7=80?= =?UTF-8?q?=EB=A1=9C=20=ED=86=B5=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../steady/storage/ImageUploadPurpose.java | 31 ++++++++++++++++++ .../controller/StorageImageController.java | 32 +++++++++++++++++++ .../storage/exception/StorageErrorCode.java | 5 +-- .../storage/{ => service}/StorageService.java | 2 +- 4 files changed, 67 insertions(+), 3 deletions(-) create mode 100644 src/main/java/dev/steady/storage/ImageUploadPurpose.java create mode 100644 src/main/java/dev/steady/storage/controller/StorageImageController.java rename src/main/java/dev/steady/storage/{ => service}/StorageService.java (98%) diff --git a/src/main/java/dev/steady/storage/ImageUploadPurpose.java b/src/main/java/dev/steady/storage/ImageUploadPurpose.java new file mode 100644 index 0000000..f3811ce --- /dev/null +++ b/src/main/java/dev/steady/storage/ImageUploadPurpose.java @@ -0,0 +1,31 @@ +package dev.steady.storage; + +import dev.steady.global.exception.InvalidValueException; +import lombok.Getter; + +import java.util.Arrays; + +import static dev.steady.storage.exception.StorageErrorCode.NOT_SUPPORTED_PURPOSE; + +@Getter +public enum ImageUploadPurpose { + + USER_PROFILE_IMAGE("profile", "profile/%s"), + STEADY_CONTENT_IMAGE("steady", "steady/content/%s"); + + private final String purpose; + private final String keyPattern; + + ImageUploadPurpose(String purpose, String keyPattern) { + this.purpose = purpose; + this.keyPattern = keyPattern; + } + + public static ImageUploadPurpose from(String purpose) { + return Arrays.stream(ImageUploadPurpose.values()) + .filter(v -> v.getPurpose().equals(purpose)) + .findAny() + .orElseThrow(() -> new InvalidValueException(NOT_SUPPORTED_PURPOSE)); + } + +} diff --git a/src/main/java/dev/steady/storage/controller/StorageImageController.java b/src/main/java/dev/steady/storage/controller/StorageImageController.java new file mode 100644 index 0000000..09be2ad --- /dev/null +++ b/src/main/java/dev/steady/storage/controller/StorageImageController.java @@ -0,0 +1,32 @@ +package dev.steady.storage.controller; + +import dev.steady.global.auth.Auth; +import dev.steady.global.auth.UserInfo; +import dev.steady.storage.ImageUploadPurpose; +import dev.steady.storage.service.StorageService; +import dev.steady.user.dto.response.PutObjectUrlResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v1/storage/image") +public class StorageImageController { + + private final StorageService storageService; + + @GetMapping("/{purpose}") + public ResponseEntity getImageUploadUrl(@PathVariable String purpose, + @RequestParam String fileName, + @Auth UserInfo userInfo) { + String keyPattern = ImageUploadPurpose.from(purpose).getKeyPattern(); + PutObjectUrlResponse response = storageService.generatePutObjectUrl(fileName, keyPattern); + return ResponseEntity.ok(response); + } + +} diff --git a/src/main/java/dev/steady/storage/exception/StorageErrorCode.java b/src/main/java/dev/steady/storage/exception/StorageErrorCode.java index 0f0e180..563949b 100644 --- a/src/main/java/dev/steady/storage/exception/StorageErrorCode.java +++ b/src/main/java/dev/steady/storage/exception/StorageErrorCode.java @@ -8,7 +8,8 @@ @RequiredArgsConstructor public enum StorageErrorCode implements ErrorCode { - NOT_SUPPORTED_FILE_TYPE("ST01", "지원하지 않는 파일 유형입니다."); + NOT_SUPPORTED_FILE_TYPE("ST01", "지원하지 않는 파일 유형입니다."), + NOT_SUPPORTED_PURPOSE("ST02", "지원하지 않는 용도입니다."); private final String code; private final String message; @@ -22,5 +23,5 @@ public String code() { public String message() { return this.message; } - + } diff --git a/src/main/java/dev/steady/storage/StorageService.java b/src/main/java/dev/steady/storage/service/StorageService.java similarity index 98% rename from src/main/java/dev/steady/storage/StorageService.java rename to src/main/java/dev/steady/storage/service/StorageService.java index 43779e1..66daf1b 100644 --- a/src/main/java/dev/steady/storage/StorageService.java +++ b/src/main/java/dev/steady/storage/service/StorageService.java @@ -1,4 +1,4 @@ -package dev.steady.storage; +package dev.steady.storage.service; import dev.steady.global.exception.InvalidValueException; import dev.steady.user.dto.response.PutObjectUrlResponse; From b626b23e46e9137a525e9d1eada1c510fa8c496d Mon Sep 17 00:00:00 2001 From: na-yk Date: Thu, 30 Nov 2023 16:41:24 +0900 Subject: [PATCH 2/3] =?UTF-8?q?:fire:=20User=20=EB=8F=84=EB=A9=94=EC=9D=B8?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=ED=94=84=EB=A1=9C=ED=95=84=20=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=20=EC=97=85=EB=A1=9C=EB=93=9C=20url=20?= =?UTF-8?q?=EC=9A=94=EC=B2=AD=20=EA=B8=B0=EB=8A=A5=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/controller/UserController.java | 7 ----- .../dev/steady/user/service/UserService.java | 7 +---- .../user/controller/UserControllerTest.java | 29 ------------------- 3 files changed, 1 insertion(+), 42 deletions(-) diff --git a/src/main/java/dev/steady/user/controller/UserController.java b/src/main/java/dev/steady/user/controller/UserController.java index 944d1dd..fda059b 100644 --- a/src/main/java/dev/steady/user/controller/UserController.java +++ b/src/main/java/dev/steady/user/controller/UserController.java @@ -7,7 +7,6 @@ import dev.steady.global.auth.UserInfo; import dev.steady.user.dto.request.UserCreateRequest; import dev.steady.user.dto.request.UserUpdateRequest; -import dev.steady.user.dto.response.PutObjectUrlResponse; import dev.steady.user.dto.response.UserMyDetailResponse; import dev.steady.user.dto.response.UserNicknameExistResponse; import dev.steady.user.dto.response.UserOtherDetailResponse; @@ -75,10 +74,4 @@ public ResponseEntity withdrawUser(@Auth UserInfo userInfo) { return ResponseEntity.noContent().build(); } - @GetMapping("/profile/image") - public ResponseEntity getProfileUploadUrl(@RequestParam String fileName) { - PutObjectUrlResponse response = userService.getProfileUploadUrl(fileName); - return ResponseEntity.ok(response); - } - } diff --git a/src/main/java/dev/steady/user/service/UserService.java b/src/main/java/dev/steady/user/service/UserService.java index 2eb72ea..d79ae32 100644 --- a/src/main/java/dev/steady/user/service/UserService.java +++ b/src/main/java/dev/steady/user/service/UserService.java @@ -8,7 +8,7 @@ import dev.steady.review.dto.response.UserCardResponse; import dev.steady.steady.domain.Participant; import dev.steady.steady.domain.repository.ParticipantRepository; -import dev.steady.storage.StorageService; +import dev.steady.storage.service.StorageService; import dev.steady.user.domain.Position; import dev.steady.user.domain.Stack; import dev.steady.user.domain.User; @@ -19,7 +19,6 @@ import dev.steady.user.domain.repository.UserStackRepository; import dev.steady.user.dto.request.UserCreateRequest; import dev.steady.user.dto.request.UserUpdateRequest; -import dev.steady.user.dto.response.PutObjectUrlResponse; import dev.steady.user.dto.response.UserDetailResponse; import dev.steady.user.dto.response.UserMyDetailResponse; import dev.steady.user.dto.response.UserNicknameExistResponse; @@ -117,10 +116,6 @@ public void withdrawUser(UserInfo userInfo) { accountRepository.deleteByUser(user); } - public PutObjectUrlResponse getProfileUploadUrl(String fileName) { - return storageService.generatePutObjectUrl(fileName, PROFILE_IMAGE_KEY_PATTERN); - } - private Stack getStack(Long stackId) { return stackRepository.getById(stackId); } diff --git a/src/test/java/dev/steady/user/controller/UserControllerTest.java b/src/test/java/dev/steady/user/controller/UserControllerTest.java index 6c68a04..0f3a04a 100644 --- a/src/test/java/dev/steady/user/controller/UserControllerTest.java +++ b/src/test/java/dev/steady/user/controller/UserControllerTest.java @@ -16,7 +16,6 @@ import static dev.steady.auth.domain.Platform.KAKAO; import static dev.steady.auth.fixture.OAuthFixture.createAuthCodeRequestUrl; import static dev.steady.global.auth.AuthFixture.createUserInfo; -import static dev.steady.user.fixture.UserFixtures.createProfileUploadUrlResponse; import static dev.steady.user.fixture.UserFixtures.createUserCreateRequest; import static dev.steady.user.fixture.UserFixtures.createUserMyDetailResponse; import static dev.steady.user.fixture.UserFixtures.createUserOtherDetailResponse; @@ -232,32 +231,4 @@ void withdrawTest() throws Exception { )).andExpect(status().isNoContent()); } - - @Test - @DisplayName("프로필 이미지 업로드용 Presigned Url을 반환할 수 있다.") - void getProfileUploadUrl() throws Exception { - // given - var response = createProfileUploadUrlResponse(); - var fileName = "profileimage.png"; - given(userService.getProfileUploadUrl(fileName)).willReturn(response); - - // when, then - mockMvc.perform(get("/api/v1/user/profile/image") - .queryParam("fileName", fileName) - ) - .andDo(document("user-v1-get-PutObjectUrlResponse", - resourceDetails().tag("사용자").description("사용자 프로필 이미지 업로드 URL 불러오기") - .responseSchema(Schema.schema("PutObjectUrlResponse")), - queryParameters( - parameterWithName("fileName").description("확장자를 포함한 이미지 파일 이름") - ), - responseFields( - fieldWithPath("presignedUrl").type(STRING).description("사용자 프로필 이미지 업로드 URL"), - fieldWithPath("objectUrl").type(STRING).description("업로드된 이미지 URL") - )) - ) - .andExpect(status().isOk()) - .andExpect(content().string(objectMapper.writeValueAsString(response))); - } - } From 235a401fbb0bee2913ff9577d5c8d87fb71f8234 Mon Sep 17 00:00:00 2001 From: na-yk Date: Thu, 30 Nov 2023 16:41:59 +0900 Subject: [PATCH 3/3] =?UTF-8?q?:white=5Fcheck=5Fmark:=20=EC=9D=B4=EB=AF=B8?= =?UTF-8?q?=EC=A7=80=20=EC=97=85=EB=A1=9C=EB=93=9C=20url=20=EC=9A=94?= =?UTF-8?q?=EC=B2=AD=20=EA=B8=B0=EB=8A=A5=20Controller=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/config/ControllerTestConfig.java | 5 ++ .../StorageImageControllerTest.java | 68 +++++++++++++++++++ .../storage/fixture/StorageFixture.java | 22 ++++++ .../dev/steady/user/fixture/UserFixtures.java | 16 ----- 4 files changed, 95 insertions(+), 16 deletions(-) create mode 100644 src/test/java/dev/steady/storage/controller/StorageImageControllerTest.java create mode 100644 src/test/java/dev/steady/storage/fixture/StorageFixture.java diff --git a/src/test/java/dev/steady/global/config/ControllerTestConfig.java b/src/test/java/dev/steady/global/config/ControllerTestConfig.java index 7a44b99..7fab0f6 100644 --- a/src/test/java/dev/steady/global/config/ControllerTestConfig.java +++ b/src/test/java/dev/steady/global/config/ControllerTestConfig.java @@ -17,6 +17,8 @@ import dev.steady.steady.controller.SteadyLikeController; import dev.steady.steady.service.SteadyLikeService; import dev.steady.steady.service.SteadyService; +import dev.steady.storage.controller.StorageImageController; +import dev.steady.storage.service.StorageService; import dev.steady.template.controller.TemplateController; import dev.steady.template.service.TemplateService; import dev.steady.user.controller.PositionController; @@ -51,6 +53,7 @@ PositionController.class, NotificationController.class, ReviewController.class, + StorageImageController.class, AuthContext.class, JwtResolver.class, JwtProperties.class, @@ -88,6 +91,8 @@ public abstract class ControllerTestConfig { @MockBean protected ReviewService reviewService; @MockBean + protected StorageService storageService; + @MockBean protected JwtResolver jwtResolver; @BeforeEach diff --git a/src/test/java/dev/steady/storage/controller/StorageImageControllerTest.java b/src/test/java/dev/steady/storage/controller/StorageImageControllerTest.java new file mode 100644 index 0000000..c559125 --- /dev/null +++ b/src/test/java/dev/steady/storage/controller/StorageImageControllerTest.java @@ -0,0 +1,68 @@ +package dev.steady.storage.controller; + +import com.epages.restdocs.apispec.Schema; +import dev.steady.global.auth.Authentication; +import dev.steady.global.config.ControllerTestConfig; +import dev.steady.storage.ImageUploadPurpose; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +import static com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper.document; +import static com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper.resourceDetails; +import static dev.steady.global.auth.AuthFixture.createUserInfo; +import static dev.steady.storage.fixture.StorageFixture.createPutObjectUrlResponse; +import static org.mockito.BDDMockito.given; +import static org.springframework.http.HttpHeaders.AUTHORIZATION; +import static org.springframework.restdocs.headers.HeaderDocumentation.headerWithName; +import static org.springframework.restdocs.headers.HeaderDocumentation.requestHeaders; +import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; +import static org.springframework.restdocs.payload.JsonFieldType.STRING; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.queryParameters; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +class StorageImageControllerTest extends ControllerTestConfig { + + @ParameterizedTest + @EnumSource(ImageUploadPurpose.class) + @DisplayName("이미지 업로드용 Presigned Url을 반환할 수 있다.") + void getImageUploadUrl(ImageUploadPurpose imageUploadPurpose) throws Exception { + // given + var userId = 1L; + var userInfo = createUserInfo(userId); + var authentication = new Authentication(userId); + + given(jwtResolver.getAuthentication(TOKEN)).willReturn(authentication); + var response = createPutObjectUrlResponse(); + var fileName = "image.png"; + var purpose = imageUploadPurpose.getPurpose(); + var keyPattern = imageUploadPurpose.getKeyPattern(); + given(storageService.generatePutObjectUrl(fileName, keyPattern)).willReturn(response); + + // when, then + mockMvc.perform(get("/api/v1/storage/image/{purpose}", purpose) + .queryParam("fileName", fileName) + .header(AUTHORIZATION, TOKEN)) + .andDo(document("storage-v1-get-PutObjectUrlResponse", + resourceDetails().tag("스토리지").description("이미지 업로드 URL 불러오기") + .responseSchema(Schema.schema("PutObjectUrlResponse")), + queryParameters( + parameterWithName("fileName").description("확장자를 포함한 이미지 파일 이름") + ), + requestHeaders( + headerWithName(AUTHORIZATION).description("토큰") + ), + responseFields( + fieldWithPath("presignedUrl").type(STRING).description("사용자 프로필 이미지 업로드 URL"), + fieldWithPath("objectUrl").type(STRING).description("업로드된 이미지 URL") + )) + ) + .andExpect(status().isOk()) + .andExpect(content().string(objectMapper.writeValueAsString(response))); + } + +} diff --git a/src/test/java/dev/steady/storage/fixture/StorageFixture.java b/src/test/java/dev/steady/storage/fixture/StorageFixture.java new file mode 100644 index 0000000..e1ff3e3 --- /dev/null +++ b/src/test/java/dev/steady/storage/fixture/StorageFixture.java @@ -0,0 +1,22 @@ +package dev.steady.storage.fixture; + +import dev.steady.user.dto.response.PutObjectUrlResponse; +import org.springframework.web.util.UriComponentsBuilder; + +public class StorageFixture { + + public static PutObjectUrlResponse createPutObjectUrlResponse() { + String presignedUrl = UriComponentsBuilder + .fromUriString("bucket-name.s3.region.amazonaws.com/path/{fileName}") + .queryParam("X-Amz-Algorithm", "{Algorithm}") + .queryParam("X-Amz-Date", "{Date}") + .queryParam("X-Amz-SignedHeaders", "{SignedHeaders}") + .queryParam("X-Amz-Credential", "{Credential}") + .queryParam("X-Amz-Expires", "{Expires}") + .queryParam("X-Amz-Signature", "{Signature}") + .build().toString(); + String objectUrl = "https:{bucket_name}.s3.{region}.com/{key}"; + return PutObjectUrlResponse.of(presignedUrl, objectUrl); + } + +} diff --git a/src/test/java/dev/steady/user/fixture/UserFixtures.java b/src/test/java/dev/steady/user/fixture/UserFixtures.java index 29ee043..5721e1b 100644 --- a/src/test/java/dev/steady/user/fixture/UserFixtures.java +++ b/src/test/java/dev/steady/user/fixture/UserFixtures.java @@ -9,13 +9,11 @@ import dev.steady.user.dto.request.UserUpdateRequest; import dev.steady.user.dto.response.PositionResponse; import dev.steady.user.dto.response.PositionsResponse; -import dev.steady.user.dto.response.PutObjectUrlResponse; import dev.steady.user.dto.response.StackResponse; import dev.steady.user.dto.response.StacksResponse; import dev.steady.user.dto.response.UserDetailResponse; import dev.steady.user.dto.response.UserMyDetailResponse; import dev.steady.user.dto.response.UserOtherDetailResponse; -import org.springframework.web.util.UriComponentsBuilder; import java.util.List; @@ -167,18 +165,4 @@ public static UserUpdateRequest createUserUpdateRequest() { ); } - public static PutObjectUrlResponse createProfileUploadUrlResponse() { - String presignedUrl = UriComponentsBuilder - .fromUriString("bucket-name.s3.region.amazonaws.com/path/{fileName}") - .queryParam("X-Amz-Algorithm", "{Algorithm}") - .queryParam("X-Amz-Date", "{Date}") - .queryParam("X-Amz-SignedHeaders", "{SignedHeaders}") - .queryParam("X-Amz-Credential", "{Credential}") - .queryParam("X-Amz-Expires", "{Expires}") - .queryParam("X-Amz-Signature", "{Signature}") - .build().toString(); - String objectUrl = "https:{bucket_name}.s3.{region}.com/{key}"; - return PutObjectUrlResponse.of(presignedUrl, objectUrl); - } - }