Skip to content

Commit

Permalink
[BE] 유저 닉네임 수정, 프로필 사진 변경 기능 (#274) (#301)
Browse files Browse the repository at this point in the history
* feat: 사용자 프로필 업데이트 기능 추가

* refactor: 도메인쪽으로 비즈니스 로직을 이동

* test: 기존의 터지던 테스트들을 수정

* test: 닉네임, 프로필 이미지 수정하는 메서드에 대한 테스트 코드 추가

* test: API 문서 수정

* feat: 로그인 할 때마다 로그인 정보 업데이트 되는 기능 삭제

* refactor: RequestParam을 ModelAttribute로 교체

* refactor: 리뷰 반영

* test: ControllerAdvice에 대한 테스트 코드 추가
  • Loading branch information
jaeseongDev authored and Sehwan-Jang committed Aug 9, 2021
1 parent fdf5767 commit f459e68
Show file tree
Hide file tree
Showing 20 changed files with 309 additions and 53 deletions.
4 changes: 4 additions & 0 deletions backend/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ dependencies {

// 로깅
implementation 'net.rakugakibox.spring.boot:logback-access-spring-boot-starter:2.7.1'

// aws s3 sdk
implementation platform('software.amazon.awssdk:bom:2.15.0')
implementation 'software.amazon.awssdk:s3:2.17.5'
}

test {
Expand Down
2 changes: 1 addition & 1 deletion backend/src/docs/asciidoc/api/v1/users.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ include::{snippets}/api/v1/users/get/fail/response-fields.adoc[]

include::{snippets}/api/v1/users/patch/success/http-request.adoc[]
include::{snippets}/api/v1/users/patch/success/request-headers.adoc[]
include::{snippets}/api/v1/users/patch/success/request-fields.adoc[]
include::{snippets}/api/v1/users/patch/success/request-parts.adoc[]

==== Response (성공 - 소셜 로그인 유저 닉네임 수정)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,11 @@ public TokenResponse oauthLogin(String oauthProviderName, String oauthAccessToke
.findByOauthId(socialLoginUser.getOauthId());

if (possibleSocialLoginUser.isEmpty()) { //TODO: 옵셔널로 변경 가능?

socialLoginUserRepository.save(socialLoginUser);
return TokenResponse.of(jwtTokenProvider.createAccessToken(socialLoginUser.getId().toString()));
}

SocialLoginUser foundSocialLoginUser = possibleSocialLoginUser.get();
foundSocialLoginUser.update(socialLoginUser);

return TokenResponse.of(jwtTokenProvider.createAccessToken(foundSocialLoginUser.getId().toString()));
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.darass.darass.exception;

import com.darass.darass.exception.httpbasicexception.BadRequestException;
import com.darass.darass.exception.httpbasicexception.ConflictException;
import com.darass.darass.exception.httpbasicexception.CustomException;
import com.darass.darass.exception.httpbasicexception.InternalServerException;
Expand Down Expand Up @@ -29,7 +30,10 @@ public enum ExceptionWithMessageAndCode {
NOT_FOUND_COMMENT(new NotFoundException("해당하는 댓글이 없습니다.", 900)),
INVALID_GUEST_PASSWORD(new UnauthorizedException("Guest 사용자의 비밀번호가 일치하지 않습니다.", 901)),
NOT_GUEST_USER(new UnauthorizedException("Guest 사용자가 아닙니다.", 902)),
UNAUTHORIZED_FOR_COMMENT(new UnauthorizedException("해당 댓글을 관리할 권한이 없습니다.", 903));
UNAUTHORIZED_FOR_COMMENT(new UnauthorizedException("해당 댓글을 관리할 권한이 없습니다.", 903)),

// 파일 관련 : 10xx
IO_EXCEPTION(new BadRequestException("업로드할 파일이 잘못되었습니다.", 1000));

private final CustomException exception;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.darass.darass.exception.controller;

import com.darass.darass.exception.dto.ExceptionResponse;
import com.darass.darass.exception.httpbasicexception.BadRequestException;
import com.darass.darass.exception.httpbasicexception.ConflictException;
import com.darass.darass.exception.httpbasicexception.NotFoundException;
import com.darass.darass.exception.httpbasicexception.UnauthorizedException;
Expand All @@ -21,6 +22,12 @@ public ExceptionResponse handleMethodArgumentNotValidException(MethodArgumentNot
return new ExceptionResponse(e.getMessage(), HttpStatus.BAD_REQUEST.value());
}

@ExceptionHandler(BadRequestException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ExceptionResponse handleBadRequestException(BadRequestException e) {
return new ExceptionResponse(e.getMessage(), e.getCode());
}

@ExceptionHandler(UnauthorizedException.class)
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public ExceptionResponse handleUnauthorizedException(UnauthorizedException e) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.darass.darass.exception.httpbasicexception;

public class BadRequestException extends CustomException {

public BadRequestException(String message, Integer code) {
super(message, code);
}
}
16 changes: 16 additions & 0 deletions backend/src/main/java/com/darass/darass/user/S3ClientConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.darass.darass.user;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;

@Configuration
public class S3ClientConfig {
@Bean
public S3Client s3Client() {
return S3Client.builder()
.region(Region.AP_NORTHEAST_2)
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,12 @@
import com.darass.darass.user.dto.UserResponse;
import com.darass.darass.user.dto.UserUpdateRequest;
import com.darass.darass.user.service.UserService;
import javax.validation.Valid;
import lombok.AllArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

Expand All @@ -33,8 +31,9 @@ public ResponseEntity<UserResponse> find(@RequiredLogin User user) {

@PatchMapping
public ResponseEntity<UserResponse> updateNickname(@RequiredLogin User user,
@Valid @RequestBody UserUpdateRequest userUpdateRequest) {
UserResponse userResponse = userService.updateNickName(user.getId(), userUpdateRequest);
@ModelAttribute UserUpdateRequest userUpdateRequest
) {
UserResponse userResponse = userService.update(user.getId(), userUpdateRequest);
return ResponseEntity.ok(userResponse);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@

import com.darass.darass.auth.oauth.api.domain.OAuthProviderType;
import com.darass.darass.exception.ExceptionWithMessageAndCode;
import com.darass.darass.user.infrastructure.S3Uploader;
import java.io.IOException;
import java.util.Objects;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.web.multipart.MultipartFile;
import software.amazon.awssdk.core.exception.SdkClientException;

@Getter
@NoArgsConstructor
Expand Down Expand Up @@ -40,11 +45,14 @@ public boolean isValidGuestPassword(String guestUserPassword) {
throw ExceptionWithMessageAndCode.NOT_GUEST_USER.getException();
}

public void update(SocialLoginUser socialLoginUser) {
this.changeNickName(socialLoginUser.getNickName());
this.changeProfileImageUrl(socialLoginUser.getProfileImageUrl());
this.email = socialLoginUser.getEmail();
this.oauthId = socialLoginUser.getOauthId();
this.oauthProviderType = socialLoginUser.getOauthProviderType();
public void changeNickNameOrProfileImageIfExists(S3Uploader s3Uploader, String nickName,
MultipartFile profileImageFile) {
if (!Objects.isNull(nickName)) {
changeNickName(nickName);
}
if (!Objects.isNull(profileImageFile)) {
String imageUrl = s3Uploader.upload(profileImageFile);
changeProfileImageUrl(imageUrl);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@
import com.darass.darass.comment.domain.CommentLike;
import com.darass.darass.common.domain.BaseTimeEntity;
import com.darass.darass.project.domain.Project;
import com.darass.darass.user.infrastructure.S3Uploader;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import javax.persistence.Column;
import javax.persistence.DiscriminatorColumn;
import javax.persistence.Entity;
Expand All @@ -20,6 +22,7 @@
import javax.persistence.OneToMany;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.web.multipart.MultipartFile;

@Getter
@NoArgsConstructor
Expand Down Expand Up @@ -85,5 +88,4 @@ public void changeNickName(String nickName) {
public void changeProfileImageUrl(String profileImageUrl) {
this.profileImageUrl = profileImageUrl;
}

}
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
package com.darass.darass.user.dto;

import javax.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.springframework.web.multipart.MultipartFile;

@NoArgsConstructor
@Getter
@Setter
@AllArgsConstructor
public class UserUpdateRequest {

@NotNull
private String nickName;

private MultipartFile profileImageFile;

}

Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.darass.darass.user.infrastructure;

import com.darass.darass.exception.ExceptionWithMessageAndCode;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Date;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import software.amazon.awssdk.core.exception.SdkClientException;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;

@Component
@RequiredArgsConstructor
public class S3Uploader {

private static final String S3_BUCKET_NAME = "darass-user-profile-image";
private static final String CLOUDFRONT_URL = "https://d3qmnph7nb4773.cloudfront.net/";
private final S3Client s3;

public String upload(MultipartFile multipartFile) {
File uploadFile = convert(multipartFile);
String uploadFileUrl = uploadToS3(uploadFile);
return uploadFileUrl;
}

private File convert(MultipartFile multipartFile) {
File file = new File(multipartFile.getOriginalFilename());
try (FileOutputStream fos = new FileOutputStream(file)) {
fos.write(multipartFile.getBytes());
} catch (IOException e) {
throw ExceptionWithMessageAndCode.IO_EXCEPTION.getException();
}
return file;
}

private String uploadToS3(File uploadFile) {
String fileName = new Date().getTime() + uploadFile.getName();
PutObjectRequest objectRequest = PutObjectRequest.builder()
.bucket(S3_BUCKET_NAME)
.key(fileName)
.build();
try {
s3.putObject(objectRequest, RequestBody.fromFile(uploadFile));
} catch (SdkClientException e) {
throw ExceptionWithMessageAndCode.INTERNAL_SERVER.getException();
} finally {
removeNewFile(uploadFile);
}
String uploadFileUrl = CLOUDFRONT_URL + fileName;
return uploadFileUrl;
}

private void removeNewFile(File uploadFile) {
uploadFile.delete();
}
}
Original file line number Diff line number Diff line change
@@ -1,35 +1,41 @@
package com.darass.darass.user.service;

import com.darass.darass.exception.ExceptionWithMessageAndCode;
import com.darass.darass.user.domain.SocialLoginUser;
import com.darass.darass.user.domain.User;
import com.darass.darass.user.dto.PasswordCheckRequest;
import com.darass.darass.user.dto.PasswordCheckResponse;
import com.darass.darass.user.dto.UserResponse;
import com.darass.darass.user.dto.UserUpdateRequest;
import com.darass.darass.user.infrastructure.S3Uploader;
import com.darass.darass.user.repository.UserRepository;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

@RequiredArgsConstructor
@Transactional
@Service
public class UserService {

private final UserRepository userRepository;
private final S3Uploader s3Uploader;

public UserResponse findById(Long id) {
Optional<User> possibleUser = userRepository.findById(id);
User user = possibleUser.orElseThrow(ExceptionWithMessageAndCode.NOT_FOUND_USER::getException);
return UserResponse.of(user, user.getUserType(), user.getProfileImageUrl());
}

public UserResponse updateNickName(Long id, UserUpdateRequest userUpdateRequest) {
Optional<User> expectedUser = userRepository.findById(id);
User user = expectedUser.orElseThrow(ExceptionWithMessageAndCode.NOT_FOUND_USER::getException);
user.changeNickName(userUpdateRequest.getNickName());

public UserResponse update(Long id, UserUpdateRequest userUpdateRequest) {
Optional<User> possibleUser = userRepository.findById(id);
SocialLoginUser user = (SocialLoginUser) possibleUser
.orElseThrow(ExceptionWithMessageAndCode.NOT_FOUND_USER::getException);
String nickName = userUpdateRequest.getNickName();
MultipartFile profileImageFile = userUpdateRequest.getProfileImageFile();
user.changeNickNameOrProfileImageIfExists(s3Uploader, nickName, profileImageFile);
return UserResponse.of(user, user.getUserType(), user.getProfileImageUrl());
}

Expand All @@ -38,8 +44,8 @@ public void deleteById(Long id) {
}

public PasswordCheckResponse checkGuestUserPassword(PasswordCheckRequest passwordCheckRequest) {
Optional<User> expectedUser = userRepository.findById(passwordCheckRequest.getGuestUserId());
User user = expectedUser.orElseThrow(ExceptionWithMessageAndCode.NOT_FOUND_USER::getException);
Optional<User> possibleUser = userRepository.findById(passwordCheckRequest.getGuestUserId());
User user = possibleUser.orElseThrow(ExceptionWithMessageAndCode.NOT_FOUND_USER::getException);

if (user.isValidGuestPassword(passwordCheckRequest.getGuestUserPassword())) {
return new PasswordCheckResponse(true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;


@SpringBootTest
@SpringBootTest(webEnvironment = WebEnvironment.NONE)
@DisplayName("DarassApplication 클래스 ")
class DarassApplicationTest {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,35 +67,20 @@ void oauthLogin_register() {
}

@Transactional
@DisplayName("(로그인 - 이미 DB에 회원정보가 있는경우) login 메서드는 oauth 토큰이 주어지면, 인증서버에서 사용자 정보를 받아와서 DB 업데이트를 하고 primary key를 payload 삼아 accessToken을 리턴한다.")
@DisplayName("(로그인 - 이미 DB에 회원정보가 있는경우) login 메서드는 oauth 토큰이 주어지면, primary key를 payload 삼아 accessToken을 리턴한다.")
@Test
void oauthLogin_login() {
//given
socialLoginUserRepository.save(socialLoginUser);

SocialLoginUser updatedSocialLoginUser = SocialLoginUser
.builder()
.nickName("병욱")
.oauthId(socialLoginUser.getOauthId())
.oauthProviderType(OAuthProviderType.KAKAO)
.email("[email protected]")
.profileImageUrl("http://kakao/updated_profile_image.png")
.build();

given(oAuthProvider.findSocialLoginUser(any(), any())).willReturn(updatedSocialLoginUser);
given(oAuthProvider.findSocialLoginUser(any(), any())).willReturn(socialLoginUser);

//then
TokenResponse tokenResponse = oAuthService.oauthLogin(OAuthProviderType.KAKAO.getName(), oauthAccessToken);

//when
String payload = jwtTokenProvider.getPayload(tokenResponse.getAccessToken());

SocialLoginUser result = socialLoginUserRepository.findById(Long.parseLong(payload)).get();
assertThat(result.getNickName()).isEqualTo(updatedSocialLoginUser.getNickName());
assertThat(result.getProfileImageUrl()).isEqualTo(updatedSocialLoginUser.getProfileImageUrl());
assertThat(result.getOauthId()).isEqualTo(updatedSocialLoginUser.getOauthId());
assertThat(result.getOauthProviderType()).isEqualTo(updatedSocialLoginUser.getOauthProviderType());
assertThat(result.getEmail()).isEqualTo(updatedSocialLoginUser.getEmail());
assertThat(payload).isNotNull();
}

@DisplayName("findSocialLoginUserByAccessToken 메서드는 accessToken이 주어지면 SocialLoginUser를 리턴한다.")
Expand Down
Loading

0 comments on commit f459e68

Please sign in to comment.