Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

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

Merged
merged 15 commits into from
Mar 26, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -130,4 +130,17 @@ private static InputStream getInputStreamFromMultipartFile(MultipartFile multipa
throw new MultipartFileNotReadableException(ex);
}
}

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

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

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

import java.net.URI;

import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.validation.annotation.Validated;
Expand All @@ -13,7 +14,9 @@
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.validator.PhoneNumber;
Expand Down Expand Up @@ -117,6 +120,24 @@ public ResponseEntity<UserResponse> signUpV1(
.body(UserResponse.from(userCreated));
}

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

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

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

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

public void changeContactLink(String contactLink) {
this.contactLink = contactLink;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -33,4 +33,14 @@ private UserProfileImage(Long id, User user, String originalName, String storedN
this.id = id;
this.user = user;
}

public static UserProfileImage create(
User user,
String originalName,
String storedName,
String url
) {
return new UserProfileImage(null, user, originalName, storedName, url);
}

}
Original file line number Diff line number Diff line change
@@ -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<UserProfileImage, Long> {
Optional<UserProfileImage> findByUser_Id(Long userId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
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.kakao.dto.response.KakaoUserInfoResponse;
import com.ajou.hertz.common.properties.HertzProperties;
Expand All @@ -29,6 +30,7 @@ public class UserCommandService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final HertzProperties hertzProperties;
private final UserProfileImageCommandService userProfileImageCommandService;

/**
* 새로운 회원을 등록한다.
Expand Down Expand Up @@ -130,13 +132,28 @@ private String generateRandom16CharString() {
.substring(0, 16);
}

/**
* 유저의 프로필 이미지를 업데이트합니다.
*
* @param userId 유저의 ID
* @param profileImage 변경할 프로필 이미지
*
* @return 변경된 유저 정보
*/
public UserDto updateUserProfileImage(Long userId, MultipartFile profileImage) {
User user = userQueryService.getById(userId);
String newProfileImageUrl = userProfileImageCommandService.updateProfileImage(user, profileImage);
user.changeProfileImageUrl(newProfileImageUrl);
return UserDto.from(user);
}

/**
*연락 수단을 변경합니다.
*
* @param userId 유저의 ID
* @param contactLink 변경할 연락 수단
*
*@return 변경된 유저 정보
* @return 변경된 유저 정보
*/
public UserDto updateContactLink(Long userId, String contactLink) {
User user = userQueryService.getById(userId);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
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 FileService fileService;
private final UserProfileImageRepository userProfileImageRepository;

private static final String USER_PROFILE_IMAGE_UPLOAD_PATH = "user-profile-images/";

/**
* 유저의 프로필 이미지를 업데이트한다.
*
* @param user 프로필 이미지를 업데이트할 유저
* @param newProfileImage 새로운 프로필 이미지
*
* @return 새로운 프로필 이미지 URL
*/
public String updateProfileImage(User user, MultipartFile newProfileImage) {
deleteOldProfileImage(user.getId());
UserProfileImage newUserProfileImage = uploadNewProfileImage(user, newProfileImage);
Wo-ogie marked this conversation as resolved.
Show resolved Hide resolved
return newUserProfileImage.getUrl();
}

/**
* 유저의 프로필 이미지를 삭제한다.
*
* @param userId 프로필 이미지를 삭제할 유저의 id
*/
private void deleteOldProfileImage(Long userId) {
Optional<UserProfileImage> optionalOldProfileImage = userProfileImageRepository.findByUser_Id(userId);
if (optionalOldProfileImage.isPresent()) {
UserProfileImage oldProfileImage = optionalOldProfileImage.get();
userProfileImageRepository.delete(oldProfileImage);
userProfileImageRepository.flush();
fileService.deleteFile(oldProfileImage.getStoredName());
}
}

/**
* 새로운 프로필 이미지를 업로드한다.
* @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);
return newUserProfileImage;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,20 @@ void setUpMockProperties() {
verifyEveryMocksShouldHaveNoMoreInteractions();
}

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

// when
sut.deleteFile(storedFileName);

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

private void verifyEveryMocksShouldHaveNoMoreInteractions() {
then(s3Client).shouldHaveNoMoreInteractions();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import org.springframework.context.annotation.Import;
import org.springframework.data.jpa.mapping.JpaMetamodelMappingContext;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.test.context.event.annotation.BeforeTestMethod;
import org.springframework.test.web.servlet.MockMvc;
Expand All @@ -36,6 +37,7 @@
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;
Expand All @@ -60,6 +62,9 @@ class UserControllerTest {
@MockBean
private UserQueryService userQueryService;

@MockBean
private UserProfileImageCommandService userProfileImageCommandService;

private final MockMvc mvc;

private final ObjectMapper objectMapper;
Expand Down Expand Up @@ -212,6 +217,38 @@ public void securitySetUp() throws Exception {
verifyEveryMocksShouldHaveNoMoreInteractions();
}

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

given(userCommandService.updateUserProfileImage(userId, profileImage)).willReturn(expectedResult);

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

@Test
void 주어진_연락수단을_새로운_연락수단으로_변경한다() throws Exception {
// given
Expand All @@ -238,6 +275,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 {
Expand Down Expand Up @@ -281,5 +319,4 @@ private UserDto createUserDto() throws Exception {
private UserDetails createTestUser(Long userId) throws Exception {
return new UserPrincipal(createUserDto(userId));
}

}
Loading
Loading