Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions src/main/java/dev/steady/storage/ImageUploadPurpose.java
Original file line number Diff line number Diff line change
@@ -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));
}

}
Original file line number Diff line number Diff line change
@@ -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<PutObjectUrlResponse> getImageUploadUrl(@PathVariable String purpose,
@RequestParam String fileName,
@Auth UserInfo userInfo) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

userInfo가 사용되지 않는 거라면 지워도 좋을 것 같아요.
아니면 기존의 JwtAuthenticationInterceptor의 동작을 좀 추가해서 파라미터 어노테이션 정보만 확인하는 것이 아니라 메서드 어노태이션 정보도 확인하게끔 하면 UserInfo를 사용하지 않을 수 있을 것 같네요!

String keyPattern = ImageUploadPurpose.from(purpose).getKeyPattern();
PutObjectUrlResponse response = storageService.generatePutObjectUrl(fileName, keyPattern);
return ResponseEntity.ok(response);
}
Comment on lines +23 to +30
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

api 설계가 좋네요!


}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -22,5 +23,5 @@ public String code() {
public String message() {
return this.message;
}

}
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
7 changes: 0 additions & 7 deletions src/main/java/dev/steady/user/controller/UserController.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -75,10 +74,4 @@ public ResponseEntity<Void> withdrawUser(@Auth UserInfo userInfo) {
return ResponseEntity.noContent().build();
}

@GetMapping("/profile/image")
public ResponseEntity<PutObjectUrlResponse> getProfileUploadUrl(@RequestParam String fileName) {
PutObjectUrlResponse response = userService.getProfileUploadUrl(fileName);
return ResponseEntity.ok(response);
}

}
7 changes: 1 addition & 6 deletions src/main/java/dev/steady/user/service/UserService.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -51,6 +53,7 @@
PositionController.class,
NotificationController.class,
ReviewController.class,
StorageImageController.class,
AuthContext.class,
JwtResolver.class,
JwtProperties.class,
Expand Down Expand Up @@ -88,6 +91,8 @@ public abstract class ControllerTestConfig {
@MockBean
protected ReviewService reviewService;
@MockBean
protected StorageService storageService;
@MockBean
protected JwtResolver jwtResolver;

@BeforeEach
Expand Down
Original file line number Diff line number Diff line change
@@ -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)));
}

}
22 changes: 22 additions & 0 deletions src/test/java/dev/steady/storage/fixture/StorageFixture.java
Original file line number Diff line number Diff line change
@@ -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);
}

}
29 changes: 0 additions & 29 deletions src/test/java/dev/steady/user/controller/UserControllerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)));
}

}
16 changes: 0 additions & 16 deletions src/test/java/dev/steady/user/fixture/UserFixtures.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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);
}

}