Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

내 프로필 사진 변경 API 구현 #86

Merged
merged 15 commits into from
Mar 26, 2024
Merged
Show file tree
Hide file tree
Changes from 8 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
5 changes: 5 additions & 0 deletions src/main/java/com/ajou/hertz/common/file/dto/FileDto.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,9 @@ public static FileDto create(
) {
return new FileDto(originalName, storedName, url);
}

public String getStoredFileUrl() {
return url + "/" + storedName;
}

Wo-ogie marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
Expand Up @@ -130,4 +130,17 @@ private static InputStream getInputStreamFromMultipartFile(MultipartFile multipa
throw new MultipartFileNotReadableException(ex);
}
}

/**
* S3 bucket에서 파일을 삭제한다.
*
* @param storedFileName 삭제할 파일의 이름 (key of bucket object)
*/
@Override
public void deleteFile(String storedFileName) {
s3Client.deleteObject(
new DeleteObjectRequest(awsProperties.s3().bucketName(), storedFileName)
);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,11 @@ public interface FileService {
* @param storedFileNames 삭제할 파일들의 이름 목록
*/
void deleteAll(Collection<String> storedFileNames);

/**
* 파일을 삭제한다.
*
* @param storedFileName 삭제할 파일의 이름
*/
void deleteFile(String storedFileName);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import java.net.URI;

import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.validation.annotation.Validated;
Expand All @@ -14,6 +15,7 @@
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import com.ajou.hertz.common.auth.UserPrincipal;
import com.ajou.hertz.common.validator.PhoneNumber;
Expand Down Expand Up @@ -117,6 +119,24 @@ public ResponseEntity<UserResponse> signUpV1(
.body(UserResponse.from(userCreated));
}

@Operation(
summary = "프로필 이미지 변경",
description = "프로필 이미지를 변경합니다.",
security = @SecurityRequirement(name = "access-token")
)
@PutMapping(
value = "/me/profile-image",
Wo-ogie marked this conversation as resolved.
Show resolved Hide resolved
headers = API_VERSION_HEADER_NAME + "=" + 1,
consumes = MediaType.MULTIPART_FORM_DATA_VALUE
)
public UserResponse updateProfileImageUrlV1(
Wo-ogie marked this conversation as resolved.
Show resolved Hide resolved
@AuthenticationPrincipal UserPrincipal userPrincipal,
@RequestParam("profileImage") MultipartFile profileImage
Wo-ogie marked this conversation as resolved.
Show resolved Hide resolved
) {
UserDto userUpdated = userCommandService.updateProfileImageUrl(userPrincipal.getUserId(), profileImage);
return UserResponse.from(userUpdated);
}

@Operation(
summary = "연락 수단 변경",
description = "연락 수단을 변경합니다.",
Expand All @@ -131,4 +151,5 @@ public UserResponse updateContactLinkV1(
updateContactLinkRequest.getContactLink());
return UserResponse.from(userUpdated);
}
}
}

Wo-ogie marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.ajou.hertz.domain.user.dto.request;

import org.springframework.web.multipart.MultipartFile;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@AllArgsConstructor(access = AccessLevel.PRIVATE)
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@Getter
public class UpdateProfileImageUrlRequest {

@Schema(description = "프로필 이미지")
private MultipartFile file;

}
8 changes: 8 additions & 0 deletions src/main/java/com/ajou/hertz/domain/user/entity/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,14 @@ public static User create(
);
}

public void changeProfileImageUrl(String profileImageUrl) {
this.profileImageUrl = profileImageUrl;
}

public UserProfileImage getProfileImage() {
return UserProfileImage.of(profileImageUrl);
}
Wo-ogie marked this conversation as resolved.
Show resolved Hide resolved

