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

[Feature/user withdrawal] 마이페이지-회원탈퇴 API #67

Merged
merged 5 commits into from
Nov 26, 2023
Merged
Show file tree
Hide file tree
Changes from all 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
36 changes: 36 additions & 0 deletions src/main/java/everymeal/server/user/controller/UserController.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import everymeal.server.user.controller.dto.request.UserEmailAuthReq;
import everymeal.server.user.controller.dto.request.UserEmailLoginReq;
import everymeal.server.user.controller.dto.request.UserEmailSingReq;
import everymeal.server.user.controller.dto.request.UserProfileUpdateReq;
import everymeal.server.user.controller.dto.request.WithdrawalReq;
import everymeal.server.user.controller.dto.response.UserEmailAuthRes;
import everymeal.server.user.controller.dto.response.UserLoginRes;
import everymeal.server.user.controller.dto.response.UserProfileRes;
Expand All @@ -26,6 +28,7 @@
import org.springframework.http.ResponseEntity;
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;
Expand Down Expand Up @@ -145,6 +148,39 @@ public ApplicationResponse<UserProfileRes> getUserProfile(
return ApplicationResponse.ok(userService.getUserProfile(authenticatedUser));
}

@Auth(require = true)
@PutMapping("/profile")
@SecurityRequirement(name = "jwt-user-auth")
@Operation(summary = "인증된 사용자의 프로필 정보 수정", description = "인증된 사용자의 프로필 정보를 수정합니다.")
@ApiResponse(
responseCode = "409",
description = """
(U0005)이미 등록된 닉네임입니다.<br>
""",
content = @Content(schema = @Schema()))
public ApplicationResponse<Boolean> updateUserProfile(
@Parameter(hidden = true) @AuthUser AuthenticatedUser authenticatedUser,
@RequestBody UserProfileUpdateReq userProfileUpdateReq) {
return ApplicationResponse.ok(
userService.updateUserProfile(authenticatedUser, userProfileUpdateReq));
}

@Auth(require = true)
@PostMapping("/withdrawal")
@SecurityRequirement(name = "jwt-user-auth")
@Operation(summary = "회원탈퇴", description = "서비스 회원 탈퇴를 합니다.")
@ApiResponse(
responseCode = "404",
description = """
(U0001)등록된 유저가 아닙니다.<br>
""",
content = @Content(schema = @Schema()))
public ApplicationResponse<Boolean> withdrawal(
@Parameter(hidden = true) @AuthUser AuthenticatedUser authenticatedUser,
@RequestBody WithdrawalReq withdrawalReq) {
return ApplicationResponse.ok(userService.withdrawal(authenticatedUser, withdrawalReq));
}

