Skip to content

Commit 3543619

Browse files
authored
[feat] 회원 탈퇴(soft delete) 기능 구현 #146 (#157)
* feat: 사용자 상태에 DELETED 추가 * feat: 사용자 탈퇴 관련 로직 추가 - `status`와 `deletedAt` 필드 추가 - `UserStatus` 열거형을 이용해 사용자 상태 관리 - `markDeleted` 메서드를 통해 회원 탈퇴 처리 - 탈퇴 시 이메일, OAuth ID 등 개인정보 삭제 * feat: 사용자 전체 리프레시 토큰 폐기 기능 추가 * feat: 회원 탈퇴 기능 구현 (비활성화) - `UserService`에 `deactivateAccount` 메서드 추가 - 사용자의 상태를 `DELETED`로 변경하고 개인정보 익명화 처리 - 모든 세션(리프레시 토큰)을 폐기하여 로그아웃 처리 - 탈퇴 시 예외 처리 로직 추가 * feat: 회원 계정 비활성화 API 구현 - DELETE /me/account 엔드포인트를 통해 계정 탈퇴 기능 제공 - UserService의 deactivateAccount 메서드 호출로 사용자 상태 변경 및 개인정보 익명화 처리 - 탈퇴 후 현재 세션 및 리프레시 토큰을 정리하는 로그아웃 로직 추가
1 parent 95d91eb commit 3543619

File tree

5 files changed

+88
-2
lines changed

5 files changed

+88
-2
lines changed
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package com.back.domain.user.controller;
2+
3+
import com.back.domain.user.service.UserService;
4+
import com.back.domain.user.service.UserAuthService;
5+
import com.back.global.rsData.RsData;
6+
import io.swagger.v3.oas.annotations.Operation;
7+
import jakarta.servlet.http.HttpServletRequest;
8+
import jakarta.servlet.http.HttpServletResponse;
9+
import lombok.RequiredArgsConstructor;
10+
import org.springframework.security.core.annotation.AuthenticationPrincipal;
11+
import org.springframework.web.bind.annotation.DeleteMapping;
12+
import org.springframework.web.bind.annotation.RequestMapping;
13+
import org.springframework.web.bind.annotation.RestController;
14+
15+
@RestController
16+
@RequestMapping("/me/account")
17+
@RequiredArgsConstructor
18+
public class UserAccountController {
19+
20+
private final UserService userService;
21+
private final UserAuthService userAuthService;
22+
23+
@DeleteMapping
24+
@Operation(summary = "계정 비활성화(Soft Delete)", description = "DELETE /me/account: 사용자 상태를 DELETED로 전환하고 세션/토큰을 정리합니다.")
25+
public RsData<Void> deactivate(
26+
@AuthenticationPrincipal(expression = "id") Long userId,
27+
HttpServletRequest request,
28+
HttpServletResponse response
29+
) {
30+
userService.deactivateAccount(userId);
31+
32+
// 현재 세션 쿠키 및 리프레시토큰 제거
33+
userAuthService.logout(request, response);
34+
35+
return RsData.of(200, "계정 비활성화(탈퇴)가 완료되었습니다.");
36+
}
37+
}

src/main/java/com/back/domain/user/entity/User.java

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.back.domain.post.post.entity.PostLike;
44
import jakarta.persistence.*;
5+
import com.back.domain.user.enums.UserStatus;
56
import lombok.*;
67
import org.springframework.data.annotation.CreatedDate;
78
import org.springframework.data.annotation.LastModifiedDate;
@@ -56,6 +57,13 @@ public class User {
5657
@Column(nullable = false)
5758
private boolean isFirstLogin = true;
5859

60+
@Builder.Default
61+
@Enumerated(EnumType.STRING)
62+
@Column(nullable = false, length = 20)
63+
private UserStatus status = UserStatus.ACTIVE;
64+
65+
private LocalDateTime deletedAt;
66+
5967
// 양방향 매핑을 위한 필드
6068
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
6169
private List<PostLike> postLikes = new ArrayList<>();
@@ -82,4 +90,17 @@ public Collection<? extends GrantedAuthority> getAuthorities() {
8290
.map(auth -> new SimpleGrantedAuthority("ROLE_" + auth))
8391
.toList();
8492
}
85-
}
93+
94+
public boolean isDeleted() {
95+
return this.status == UserStatus.DELETED;
96+
}
97+
98+
public void markDeleted(String anonymizedNickname) {
99+
this.status = UserStatus.DELETED;
100+
this.deletedAt = LocalDateTime.now();
101+
this.nickname = anonymizedNickname;
102+
// 민감정보 최소화: 재가입 허용을 위해 이메일/OAuth 식별자 제거
103+
this.email = null;
104+
this.oauthId = null;
105+
}
106+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package com.back.domain.user.enums;
2+
3+
public enum UserStatus {
4+
ACTIVE,
5+
DELETED
6+
}

src/main/java/com/back/domain/user/service/UserService.java

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
package com.back.domain.user.service;
22

33
import com.back.domain.user.entity.User;
4+
import com.back.domain.user.enums.UserStatus;
45
import com.back.domain.user.repository.UserRepository;
6+
import com.back.global.exception.ServiceException;
7+
import com.back.global.jwt.refreshToken.service.RefreshTokenService;
58
import lombok.RequiredArgsConstructor;
69
import org.springframework.stereotype.Service;
710
import org.springframework.transaction.annotation.Transactional;
@@ -11,14 +14,27 @@
1114
public class UserService {
1215

1316
private final UserRepository userRepository;
17+
private final RefreshTokenService refreshTokenService;
1418

1519
@Transactional(readOnly = true)
1620
public User findById(Long id) {
1721
return userRepository.findById(id)
1822
.orElseThrow(() -> new IllegalArgumentException("User not found. id=" + id));
1923
}
2024

25+
@Transactional
26+
public void deactivateAccount(Long id) {
27+
User user = userRepository.findById(id)
28+
.orElseThrow(() -> new ServiceException(404, "사용자를 찾을 수 없습니다."));
2129

30+
if (user.getStatus() == UserStatus.DELETED) {
31+
throw new ServiceException(409, "이미 탈퇴한 사용자입니다.");
32+
}
2233

34+
user.markDeleted("탈퇴한 사용자");
35+
userRepository.save(user);
2336

24-
}
37+
// 모든 세션(리프레시 토큰) 폐기
38+
refreshTokenService.revokeAllForUser(id);
39+
}
40+
}

src/main/java/com/back/global/jwt/refreshToken/service/RefreshTokenService.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,12 @@ public void revokeToken(String token) {
7474
refreshTokenRepository.deleteByToken(token);
7575
}
7676

77+
// 사용자 전체 세션(리프레시 토큰) 폐기
78+
@Transactional
79+
public void revokeAllForUser(Long userId) {
80+
refreshTokenRepository.deleteByUserId(userId);
81+
}
82+
7783
//문자열 난수 조합
7884
private String generateSecureToken() {
7985
byte[] randomBytes = new byte[32];

0 commit comments

Comments
 (0)