From c8334ecd3fb303fe6015871921cf78035493b068 Mon Sep 17 00:00:00 2001 From: tinon1004 Date: Sun, 17 Mar 2024 22:40:54 +0900 Subject: [PATCH 01/14] =?UTF-8?q?feat:=20#73=20AmazonS3Service=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EB=8B=A8=EA=B1=B4=20=EC=82=AD=EC=A0=9C=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/file/service/AmazonS3FileService.java | 13 +++++++++++++ .../ajou/hertz/common/file/service/FileService.java | 7 +++++++ 2 files changed, 20 insertions(+) diff --git a/src/main/java/com/ajou/hertz/common/file/service/AmazonS3FileService.java b/src/main/java/com/ajou/hertz/common/file/service/AmazonS3FileService.java index f3a0c8e..76acac8 100644 --- a/src/main/java/com/ajou/hertz/common/file/service/AmazonS3FileService.java +++ b/src/main/java/com/ajou/hertz/common/file/service/AmazonS3FileService.java @@ -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) + ); + } + } diff --git a/src/main/java/com/ajou/hertz/common/file/service/FileService.java b/src/main/java/com/ajou/hertz/common/file/service/FileService.java index c7ba27d..af75a5d 100644 --- a/src/main/java/com/ajou/hertz/common/file/service/FileService.java +++ b/src/main/java/com/ajou/hertz/common/file/service/FileService.java @@ -33,4 +33,11 @@ public interface FileService { * @param storedFileNames 삭제할 파일들의 이름 목록 */ void deleteAll(Collection storedFileNames); + + /** + * 파일을 삭제한다. + * + * @param storedFileName 삭제할 파일의 이름 + */ + void deleteFile(String storedFileName); } From d9cb14f69163c8023a533a3b22648c61c188e45d Mon Sep 17 00:00:00 2001 From: tinon1004 Date: Sun, 17 Mar 2024 22:51:23 +0900 Subject: [PATCH 02/14] =?UTF-8?q?feat:=20#73=20AmazonS3Service=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EB=8B=A8=EA=B1=B4=20=EC=82=AD=EC=A0=9C=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../file/service/AmazonS3FileServiceTest.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/test/java/com/ajou/hertz/unit/common/file/service/AmazonS3FileServiceTest.java b/src/test/java/com/ajou/hertz/unit/common/file/service/AmazonS3FileServiceTest.java index af4933f..a0c960f 100644 --- a/src/test/java/com/ajou/hertz/unit/common/file/service/AmazonS3FileServiceTest.java +++ b/src/test/java/com/ajou/hertz/unit/common/file/service/AmazonS3FileServiceTest.java @@ -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(); } From 40b17cb790e7d3f18f442f2f05fe77f794412c59 Mon Sep 17 00:00:00 2001 From: tinon1004 Date: Mon, 18 Mar 2024 20:25:51 +0900 Subject: [PATCH 03/14] =?UTF-8?q?feat:=20#73=20=EB=82=B4=20=ED=94=84?= =?UTF-8?q?=EB=A1=9C=ED=95=84=20=EC=82=AC=EC=A7=84=20=EB=B3=80=EA=B2=BD=20?= =?UTF-8?q?API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/controller/UserController.java | 19 +++++++++++ .../ajou/hertz/domain/user/entity/User.java | 9 +++++ .../domain/user/entity/UserProfileImage.java | 4 +++ .../user/service/UserCommandService.java | 15 ++++++++ .../user/controller/UserControllerTest.java | 28 +++++++++++++++ .../user/service/UserCommandServiceTest.java | 34 +++++++++++++++++++ 6 files changed, 109 insertions(+) diff --git a/src/main/java/com/ajou/hertz/domain/user/controller/UserController.java b/src/main/java/com/ajou/hertz/domain/user/controller/UserController.java index d3c4ed7..5bcb343 100644 --- a/src/main/java/com/ajou/hertz/domain/user/controller/UserController.java +++ b/src/main/java/com/ajou/hertz/domain/user/controller/UserController.java @@ -9,6 +9,7 @@ import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; @@ -18,6 +19,7 @@ import com.ajou.hertz.common.validator.PhoneNumber; import com.ajou.hertz.domain.user.dto.UserDto; import com.ajou.hertz.domain.user.dto.request.SignUpRequest; +import com.ajou.hertz.domain.user.dto.request.UpdateProfileImageUrlRequest; import com.ajou.hertz.domain.user.dto.response.UserEmailResponse; import com.ajou.hertz.domain.user.dto.response.UserExistenceResponse; import com.ajou.hertz.domain.user.dto.response.UserResponse; @@ -114,4 +116,21 @@ public ResponseEntity signUpV1( .created(URI.create("/users/" + userCreated.getId())) .body(UserResponse.from(userCreated)); } + + @Operation( + summary = "프로필 이미지 변경", + description = "프로필 이미지를 변경합니다.", + security = @SecurityRequirement(name = "access-token") + ) + @PutMapping(value = "/me/profile-image", headers = API_VERSION_HEADER_NAME + "=" + 1) + public UserResponse updateProfileImageUrlV1( + @AuthenticationPrincipal UserPrincipal userPrincipal, + @RequestBody @Valid UpdateProfileImageUrlRequest updateProfileImageUrlRequest + ) { + UserDto userUpdated = userCommandService.updateProfileImageUrl(userPrincipal.getUserId(), + updateProfileImageUrlRequest.getProfileImageUrl()); + UserDto userDto = userQueryService.getDtoById(userPrincipal.getUserId()); + return UserResponse.from(userUpdated); + } + } diff --git a/src/main/java/com/ajou/hertz/domain/user/entity/User.java b/src/main/java/com/ajou/hertz/domain/user/entity/User.java index f900e55..cb008a3 100644 --- a/src/main/java/com/ajou/hertz/domain/user/entity/User.java +++ b/src/main/java/com/ajou/hertz/domain/user/entity/User.java @@ -100,4 +100,13 @@ public static User create( profileImageUrl, birth, gender, phone, null ); } + + public void changeProfileImageUrl(String profileImageUrl) { + this.profileImageUrl = profileImageUrl; + } + + public UserProfileImage getProfileImage() { + return UserProfileImage.of(profileImageUrl); + } + } diff --git a/src/main/java/com/ajou/hertz/domain/user/entity/UserProfileImage.java b/src/main/java/com/ajou/hertz/domain/user/entity/UserProfileImage.java index e899fa8..5d1ba44 100644 --- a/src/main/java/com/ajou/hertz/domain/user/entity/UserProfileImage.java +++ b/src/main/java/com/ajou/hertz/domain/user/entity/UserProfileImage.java @@ -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); + } } diff --git a/src/main/java/com/ajou/hertz/domain/user/service/UserCommandService.java b/src/main/java/com/ajou/hertz/domain/user/service/UserCommandService.java index 59bb57d..ebb0d83 100644 --- a/src/main/java/com/ajou/hertz/domain/user/service/UserCommandService.java +++ b/src/main/java/com/ajou/hertz/domain/user/service/UserCommandService.java @@ -129,4 +129,19 @@ private String generateRandom16CharString() { .toString() .substring(0, 16); } + + /** + * 회원의 프로필 이미지를 변경한다. + * + * @param userId 회원 id + * @param profileImageUrl 변경할 프로필 이미지 url + * + * @return 변경된 회원 정보 + */ + public UserDto updateProfileImageUrl(Long userId, String profileImageUrl) { + User user = userQueryService.getById(userId); + user.changeProfileImageUrl(profileImageUrl); + return UserDto.from(user); + } + } diff --git a/src/test/java/com/ajou/hertz/unit/domain/user/controller/UserControllerTest.java b/src/test/java/com/ajou/hertz/unit/domain/user/controller/UserControllerTest.java index 281631b..767f4d8 100644 --- a/src/test/java/com/ajou/hertz/unit/domain/user/controller/UserControllerTest.java +++ b/src/test/java/com/ajou/hertz/unit/domain/user/controller/UserControllerTest.java @@ -35,6 +35,7 @@ import com.ajou.hertz.domain.user.controller.UserController; import com.ajou.hertz.domain.user.dto.UserDto; import com.ajou.hertz.domain.user.dto.request.SignUpRequest; +import com.ajou.hertz.domain.user.dto.request.UpdateProfileImageUrlRequest; import com.ajou.hertz.domain.user.service.UserCommandService; import com.ajou.hertz.domain.user.service.UserQueryService; import com.ajou.hertz.util.ReflectionUtils; @@ -212,6 +213,33 @@ public void securitySetUp() throws Exception { verifyEveryMocksShouldHaveNoMoreInteractions(); } + @Test + void 주어진_id와_새로운_프로필_이미지로_프로필_이미지를_변경한다() throws Exception { + // given + long userId = 1L; + String profileImageUrl = "https://example.com/new_profile_image.jpg"; + UpdateProfileImageUrlRequest updateProfileImageUrlRequest = new UpdateProfileImageUrlRequest(profileImageUrl); + UserDetails userDetails = createTestUser(userId); + UserDto updatedUserDto = createUserDto(userId); + given(userCommandService.updateProfileImageUrl(userId, profileImageUrl)).willReturn(updatedUserDto); + given(userQueryService.getDtoById(userId)).willReturn(createUserDto(userId)); + + // when & then + mvc.perform( + put("/api/users/me/profile-image") + .header(API_VERSION_HEADER_NAME, 1) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(updateProfileImageUrlRequest)) + .with(user(userDetails)) + ) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.profileImageUrl").value(updatedUserDto.getProfileImageUrl())); + + then(userCommandService).should().updateProfileImageUrl(userId, profileImageUrl); + then(userQueryService).should().getDtoById(userId); + verifyEveryMocksShouldHaveNoMoreInteractions(); + } + private void verifyEveryMocksShouldHaveNoMoreInteractions() { then(userCommandService).shouldHaveNoMoreInteractions(); then(userQueryService).shouldHaveNoMoreInteractions(); diff --git a/src/test/java/com/ajou/hertz/unit/domain/user/service/UserCommandServiceTest.java b/src/test/java/com/ajou/hertz/unit/domain/user/service/UserCommandServiceTest.java index 1e1dedd..8e94fa9 100644 --- a/src/test/java/com/ajou/hertz/unit/domain/user/service/UserCommandServiceTest.java +++ b/src/test/java/com/ajou/hertz/unit/domain/user/service/UserCommandServiceTest.java @@ -29,6 +29,7 @@ import com.ajou.hertz.domain.user.entity.User; import com.ajou.hertz.domain.user.exception.UserEmailDuplicationException; import com.ajou.hertz.domain.user.exception.UserKakaoUidDuplicationException; +import com.ajou.hertz.domain.user.exception.UserNotFoundByIdException; import com.ajou.hertz.domain.user.exception.UserPhoneDuplicationException; import com.ajou.hertz.domain.user.repository.UserRepository; import com.ajou.hertz.domain.user.service.UserCommandService; @@ -171,6 +172,39 @@ static Stream testDataForCreateNewUserWithKakao() throws Exception { assertThat(t).isInstanceOf(UserKakaoUidDuplicationException.class); } + @Test + void 주어진_id와_새_프로필_이미지로_프로필_이미지를_변경한다() throws Exception { + // given + Long userId = 1L; + String newProfileImageUrl = "https://new-profile-image-url"; + User user = createUser(userId, "$2a$abc123", "12345"); + given(userQueryService.getById(userId)).willReturn(user); + + // when + UserDto updatedUserDto = sut.updateProfileImageUrl(userId, newProfileImageUrl); + + // then + then(userQueryService).should().getById(userId); + verifyEveryMocksShouldHaveNoMoreInteractions(); + assertThat(updatedUserDto.getProfileImageUrl()).isEqualTo(newProfileImageUrl); + } + + @Test + void 주어진_유저_ID와_새로운_프로필_이미지로_기존의_프로필_이미지를_변경한다_존재하지_않는_유저라면_예외가_발생한다() throws Exception { + // given + Long userId = 1L; + String newProfileImageUrl = "https://new-profile-image-url"; + given(userQueryService.getById(userId)).willThrow(UserNotFoundByIdException.class); + + // when + Throwable t = catchThrowable(() -> sut.updateProfileImageUrl(userId, newProfileImageUrl)); + + // then + then(userQueryService).should().getById(userId); + verifyEveryMocksShouldHaveNoMoreInteractions(); + assertThat(t).isInstanceOf(UserNotFoundByIdException.class); + } + private void verifyEveryMocksShouldHaveNoMoreInteractions() { then(userQueryService).shouldHaveNoMoreInteractions(); then(userRepository).shouldHaveNoMoreInteractions(); From 5537aa4ee0007faccdb4d6458784f6751a33931b Mon Sep 17 00:00:00 2001 From: tinon1004 Date: Mon, 18 Mar 2024 21:27:10 +0900 Subject: [PATCH 04/14] =?UTF-8?q?feat:=20#72=20UpdateProfileImageUrlReques?= =?UTF-8?q?t=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../request/UpdateProfileImageUrlRequest.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/main/java/com/ajou/hertz/domain/user/dto/request/UpdateProfileImageUrlRequest.java diff --git a/src/main/java/com/ajou/hertz/domain/user/dto/request/UpdateProfileImageUrlRequest.java b/src/main/java/com/ajou/hertz/domain/user/dto/request/UpdateProfileImageUrlRequest.java new file mode 100644 index 0000000..e78d29b --- /dev/null +++ b/src/main/java/com/ajou/hertz/domain/user/dto/request/UpdateProfileImageUrlRequest.java @@ -0,0 +1,19 @@ +package com.ajou.hertz.domain.user.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PRIVATE) +@Getter +public class UpdateProfileImageUrlRequest { + + @Schema(description = "프로필 이미지", example = "https://user-default-profile-image") + @NotBlank + private String profileImageUrl; + +} From 616b50033b4dc187863a5a6f7f42f1b24963d81a Mon Sep 17 00:00:00 2001 From: tinon1004 Date: Mon, 18 Mar 2024 22:01:08 +0900 Subject: [PATCH 05/14] =?UTF-8?q?feat:=20#73=20=ED=94=84=EB=A1=9C=ED=95=84?= =?UTF-8?q?=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EB=B3=80=EA=B2=BD=20=EC=8B=9C?= =?UTF-8?q?=20=EC=9D=B4=EC=A0=84=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/user/service/UserCommandService.java | 10 +++++++--- .../domain/user/service/UserCommandServiceTest.java | 13 ++++++++++--- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/ajou/hertz/domain/user/service/UserCommandService.java b/src/main/java/com/ajou/hertz/domain/user/service/UserCommandService.java index ebb0d83..3c8e661 100644 --- a/src/main/java/com/ajou/hertz/domain/user/service/UserCommandService.java +++ b/src/main/java/com/ajou/hertz/domain/user/service/UserCommandService.java @@ -7,6 +7,7 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; +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; @@ -29,6 +30,7 @@ public class UserCommandService { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; private final HertzProperties hertzProperties; + private final FileService fileService; /** * 새로운 회원을 등록한다. @@ -131,16 +133,18 @@ private String generateRandom16CharString() { } /** - * 회원의 프로필 이미지를 변경한다. + * 회원의 프로필 이미지를 변경하고, 이전 이미지를 삭제한다. * - * @param userId 회원 id - * @param profileImageUrl 변경할 프로필 이미지 url + * @param userId 회원 id + * @param profileImageUrl 변경할 프로필 이미지 url * * @return 변경된 회원 정보 */ public UserDto updateProfileImageUrl(Long userId, String profileImageUrl) { User user = userQueryService.getById(userId); + String oldProfileImageUrl = user.getProfileImageUrl(); user.changeProfileImageUrl(profileImageUrl); + fileService.deleteFile(oldProfileImageUrl); return UserDto.from(user); } diff --git a/src/test/java/com/ajou/hertz/unit/domain/user/service/UserCommandServiceTest.java b/src/test/java/com/ajou/hertz/unit/domain/user/service/UserCommandServiceTest.java index 8e94fa9..c1f743d 100644 --- a/src/test/java/com/ajou/hertz/unit/domain/user/service/UserCommandServiceTest.java +++ b/src/test/java/com/ajou/hertz/unit/domain/user/service/UserCommandServiceTest.java @@ -20,6 +20,7 @@ import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.test.context.event.annotation.BeforeTestMethod; +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; @@ -55,6 +56,9 @@ class UserCommandServiceTest { @Mock private HertzProperties hertzProperties; + @Mock + private FileService fileService; + @BeforeTestMethod public void setUp() { given(hertzProperties.userDefaultProfileImageUrl()).willReturn("https://user-default-profile-image"); @@ -173,11 +177,12 @@ static Stream testDataForCreateNewUserWithKakao() throws Exception { } @Test - void 주어진_id와_새_프로필_이미지로_프로필_이미지를_변경한다() throws Exception { + void 주어진_id와_새_프로필_이미지로_프로필_이미지를_변경하고_이전_프로필_이미지는_삭제한다() throws Exception { // given Long userId = 1L; String newProfileImageUrl = "https://new-profile-image-url"; - User user = createUser(userId, "$2a$abc123", "12345"); + String oldProfileImageUrl = "https://user-default-profile-image-url"; + User user = createUser(userId, "$2a$abc123", oldProfileImageUrl); given(userQueryService.getById(userId)).willReturn(user); // when @@ -185,7 +190,8 @@ static Stream testDataForCreateNewUserWithKakao() throws Exception { // then then(userQueryService).should().getById(userId); - verifyEveryMocksShouldHaveNoMoreInteractions(); + then(fileService).should().deleteFile(oldProfileImageUrl); + verifyNoMoreInteractions(userQueryService, fileService); assertThat(updatedUserDto.getProfileImageUrl()).isEqualTo(newProfileImageUrl); } @@ -209,6 +215,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 { From 5a34020d158ca4396edd4618a03a982696e112f2 Mon Sep 17 00:00:00 2001 From: tinon1004 Date: Thu, 21 Mar 2024 18:38:05 +0900 Subject: [PATCH 06/14] =?UTF-8?q?refactor:=20#73=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=97=85=EB=A1=9C=EB=93=9C=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ajou/hertz/common/file/dto/FileDto.java | 5 +++ .../user/controller/UserController.java | 16 ++++---- .../request/UpdateProfileImageUrlRequest.java | 10 ++--- .../user/service/UserCommandService.java | 26 +++++++++---- .../user/controller/UserControllerTest.java | 30 ++++++++------- .../user/service/UserCommandServiceTest.java | 38 +++++++++++++------ 6 files changed, 80 insertions(+), 45 deletions(-) diff --git a/src/main/java/com/ajou/hertz/common/file/dto/FileDto.java b/src/main/java/com/ajou/hertz/common/file/dto/FileDto.java index f919022..3abe465 100644 --- a/src/main/java/com/ajou/hertz/common/file/dto/FileDto.java +++ b/src/main/java/com/ajou/hertz/common/file/dto/FileDto.java @@ -21,4 +21,9 @@ public static FileDto create( ) { return new FileDto(originalName, storedName, url); } + + public String getStoredFileUrl() { + return url + "/" + storedName; + } + } diff --git a/src/main/java/com/ajou/hertz/domain/user/controller/UserController.java b/src/main/java/com/ajou/hertz/domain/user/controller/UserController.java index 5bcb343..d8ca637 100644 --- a/src/main/java/com/ajou/hertz/domain/user/controller/UserController.java +++ b/src/main/java/com/ajou/hertz/domain/user/controller/UserController.java @@ -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; @@ -14,12 +15,12 @@ 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; import com.ajou.hertz.domain.user.dto.UserDto; import com.ajou.hertz.domain.user.dto.request.SignUpRequest; -import com.ajou.hertz.domain.user.dto.request.UpdateProfileImageUrlRequest; import com.ajou.hertz.domain.user.dto.response.UserEmailResponse; import com.ajou.hertz.domain.user.dto.response.UserExistenceResponse; import com.ajou.hertz.domain.user.dto.response.UserResponse; @@ -122,15 +123,16 @@ public ResponseEntity signUpV1( description = "프로필 이미지를 변경합니다.", security = @SecurityRequirement(name = "access-token") ) - @PutMapping(value = "/me/profile-image", headers = API_VERSION_HEADER_NAME + "=" + 1) + @PutMapping( + value = "/me/profile-image", + headers = API_VERSION_HEADER_NAME + "=" + 1, + consumes = MediaType.MULTIPART_FORM_DATA_VALUE + ) public UserResponse updateProfileImageUrlV1( @AuthenticationPrincipal UserPrincipal userPrincipal, - @RequestBody @Valid UpdateProfileImageUrlRequest updateProfileImageUrlRequest + @RequestParam("profileImage") MultipartFile profileImage ) { - UserDto userUpdated = userCommandService.updateProfileImageUrl(userPrincipal.getUserId(), - updateProfileImageUrlRequest.getProfileImageUrl()); - UserDto userDto = userQueryService.getDtoById(userPrincipal.getUserId()); + UserDto userUpdated = userCommandService.updateProfileImageUrl(userPrincipal.getUserId(), profileImage); return UserResponse.from(userUpdated); } - } diff --git a/src/main/java/com/ajou/hertz/domain/user/dto/request/UpdateProfileImageUrlRequest.java b/src/main/java/com/ajou/hertz/domain/user/dto/request/UpdateProfileImageUrlRequest.java index e78d29b..66957e3 100644 --- a/src/main/java/com/ajou/hertz/domain/user/dto/request/UpdateProfileImageUrlRequest.java +++ b/src/main/java/com/ajou/hertz/domain/user/dto/request/UpdateProfileImageUrlRequest.java @@ -1,19 +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 jakarta.validation.constraints.NotBlank; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; -@AllArgsConstructor +@AllArgsConstructor(access = AccessLevel.PRIVATE) @NoArgsConstructor(access = AccessLevel.PRIVATE) @Getter public class UpdateProfileImageUrlRequest { - @Schema(description = "프로필 이미지", example = "https://user-default-profile-image") - @NotBlank - private String profileImageUrl; + @Schema(description = "프로필 이미지") + private MultipartFile file; } diff --git a/src/main/java/com/ajou/hertz/domain/user/service/UserCommandService.java b/src/main/java/com/ajou/hertz/domain/user/service/UserCommandService.java index 3c8e661..8690f92 100644 --- a/src/main/java/com/ajou/hertz/domain/user/service/UserCommandService.java +++ b/src/main/java/com/ajou/hertz/domain/user/service/UserCommandService.java @@ -6,7 +6,9 @@ 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; @@ -133,18 +135,26 @@ private String generateRandom16CharString() { } /** - * 회원의 프로필 이미지를 변경하고, 이전 이미지를 삭제한다. + * 유저의 프로필 이미지를 업데이트한다. * - * @param userId 회원 id - * @param profileImageUrl 변경할 프로필 이미지 url - * - * @return 변경된 회원 정보 + * @param userId 유저 id + * @param newProfileImage 새로운 프로필 이미지 + * @return 업데이트된 유저 정보 */ - public UserDto updateProfileImageUrl(Long userId, String profileImageUrl) { + 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(); - user.changeProfileImageUrl(profileImageUrl); - fileService.deleteFile(oldProfileImageUrl); + + String oldFileName = oldProfileImageUrl.substring(oldProfileImageUrl.lastIndexOf('/') + 1); + String fullPathToDelete = uploadPath + oldFileName; + fileService.deleteFile(fullPathToDelete); + + user.changeProfileImageUrl(newProfileImageUrl); return UserDto.from(user); } diff --git a/src/test/java/com/ajou/hertz/unit/domain/user/controller/UserControllerTest.java b/src/test/java/com/ajou/hertz/unit/domain/user/controller/UserControllerTest.java index 767f4d8..fb10cf2 100644 --- a/src/test/java/com/ajou/hertz/unit/domain/user/controller/UserControllerTest.java +++ b/src/test/java/com/ajou/hertz/unit/domain/user/controller/UserControllerTest.java @@ -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; @@ -35,7 +36,6 @@ import com.ajou.hertz.domain.user.controller.UserController; import com.ajou.hertz.domain.user.dto.UserDto; import com.ajou.hertz.domain.user.dto.request.SignUpRequest; -import com.ajou.hertz.domain.user.dto.request.UpdateProfileImageUrlRequest; import com.ajou.hertz.domain.user.service.UserCommandService; import com.ajou.hertz.domain.user.service.UserQueryService; import com.ajou.hertz.util.ReflectionUtils; @@ -217,26 +217,30 @@ public void securitySetUp() throws Exception { void 주어진_id와_새로운_프로필_이미지로_프로필_이미지를_변경한다() throws Exception { // given long userId = 1L; - String profileImageUrl = "https://example.com/new_profile_image.jpg"; - UpdateProfileImageUrlRequest updateProfileImageUrlRequest = new UpdateProfileImageUrlRequest(profileImageUrl); + MockMultipartFile profileImage = new MockMultipartFile( + "profileImage", + "test.jpg", + "image/jpeg", + "test".getBytes() + ); UserDetails userDetails = createTestUser(userId); - UserDto updatedUserDto = createUserDto(userId); - given(userCommandService.updateProfileImageUrl(userId, profileImageUrl)).willReturn(updatedUserDto); - given(userQueryService.getDtoById(userId)).willReturn(createUserDto(userId)); + UserDto expectedResult = createUserDto(userId); + given(userCommandService.updateProfileImageUrl(userId, profileImage)).willReturn(expectedResult); // when & then mvc.perform( - put("/api/users/me/profile-image") + multipart("/api/users/me/profile-image") + .file(profileImage) .header(API_VERSION_HEADER_NAME, 1) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(updateProfileImageUrlRequest)) .with(user(userDetails)) + .with(request -> { + request.setMethod("PUT"); + return request; + }) ) .andExpect(status().isOk()) - .andExpect(jsonPath("$.profileImageUrl").value(updatedUserDto.getProfileImageUrl())); - - then(userCommandService).should().updateProfileImageUrl(userId, profileImageUrl); - then(userQueryService).should().getDtoById(userId); + .andExpect(jsonPath("$.profileImageUrl").value(expectedResult.getProfileImageUrl())); + then(userCommandService).should().updateProfileImageUrl(userId, profileImage); verifyEveryMocksShouldHaveNoMoreInteractions(); } diff --git a/src/test/java/com/ajou/hertz/unit/domain/user/service/UserCommandServiceTest.java b/src/test/java/com/ajou/hertz/unit/domain/user/service/UserCommandServiceTest.java index c1f743d..2ba58e2 100644 --- a/src/test/java/com/ajou/hertz/unit/domain/user/service/UserCommandServiceTest.java +++ b/src/test/java/com/ajou/hertz/unit/domain/user/service/UserCommandServiceTest.java @@ -17,9 +17,11 @@ 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; @@ -178,32 +180,44 @@ static Stream testDataForCreateNewUserWithKakao() throws Exception { @Test void 주어진_id와_새_프로필_이미지로_프로필_이미지를_변경하고_이전_프로필_이미지는_삭제한다() throws Exception { - // given Long userId = 1L; - String newProfileImageUrl = "https://new-profile-image-url"; - String oldProfileImageUrl = "https://user-default-profile-image-url"; - User user = createUser(userId, "$2a$abc123", oldProfileImageUrl); + 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 updatedUserDto = sut.updateProfileImageUrl(userId, newProfileImageUrl); + // When + UserDto updatedUser = sut.updateProfileImageUrl(userId, newProfileImage); - // then + // Then then(userQueryService).should().getById(userId); - then(fileService).should().deleteFile(oldProfileImageUrl); - verifyNoMoreInteractions(userQueryService, fileService); - assertThat(updatedUserDto.getProfileImageUrl()).isEqualTo(newProfileImageUrl); + 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; - String newProfileImageUrl = "https://new-profile-image-url"; + 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, newProfileImageUrl)); + Throwable t = catchThrowable(() -> sut.updateProfileImageUrl(userId, newProfileImage)); // then then(userQueryService).should().getById(userId); From e05d956918d59e6d72e25a76679a777fdc431842 Mon Sep 17 00:00:00 2001 From: tinon1004 Date: Thu, 21 Mar 2024 18:59:36 +0900 Subject: [PATCH 07/14] =?UTF-8?q?chore:=20#73=20issue=20#72=EC=99=80=20?= =?UTF-8?q?=EB=B0=9C=EC=83=9D=ED=95=9C=20=EB=A8=B8=EC=A7=80=20=EC=B6=A9?= =?UTF-8?q?=EB=8F=8C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ajou/hertz/domain/user/controller/UserController.java | 3 +-- src/main/java/com/ajou/hertz/domain/user/entity/User.java | 2 +- .../unit/domain/user/controller/UserControllerTest.java | 6 +++--- .../unit/domain/user/service/UserCommandServiceTest.java | 8 ++++---- 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/ajou/hertz/domain/user/controller/UserController.java b/src/main/java/com/ajou/hertz/domain/user/controller/UserController.java index c7cad1f..a9183f0 100644 --- a/src/main/java/com/ajou/hertz/domain/user/controller/UserController.java +++ b/src/main/java/com/ajou/hertz/domain/user/controller/UserController.java @@ -136,8 +136,7 @@ public UserResponse updateProfileImageUrlV1( UserDto userUpdated = userCommandService.updateProfileImageUrl(userPrincipal.getUserId(), profileImage); return UserResponse.from(userUpdated); } -} - + @Operation( summary = "연락 수단 변경", description = "연락 수단을 변경합니다.", diff --git a/src/main/java/com/ajou/hertz/domain/user/entity/User.java b/src/main/java/com/ajou/hertz/domain/user/entity/User.java index 4fa7dfd..48594d8 100644 --- a/src/main/java/com/ajou/hertz/domain/user/entity/User.java +++ b/src/main/java/com/ajou/hertz/domain/user/entity/User.java @@ -101,13 +101,13 @@ public static User create( ); } - public void changeProfileImageUrl(String profileImageUrl) { this.profileImageUrl = profileImageUrl; } public UserProfileImage getProfileImage() { return UserProfileImage.of(profileImageUrl); + } public void changeContactLink(String contactLink) { this.contactLink = contactLink; diff --git a/src/test/java/com/ajou/hertz/unit/domain/user/controller/UserControllerTest.java b/src/test/java/com/ajou/hertz/unit/domain/user/controller/UserControllerTest.java index ff4a09c..bd4b7bc 100644 --- a/src/test/java/com/ajou/hertz/unit/domain/user/controller/UserControllerTest.java +++ b/src/test/java/com/ajou/hertz/unit/domain/user/controller/UserControllerTest.java @@ -241,10 +241,10 @@ public void securitySetUp() throws Exception { .andExpect(status().isOk()) .andExpect(jsonPath("$.profileImageUrl").value(expectedResult.getProfileImageUrl())); then(userCommandService).should().updateProfileImageUrl(userId, profileImage); - verifyEveryMocksShouldHaveNoMoreInteractions(); + verifyEveryMocksShouldHaveNoMoreInteractions(); } - - + + @Test void 주어진_연락수단을_새로운_연락수단으로_변경한다() throws Exception { // given long userId = 1L; diff --git a/src/test/java/com/ajou/hertz/unit/domain/user/service/UserCommandServiceTest.java b/src/test/java/com/ajou/hertz/unit/domain/user/service/UserCommandServiceTest.java index bfc0016..ca75e0f 100644 --- a/src/test/java/com/ajou/hertz/unit/domain/user/service/UserCommandServiceTest.java +++ b/src/test/java/com/ajou/hertz/unit/domain/user/service/UserCommandServiceTest.java @@ -218,14 +218,14 @@ static Stream testDataForCreateNewUserWithKakao() throws Exception { // when Throwable t = catchThrowable(() -> sut.updateProfileImageUrl(userId, newProfileImage)); - - // then + + // then then(userQueryService).should().getById(userId); verifyEveryMocksShouldHaveNoMoreInteractions(); assertThat(t).isInstanceOf(UserNotFoundByIdException.class); } - - + + @Test void 주어진_유저_ID와_연락_수단으로_연락_수단을_변경한다() throws Exception { // given Long userId = 1L; From 484f529219d9082ebf1132ea98865103a4109777 Mon Sep 17 00:00:00 2001 From: tinon1004 Date: Sun, 24 Mar 2024 04:41:08 +0900 Subject: [PATCH 08/14] =?UTF-8?q?refactor:=20#73=20service=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95=20=EB=B0=8F?= =?UTF-8?q?=20=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ajou/hertz/common/file/dto/FileDto.java | 3 +- .../user/controller/UserController.java | 14 +- .../ajou/hertz/domain/user/entity/User.java | 4 - .../domain/user/entity/UserProfileImage.java | 12 +- .../UserProfileImageRepository.java | 11 ++ .../user/service/UserCommandService.java | 33 ++-- .../UserProfileImageCommandService.java | 54 ++++++ .../user/controller/UserControllerTest.java | 24 ++- .../user/service/UserCommandServiceTest.java | 91 +++++++--- .../UserProfileImageCommandServiceTest.java | 164 ++++++++++++++++++ 10 files changed, 346 insertions(+), 64 deletions(-) create mode 100644 src/main/java/com/ajou/hertz/domain/user/repository/UserProfileImageRepository.java create mode 100644 src/main/java/com/ajou/hertz/domain/user/service/UserProfileImageCommandService.java create mode 100644 src/test/java/com/ajou/hertz/unit/domain/user/service/UserProfileImageCommandServiceTest.java diff --git a/src/main/java/com/ajou/hertz/common/file/dto/FileDto.java b/src/main/java/com/ajou/hertz/common/file/dto/FileDto.java index 3abe465..0ad6c16 100644 --- a/src/main/java/com/ajou/hertz/common/file/dto/FileDto.java +++ b/src/main/java/com/ajou/hertz/common/file/dto/FileDto.java @@ -23,7 +23,6 @@ public static FileDto create( } public String getStoredFileUrl() { - return url + "/" + storedName; + return url; } - } diff --git a/src/main/java/com/ajou/hertz/domain/user/controller/UserController.java b/src/main/java/com/ajou/hertz/domain/user/controller/UserController.java index a9183f0..081a0f7 100644 --- a/src/main/java/com/ajou/hertz/domain/user/controller/UserController.java +++ b/src/main/java/com/ajou/hertz/domain/user/controller/UserController.java @@ -14,10 +14,12 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RequestPart; 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.file.dto.FileDto; import com.ajou.hertz.common.validator.PhoneNumber; import com.ajou.hertz.domain.user.dto.UserDto; import com.ajou.hertz.domain.user.dto.request.SignUpRequest; @@ -27,6 +29,7 @@ import com.ajou.hertz.domain.user.dto.response.UserResponse; import com.ajou.hertz.domain.user.dto.response.UserWithLinkedAccountInfoResponse; import com.ajou.hertz.domain.user.service.UserCommandService; +import com.ajou.hertz.domain.user.service.UserProfileImageCommandService; import com.ajou.hertz.domain.user.service.UserQueryService; import io.swagger.v3.oas.annotations.Operation; @@ -49,6 +52,7 @@ public class UserController { private final UserCommandService userCommandService; + private final UserProfileImageCommandService userProfileImageCommandService; private final UserQueryService userQueryService; @Operation( @@ -125,15 +129,17 @@ public ResponseEntity signUpV1( security = @SecurityRequirement(name = "access-token") ) @PutMapping( - value = "/me/profile-image", + value = "/me/profile-images", headers = API_VERSION_HEADER_NAME + "=" + 1, consumes = MediaType.MULTIPART_FORM_DATA_VALUE ) - public UserResponse updateProfileImageUrlV1( + public UserResponse updateProfileImageV1( @AuthenticationPrincipal UserPrincipal userPrincipal, - @RequestParam("profileImage") MultipartFile profileImage + @RequestPart("profileImage") MultipartFile profileImage ) { - UserDto userUpdated = userCommandService.updateProfileImageUrl(userPrincipal.getUserId(), profileImage); + FileDto uploadedFile = userProfileImageCommandService.uploadProfileImage(userPrincipal.getUserId(), + profileImage); + UserDto userUpdated = userCommandService.updateProfileImage(userPrincipal.getUserId(), uploadedFile); return UserResponse.from(userUpdated); } diff --git a/src/main/java/com/ajou/hertz/domain/user/entity/User.java b/src/main/java/com/ajou/hertz/domain/user/entity/User.java index 48594d8..e1340e5 100644 --- a/src/main/java/com/ajou/hertz/domain/user/entity/User.java +++ b/src/main/java/com/ajou/hertz/domain/user/entity/User.java @@ -105,10 +105,6 @@ public void changeProfileImageUrl(String profileImageUrl) { this.profileImageUrl = profileImageUrl; } - public UserProfileImage getProfileImage() { - return UserProfileImage.of(profileImageUrl); - } - public void changeContactLink(String contactLink) { this.contactLink = contactLink; } diff --git a/src/main/java/com/ajou/hertz/domain/user/entity/UserProfileImage.java b/src/main/java/com/ajou/hertz/domain/user/entity/UserProfileImage.java index 5d1ba44..aacdab8 100644 --- a/src/main/java/com/ajou/hertz/domain/user/entity/UserProfileImage.java +++ b/src/main/java/com/ajou/hertz/domain/user/entity/UserProfileImage.java @@ -24,7 +24,7 @@ public class UserProfileImage extends FileEntity { @Column(name = "user_profile_image_id", nullable = false) private Long id; - @JoinColumn(name = "user_id", nullable = false) + @JoinColumn(name = "user_id", nullable = false, unique = true) @OneToOne(fetch = FetchType.LAZY) private User user; @@ -34,7 +34,13 @@ private UserProfileImage(Long id, User user, String originalName, String storedN this.user = user; } - public static UserProfileImage of(String profileImageUrl) { - return new UserProfileImage(null, null, null, null, profileImageUrl); + public static UserProfileImage create( + User user, + String originalName, + String storedName, + String url + ) { + return new UserProfileImage(null, user, originalName, storedName, url); } + } diff --git a/src/main/java/com/ajou/hertz/domain/user/repository/UserProfileImageRepository.java b/src/main/java/com/ajou/hertz/domain/user/repository/UserProfileImageRepository.java new file mode 100644 index 0000000..7cf665b --- /dev/null +++ b/src/main/java/com/ajou/hertz/domain/user/repository/UserProfileImageRepository.java @@ -0,0 +1,11 @@ +package com.ajou.hertz.domain.user.repository; + +import java.util.Optional; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.ajou.hertz.domain.user.entity.UserProfileImage; + +public interface UserProfileImageRepository extends JpaRepository { + Optional findById(Long userId); +} diff --git a/src/main/java/com/ajou/hertz/domain/user/service/UserCommandService.java b/src/main/java/com/ajou/hertz/domain/user/service/UserCommandService.java index 80080df..f400983 100644 --- a/src/main/java/com/ajou/hertz/domain/user/service/UserCommandService.java +++ b/src/main/java/com/ajou/hertz/domain/user/service/UserCommandService.java @@ -6,10 +6,8 @@ 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; @@ -19,6 +17,7 @@ import com.ajou.hertz.domain.user.exception.UserEmailDuplicationException; import com.ajou.hertz.domain.user.exception.UserKakaoUidDuplicationException; import com.ajou.hertz.domain.user.exception.UserPhoneDuplicationException; +import com.ajou.hertz.domain.user.repository.UserProfileImageRepository; import com.ajou.hertz.domain.user.repository.UserRepository; import lombok.RequiredArgsConstructor; @@ -32,7 +31,7 @@ public class UserCommandService { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; private final HertzProperties hertzProperties; - private final FileService fileService; + private final UserProfileImageRepository userProfileImageRepository; /** * 새로운 회원을 등록한다. @@ -135,31 +134,21 @@ private String generateRandom16CharString() { } /** - * 유저의 프로필 이미지를 업데이트한다. + * 전달된 이미지로 프로필 이미지 url을 변경한다. * - * @param userId 유저 id - * @param newProfileImage 새로운 프로필 이미지 - * - * @return 업데이트된 유저 정보 + * @param userId 유저의 ID + * @param uploadedFile 변경할 이미지 파일 + * + * @return 변경된 유저 정보 */ - public UserDto updateProfileImageUrl(Long userId, MultipartFile newProfileImage) { + public UserDto updateProfileImage(Long userId, FileDto uploadedFile) { 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); - } + return UserDto.from(user); + } - /** + /** *연락 수단을 변경합니다. * * @param userId 유저의 ID diff --git a/src/main/java/com/ajou/hertz/domain/user/service/UserProfileImageCommandService.java b/src/main/java/com/ajou/hertz/domain/user/service/UserProfileImageCommandService.java new file mode 100644 index 0000000..cc42a7e --- /dev/null +++ b/src/main/java/com/ajou/hertz/domain/user/service/UserProfileImageCommandService.java @@ -0,0 +1,54 @@ +package com.ajou.hertz.domain.user.service; + +import java.util.Optional; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +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.domain.user.entity.User; +import com.ajou.hertz.domain.user.entity.UserProfileImage; +import com.ajou.hertz.domain.user.repository.UserProfileImageRepository; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +@Transactional +@Service +public class UserProfileImageCommandService { + + private final UserQueryService userQueryService; + private final FileService fileService; + private final UserProfileImageRepository userProfileImageRepository; + + /** + * 유저의 프로필 이미지를 업데이트한다. + * + * @param userId 유저 id + * @param newProfileImage 새로운 프로필 이미지 + * + * @return 업로드된 파일 정보가 담긴 dto + */ + public FileDto uploadProfileImage(Long userId, MultipartFile newProfileImage) { + User user = userQueryService.getById(userId); + + Optional optionalOldProfileImage = userProfileImageRepository.findById(userId); + if (optionalOldProfileImage.isPresent()) { + UserProfileImage oldProfileImage = optionalOldProfileImage.get(); + userProfileImageRepository.delete(oldProfileImage); + userProfileImageRepository.flush(); + fileService.deleteFile(oldProfileImage.getStoredName()); + } + + String uploadPath = "user-profile-images/"; + FileDto uploadedFile = fileService.uploadFile(newProfileImage, uploadPath); + + UserProfileImage newUserProfileImage = UserProfileImage.create( + user, uploadedFile.getOriginalName(), uploadedFile.getStoredName(), uploadedFile.getStoredFileUrl()); + userProfileImageRepository.save(newUserProfileImage); + + return uploadedFile; + } +} diff --git a/src/test/java/com/ajou/hertz/unit/domain/user/controller/UserControllerTest.java b/src/test/java/com/ajou/hertz/unit/domain/user/controller/UserControllerTest.java index bd4b7bc..d782366 100644 --- a/src/test/java/com/ajou/hertz/unit/domain/user/controller/UserControllerTest.java +++ b/src/test/java/com/ajou/hertz/unit/domain/user/controller/UserControllerTest.java @@ -31,12 +31,14 @@ import com.ajou.hertz.common.auth.JwtTokenProvider; import com.ajou.hertz.common.auth.UserPrincipal; import com.ajou.hertz.common.config.SecurityConfig; +import com.ajou.hertz.common.file.dto.FileDto; import com.ajou.hertz.domain.user.constant.Gender; import com.ajou.hertz.domain.user.constant.RoleType; import com.ajou.hertz.domain.user.controller.UserController; import com.ajou.hertz.domain.user.dto.UserDto; import com.ajou.hertz.domain.user.dto.request.SignUpRequest; import com.ajou.hertz.domain.user.service.UserCommandService; +import com.ajou.hertz.domain.user.service.UserProfileImageCommandService; import com.ajou.hertz.domain.user.service.UserQueryService; import com.ajou.hertz.util.ReflectionUtils; import com.fasterxml.jackson.databind.ObjectMapper; @@ -61,6 +63,9 @@ class UserControllerTest { @MockBean private UserQueryService userQueryService; + @MockBean + private UserProfileImageCommandService userProfileImageCommandService; + private final MockMvc mvc; private final ObjectMapper objectMapper; @@ -224,12 +229,16 @@ public void securitySetUp() throws Exception { "test".getBytes() ); UserDetails userDetails = createTestUser(userId); + FileDto uploadedFile = createFileDto(); UserDto expectedResult = createUserDto(userId); - given(userCommandService.updateProfileImageUrl(userId, profileImage)).willReturn(expectedResult); + + given(userProfileImageCommandService.uploadProfileImage(userId, profileImage)).willReturn( + uploadedFile); + given(userCommandService.updateProfileImage(userId, uploadedFile)).willReturn(expectedResult); // when & then mvc.perform( - multipart("/api/users/me/profile-image") + multipart("/api/users/me/profile-images") .file(profileImage) .header(API_VERSION_HEADER_NAME, 1) .with(user(userDetails)) @@ -240,7 +249,8 @@ public void securitySetUp() throws Exception { ) .andExpect(status().isOk()) .andExpect(jsonPath("$.profileImageUrl").value(expectedResult.getProfileImageUrl())); - then(userCommandService).should().updateProfileImageUrl(userId, profileImage); + then(userProfileImageCommandService).should().uploadProfileImage(userId, profileImage); + then(userCommandService).should().updateProfileImage(userId, uploadedFile); verifyEveryMocksShouldHaveNoMoreInteractions(); } @@ -270,6 +280,7 @@ public void securitySetUp() throws Exception { private void verifyEveryMocksShouldHaveNoMoreInteractions() { then(userCommandService).shouldHaveNoMoreInteractions(); then(userQueryService).shouldHaveNoMoreInteractions(); + then(userProfileImageCommandService).shouldHaveNoMoreInteractions(); } private SignUpRequest createSignUpRequest(String email, String password, String phone) throws Exception { @@ -314,4 +325,11 @@ private UserDetails createTestUser(Long userId) throws Exception { return new UserPrincipal(createUserDto(userId)); } + private FileDto createFileDto() throws Exception { + return ReflectionUtils.createFileDto( + "test.jpg", + "test-stored.jpg", + "https://example.com/user-profile-images/storedFileName.jpg"); + } + } \ No newline at end of file diff --git a/src/test/java/com/ajou/hertz/unit/domain/user/service/UserCommandServiceTest.java b/src/test/java/com/ajou/hertz/unit/domain/user/service/UserCommandServiceTest.java index ca75e0f..7ea0580 100644 --- a/src/test/java/com/ajou/hertz/unit/domain/user/service/UserCommandServiceTest.java +++ b/src/test/java/com/ajou/hertz/unit/domain/user/service/UserCommandServiceTest.java @@ -17,7 +17,6 @@ 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; @@ -178,46 +177,79 @@ static Stream 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 { + void 주어진_유저_ID와_저장된_이미지로_프로필_이미지_URL을_변경한다() throws Exception { + // given Long userId = 1L; - MockMultipartFile newProfileImage = new MockMultipartFile( - "profileImage", - "test.jpg", - "image/jpeg", - "test".getBytes() - ); - User user = createUser(userId, "password", null, Gender.MALE); + User user = createUser(userId, "$2a$abc123", "12345"); + FileDto uploadedFile = createFileDto(); 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); + // when + UserDto updatedUserDto = sut.updateProfileImage(userId, uploadedFile); - // Then + // 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()); + assertThat(updatedUserDto.getProfileImageUrl()).isEqualTo(uploadedFile.getUrl()); } @Test - void 주어진_유저_ID와_새로운_프로필_이미지로_기존의_프로필_이미지를_변경한다_존재하지_않는_유저라면_예외가_발생한다() throws Exception { + void 주어진_유저_ID와_저장된_이미지로_프로필_이미지를_변경한다_존재하지_않는_유저라면_예외가_발생한다() throws Exception { // given Long userId = 1L; - MockMultipartFile newProfileImage = new MockMultipartFile( - "profileImage", - "test.jpg", - "image/jpeg", - "test".getBytes() - ); + FileDto uploadedFile = createFileDto(); given(userQueryService.getById(userId)).willThrow(UserNotFoundByIdException.class); // when - Throwable t = catchThrowable(() -> sut.updateProfileImageUrl(userId, newProfileImage)); + Throwable t = catchThrowable(() -> sut.updateProfileImage(userId, uploadedFile)); // then then(userQueryService).should().getById(userId); @@ -331,4 +363,11 @@ private static KakaoUserInfoResponse createKakaoUserInfoResponse(String gender) private static KakaoUserInfoResponse createKakaoUserInfoResponse() { return createKakaoUserInfoResponse("male"); } + + private FileDto createFileDto() throws Exception { + return ReflectionUtils.createFileDto( + "test.jpg", + "test-stored.jpg", + "https://new-contactLink"); + } } \ No newline at end of file diff --git a/src/test/java/com/ajou/hertz/unit/domain/user/service/UserProfileImageCommandServiceTest.java b/src/test/java/com/ajou/hertz/unit/domain/user/service/UserProfileImageCommandServiceTest.java new file mode 100644 index 0000000..43e4cdc --- /dev/null +++ b/src/test/java/com/ajou/hertz/unit/domain/user/service/UserProfileImageCommandServiceTest.java @@ -0,0 +1,164 @@ +package com.ajou.hertz.unit.domain.user.service; + +import static org.mockito.BDDMockito.*; + +import java.time.LocalDate; +import java.util.Optional; +import java.util.Set; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +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 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.properties.HertzProperties; +import com.ajou.hertz.domain.user.constant.Gender; +import com.ajou.hertz.domain.user.constant.RoleType; +import com.ajou.hertz.domain.user.entity.User; +import com.ajou.hertz.domain.user.entity.UserProfileImage; +import com.ajou.hertz.domain.user.repository.UserProfileImageRepository; +import com.ajou.hertz.domain.user.repository.UserRepository; +import com.ajou.hertz.domain.user.service.UserProfileImageCommandService; +import com.ajou.hertz.domain.user.service.UserQueryService; +import com.ajou.hertz.util.ReflectionUtils; + +@DisplayName("[Unit] Service(Command) - User Profile Image") +@ExtendWith(MockitoExtension.class) +public class UserProfileImageCommandServiceTest { + @InjectMocks + private UserProfileImageCommandService sut; + + @Mock + private UserQueryService userQueryService; + + @Mock + private UserRepository userRepository; + + @Mock + private UserProfileImageRepository userProfileImageRepository; + + @Mock + private PasswordEncoder passwordEncoder; + + @Mock + private HertzProperties hertzProperties; + + @Mock + private FileService fileService; + + @BeforeTestMethod + public void setUp() { + given(hertzProperties.userDefaultProfileImageUrl()).willReturn("https://user-default-profile-image"); + } + + @Test + void 기본_프로필_이미지인_경우_유저의_프로필_이미지를_최초로_업데이트한다() throws Exception { + // given + Long userId = 1L; + User user = createUser(userId, "password", "kakaoUid"); + given(userQueryService.getById(userId)).willReturn(user); + + MultipartFile newProfileImage = new MockMultipartFile( + "profileImage", + "newProfile.jpg", + "image/jpeg", + "new image content".getBytes() + ); + + String uploadPath = "user-profile-images/"; + FileDto uploadedFile = createFileDto(); + given(fileService.uploadFile(newProfileImage, uploadPath)).willReturn(uploadedFile); + + // when + sut.uploadProfileImage(userId, newProfileImage); + + // then + then(userQueryService).should().getById(userId); + then(fileService).should().uploadFile(newProfileImage, uploadPath); + then(userProfileImageRepository).should().save(any(UserProfileImage.class)); + verifyEveryMocksShouldHaveNoMoreInteractions(); + } + + @Test + void 기존_프로필_이미지가_존재하는_경우_이미지를_삭제하고_다시_이미지를_저장한다() throws Exception { + // given + Long userId = 1L; + User user = createUser(userId, "password", "kakaoUid"); + given(userQueryService.getById(userId)).willReturn(user); + + UserProfileImage oldProfileImage = createUserProfileImage(user, "oldOriginalName.jpg", "oldStoredName.jpg", + "https://example.com/old-image-url"); + given(userProfileImageRepository.findById(userId)).willReturn(Optional.of(oldProfileImage)); + + MultipartFile newProfileImage = new MockMultipartFile( + "profileImage", + "newProfile.jpg", + "image/jpeg", + "new image content".getBytes() + ); + + String uploadPath = "user-profile-images/"; + FileDto uploadedFile = createFileDto(); + + given(fileService.uploadFile(newProfileImage, uploadPath)).willReturn(uploadedFile); + + // when + sut.uploadProfileImage(userId, newProfileImage); + + // then + then(userQueryService).should().getById(userId); + then(userProfileImageRepository).should().findById(userId); + then(userProfileImageRepository).should().delete(oldProfileImage); + then(fileService).should().deleteFile(oldProfileImage.getStoredName()); + then(fileService).should().uploadFile(newProfileImage, uploadPath); + then(userProfileImageRepository).should().save(any(UserProfileImage.class)); + verifyEveryMocksShouldHaveNoMoreInteractions(); + } + + 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 { + return ReflectionUtils.createUser( + id, + Set.of(RoleType.USER), + "test@test.com", + password, + kakaoUid, + "https://user-default-profile-image-url", + LocalDate.of(2024, 1, 1), + gender, + "010-1234-5678", + "https://contactLink" + ); + } + + private static User createUser(Long id, String password, String kakaoUid) throws Exception { + return createUser(id, password, kakaoUid, Gender.ETC); + } + + private static UserProfileImage createUserProfileImage(User user, String originalName, String storedName, + String url) { + return UserProfileImage.create(user, originalName, storedName, url); + } + + private FileDto createFileDto() throws Exception { + return ReflectionUtils.createFileDto( + "test.jpg", + "test-stored.jpg", + "https://example.com/user-profile-images/storedFileName.jpg"); + } + +} From d09adb5e981f9537c468493fcce9ac273c0e0af0 Mon Sep 17 00:00:00 2001 From: tinon1004 Date: Sun, 24 Mar 2024 23:29:47 +0900 Subject: [PATCH 09/14] =?UTF-8?q?refactor:=20#73=20service=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95=20=EB=B0=8F?= =?UTF-8?q?=20=EB=B6=88=ED=95=84=EC=9A=94=ED=95=9C=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ajou/hertz/common/file/dto/FileDto.java | 4 - .../user/controller/UserController.java | 5 +- .../request/UpdateProfileImageUrlRequest.java | 19 ---- .../user/service/UserCommandService.java | 16 ++-- .../UserProfileImageCommandService.java | 21 +++-- .../user/controller/UserControllerTest.java | 19 +--- .../user/service/UserCommandServiceTest.java | 89 ++++--------------- .../UserProfileImageCommandServiceTest.java | 38 ++++++-- 8 files changed, 71 insertions(+), 140 deletions(-) delete mode 100644 src/main/java/com/ajou/hertz/domain/user/dto/request/UpdateProfileImageUrlRequest.java diff --git a/src/main/java/com/ajou/hertz/common/file/dto/FileDto.java b/src/main/java/com/ajou/hertz/common/file/dto/FileDto.java index 0ad6c16..f919022 100644 --- a/src/main/java/com/ajou/hertz/common/file/dto/FileDto.java +++ b/src/main/java/com/ajou/hertz/common/file/dto/FileDto.java @@ -21,8 +21,4 @@ public static FileDto create( ) { return new FileDto(originalName, storedName, url); } - - public String getStoredFileUrl() { - return url; - } } diff --git a/src/main/java/com/ajou/hertz/domain/user/controller/UserController.java b/src/main/java/com/ajou/hertz/domain/user/controller/UserController.java index 081a0f7..32fd064 100644 --- a/src/main/java/com/ajou/hertz/domain/user/controller/UserController.java +++ b/src/main/java/com/ajou/hertz/domain/user/controller/UserController.java @@ -19,7 +19,6 @@ import org.springframework.web.multipart.MultipartFile; import com.ajou.hertz.common.auth.UserPrincipal; -import com.ajou.hertz.common.file.dto.FileDto; import com.ajou.hertz.common.validator.PhoneNumber; import com.ajou.hertz.domain.user.dto.UserDto; import com.ajou.hertz.domain.user.dto.request.SignUpRequest; @@ -137,9 +136,7 @@ public UserResponse updateProfileImageV1( @AuthenticationPrincipal UserPrincipal userPrincipal, @RequestPart("profileImage") MultipartFile profileImage ) { - FileDto uploadedFile = userProfileImageCommandService.uploadProfileImage(userPrincipal.getUserId(), - profileImage); - UserDto userUpdated = userCommandService.updateProfileImage(userPrincipal.getUserId(), uploadedFile); + UserDto userUpdated = userCommandService.updateUserProfileImage(userPrincipal.getUserId(), profileImage); return UserResponse.from(userUpdated); } diff --git a/src/main/java/com/ajou/hertz/domain/user/dto/request/UpdateProfileImageUrlRequest.java b/src/main/java/com/ajou/hertz/domain/user/dto/request/UpdateProfileImageUrlRequest.java deleted file mode 100644 index 66957e3..0000000 --- a/src/main/java/com/ajou/hertz/domain/user/dto/request/UpdateProfileImageUrlRequest.java +++ /dev/null @@ -1,19 +0,0 @@ -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; - -} diff --git a/src/main/java/com/ajou/hertz/domain/user/service/UserCommandService.java b/src/main/java/com/ajou/hertz/domain/user/service/UserCommandService.java index f400983..0d2d67b 100644 --- a/src/main/java/com/ajou/hertz/domain/user/service/UserCommandService.java +++ b/src/main/java/com/ajou/hertz/domain/user/service/UserCommandService.java @@ -6,8 +6,8 @@ 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.kakao.dto.response.KakaoUserInfoResponse; import com.ajou.hertz.common.properties.HertzProperties; import com.ajou.hertz.domain.user.constant.Gender; @@ -32,6 +32,7 @@ public class UserCommandService { private final PasswordEncoder passwordEncoder; private final HertzProperties hertzProperties; private final UserProfileImageRepository userProfileImageRepository; + private final UserProfileImageCommandService userProfileImageCommandService; /** * 새로운 회원을 등록한다. @@ -134,18 +135,15 @@ private String generateRandom16CharString() { } /** - * 전달된 이미지로 프로필 이미지 url을 변경한다. + * 유저의 프로필 이미지를 업데이트합니다. * * @param userId 유저의 ID - * @param uploadedFile 변경할 이미지 파일 + * @param profileImage 변경할 프로필 이미지 * * @return 변경된 유저 정보 */ - public UserDto updateProfileImage(Long userId, FileDto uploadedFile) { - User user = userQueryService.getById(userId); - String newProfileImageUrl = uploadedFile.getStoredFileUrl(); - user.changeProfileImageUrl(newProfileImageUrl); - return UserDto.from(user); + public UserDto updateUserProfileImage(Long userId, MultipartFile profileImage) { + return userProfileImageCommandService.updateProfileImage(userId, profileImage); } /** @@ -154,7 +152,7 @@ public UserDto updateProfileImage(Long userId, FileDto uploadedFile) { * @param userId 유저의 ID * @param contactLink 변경할 연락 수단 * - *@return 변경된 유저 정보 + * @return 변경된 유저 정보 */ public UserDto updateContactLink(Long userId, String contactLink) { User user = userQueryService.getById(userId); diff --git a/src/main/java/com/ajou/hertz/domain/user/service/UserProfileImageCommandService.java b/src/main/java/com/ajou/hertz/domain/user/service/UserProfileImageCommandService.java index cc42a7e..3c10d50 100644 --- a/src/main/java/com/ajou/hertz/domain/user/service/UserProfileImageCommandService.java +++ b/src/main/java/com/ajou/hertz/domain/user/service/UserProfileImageCommandService.java @@ -8,6 +8,7 @@ import com.ajou.hertz.common.file.dto.FileDto; import com.ajou.hertz.common.file.service.FileService; +import com.ajou.hertz.domain.user.dto.UserDto; import com.ajou.hertz.domain.user.entity.User; import com.ajou.hertz.domain.user.entity.UserProfileImage; import com.ajou.hertz.domain.user.repository.UserProfileImageRepository; @@ -29,12 +30,16 @@ public class UserProfileImageCommandService { * @param userId 유저 id * @param newProfileImage 새로운 프로필 이미지 * - * @return 업로드된 파일 정보가 담긴 dto + * @return 업데이트된 유저 정보 */ - public FileDto uploadProfileImage(Long userId, MultipartFile newProfileImage) { - User user = userQueryService.getById(userId); + public UserDto updateProfileImage(Long userId, MultipartFile newProfileImage) { + User user = userQueryService.getById(userId); Optional optionalOldProfileImage = userProfileImageRepository.findById(userId); + + String uploadPath = "user-profile-images/"; + FileDto uploadedFile = fileService.uploadFile(newProfileImage, uploadPath); + String newProfileImageUrl = uploadedFile.getUrl(); if (optionalOldProfileImage.isPresent()) { UserProfileImage oldProfileImage = optionalOldProfileImage.get(); userProfileImageRepository.delete(oldProfileImage); @@ -42,13 +47,11 @@ public FileDto uploadProfileImage(Long userId, MultipartFile newProfileImage) { fileService.deleteFile(oldProfileImage.getStoredName()); } - String uploadPath = "user-profile-images/"; - FileDto uploadedFile = fileService.uploadFile(newProfileImage, uploadPath); - - UserProfileImage newUserProfileImage = UserProfileImage.create( - user, uploadedFile.getOriginalName(), uploadedFile.getStoredName(), uploadedFile.getStoredFileUrl()); + UserProfileImage newUserProfileImage = UserProfileImage.create(user, uploadedFile.getOriginalName(), + uploadedFile.getStoredName(), newProfileImageUrl); userProfileImageRepository.save(newUserProfileImage); - return uploadedFile; + user.changeProfileImageUrl(newProfileImageUrl); + return UserDto.from(user); } } diff --git a/src/test/java/com/ajou/hertz/unit/domain/user/controller/UserControllerTest.java b/src/test/java/com/ajou/hertz/unit/domain/user/controller/UserControllerTest.java index d782366..766ddc3 100644 --- a/src/test/java/com/ajou/hertz/unit/domain/user/controller/UserControllerTest.java +++ b/src/test/java/com/ajou/hertz/unit/domain/user/controller/UserControllerTest.java @@ -31,7 +31,6 @@ import com.ajou.hertz.common.auth.JwtTokenProvider; import com.ajou.hertz.common.auth.UserPrincipal; import com.ajou.hertz.common.config.SecurityConfig; -import com.ajou.hertz.common.file.dto.FileDto; import com.ajou.hertz.domain.user.constant.Gender; import com.ajou.hertz.domain.user.constant.RoleType; import com.ajou.hertz.domain.user.controller.UserController; @@ -219,7 +218,7 @@ public void securitySetUp() throws Exception { } @Test - void 주어진_id와_새로운_프로필_이미지로_프로필_이미지를_변경한다() throws Exception { + void 주어진_id와_변경할_프로필_이미지로_프로필_이미지를_변경한다() throws Exception { // given long userId = 1L; MockMultipartFile profileImage = new MockMultipartFile( @@ -229,12 +228,9 @@ public void securitySetUp() throws Exception { "test".getBytes() ); UserDetails userDetails = createTestUser(userId); - FileDto uploadedFile = createFileDto(); UserDto expectedResult = createUserDto(userId); - given(userProfileImageCommandService.uploadProfileImage(userId, profileImage)).willReturn( - uploadedFile); - given(userCommandService.updateProfileImage(userId, uploadedFile)).willReturn(expectedResult); + given(userCommandService.updateUserProfileImage(userId, profileImage)).willReturn(expectedResult); // when & then mvc.perform( @@ -249,8 +245,7 @@ public void securitySetUp() throws Exception { ) .andExpect(status().isOk()) .andExpect(jsonPath("$.profileImageUrl").value(expectedResult.getProfileImageUrl())); - then(userProfileImageCommandService).should().uploadProfileImage(userId, profileImage); - then(userCommandService).should().updateProfileImage(userId, uploadedFile); + then(userCommandService).should().updateUserProfileImage(userId, profileImage); verifyEveryMocksShouldHaveNoMoreInteractions(); } @@ -324,12 +319,4 @@ private UserDto createUserDto() throws Exception { private UserDetails createTestUser(Long userId) throws Exception { return new UserPrincipal(createUserDto(userId)); } - - private FileDto createFileDto() throws Exception { - return ReflectionUtils.createFileDto( - "test.jpg", - "test-stored.jpg", - "https://example.com/user-profile-images/storedFileName.jpg"); - } - } \ No newline at end of file diff --git a/src/test/java/com/ajou/hertz/unit/domain/user/service/UserCommandServiceTest.java b/src/test/java/com/ajou/hertz/unit/domain/user/service/UserCommandServiceTest.java index 7ea0580..8086763 100644 --- a/src/test/java/com/ajou/hertz/unit/domain/user/service/UserCommandServiceTest.java +++ b/src/test/java/com/ajou/hertz/unit/domain/user/service/UserCommandServiceTest.java @@ -19,6 +19,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.test.context.event.annotation.BeforeTestMethod; +import org.springframework.web.multipart.MultipartFile; import com.ajou.hertz.common.file.dto.FileDto; import com.ajou.hertz.common.file.service.FileService; @@ -35,6 +36,7 @@ import com.ajou.hertz.domain.user.exception.UserPhoneDuplicationException; import com.ajou.hertz.domain.user.repository.UserRepository; import com.ajou.hertz.domain.user.service.UserCommandService; +import com.ajou.hertz.domain.user.service.UserProfileImageCommandService; import com.ajou.hertz.domain.user.service.UserQueryService; import com.ajou.hertz.util.ReflectionUtils; @@ -60,6 +62,9 @@ class UserCommandServiceTest { @Mock private FileService fileService; + @Mock + private UserProfileImageCommandService userProfileImageCommandService; + @BeforeTestMethod public void setUp() { given(hertzProperties.userDefaultProfileImageUrl()).willReturn("https://user-default-profile-image"); @@ -177,84 +182,23 @@ static Stream 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와_저장된_이미지로_프로필_이미지_URL을_변경한다() throws Exception { - // given + void 프로필_이미지_변경_시_uploadProfileImage가_잘_호출되는지_확인한다() throws Exception { + // Given Long userId = 1L; - User user = createUser(userId, "$2a$abc123", "12345"); - FileDto uploadedFile = createFileDto(); - given(userQueryService.getById(userId)).willReturn(user); + MultipartFile profileImage = org.mockito.Mockito.mock(MultipartFile.class); + User testUser = createUser(userId, "password", "kakaoUid"); + UserDto expectedUserDto = UserDto.from(testUser); + given(userProfileImageCommandService.updateProfileImage(userId, profileImage)).willReturn(expectedUserDto); - // when - UserDto updatedUserDto = sut.updateProfileImage(userId, uploadedFile); + // When + UserDto result = sut.updateUserProfileImage(userId, profileImage); - // then - then(userQueryService).should().getById(userId); - verifyEveryMocksShouldHaveNoMoreInteractions(); - assertThat(updatedUserDto.getProfileImageUrl()).isEqualTo(uploadedFile.getUrl()); - } + // Then + then(userProfileImageCommandService).should().updateProfileImage(userId, profileImage); + assertThat(result).isEqualTo(expectedUserDto); - @Test - void 주어진_유저_ID와_저장된_이미지로_프로필_이미지를_변경한다_존재하지_않는_유저라면_예외가_발생한다() throws Exception { - // given - Long userId = 1L; - FileDto uploadedFile = createFileDto(); - given(userQueryService.getById(userId)).willThrow(UserNotFoundByIdException.class); - - // when - Throwable t = catchThrowable(() -> sut.updateProfileImage(userId, uploadedFile)); - - // then - then(userQueryService).should().getById(userId); verifyEveryMocksShouldHaveNoMoreInteractions(); - assertThat(t).isInstanceOf(UserNotFoundByIdException.class); } @Test @@ -295,6 +239,7 @@ private void verifyEveryMocksShouldHaveNoMoreInteractions() { then(userRepository).shouldHaveNoMoreInteractions(); then(passwordEncoder).shouldHaveNoMoreInteractions(); then(fileService).shouldHaveNoMoreInteractions(); + then(userProfileImageCommandService).shouldHaveNoMoreInteractions(); } private static User createUser(Long id, String password, String kakaoUid, Gender gender) throws Exception { diff --git a/src/test/java/com/ajou/hertz/unit/domain/user/service/UserProfileImageCommandServiceTest.java b/src/test/java/com/ajou/hertz/unit/domain/user/service/UserProfileImageCommandServiceTest.java index 43e4cdc..e800f9b 100644 --- a/src/test/java/com/ajou/hertz/unit/domain/user/service/UserProfileImageCommandServiceTest.java +++ b/src/test/java/com/ajou/hertz/unit/domain/user/service/UserProfileImageCommandServiceTest.java @@ -1,5 +1,6 @@ package com.ajou.hertz.unit.domain.user.service; +import static org.assertj.core.api.Assertions.*; import static org.mockito.BDDMockito.*; import java.time.LocalDate; @@ -24,6 +25,7 @@ import com.ajou.hertz.domain.user.constant.RoleType; import com.ajou.hertz.domain.user.entity.User; import com.ajou.hertz.domain.user.entity.UserProfileImage; +import com.ajou.hertz.domain.user.exception.UserNotFoundByIdException; import com.ajou.hertz.domain.user.repository.UserProfileImageRepository; import com.ajou.hertz.domain.user.repository.UserRepository; import com.ajou.hertz.domain.user.service.UserProfileImageCommandService; @@ -78,7 +80,7 @@ public void setUp() { given(fileService.uploadFile(newProfileImage, uploadPath)).willReturn(uploadedFile); // when - sut.uploadProfileImage(userId, newProfileImage); + sut.updateProfileImage(userId, newProfileImage); // then then(userQueryService).should().getById(userId); @@ -88,7 +90,7 @@ public void setUp() { } @Test - void 기존_프로필_이미지가_존재하는_경우_이미지를_삭제하고_다시_이미지를_저장한다() throws Exception { + void 기존에_프로필_이미지가_존재하는_경우_이미지를_삭제하고_다시_이미지를_저장한다() throws Exception { // given Long userId = 1L; User user = createUser(userId, "password", "kakaoUid"); @@ -108,21 +110,43 @@ public void setUp() { String uploadPath = "user-profile-images/"; FileDto uploadedFile = createFileDto(); - given(fileService.uploadFile(newProfileImage, uploadPath)).willReturn(uploadedFile); + given(fileService.uploadFile(eq(newProfileImage), eq(uploadPath))).willReturn(uploadedFile); // when - sut.uploadProfileImage(userId, newProfileImage); + sut.updateProfileImage(userId, newProfileImage); // then then(userQueryService).should().getById(userId); then(userProfileImageRepository).should().findById(userId); - then(userProfileImageRepository).should().delete(oldProfileImage); - then(fileService).should().deleteFile(oldProfileImage.getStoredName()); - then(fileService).should().uploadFile(newProfileImage, uploadPath); + then(userProfileImageRepository).should().delete(any(UserProfileImage.class)); + then(fileService).should().deleteFile(anyString()); + then(fileService).should().uploadFile(eq(newProfileImage), eq(uploadPath)); then(userProfileImageRepository).should().save(any(UserProfileImage.class)); verifyEveryMocksShouldHaveNoMoreInteractions(); } + @Test + void 프로필_이미지_변경_시_존재하지_않는_유저라면_예외를_반환한다() throws Exception { + // given + Long userId = 1L; + given(userQueryService.getById(userId)).willThrow(UserNotFoundByIdException.class); + + MultipartFile newProfileImage = new MockMultipartFile( + "profileImage", + "newProfile.jpg", + "image/jpeg", + "new image content".getBytes() + ); + + // when + Throwable t = catchThrowable(() -> sut.updateProfileImage(userId, newProfileImage)); + + // then + assertThat(t).isInstanceOf(UserNotFoundByIdException.class); + then(userQueryService).should().getById(userId); + verifyEveryMocksShouldHaveNoMoreInteractions(); + } + private void verifyEveryMocksShouldHaveNoMoreInteractions() { then(userQueryService).shouldHaveNoMoreInteractions(); then(userRepository).shouldHaveNoMoreInteractions(); From 189a44f6682740282a033e96f73b8f6d0842ffbb Mon Sep 17 00:00:00 2001 From: tinon1004 Date: Mon, 25 Mar 2024 14:13:43 +0900 Subject: [PATCH 10/14] =?UTF-8?q?refactor:=20#73=20service=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EB=B6=84=EB=A6=AC=20=EB=B0=8F=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/controller/UserController.java | 4 +- .../UserProfileImageRepository.java | 2 +- .../user/service/UserCommandService.java | 7 +-- .../UserProfileImageCommandService.java | 48 ++++++++++++------- 4 files changed, 38 insertions(+), 23 deletions(-) diff --git a/src/main/java/com/ajou/hertz/domain/user/controller/UserController.java b/src/main/java/com/ajou/hertz/domain/user/controller/UserController.java index 32fd064..e677f9a 100644 --- a/src/main/java/com/ajou/hertz/domain/user/controller/UserController.java +++ b/src/main/java/com/ajou/hertz/domain/user/controller/UserController.java @@ -28,7 +28,6 @@ import com.ajou.hertz.domain.user.dto.response.UserResponse; import com.ajou.hertz.domain.user.dto.response.UserWithLinkedAccountInfoResponse; import com.ajou.hertz.domain.user.service.UserCommandService; -import com.ajou.hertz.domain.user.service.UserProfileImageCommandService; import com.ajou.hertz.domain.user.service.UserQueryService; import io.swagger.v3.oas.annotations.Operation; @@ -51,7 +50,6 @@ public class UserController { private final UserCommandService userCommandService; - private final UserProfileImageCommandService userProfileImageCommandService; private final UserQueryService userQueryService; @Operation( @@ -134,7 +132,7 @@ public ResponseEntity signUpV1( ) public UserResponse updateProfileImageV1( @AuthenticationPrincipal UserPrincipal userPrincipal, - @RequestPart("profileImage") MultipartFile profileImage + @RequestPart MultipartFile profileImage ) { UserDto userUpdated = userCommandService.updateUserProfileImage(userPrincipal.getUserId(), profileImage); return UserResponse.from(userUpdated); diff --git a/src/main/java/com/ajou/hertz/domain/user/repository/UserProfileImageRepository.java b/src/main/java/com/ajou/hertz/domain/user/repository/UserProfileImageRepository.java index 7cf665b..de1bfb0 100644 --- a/src/main/java/com/ajou/hertz/domain/user/repository/UserProfileImageRepository.java +++ b/src/main/java/com/ajou/hertz/domain/user/repository/UserProfileImageRepository.java @@ -7,5 +7,5 @@ import com.ajou.hertz.domain.user.entity.UserProfileImage; public interface UserProfileImageRepository extends JpaRepository { - Optional findById(Long userId); + Optional findByUser_Id(Long userId); } diff --git a/src/main/java/com/ajou/hertz/domain/user/service/UserCommandService.java b/src/main/java/com/ajou/hertz/domain/user/service/UserCommandService.java index 0d2d67b..088ad6f 100644 --- a/src/main/java/com/ajou/hertz/domain/user/service/UserCommandService.java +++ b/src/main/java/com/ajou/hertz/domain/user/service/UserCommandService.java @@ -17,7 +17,6 @@ import com.ajou.hertz.domain.user.exception.UserEmailDuplicationException; import com.ajou.hertz.domain.user.exception.UserKakaoUidDuplicationException; import com.ajou.hertz.domain.user.exception.UserPhoneDuplicationException; -import com.ajou.hertz.domain.user.repository.UserProfileImageRepository; import com.ajou.hertz.domain.user.repository.UserRepository; import lombok.RequiredArgsConstructor; @@ -31,7 +30,6 @@ public class UserCommandService { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; private final HertzProperties hertzProperties; - private final UserProfileImageRepository userProfileImageRepository; private final UserProfileImageCommandService userProfileImageCommandService; /** @@ -143,7 +141,10 @@ private String generateRandom16CharString() { * @return 변경된 유저 정보 */ public UserDto updateUserProfileImage(Long userId, MultipartFile profileImage) { - return userProfileImageCommandService.updateProfileImage(userId, profileImage); + User user = userQueryService.getById(userId); + String newProfileImageUrl = userProfileImageCommandService.updateProfileImage(user, profileImage); + user.changeProfileImageUrl(newProfileImageUrl); + return UserDto.from(user); } /** diff --git a/src/main/java/com/ajou/hertz/domain/user/service/UserProfileImageCommandService.java b/src/main/java/com/ajou/hertz/domain/user/service/UserProfileImageCommandService.java index 3c10d50..64578c0 100644 --- a/src/main/java/com/ajou/hertz/domain/user/service/UserProfileImageCommandService.java +++ b/src/main/java/com/ajou/hertz/domain/user/service/UserProfileImageCommandService.java @@ -8,7 +8,6 @@ import com.ajou.hertz.common.file.dto.FileDto; import com.ajou.hertz.common.file.service.FileService; -import com.ajou.hertz.domain.user.dto.UserDto; import com.ajou.hertz.domain.user.entity.User; import com.ajou.hertz.domain.user.entity.UserProfileImage; import com.ajou.hertz.domain.user.repository.UserProfileImageRepository; @@ -20,38 +19,55 @@ @Service public class UserProfileImageCommandService { - private final UserQueryService userQueryService; private final FileService fileService; private final UserProfileImageRepository userProfileImageRepository; + private static final String USER_PROFILE_IMAGE_UPLOAD_PATH = "user-profile-images/"; + /** * 유저의 프로필 이미지를 업데이트한다. * - * @param userId 유저 id + * @param user 프로필 이미지를 업데이트할 유저 * @param newProfileImage 새로운 프로필 이미지 * - * @return 업데이트된 유저 정보 + * @return 새로운 프로필 이미지 URL */ - public UserDto updateProfileImage(Long userId, MultipartFile newProfileImage) { - - User user = userQueryService.getById(userId); - Optional optionalOldProfileImage = userProfileImageRepository.findById(userId); + public String updateProfileImage(User user, MultipartFile newProfileImage) { + deleteOldProfileImage(user.getId()); + UserProfileImage newUserProfileImage = uploadNewProfileImage(user, newProfileImage); + return newUserProfileImage.getUrl(); + } - String uploadPath = "user-profile-images/"; - FileDto uploadedFile = fileService.uploadFile(newProfileImage, uploadPath); - String newProfileImageUrl = uploadedFile.getUrl(); + /** + * 유저의 프로필 이미지를 삭제한다. + * + * @param userId 프로필 이미지를 삭제할 유저의 id + */ + private void deleteOldProfileImage(Long userId) { + Optional optionalOldProfileImage = userProfileImageRepository.findByUser_Id(userId); if (optionalOldProfileImage.isPresent()) { UserProfileImage oldProfileImage = optionalOldProfileImage.get(); userProfileImageRepository.delete(oldProfileImage); userProfileImageRepository.flush(); fileService.deleteFile(oldProfileImage.getStoredName()); } + } - UserProfileImage newUserProfileImage = UserProfileImage.create(user, uploadedFile.getOriginalName(), - uploadedFile.getStoredName(), newProfileImageUrl); + /** + * 새로운 프로필 이미지를 업로드한다. + * @param user 프로필 이미지를 업데이트할 유저 + * @param newProfileImage 새로운 프로필 이미지 + * + * @return 새로운 프로필 이미지 entity + */ + private UserProfileImage uploadNewProfileImage(User user, MultipartFile newProfileImage) { + FileDto uploadedFile = fileService.uploadFile(newProfileImage, USER_PROFILE_IMAGE_UPLOAD_PATH); + UserProfileImage newUserProfileImage = UserProfileImage.create( + user, + uploadedFile.getOriginalName(), + uploadedFile.getStoredName(), + uploadedFile.getUrl()); userProfileImageRepository.save(newUserProfileImage); - - user.changeProfileImageUrl(newProfileImageUrl); - return UserDto.from(user); + return newUserProfileImage; } } From dbfa0f540834968e5d31ae2e1b73d305f86f0f7d Mon Sep 17 00:00:00 2001 From: tinon1004 Date: Mon, 25 Mar 2024 16:05:48 +0900 Subject: [PATCH 11/14] =?UTF-8?q?refactor:=20#73=20flush()=20=ED=95=A8?= =?UTF-8?q?=EC=88=98=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/user/service/UserProfileImageCommandService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/ajou/hertz/domain/user/service/UserProfileImageCommandService.java b/src/main/java/com/ajou/hertz/domain/user/service/UserProfileImageCommandService.java index 64578c0..3d46e1b 100644 --- a/src/main/java/com/ajou/hertz/domain/user/service/UserProfileImageCommandService.java +++ b/src/main/java/com/ajou/hertz/domain/user/service/UserProfileImageCommandService.java @@ -34,6 +34,7 @@ public class UserProfileImageCommandService { */ public String updateProfileImage(User user, MultipartFile newProfileImage) { deleteOldProfileImage(user.getId()); + userProfileImageRepository.flush(); UserProfileImage newUserProfileImage = uploadNewProfileImage(user, newProfileImage); return newUserProfileImage.getUrl(); } @@ -48,7 +49,6 @@ private void deleteOldProfileImage(Long userId) { if (optionalOldProfileImage.isPresent()) { UserProfileImage oldProfileImage = optionalOldProfileImage.get(); userProfileImageRepository.delete(oldProfileImage); - userProfileImageRepository.flush(); fileService.deleteFile(oldProfileImage.getStoredName()); } } From 1199bcbc96992d4b0b3244106e89e611c28d5e0d Mon Sep 17 00:00:00 2001 From: tinon1004 Date: Mon, 25 Mar 2024 16:06:16 +0900 Subject: [PATCH 12/14] =?UTF-8?q?test:=20#73=20test=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/controller/UserControllerTest.java | 5 -- .../user/service/UserCommandServiceTest.java | 46 +++++++++++++------ .../UserProfileImageCommandServiceTest.java | 44 ++++-------------- 3 files changed, 39 insertions(+), 56 deletions(-) diff --git a/src/test/java/com/ajou/hertz/unit/domain/user/controller/UserControllerTest.java b/src/test/java/com/ajou/hertz/unit/domain/user/controller/UserControllerTest.java index 766ddc3..6ea3135 100644 --- a/src/test/java/com/ajou/hertz/unit/domain/user/controller/UserControllerTest.java +++ b/src/test/java/com/ajou/hertz/unit/domain/user/controller/UserControllerTest.java @@ -37,7 +37,6 @@ import com.ajou.hertz.domain.user.dto.UserDto; import com.ajou.hertz.domain.user.dto.request.SignUpRequest; import com.ajou.hertz.domain.user.service.UserCommandService; -import com.ajou.hertz.domain.user.service.UserProfileImageCommandService; import com.ajou.hertz.domain.user.service.UserQueryService; import com.ajou.hertz.util.ReflectionUtils; import com.fasterxml.jackson.databind.ObjectMapper; @@ -62,9 +61,6 @@ class UserControllerTest { @MockBean private UserQueryService userQueryService; - @MockBean - private UserProfileImageCommandService userProfileImageCommandService; - private final MockMvc mvc; private final ObjectMapper objectMapper; @@ -275,7 +271,6 @@ public void securitySetUp() throws Exception { private void verifyEveryMocksShouldHaveNoMoreInteractions() { then(userCommandService).shouldHaveNoMoreInteractions(); then(userQueryService).shouldHaveNoMoreInteractions(); - then(userProfileImageCommandService).shouldHaveNoMoreInteractions(); } private SignUpRequest createSignUpRequest(String email, String password, String phone) throws Exception { diff --git a/src/test/java/com/ajou/hertz/unit/domain/user/service/UserCommandServiceTest.java b/src/test/java/com/ajou/hertz/unit/domain/user/service/UserCommandServiceTest.java index 8086763..4465dd1 100644 --- a/src/test/java/com/ajou/hertz/unit/domain/user/service/UserCommandServiceTest.java +++ b/src/test/java/com/ajou/hertz/unit/domain/user/service/UserCommandServiceTest.java @@ -17,11 +17,11 @@ 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 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; @@ -183,22 +183,45 @@ static Stream testDataForCreateNewUserWithKakao() throws Exception { } @Test - void 프로필_이미지_변경_시_uploadProfileImage가_잘_호출되는지_확인한다() throws Exception { + void 주어진_유저_ID와_이미지_URL로_유저의_프로필_이미지를_업데이트한다() throws Exception { // Given Long userId = 1L; - MultipartFile profileImage = org.mockito.Mockito.mock(MultipartFile.class); - User testUser = createUser(userId, "password", "kakaoUid"); - UserDto expectedUserDto = UserDto.from(testUser); - given(userProfileImageCommandService.updateProfileImage(userId, profileImage)).willReturn(expectedUserDto); + User user = createUser(userId, "password", "kakaoUid"); + String newProfileImageUrl = "https://new-profile-image-url"; + + MultipartFile profileImage = new MockMultipartFile("file", "test.jpg", "image/jpeg", + "test image content".getBytes()); + + given(userQueryService.getById(userId)).willReturn(user); + given(userProfileImageCommandService.updateProfileImage(user, profileImage)).willReturn( + newProfileImageUrl); // When UserDto result = sut.updateUserProfileImage(userId, profileImage); // Then - then(userProfileImageCommandService).should().updateProfileImage(userId, profileImage); - assertThat(result).isEqualTo(expectedUserDto); + then(userQueryService).should().getById(userId); + then(userProfileImageCommandService).should().updateProfileImage(user, profileImage); + assertThat(result.getProfileImageUrl()).isEqualTo(newProfileImageUrl); + verifyEveryMocksShouldHaveNoMoreInteractions(); + } + + @Test + void 주어진_유저_ID와_이미지_URL로_유저의_프로필_이미지를_업데이트한다_존재하지_않는_유저라면_예외가_발생한다() throws Exception { + // Given + Long userId = 1L; + MultipartFile profileImage = new MockMultipartFile("file", "test.jpg", "image/jpeg", + "test image content".getBytes()); + given(userQueryService.getById(userId)).willThrow(UserNotFoundByIdException.class); + + // When + Throwable t = catchThrowable(() -> sut.updateUserProfileImage(userId, profileImage)); + + // Then + then(userQueryService).should().getById(userId); verifyEveryMocksShouldHaveNoMoreInteractions(); + assertThat(t).isInstanceOf(UserNotFoundByIdException.class); } @Test @@ -308,11 +331,4 @@ private static KakaoUserInfoResponse createKakaoUserInfoResponse(String gender) private static KakaoUserInfoResponse createKakaoUserInfoResponse() { return createKakaoUserInfoResponse("male"); } - - private FileDto createFileDto() throws Exception { - return ReflectionUtils.createFileDto( - "test.jpg", - "test-stored.jpg", - "https://new-contactLink"); - } } \ No newline at end of file diff --git a/src/test/java/com/ajou/hertz/unit/domain/user/service/UserProfileImageCommandServiceTest.java b/src/test/java/com/ajou/hertz/unit/domain/user/service/UserProfileImageCommandServiceTest.java index e800f9b..4d6410c 100644 --- a/src/test/java/com/ajou/hertz/unit/domain/user/service/UserProfileImageCommandServiceTest.java +++ b/src/test/java/com/ajou/hertz/unit/domain/user/service/UserProfileImageCommandServiceTest.java @@ -1,6 +1,5 @@ package com.ajou.hertz.unit.domain.user.service; -import static org.assertj.core.api.Assertions.*; import static org.mockito.BDDMockito.*; import java.time.LocalDate; @@ -25,7 +24,6 @@ import com.ajou.hertz.domain.user.constant.RoleType; import com.ajou.hertz.domain.user.entity.User; import com.ajou.hertz.domain.user.entity.UserProfileImage; -import com.ajou.hertz.domain.user.exception.UserNotFoundByIdException; import com.ajou.hertz.domain.user.repository.UserProfileImageRepository; import com.ajou.hertz.domain.user.repository.UserRepository; import com.ajou.hertz.domain.user.service.UserProfileImageCommandService; @@ -66,7 +64,6 @@ public void setUp() { // given Long userId = 1L; User user = createUser(userId, "password", "kakaoUid"); - given(userQueryService.getById(userId)).willReturn(user); MultipartFile newProfileImage = new MockMultipartFile( "profileImage", @@ -80,10 +77,9 @@ public void setUp() { given(fileService.uploadFile(newProfileImage, uploadPath)).willReturn(uploadedFile); // when - sut.updateProfileImage(userId, newProfileImage); + sut.updateProfileImage(user, newProfileImage); // then - then(userQueryService).should().getById(userId); then(fileService).should().uploadFile(newProfileImage, uploadPath); then(userProfileImageRepository).should().save(any(UserProfileImage.class)); verifyEveryMocksShouldHaveNoMoreInteractions(); @@ -94,11 +90,10 @@ public void setUp() { // given Long userId = 1L; User user = createUser(userId, "password", "kakaoUid"); - given(userQueryService.getById(userId)).willReturn(user); UserProfileImage oldProfileImage = createUserProfileImage(user, "oldOriginalName.jpg", "oldStoredName.jpg", "https://example.com/old-image-url"); - given(userProfileImageRepository.findById(userId)).willReturn(Optional.of(oldProfileImage)); + given(userProfileImageRepository.findByUser_Id(userId)).willReturn(Optional.of(oldProfileImage)); MultipartFile newProfileImage = new MockMultipartFile( "profileImage", @@ -110,43 +105,20 @@ public void setUp() { String uploadPath = "user-profile-images/"; FileDto uploadedFile = createFileDto(); - given(fileService.uploadFile(eq(newProfileImage), eq(uploadPath))).willReturn(uploadedFile); + given(fileService.uploadFile(newProfileImage, uploadPath)).willReturn(uploadedFile); // when - sut.updateProfileImage(userId, newProfileImage); + sut.updateProfileImage(user, newProfileImage); // then - then(userQueryService).should().getById(userId); - then(userProfileImageRepository).should().findById(userId); - then(userProfileImageRepository).should().delete(any(UserProfileImage.class)); - then(fileService).should().deleteFile(anyString()); - then(fileService).should().uploadFile(eq(newProfileImage), eq(uploadPath)); + then(userProfileImageRepository).should().findByUser_Id(userId); + then(userProfileImageRepository).should().delete(oldProfileImage); + then(fileService).should().deleteFile(oldProfileImage.getStoredName()); + then(fileService).should().uploadFile(newProfileImage, uploadPath); then(userProfileImageRepository).should().save(any(UserProfileImage.class)); verifyEveryMocksShouldHaveNoMoreInteractions(); } - @Test - void 프로필_이미지_변경_시_존재하지_않는_유저라면_예외를_반환한다() throws Exception { - // given - Long userId = 1L; - given(userQueryService.getById(userId)).willThrow(UserNotFoundByIdException.class); - - MultipartFile newProfileImage = new MockMultipartFile( - "profileImage", - "newProfile.jpg", - "image/jpeg", - "new image content".getBytes() - ); - - // when - Throwable t = catchThrowable(() -> sut.updateProfileImage(userId, newProfileImage)); - - // then - assertThat(t).isInstanceOf(UserNotFoundByIdException.class); - then(userQueryService).should().getById(userId); - verifyEveryMocksShouldHaveNoMoreInteractions(); - } - private void verifyEveryMocksShouldHaveNoMoreInteractions() { then(userQueryService).shouldHaveNoMoreInteractions(); then(userRepository).shouldHaveNoMoreInteractions(); From 3101ace6f138362d18dcbe31ef9ace4dfc506c3f Mon Sep 17 00:00:00 2001 From: tinon1004 Date: Tue, 26 Mar 2024 14:58:29 +0900 Subject: [PATCH 13/14] =?UTF-8?q?refactor:=20#73=20=EB=B6=88=ED=95=84?= =?UTF-8?q?=EC=9A=94=ED=95=9C=20=EC=BD=94=EB=93=9C=20=EC=A0=9C=EA=B1=B0=20?= =?UTF-8?q?=EB=B0=8F=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/service/UserCommandServiceTest.java | 18 ------------------ .../UserProfileImageCommandServiceTest.java | 15 --------------- 2 files changed, 33 deletions(-) diff --git a/src/test/java/com/ajou/hertz/unit/domain/user/service/UserCommandServiceTest.java b/src/test/java/com/ajou/hertz/unit/domain/user/service/UserCommandServiceTest.java index 4465dd1..83e571b 100644 --- a/src/test/java/com/ajou/hertz/unit/domain/user/service/UserCommandServiceTest.java +++ b/src/test/java/com/ajou/hertz/unit/domain/user/service/UserCommandServiceTest.java @@ -206,24 +206,6 @@ static Stream testDataForCreateNewUserWithKakao() throws Exception { verifyEveryMocksShouldHaveNoMoreInteractions(); } - @Test - void 주어진_유저_ID와_이미지_URL로_유저의_프로필_이미지를_업데이트한다_존재하지_않는_유저라면_예외가_발생한다() throws Exception { - // Given - Long userId = 1L; - MultipartFile profileImage = new MockMultipartFile("file", "test.jpg", "image/jpeg", - "test image content".getBytes()); - - given(userQueryService.getById(userId)).willThrow(UserNotFoundByIdException.class); - - // When - Throwable t = catchThrowable(() -> sut.updateUserProfileImage(userId, profileImage)); - - // Then - then(userQueryService).should().getById(userId); - verifyEveryMocksShouldHaveNoMoreInteractions(); - assertThat(t).isInstanceOf(UserNotFoundByIdException.class); - } - @Test void 주어진_유저_ID와_연락_수단으로_연락_수단을_변경한다() throws Exception { // given diff --git a/src/test/java/com/ajou/hertz/unit/domain/user/service/UserProfileImageCommandServiceTest.java b/src/test/java/com/ajou/hertz/unit/domain/user/service/UserProfileImageCommandServiceTest.java index 4d6410c..b7170d8 100644 --- a/src/test/java/com/ajou/hertz/unit/domain/user/service/UserProfileImageCommandServiceTest.java +++ b/src/test/java/com/ajou/hertz/unit/domain/user/service/UserProfileImageCommandServiceTest.java @@ -13,7 +13,6 @@ 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 org.springframework.web.multipart.MultipartFile; @@ -25,9 +24,7 @@ import com.ajou.hertz.domain.user.entity.User; import com.ajou.hertz.domain.user.entity.UserProfileImage; import com.ajou.hertz.domain.user.repository.UserProfileImageRepository; -import com.ajou.hertz.domain.user.repository.UserRepository; import com.ajou.hertz.domain.user.service.UserProfileImageCommandService; -import com.ajou.hertz.domain.user.service.UserQueryService; import com.ajou.hertz.util.ReflectionUtils; @DisplayName("[Unit] Service(Command) - User Profile Image") @@ -36,18 +33,9 @@ public class UserProfileImageCommandServiceTest { @InjectMocks private UserProfileImageCommandService sut; - @Mock - private UserQueryService userQueryService; - - @Mock - private UserRepository userRepository; - @Mock private UserProfileImageRepository userProfileImageRepository; - @Mock - private PasswordEncoder passwordEncoder; - @Mock private HertzProperties hertzProperties; @@ -120,9 +108,6 @@ public void setUp() { } private void verifyEveryMocksShouldHaveNoMoreInteractions() { - then(userQueryService).shouldHaveNoMoreInteractions(); - then(userRepository).shouldHaveNoMoreInteractions(); - then(passwordEncoder).shouldHaveNoMoreInteractions(); then(fileService).shouldHaveNoMoreInteractions(); } From d8ef54d22ed4b5d6b84ca19fd22948f7aa4066eb Mon Sep 17 00:00:00 2001 From: tinon1004 Date: Tue, 26 Mar 2024 15:07:29 +0900 Subject: [PATCH 14/14] =?UTF-8?q?refactor:=20#73=20=EB=B6=88=ED=95=84?= =?UTF-8?q?=EC=9A=94=ED=95=9C=20=EC=BD=94=EB=93=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/UserProfileImageCommandServiceTest.java | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/test/java/com/ajou/hertz/unit/domain/user/service/UserProfileImageCommandServiceTest.java b/src/test/java/com/ajou/hertz/unit/domain/user/service/UserProfileImageCommandServiceTest.java index b7170d8..5ebf454 100644 --- a/src/test/java/com/ajou/hertz/unit/domain/user/service/UserProfileImageCommandServiceTest.java +++ b/src/test/java/com/ajou/hertz/unit/domain/user/service/UserProfileImageCommandServiceTest.java @@ -13,12 +13,10 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.mock.web.MockMultipartFile; -import org.springframework.test.context.event.annotation.BeforeTestMethod; 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.properties.HertzProperties; import com.ajou.hertz.domain.user.constant.Gender; import com.ajou.hertz.domain.user.constant.RoleType; import com.ajou.hertz.domain.user.entity.User; @@ -36,17 +34,9 @@ public class UserProfileImageCommandServiceTest { @Mock private UserProfileImageRepository userProfileImageRepository; - @Mock - private HertzProperties hertzProperties; - @Mock private FileService fileService; - @BeforeTestMethod - public void setUp() { - given(hertzProperties.userDefaultProfileImageUrl()).willReturn("https://user-default-profile-image"); - } - @Test void 기본_프로필_이미지인_경우_유저의_프로필_이미지를_최초로_업데이트한다() throws Exception { // given