From efa6962bd87af854a07400c1457c91246fe97818 Mon Sep 17 00:00:00 2001 From: dldmsql Date: Sun, 26 Nov 2023 00:36:56 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20#66=20=EB=A7=88=EC=9D=B4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80-=ED=9A=8C=EC=9B=90=ED=83=88=ED=87=B4=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/controller/UserController.java | 17 ++++++++ .../controller/dto/request/WithdrawalReq.java | 12 ++++++ .../everymeal/server/user/entity/User.java | 8 ++++ .../server/user/entity/Withdrawal.java | 42 +++++++++++++++++++ .../server/user/entity/WithdrawalReason.java | 17 ++++++++ .../user/repository/WithdrawalRepository.java | 7 ++++ .../server/user/service/UserService.java | 3 ++ .../server/user/service/UserServiceImpl.java | 31 ++++++++++++++ .../user/controller/UserControllerTest.java | 21 ++++++++++ .../user/service/UserServiceImplTest.java | 33 +++++++++++++++ 10 files changed, 191 insertions(+) create mode 100644 src/main/java/everymeal/server/user/controller/dto/request/WithdrawalReq.java create mode 100644 src/main/java/everymeal/server/user/entity/Withdrawal.java create mode 100644 src/main/java/everymeal/server/user/entity/WithdrawalReason.java create mode 100644 src/main/java/everymeal/server/user/repository/WithdrawalRepository.java diff --git a/src/main/java/everymeal/server/user/controller/UserController.java b/src/main/java/everymeal/server/user/controller/UserController.java index ebd967b..5f5be99 100644 --- a/src/main/java/everymeal/server/user/controller/UserController.java +++ b/src/main/java/everymeal/server/user/controller/UserController.java @@ -9,6 +9,7 @@ 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; @@ -164,6 +165,22 @@ public ApplicationResponse updateUserProfile( userService.updateUserProfile(authenticatedUser, userProfileUpdateReq)); } + @Auth(require = true) + @PostMapping("/withdrawal") + @SecurityRequirement(name = "jwt-user-auth") + @Operation(summary = "회원탈퇴", description = "서비스 회원 탈퇴를 합니다.") + @ApiResponse( + responseCode = "404", + description = """ + (U0001)등록된 유저가 아닙니다.
+ """, + content = @Content(schema = @Schema())) + public ApplicationResponse withdrawal( + @Parameter(hidden = true) @AuthUser AuthenticatedUser authenticatedUser, + @RequestBody WithdrawalReq withdrawalReq) { + return ApplicationResponse.ok(userService.withdrawal(authenticatedUser, withdrawalReq)); + } + private ResponseEntity> setRefreshToken( UserLoginRes response) { ResponseCookie cookie = diff --git a/src/main/java/everymeal/server/user/controller/dto/request/WithdrawalReq.java b/src/main/java/everymeal/server/user/controller/dto/request/WithdrawalReq.java new file mode 100644 index 0000000..156bc4f --- /dev/null +++ b/src/main/java/everymeal/server/user/controller/dto/request/WithdrawalReq.java @@ -0,0 +1,12 @@ +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") + WithdrawalReason withdrawalReason, + @Schema(description = "사유가 '기타'일 경우, 추가 이유 입력해주세요.", example = "다른 서비스를 사용하게 되었다.") + String etcReason) {} diff --git a/src/main/java/everymeal/server/user/entity/User.java b/src/main/java/everymeal/server/user/entity/User.java index 3d46361..d80c62e 100644 --- a/src/main/java/everymeal/server/user/entity/User.java +++ b/src/main/java/everymeal/server/user/entity/User.java @@ -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; @@ -53,6 +54,9 @@ public class User extends BaseEntity { @OneToMany(mappedBy = "user", cascade = CascadeType.ALL) Set reviewMarks = new HashSet<>(); + @OneToOne(mappedBy = "user") + private Withdrawal withdrawal; + @Builder public User(String nickname, String email, String profileImgUrl, University university) { this.nickname = nickname; @@ -70,4 +74,8 @@ public void updateProfile(String nickname, String profileImgUrl) { this.nickname = nickname; this.profileImgUrl = profileImgUrl; } + /** 회원 탈퇴 */ + public void setIsDeleted() { + this.isDeleted = Boolean.TRUE; + } } diff --git a/src/main/java/everymeal/server/user/entity/Withdrawal.java b/src/main/java/everymeal/server/user/entity/Withdrawal.java new file mode 100644 index 0000000..58525e5 --- /dev/null +++ b/src/main/java/everymeal/server/user/entity/Withdrawal.java @@ -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, length = 100) + 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; + } +} diff --git a/src/main/java/everymeal/server/user/entity/WithdrawalReason.java b/src/main/java/everymeal/server/user/entity/WithdrawalReason.java new file mode 100644 index 0000000..f4f08bb --- /dev/null +++ b/src/main/java/everymeal/server/user/entity/WithdrawalReason.java @@ -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("기타"); + + public final String MESSAGE; +} diff --git a/src/main/java/everymeal/server/user/repository/WithdrawalRepository.java b/src/main/java/everymeal/server/user/repository/WithdrawalRepository.java new file mode 100644 index 0000000..c92f9b5 --- /dev/null +++ b/src/main/java/everymeal/server/user/repository/WithdrawalRepository.java @@ -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 {} diff --git a/src/main/java/everymeal/server/user/service/UserService.java b/src/main/java/everymeal/server/user/service/UserService.java index 3e3ab43..f9e2738 100644 --- a/src/main/java/everymeal/server/user/service/UserService.java +++ b/src/main/java/everymeal/server/user/service/UserService.java @@ -6,6 +6,7 @@ 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; @@ -26,4 +27,6 @@ public interface UserService { Boolean updateUserProfile( AuthenticatedUser authenticatedUser, UserProfileUpdateReq userProfileUpdateReq); + + Boolean withdrawal(AuthenticatedUser authenticatedUser, WithdrawalReq request); } diff --git a/src/main/java/everymeal/server/user/service/UserServiceImpl.java b/src/main/java/everymeal/server/user/service/UserServiceImpl.java index c23617d..b099604 100644 --- a/src/main/java/everymeal/server/user/service/UserServiceImpl.java +++ b/src/main/java/everymeal/server/user/service/UserServiceImpl.java @@ -13,12 +13,16 @@ 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; @@ -39,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 @@ -175,4 +180,30 @@ public Boolean updateUserProfile( 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()) // 글자수 제한이 있는지 처리 X + .user(user) + .build(); + withdrawalRepository.save(withdrawal); // 탈퇴 관련 정보 저장 + user.setIsDeleted(); // 논리 삭제 + return true; + } } diff --git a/src/test/java/everymeal/server/user/controller/UserControllerTest.java b/src/test/java/everymeal/server/user/controller/UserControllerTest.java index c0c9926..def647a 100644 --- a/src/test/java/everymeal/server/user/controller/UserControllerTest.java +++ b/src/test/java/everymeal/server/user/controller/UserControllerTest.java @@ -16,7 +16,9 @@ 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; @@ -155,4 +157,23 @@ void updateUserProfile() throws Exception { .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")); + } } diff --git a/src/test/java/everymeal/server/user/service/UserServiceImplTest.java b/src/test/java/everymeal/server/user/service/UserServiceImplTest.java index 3146ff3..64ec4e2 100644 --- a/src/test/java/everymeal/server/user/service/UserServiceImplTest.java +++ b/src/test/java/everymeal/server/user/service/UserServiceImplTest.java @@ -16,10 +16,13 @@ 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.entity.User; +import everymeal.server.user.entity.WithdrawalReason; import everymeal.server.user.repository.UserRepository; +import everymeal.server.user.repository.WithdrawalRepository; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -34,9 +37,11 @@ class UserServiceImplTest extends IntegrationTestSupport { @MockBean private MailUtil mailUtil; @Autowired private UniversityRepository universityRepository; @Autowired private S3Util s3Util; + @Autowired private WithdrawalRepository withdrawalRepository; @AfterEach void tearDown() { + withdrawalRepository.deleteAllInBatch(); userRepository.deleteAllInBatch(); } @@ -314,6 +319,34 @@ void updateUserProfile_duplicated() { ExceptionList.NICKNAME_ALREADY_EXIST.getCODE()); } + @DisplayName("회원 탈퇴 - 정해진 사유를 선택한 경우") + @Test + void withdrawal() { + // given + String token = jwtUtil.generateEmailToken("test@gmail.com", "12345"); + + University university = + universityRepository.save( + University.builder().name("명지대학교").campusName("인문캠퍼스").build()); + UserEmailSingReq request = + new UserEmailSingReq("연유크림", token, "12345", university.getIdx(), "imageKey"); + + UserLoginRes userLoginRes = userService.signUp(request); + + AuthenticatedUser user = + jwtUtil.getAuthenticateUserFromAccessToken(userLoginRes.accessToken()); + + WithdrawalReq withdrawalReq = + new WithdrawalReq(WithdrawalReason.ERRORS_OCCUR_FREQUENTLY, ""); + + // when then + var result = userService.withdrawal(user, withdrawalReq); + var withdrawalUser = userRepository.findByNickname("연유크림").get(); + + assertEquals(result, Boolean.TRUE); + assertEquals(withdrawalUser.getIsDeleted(), Boolean.TRUE); + } + private User createUser(String email, String nickname) { return User.builder().email(email).nickname(nickname).build(); }