Skip to content

Commit 666c7dc

Browse files
authored
Merge pull request #67 from everymeals/feature/user-withdrawal
[Feature/user withdrawal] 마이페이지-회원탈퇴 API
2 parents 6d09b76 + 08ed18f commit 666c7dc

File tree

10 files changed

+201
-0
lines changed

10 files changed

+201
-0
lines changed

src/main/java/everymeal/server/user/controller/UserController.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import everymeal.server.user.controller.dto.request.UserEmailLoginReq;
1010
import everymeal.server.user.controller.dto.request.UserEmailSingReq;
1111
import everymeal.server.user.controller.dto.request.UserProfileUpdateReq;
12+
import everymeal.server.user.controller.dto.request.WithdrawalReq;
1213
import everymeal.server.user.controller.dto.response.UserEmailAuthRes;
1314
import everymeal.server.user.controller.dto.response.UserLoginRes;
1415
import everymeal.server.user.controller.dto.response.UserProfileRes;
@@ -164,6 +165,22 @@ public ApplicationResponse<Boolean> updateUserProfile(
164165
userService.updateUserProfile(authenticatedUser, userProfileUpdateReq));
165166
}
166167

168+
@Auth(require = true)
169+
@PostMapping("/withdrawal")
170+
@SecurityRequirement(name = "jwt-user-auth")
171+
@Operation(summary = "회원탈퇴", description = "서비스 회원 탈퇴를 합니다.")
172+
@ApiResponse(
173+
responseCode = "404",
174+
description = """
175+
(U0001)등록된 유저가 아닙니다.<br>
176+
""",
177+
content = @Content(schema = @Schema()))
178+
public ApplicationResponse<Boolean> withdrawal(
179+
@Parameter(hidden = true) @AuthUser AuthenticatedUser authenticatedUser,
180+
@RequestBody WithdrawalReq withdrawalReq) {
181+
return ApplicationResponse.ok(userService.withdrawal(authenticatedUser, withdrawalReq));
182+
}
183+
167184
private ResponseEntity<ApplicationResponse<UserLoginRes>> setRefreshToken(
168185
UserLoginRes response) {
169186
ResponseCookie cookie =
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package everymeal.server.user.controller.dto.request;
2+
3+
4+
import everymeal.server.user.entity.WithdrawalReason;
5+
import io.swagger.v3.oas.annotations.media.Schema;
6+
import jakarta.validation.constraints.NotBlank;
7+
8+
public record WithdrawalReq(
9+
@NotBlank
10+
@Schema(
11+
description = "탈퇴 사유를 입력해주세요.",
12+
example = "NOT_USE_USUALLY",
13+
allowableValues = {
14+
"NOT_USE_USUALLY",
15+
"INCONVENIENT_IN_TERMS_OF_USABILITY",
16+
"ERRORS_OCCUR_FREQUENTLY",
17+
"MY_SCHOOL_HAS_CHANGED",
18+
"ETC"
19+
})
20+
WithdrawalReason withdrawalReason,
21+
@Schema(description = "사유가 '기타'일 경우, 추가 이유 입력해주세요.", example = "다른 서비스를 사용하게 되었다.")
22+
String etcReason) {}

src/main/java/everymeal/server/user/entity/User.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import jakarta.persistence.Index;
1515
import jakarta.persistence.ManyToOne;
1616
import jakarta.persistence.OneToMany;
17+
import jakarta.persistence.OneToOne;
1718
import jakarta.persistence.Table;
1819
import java.util.HashSet;
1920
import java.util.Set;
@@ -53,6 +54,9 @@ public class User extends BaseEntity {
5354
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
5455
Set<ReviewMark> reviewMarks = new HashSet<>();
5556

57+
@OneToOne(mappedBy = "user")
58+
private Withdrawal withdrawal;
59+
5660
@Builder
5761
public User(String nickname, String email, String profileImgUrl, University university) {
5862
this.nickname = nickname;
@@ -70,4 +74,8 @@ public void updateProfile(String nickname, String profileImgUrl) {
7074
this.nickname = nickname;
7175
this.profileImgUrl = profileImgUrl;
7276
}
77+
/** 회원 탈퇴 */
78+
public void setIsDeleted() {
79+
this.isDeleted = Boolean.TRUE;
80+
}
7381
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package everymeal.server.user.entity;
2+
3+
4+
import everymeal.server.global.entity.BaseEntity;
5+
import jakarta.persistence.Column;
6+
import jakarta.persistence.Entity;
7+
import jakarta.persistence.EnumType;
8+
import jakarta.persistence.Enumerated;
9+
import jakarta.persistence.Id;
10+
import jakarta.persistence.JoinColumn;
11+
import jakarta.persistence.MapsId;
12+
import jakarta.persistence.OneToOne;
13+
import lombok.AccessLevel;
14+
import lombok.Builder;
15+
import lombok.Getter;
16+
import lombok.NoArgsConstructor;
17+
18+
@Getter
19+
@Entity
20+
@NoArgsConstructor(access = AccessLevel.PROTECTED)
21+
public class Withdrawal extends BaseEntity {
22+
@Id private Long userIdx;
23+
24+
@Column(nullable = false)
25+
@Enumerated(EnumType.STRING)
26+
private WithdrawalReason withdrawalReason;
27+
28+
@Column(nullable = true, columnDefinition = "TEXT")
29+
private String etcReason;
30+
31+
@MapsId
32+
@OneToOne
33+
@JoinColumn(name = "user_idx", referencedColumnName = "idx")
34+
private User user;
35+
36+
@Builder
37+
public Withdrawal(WithdrawalReason withdrawalReason, String etcReason, User user) {
38+
this.withdrawalReason = withdrawalReason;
39+
this.etcReason = etcReason;
40+
this.user = user;
41+
}
42+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package everymeal.server.user.entity;
2+
3+
4+
import lombok.Getter;
5+
import lombok.RequiredArgsConstructor;
6+
7+
@Getter
8+
@RequiredArgsConstructor
9+
public enum WithdrawalReason {
10+
NOT_USE_USUALLY("앱을 잘 쓰지 않아요"),
11+
INCONVENIENT_IN_TERMS_OF_USABILITY("사용성이 불편해요"),
12+
ERRORS_OCCUR_FREQUENTLY("오류가 자주 발생해요"),
13+
MY_SCHOOL_HAS_CHANGED("학교가 바뀌었어요"),
14+
ETC("기타");
15+
16+
public final String MESSAGE;
17+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package everymeal.server.user.repository;
2+
3+
4+
import everymeal.server.user.entity.Withdrawal;
5+
import org.springframework.data.jpa.repository.JpaRepository;
6+
7+
public interface WithdrawalRepository extends JpaRepository<Withdrawal, Long> {}

src/main/java/everymeal/server/user/service/UserService.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import everymeal.server.user.controller.dto.request.UserEmailLoginReq;
77
import everymeal.server.user.controller.dto.request.UserEmailSingReq;
88
import everymeal.server.user.controller.dto.request.UserProfileUpdateReq;
9+
import everymeal.server.user.controller.dto.request.WithdrawalReq;
910
import everymeal.server.user.controller.dto.response.UserEmailAuthRes;
1011
import everymeal.server.user.controller.dto.response.UserLoginRes;
1112
import everymeal.server.user.controller.dto.response.UserProfileRes;
@@ -26,4 +27,6 @@ public interface UserService {
2627

2728
Boolean updateUserProfile(
2829
AuthenticatedUser authenticatedUser, UserProfileUpdateReq userProfileUpdateReq);
30+
31+
Boolean withdrawal(AuthenticatedUser authenticatedUser, WithdrawalReq request);
2932
}

src/main/java/everymeal/server/user/service/UserServiceImpl.java

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,16 @@
1313
import everymeal.server.user.controller.dto.request.UserEmailLoginReq;
1414
import everymeal.server.user.controller.dto.request.UserEmailSingReq;
1515
import everymeal.server.user.controller.dto.request.UserProfileUpdateReq;
16+
import everymeal.server.user.controller.dto.request.WithdrawalReq;
1617
import everymeal.server.user.controller.dto.response.UserEmailAuthRes;
1718
import everymeal.server.user.controller.dto.response.UserLoginRes;
1819
import everymeal.server.user.controller.dto.response.UserProfileRes;
1920
import everymeal.server.user.entity.User;
21+
import everymeal.server.user.entity.Withdrawal;
22+
import everymeal.server.user.entity.WithdrawalReason;
2023
import everymeal.server.user.repository.UserMapper;
2124
import everymeal.server.user.repository.UserRepository;
25+
import everymeal.server.user.repository.WithdrawalRepository;
2226
import java.security.NoSuchAlgorithmException;
2327
import java.security.SecureRandom;
2428
import java.util.Map;
@@ -39,6 +43,7 @@ public class UserServiceImpl implements UserService {
3943
private final MailUtil mailUtil;
4044
private final S3Util s3Util;
4145
private final UserMapper userMapper;
46+
private final WithdrawalRepository withdrawalRepository;
4247

4348
@Override
4449
@Transactional
@@ -175,4 +180,30 @@ public Boolean updateUserProfile(
175180
user.updateProfile(request.nickName(), request.profileImageKey());
176181
return true;
177182
}
183+
184+
@Override
185+
@Transactional
186+
public Boolean withdrawal(AuthenticatedUser authenticatedUser, WithdrawalReq request) {
187+
User user =
188+
userRepository
189+
.findById(authenticatedUser.getIdx())
190+
.orElseThrow(() -> new ApplicationException(ExceptionList.USER_NOT_FOUND));
191+
Withdrawal withdrawal;
192+
if (request.withdrawalReason() != WithdrawalReason.ETC) { // 기타를 제외한 경우
193+
withdrawal =
194+
Withdrawal.builder()
195+
.withdrawalReason(request.withdrawalReason())
196+
.user(user)
197+
.build();
198+
} else // 기타를 선택한 경우
199+
withdrawal =
200+
Withdrawal.builder()
201+
.withdrawalReason(request.withdrawalReason())
202+
.etcReason(request.etcReason())
203+
.user(user)
204+
.build();
205+
withdrawalRepository.save(withdrawal); // 탈퇴 관련 정보 저장
206+
user.setIsDeleted(); // 논리 삭제
207+
return true;
208+
}
178209
}

src/test/java/everymeal/server/user/controller/UserControllerTest.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616
import everymeal.server.user.controller.dto.request.UserEmailLoginReq;
1717
import everymeal.server.user.controller.dto.request.UserEmailSingReq;
1818
import everymeal.server.user.controller.dto.request.UserProfileUpdateReq;
19+
import everymeal.server.user.controller.dto.request.WithdrawalReq;
1920
import everymeal.server.user.controller.dto.response.UserLoginRes;
21+
import everymeal.server.user.entity.WithdrawalReason;
2022
import org.junit.jupiter.api.DisplayName;
2123
import org.junit.jupiter.api.Test;
2224
import org.mockito.Mock;
@@ -155,4 +157,23 @@ void updateUserProfile() throws Exception {
155157
.andExpect(status().isOk())
156158
.andExpect(jsonPath("$.message").value("OK"));
157159
}
160+
161+
@DisplayName("회원 탈퇴")
162+
@Test
163+
void withdrawal() throws Exception {
164+
// given
165+
WithdrawalReq request = new WithdrawalReq(WithdrawalReason.ERRORS_OCCUR_FREQUENTLY, "");
166+
167+
given(userJwtResolver.resolveArgument(any(), any(), any(), any()))
168+
.willReturn(AuthenticatedUser.builder().idx(1L).build());
169+
170+
// when-then
171+
mockMvc.perform(
172+
post("/api/v1/users/withdrawal")
173+
.content(objectMapper.writeValueAsString(request))
174+
.contentType(MediaType.APPLICATION_JSON))
175+
.andDo(MockMvcResultHandlers.print())
176+
.andExpect(status().isOk())
177+
.andExpect(jsonPath("$.message").value("OK"));
178+
}
158179
}

src/test/java/everymeal/server/user/service/UserServiceImplTest.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,13 @@
1616
import everymeal.server.user.controller.dto.request.UserEmailLoginReq;
1717
import everymeal.server.user.controller.dto.request.UserEmailSingReq;
1818
import everymeal.server.user.controller.dto.request.UserProfileUpdateReq;
19+
import everymeal.server.user.controller.dto.request.WithdrawalReq;
1920
import everymeal.server.user.controller.dto.response.UserEmailAuthRes;
2021
import everymeal.server.user.controller.dto.response.UserLoginRes;
2122
import everymeal.server.user.entity.User;
23+
import everymeal.server.user.entity.WithdrawalReason;
2224
import everymeal.server.user.repository.UserRepository;
25+
import everymeal.server.user.repository.WithdrawalRepository;
2326
import org.junit.jupiter.api.AfterEach;
2427
import org.junit.jupiter.api.DisplayName;
2528
import org.junit.jupiter.api.Test;
@@ -34,9 +37,11 @@ class UserServiceImplTest extends IntegrationTestSupport {
3437
@MockBean private MailUtil mailUtil;
3538
@Autowired private UniversityRepository universityRepository;
3639
@Autowired private S3Util s3Util;
40+
@Autowired private WithdrawalRepository withdrawalRepository;
3741

3842
@AfterEach
3943
void tearDown() {
44+
withdrawalRepository.deleteAllInBatch();
4045
userRepository.deleteAllInBatch();
4146
}
4247

@@ -314,6 +319,34 @@ void updateUserProfile_duplicated() {
314319
ExceptionList.NICKNAME_ALREADY_EXIST.getCODE());
315320
}
316321

322+
@DisplayName("회원 탈퇴 - 정해진 사유를 선택한 경우")
323+
@Test
324+
void withdrawal() {
325+
// given
326+
String token = jwtUtil.generateEmailToken("test@gmail.com", "12345");
327+
328+
University university =
329+
universityRepository.save(
330+
University.builder().name("명지대학교").campusName("인문캠퍼스").build());
331+
UserEmailSingReq request =
332+
new UserEmailSingReq("연유크림", token, "12345", university.getIdx(), "imageKey");
333+
334+
UserLoginRes userLoginRes = userService.signUp(request);
335+
336+
AuthenticatedUser user =
337+
jwtUtil.getAuthenticateUserFromAccessToken(userLoginRes.accessToken());
338+
339+
WithdrawalReq withdrawalReq =
340+
new WithdrawalReq(WithdrawalReason.ERRORS_OCCUR_FREQUENTLY, "");
341+
342+
// when then
343+
var result = userService.withdrawal(user, withdrawalReq);
344+
var withdrawalUser = userRepository.findByNickname("연유크림").get();
345+
346+
assertEquals(result, Boolean.TRUE);
347+
assertEquals(withdrawalUser.getIsDeleted(), Boolean.TRUE);
348+
}
349+
317350
private User createUser(String email, String nickname) {
318351
return User.builder().email(email).nickname(nickname).build();
319352
}

0 commit comments

Comments
 (0)