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 10 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 All @@ -25,6 +28,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;
Expand All @@ -47,6 +51,7 @@
public class UserController {

private final UserCommandService userCommandService;
private final UserProfileImageCommandService userProfileImageCommandService;
private final UserQueryService userQueryService;

@Operation(
Expand Down Expand Up @@ -117,6 +122,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("profileImage") MultipartFile profileImage
Wo-ogie marked this conversation as resolved.
Show resolved Hide resolved
) {
UserDto userUpdated = userCommandService.updateUserProfileImage(userPrincipal.getUserId(), profileImage);
return UserResponse.from(userUpdated);
}

@Operation(
summary = "연락 수단 변경",
description = "연락 수단을 변경합니다.",
Expand All @@ -131,4 +154,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> findById(Long userId);
}
Wo-ogie marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -16,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;
Expand All @@ -29,6 +31,8 @@ public class UserCommandService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final HertzProperties hertzProperties;
private final UserProfileImageRepository userProfileImageRepository;
Wo-ogie marked this conversation as resolved.
Show resolved Hide resolved
private final UserProfileImageCommandService userProfileImageCommandService;

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

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

/**
*연락 수단을 변경합니다.
*
* @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,57 @@
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.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;

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 업데이트된 유저 정보
*/
public UserDto updateProfileImage(Long userId, MultipartFile newProfileImage) {

User user = userQueryService.getById(userId);
Optional<UserProfileImage> optionalOldProfileImage = userProfileImageRepository.findById(userId);

String uploadPath = "user-profile-images/";
Wo-ogie marked this conversation as resolved.
Show resolved Hide resolved
FileDto uploadedFile = fileService.uploadFile(newProfileImage, uploadPath);
String newProfileImageUrl = uploadedFile.getUrl();
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);
userProfileImageRepository.save(newUserProfileImage);

user.changeProfileImageUrl(newProfileImageUrl);
return UserDto.from(user);
}
}
Wo-ogie marked this conversation as resolved.
Show resolved Hide resolved
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