public void changeContactLink(String contactLink) {
this.contactLink = contactLink;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,8 @@ private UserProfileImage(Long id, User user, String originalName, String storedN
this.id = id;
this.user = user;
}

public static UserProfileImage of(String profileImageUrl) {
return new UserProfileImage(null, null, null, null, profileImageUrl);
}
Wo-ogie marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;

import com.ajou.hertz.common.file.dto.FileDto;
import com.ajou.hertz.common.file.service.FileService;
import com.ajou.hertz.common.kakao.dto.response.KakaoUserInfoResponse;
import com.ajou.hertz.common.properties.HertzProperties;
import com.ajou.hertz.domain.user.constant.Gender;
Expand All @@ -29,6 +32,7 @@ public class UserCommandService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final HertzProperties hertzProperties;
private final FileService fileService;

/**
* 새로운 회원을 등록한다.
Expand Down Expand Up @@ -131,6 +135,31 @@ private String generateRandom16CharString() {
}

/**
* 유저의 프로필 이미지를 업데이트한다.
*
* @param userId 유저 id
* @param newProfileImage 새로운 프로필 이미지
*
* @return 업데이트된 유저 정보
*/
Wo-ogie marked this conversation as resolved.
Show resolved Hide resolved
public UserDto updateProfileImageUrl(Long userId, MultipartFile newProfileImage) {
User user = userQueryService.getById(userId);
String uploadPath = "user-profile-image/";

FileDto uploadedFile = fileService.uploadFile(newProfileImage, uploadPath);
String newProfileImageUrl = uploadedFile.getStoredFileUrl();

String oldProfileImageUrl = user.getProfileImageUrl();

String oldFileName = oldProfileImageUrl.substring(oldProfileImageUrl.lastIndexOf('/') + 1);
String fullPathToDelete = uploadPath + oldFileName;
fileService.deleteFile(fullPathToDelete);

user.changeProfileImageUrl(newProfileImageUrl);
return UserDto.from(user);
}
Wo-ogie marked this conversation as resolved.
Show resolved Hide resolved

/**
*연락 수단을 변경합니다.
*
* @param userId 유저의 ID
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,20 @@ void setUpMockProperties() {
verifyEveryMocksShouldHaveNoMoreInteractions();
}

@Test
void S3에_저장된_파일의_이름이_주어지고_파일을_S3_버킷에서_삭제한다() throws Exception {
// given
String storedFileName = "stored-file-name";
willDoNothing().given(s3Client).deleteObject(any(DeleteObjectRequest.class));

// when
sut.deleteFile(storedFileName);

// then
then(s3Client).should().deleteObject(any(DeleteObjectRequest.class));
verifyEveryMocksShouldHaveNoMoreInteractions();
}

private void verifyEveryMocksShouldHaveNoMoreInteractions() {
then(s3Client).shouldHaveNoMoreInteractions();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import org.springframework.context.annotation.Import;
import org.springframework.data.jpa.mapping.JpaMetamodelMappingContext;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.test.context.event.annotation.BeforeTestMethod;
import org.springframework.test.web.servlet.MockMvc;
Expand Down Expand Up @@ -212,6 +213,37 @@ public void securitySetUp() throws Exception {
verifyEveryMocksShouldHaveNoMoreInteractions();
}

@Test
void 주어진_id와_새로운_프로필_이미지로_프로필_이미지를_변경한다() throws Exception {
// given
long userId = 1L;
MockMultipartFile profileImage = new MockMultipartFile(
"profileImage",
"test.jpg",
"image/jpeg",
"test".getBytes()
);
UserDetails userDetails = createTestUser(userId);
UserDto expectedResult = createUserDto(userId);
given(userCommandService.updateProfileImageUrl(userId, profileImage)).willReturn(expectedResult);

// when & then
mvc.perform(
multipart("/api/users/me/profile-image")
.file(profileImage)
.header(API_VERSION_HEADER_NAME, 1)
.with(user(userDetails))
.with(request -> {
request.setMethod("PUT");
return request;
})
)
.andExpect(status().isOk())
.andExpect(jsonPath("$.profileImageUrl").value(expectedResult.getProfileImageUrl()));
then(userCommandService).should().updateProfileImageUrl(userId, profileImage);
verifyEveryMocksShouldHaveNoMoreInteractions();
}

@Test
void 주어진_연락수단을_새로운_연락수단으로_변경한다() throws Exception {
// given
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,12 @@
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.test.context.event.annotation.BeforeTestMethod;

import com.ajou.hertz.common.file.dto.FileDto;
import com.ajou.hertz.common.file.service.FileService;
import com.ajou.hertz.common.kakao.dto.response.KakaoUserInfoResponse;
import com.ajou.hertz.common.properties.HertzProperties;
import com.ajou.hertz.domain.user.constant.Gender;
Expand Down Expand Up @@ -55,6 +58,9 @@ class UserCommandServiceTest {
@Mock
private HertzProperties hertzProperties;

@Mock
private FileService fileService;

@BeforeTestMethod
public void setUp() {
given(hertzProperties.userDefaultProfileImageUrl()).willReturn("https://user-default-profile-image");
Expand Down Expand Up @@ -172,6 +178,53 @@ static Stream<Arguments> testDataForCreateNewUserWithKakao() throws Exception {
assertThat(t).isInstanceOf(UserKakaoUidDuplicationException.class);
}

@Test
void 주어진_id와_새_프로필_이미지로_프로필_이미지를_변경하고_이전_프로필_이미지는_삭제한다() throws Exception {
Long userId = 1L;
MockMultipartFile newProfileImage = new MockMultipartFile(
"profileImage",
"test.jpg",
"image/jpeg",
"test".getBytes()
);
User user = createUser(userId, "password", null, Gender.MALE);
given(userQueryService.getById(userId)).willReturn(user);
given(fileService.uploadFile(any(), anyString())).willReturn(
FileDto.create("new_profile_image.jpg", "new_profile_image.jpg",
"https://example.com/user-profile-images"));

// When
UserDto updatedUser = sut.updateProfileImageUrl(userId, newProfileImage);

// Then
then(userQueryService).should().getById(userId);
then(fileService).should().uploadFile(eq(newProfileImage), anyString());
then(fileService).should().deleteFile(anyString());
verifyEveryMocksShouldHaveNoMoreInteractions();
assertThat(updatedUser).hasFieldOrPropertyWithValue("profileImageUrl", user.getProfileImageUrl());
}

@Test
void 주어진_유저_ID와_새로운_프로필_이미지로_기존의_프로필_이미지를_변경한다_존재하지_않는_유저라면_예외가_발생한다() throws Exception {
// given
Long userId = 1L;
MockMultipartFile newProfileImage = new MockMultipartFile(
"profileImage",
"test.jpg",
"image/jpeg",
"test".getBytes()
);
given(userQueryService.getById(userId)).willThrow(UserNotFoundByIdException.class);

// when
Throwable t = catchThrowable(() -> sut.updateProfileImageUrl(userId, newProfileImage));

// then
then(userQueryService).should().getById(userId);
verifyEveryMocksShouldHaveNoMoreInteractions();
assertThat(t).isInstanceOf(UserNotFoundByIdException.class);
}

@Test
void 주어진_유저_ID와_연락_수단으로_연락_수단을_변경한다() throws Exception {
// given
Expand Down Expand Up @@ -209,6 +262,7 @@ private void verifyEveryMocksShouldHaveNoMoreInteractions() {
then(userQueryService).shouldHaveNoMoreInteractions();
then(userRepository).shouldHaveNoMoreInteractions();
then(passwordEncoder).shouldHaveNoMoreInteractions();
then(fileService).shouldHaveNoMoreInteractions();
}

private static User createUser(Long id, String password, String kakaoUid, Gender gender) throws Exception {
Expand Down
Loading