private ResponseEntity<ApplicationResponse<UserLoginRes>> setRefreshToken(
UserLoginRes response) {
ResponseCookie cookie =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package everymeal.server.user.controller.dto.request;


import io.swagger.v3.oas.annotations.media.Schema;

public record UserProfileUpdateReq(
@Schema(description = "닉네임", example = "연유크림") String nickName,
@Schema(description = "프로필 이미지 key", example = "user/bc90af33-bc6a-4009-bfc8-2c3efe0b16bd")
String profileImageKey) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package everymeal.server.user.controller.dto.request;


import everymeal.server.user.entity.WithdrawalReason;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;

public record WithdrawalReq(
@NotBlank
@Schema(
description = "탈퇴 사유를 입력해주세요.",
example = "NOT_USE_USUALLY",
allowableValues = {
"NOT_USE_USUALLY",
"INCONVENIENT_IN_TERMS_OF_USABILITY",
"ERRORS_OCCUR_FREQUENTLY",
"MY_SCHOOL_HAS_CHANGED",
"ETC"
})
WithdrawalReason withdrawalReason,
@Schema(description = "사유가 '기타'일 경우, 추가 이유 입력해주세요.", example = "다른 서비스를 사용하게 되었다.")
String etcReason) {}
Comment on lines +21 to +22
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

만약 기타를 선택한 경우에 대해서도 남겨서 서비스 운영도 생각하는 것이 너무 좋은 방향 같아요!

Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@

public record UserProfileRes(
Long userId, String nickName, String profileImgUrl, String universityName) {
public static UserProfileRes of(Map<String, Object> user) {
public static UserProfileRes of(Map<String, Object> user, String profileImgUrl) {
return new UserProfileRes(
(Long) user.get("userId"),
(String) user.get("nickName"),
(String) user.get("profileImgUrl"),
profileImgUrl,
(String) user.get("universityName"));
}
}
13 changes: 13 additions & 0 deletions src/main/java/everymeal/server/user/entity/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import jakarta.persistence.Index;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import jakarta.persistence.OneToOne;
import jakarta.persistence.Table;
import java.util.HashSet;
import java.util.Set;
Expand Down Expand Up @@ -53,6 +54,9 @@ public class User extends BaseEntity {
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
Set<ReviewMark> reviewMarks = new HashSet<>();

@OneToOne(mappedBy = "user")
private Withdrawal withdrawal;

@Builder
public User(String nickname, String email, String profileImgUrl, University university) {
this.nickname = nickname;
Expand All @@ -65,4 +69,13 @@ public User(String nickname, String email, String profileImgUrl, University univ
public void setEmail(String email) {
this.email = email;
}

public void updateProfile(String nickname, String profileImgUrl) {
this.nickname = nickname;
this.profileImgUrl = profileImgUrl;
}
/** 회원 탈퇴 */
public void setIsDeleted() {
this.isDeleted = Boolean.TRUE;
}
}
42 changes: 42 additions & 0 deletions src/main/java/everymeal/server/user/entity/Withdrawal.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package everymeal.server.user.entity;


import everymeal.server.global.entity.BaseEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.MapsId;
import jakarta.persistence.OneToOne;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Withdrawal extends BaseEntity {
@Id private Long userIdx;

@Column(nullable = false)
@Enumerated(EnumType.STRING)
private WithdrawalReason withdrawalReason;

@Column(nullable = true, columnDefinition = "TEXT")
private String etcReason;

@MapsId
@OneToOne
@JoinColumn(name = "user_idx", referencedColumnName = "idx")
private User user;

@Builder
public Withdrawal(WithdrawalReason withdrawalReason, String etcReason, User user) {
this.withdrawalReason = withdrawalReason;
this.etcReason = etcReason;
this.user = user;
}
}
17 changes: 17 additions & 0 deletions src/main/java/everymeal/server/user/entity/WithdrawalReason.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package everymeal.server.user.entity;


import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public enum WithdrawalReason {
NOT_USE_USUALLY("앱을 잘 쓰지 않아요"),
INCONVENIENT_IN_TERMS_OF_USABILITY("사용성이 불편해요"),
ERRORS_OCCUR_FREQUENTLY("오류가 자주 발생해요"),
MY_SCHOOL_HAS_CHANGED("학교가 바뀌었어요"),
ETC("기타");
Comment on lines +10 to +14
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Enum으로 탈퇴사유 정리해두는 것 좋은 것 같습니다!!


public final String MESSAGE;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package everymeal.server.user.repository;


import everymeal.server.user.entity.Withdrawal;
import org.springframework.data.jpa.repository.JpaRepository;

public interface WithdrawalRepository extends JpaRepository<Withdrawal, Long> {}
7 changes: 7 additions & 0 deletions src/main/java/everymeal/server/user/service/UserService.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import everymeal.server.user.controller.dto.request.UserEmailAuthReq;
import everymeal.server.user.controller.dto.request.UserEmailLoginReq;
import everymeal.server.user.controller.dto.request.UserEmailSingReq;
import everymeal.server.user.controller.dto.request.UserProfileUpdateReq;
import everymeal.server.user.controller.dto.request.WithdrawalReq;
import everymeal.server.user.controller.dto.response.UserEmailAuthRes;
import everymeal.server.user.controller.dto.response.UserLoginRes;
import everymeal.server.user.controller.dto.response.UserProfileRes;
Expand All @@ -22,4 +24,9 @@ public interface UserService {
Boolean checkUser(String email);

UserProfileRes getUserProfile(AuthenticatedUser authenticatedUser);

Boolean updateUserProfile(
AuthenticatedUser authenticatedUser, UserProfileUpdateReq userProfileUpdateReq);

Boolean withdrawal(AuthenticatedUser authenticatedUser, WithdrawalReq request);
}
53 changes: 52 additions & 1 deletion src/main/java/everymeal/server/user/service/UserServiceImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,21 @@
import everymeal.server.user.controller.dto.request.UserEmailAuthReq;
import everymeal.server.user.controller.dto.request.UserEmailLoginReq;
import everymeal.server.user.controller.dto.request.UserEmailSingReq;
import everymeal.server.user.controller.dto.request.UserProfileUpdateReq;
import everymeal.server.user.controller.dto.request.WithdrawalReq;
import everymeal.server.user.controller.dto.response.UserEmailAuthRes;
import everymeal.server.user.controller.dto.response.UserLoginRes;
import everymeal.server.user.controller.dto.response.UserProfileRes;
import everymeal.server.user.entity.User;
import everymeal.server.user.entity.Withdrawal;
import everymeal.server.user.entity.WithdrawalReason;
import everymeal.server.user.repository.UserMapper;
import everymeal.server.user.repository.UserRepository;
import everymeal.server.user.repository.WithdrawalRepository;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Map;
import java.util.Optional;
import java.util.Random;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
Expand All @@ -37,6 +43,7 @@ public class UserServiceImpl implements UserService {
private final MailUtil mailUtil;
private final S3Util s3Util;
private final UserMapper userMapper;
private final WithdrawalRepository withdrawalRepository;

@Override
@Transactional
Expand Down Expand Up @@ -153,6 +160,50 @@ public Boolean checkUser(String email) {
@Override
public UserProfileRes getUserProfile(AuthenticatedUser authenticatedUser) {
Map<String, Object> result = userMapper.getUserProfile(authenticatedUser.getIdx());
return UserProfileRes.of(result);
String profileImgUrl = s3Util.getImgUrl((String) result.get("profileImgUrl"));
return UserProfileRes.of(result, profileImgUrl);
}

@Override
@Transactional
public Boolean updateUserProfile(
AuthenticatedUser authenticatedUser, UserProfileUpdateReq request) {
User user =
userRepository
.findById(authenticatedUser.getIdx())
.orElseThrow(() -> new ApplicationException(ExceptionList.USER_NOT_FOUND));
// 닉네임 중복 검사
Optional<User> duplicatedNickName = userRepository.findByNickname(request.nickName());
if (duplicatedNickName.isPresent() && user != duplicatedNickName.get()) {
throw new ApplicationException(ExceptionList.NICKNAME_ALREADY_EXIST);
}
user.updateProfile(request.nickName(), request.profileImageKey());
return true;
}

@Override
@Transactional
public Boolean withdrawal(AuthenticatedUser authenticatedUser, WithdrawalReq request) {
User user =
userRepository
.findById(authenticatedUser.getIdx())
.orElseThrow(() -> new ApplicationException(ExceptionList.USER_NOT_FOUND));
Withdrawal withdrawal;
if (request.withdrawalReason() != WithdrawalReason.ETC) { // 기타를 제외한 경우
withdrawal =
Withdrawal.builder()
.withdrawalReason(request.withdrawalReason())
.user(user)
.build();
} else // 기타를 선택한 경우
withdrawal =
Withdrawal.builder()
.withdrawalReason(request.withdrawalReason())
.etcReason(request.etcReason())
.user(user)
.build();
withdrawalRepository.save(withdrawal); // 탈퇴 관련 정보 저장
user.setIsDeleted(); // 논리 삭제
return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import static org.mockito.BDDMockito.given;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.cookie;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
Expand All @@ -14,7 +15,10 @@
import everymeal.server.user.controller.dto.request.UserEmailAuthReq;
import everymeal.server.user.controller.dto.request.UserEmailLoginReq;
import everymeal.server.user.controller.dto.request.UserEmailSingReq;
import everymeal.server.user.controller.dto.request.UserProfileUpdateReq;
import everymeal.server.user.controller.dto.request.WithdrawalReq;
import everymeal.server.user.controller.dto.response.UserLoginRes;
import everymeal.server.user.entity.WithdrawalReason;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
Expand Down Expand Up @@ -134,4 +138,42 @@ void getUserProfile() throws Exception {
.andExpect(status().isOk())
.andExpect(jsonPath("$.message").value("OK"));
}

@DisplayName("인증된 유저의 프로필 정보 수정")
@Test
void updateUserProfile() throws Exception {
// given
UserProfileUpdateReq request = new UserProfileUpdateReq("연유크림", "imageKey");

given(userJwtResolver.resolveArgument(any(), any(), any(), any()))
.willReturn(AuthenticatedUser.builder().idx(1L).build());

// when-then
mockMvc.perform(
put("/api/v1/users/profile")
.content(objectMapper.writeValueAsString(request))
.contentType(MediaType.APPLICATION_JSON))
.andDo(MockMvcResultHandlers.print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.message").value("OK"));
}

@DisplayName("회원 탈퇴")
@Test
void withdrawal() throws Exception {
// given
WithdrawalReq request = new WithdrawalReq(WithdrawalReason.ERRORS_OCCUR_FREQUENTLY, "");

given(userJwtResolver.resolveArgument(any(), any(), any(), any()))
.willReturn(AuthenticatedUser.builder().idx(1L).build());

// when-then
mockMvc.perform(
post("/api/v1/users/withdrawal")
.content(objectMapper.writeValueAsString(request))
.contentType(MediaType.APPLICATION_JSON))
.andDo(MockMvcResultHandlers.print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.message").value("OK"));
}
}
Loading