Skip to content

Commit 87764f5

Browse files
authored
[Feat] 회원 탈퇴 API 개발 (#64)
* feat: 회원 탈퇴 서비스 로직 구현 및 관련 삭제 메서드 추가 (#29) * feat: 회원 탈퇴 컨트롤러 구현 (#29) * feat: 카카오 연동 해제 로직 추가 (#29) - 카카오 연동 해제 로직 추가 (OAuth 전략 패턴 내 구현) - RestTemplate Bean 전역 설정 (config 패키지 이동) - OAuth 연동 ID 기반 unlink 요청 및 응답 로깅 처리 - 관련 커스텀 예외 코드 추가 - 관련 리포지토리 코드 추가 * feat: 회원 탈퇴 시 그룹 소유자 승계 로직 추가 (#29) * fix: 회원 탈퇴 시 처리 순서 조정 및 유저 상태 변경 시점 수정 (#29) - 그룹 소유자 승계 후 그룹 멤버 삭제로 순서 변경 (유효한 멤버 기반 승계 보장) - 카카오 연동 해제, 토큰/OAuth 삭제 이후에 회원 상태 변경 수행 - user.withdraw()를 가장 마지막에 호출하여 도메인 정합성 유지 * fix: 회원 탈퇴 시 그룹 멤버 삭제 방식을 soft delete에서 hard delete로 수정 (#29)
1 parent 93a467e commit 87764f5

File tree

19 files changed

+190
-7
lines changed

19 files changed

+190
-7
lines changed

src/main/java/com/moa/moa_server/domain/ai/config/AiConfig.java renamed to src/main/java/com/moa/moa_server/config/RestTemplateConfig.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
1-
package com.moa.moa_server.domain.ai.config;
1+
package com.moa.moa_server.config;
22

33
import org.springframework.context.annotation.Bean;
44
import org.springframework.context.annotation.Configuration;
55
import org.springframework.web.client.RestTemplate;
66

77
@Configuration
8-
public class AiConfig {
8+
public class RestTemplateConfig {
99

1010
@Bean
1111
public RestTemplate restTemplate() {
1212
return new RestTemplate();
1313
}
14-
}
14+
}

src/main/java/com/moa/moa_server/domain/auth/handler/AuthErrorCode.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ public enum AuthErrorCode implements BaseErrorCode {
1212
USER_WITHDRAWN(HttpStatus.UNAUTHORIZED),
1313
INVALID_PROVIDER(HttpStatus.BAD_REQUEST),
1414
KAKAO_TOKEN_FAILED(HttpStatus.UNAUTHORIZED),
15-
KAKAO_USERINFO_FAILED(HttpStatus.UNAUTHORIZED);
15+
KAKAO_USERINFO_FAILED(HttpStatus.UNAUTHORIZED),
16+
OAUTH_NOT_FOUND(HttpStatus.NOT_FOUND),;
1617

1718
private final HttpStatus status;
1819

src/main/java/com/moa/moa_server/domain/auth/repository/OAuthRepository.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.moa.moa_server.domain.auth.repository;
22

33
import com.moa.moa_server.domain.auth.entity.OAuth;
4+
import com.moa.moa_server.domain.user.entity.User;
45
import org.springframework.data.jpa.repository.JpaRepository;
56
import org.springframework.lang.NonNull;
67

@@ -9,4 +10,6 @@
910
public interface OAuthRepository extends JpaRepository<OAuth, Long> {
1011

1112
Optional<OAuth> findById(@NonNull Long id);
13+
void deleteByUserId(Long userId);
14+
Optional<OAuth> findByUser(User user);
1215
}

src/main/java/com/moa/moa_server/domain/auth/service/AuthService.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,11 @@ public TokenRefreshResponse refreshAccessToken(String refreshToken) {
5555
public boolean logout(Long userId) {
5656
return refreshTokenService.deleteRefreshTokenByUserId(userId); // true면 SUCCESS, false면 ALREADY_LOGGED_OUT
5757
}
58+
59+
public void unlinkKakaoAccount(Long kakaoUserId) {
60+
OAuthLoginStrategy strategy = strategies.get("kakao");
61+
if (strategy != null) {
62+
strategy.unlink(kakaoUserId);
63+
}
64+
}
5865
}

src/main/java/com/moa/moa_server/domain/auth/service/strategy/KakaoOAuthLoginStrategy.java

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,14 @@ public class KakaoOAuthLoginStrategy implements OAuthLoginStrategy {
3737
private final JwtTokenService jwtTokenService;
3838
private final RefreshTokenService refreshTokenService;
3939

40+
private final RestTemplate restTemplate;
41+
4042
@Value("${kakao.client-id}")
4143
private String kakaoClientId;
4244

45+
@Value("${kakao.admin-key}")
46+
private String kakaoAdminKey;
47+
4348
@Value("${kakao.redirect-uri}")
4449
private String kakaoRedirectUri;
4550

@@ -49,6 +54,9 @@ public class KakaoOAuthLoginStrategy implements OAuthLoginStrategy {
4954
@Value("${kakao.user-info-uri}")
5055
private String kakaoUserInfoUri;
5156

57+
@Value("${kakao.unlink-uri}")
58+
private String kakaoUnlinkUri;
59+
5260
@Transactional
5361
@Override
5462
public LoginResult login(String code) {
@@ -97,7 +105,6 @@ private String getAccessToken(String code) {
97105
body.add("code", code);
98106

99107
HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(body, headers);
100-
RestTemplate restTemplate = new RestTemplate(); // 나중에 WebClient로 변경
101108

102109
try {
103110
ResponseEntity<Map> response = restTemplate.postForEntity(kakaoTokenUri, request, Map.class);
@@ -114,7 +121,6 @@ private Long getUserInfo(String accessToken) {
114121
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
115122

116123
HttpEntity<Void> request = new HttpEntity<>(headers);
117-
RestTemplate restTemplate = new RestTemplate();
118124

119125
try {
120126
ResponseEntity<Map> response = restTemplate.postForEntity(kakaoUserInfoUri, request, Map.class);
@@ -123,4 +129,27 @@ private Long getUserInfo(String accessToken) {
123129
throw new AuthException(AuthErrorCode.KAKAO_USERINFO_FAILED);
124130
}
125131
}
132+
133+
@Override
134+
public void unlink(Long kakaoUserId) {
135+
136+
HttpHeaders headers = new HttpHeaders();
137+
headers.set("Authorization", "KakaoAK " + kakaoAdminKey);
138+
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
139+
140+
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
141+
params.add("target_id_type", "user_id");
142+
params.add("target_id", String.valueOf(kakaoUserId));
143+
144+
HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(params, headers);
145+
146+
try {
147+
ResponseEntity<Map> response = restTemplate.postForEntity(kakaoUnlinkUri, request, Map.class);
148+
Long returnedId = ((Number) response.getBody().get("id")).longValue();
149+
log.info("[KakaoOAuthLoginStrategy#unlink] 카카오 unlink 성공: 요청 userId={}, 응답 userId={}", kakaoUserId, returnedId);
150+
} catch (Exception e) {
151+
log.warn("[KakaoOAuthLoginStrategy#unlink] 카카오 unlink 실패: 요청 userId={}, error={}", kakaoUserId, e.getMessage());
152+
// 필요 시 큐 적재 등 처리
153+
}
154+
}
126155
}

src/main/java/com/moa/moa_server/domain/auth/service/strategy/OAuthLoginStrategy.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@
44

55
public interface OAuthLoginStrategy {
66
LoginResult login(String code);
7+
void unlink(Long oauthId);
78
}

src/main/java/com/moa/moa_server/domain/group/entity/Group.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
import org.hibernate.annotations.SQLDelete;
88
import org.hibernate.annotations.Where;
99

10+
import java.time.LocalDateTime;
11+
1012
@Entity
1113
@Getter
1214
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@@ -57,4 +59,12 @@ public static Group create(User user, String name, String description, String im
5759
public boolean isPublicGroup() {
5860
return this.id != null && this.id.equals(PUBLIC_GROUP_ID);
5961
}
62+
63+
public void changeOwner(User newOwner) {
64+
this.user = newOwner;
65+
}
66+
67+
public void softDelete() {
68+
this.deletedAt = LocalDateTime.now();
69+
}
6070
}

src/main/java/com/moa/moa_server/domain/group/entity/GroupMember.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,4 +74,12 @@ public static GroupMember createAsOwner(User user, Group group) {
7474
.joinedAt(LocalDateTime.now())
7575
.build();
7676
}
77+
78+
public boolean isActiveUser() {
79+
return user != null && user.getUserStatus() == User.UserStatus.ACTIVE;
80+
}
81+
82+
public void changeToOwner() {
83+
this.role = Role.OWNER;
84+
}
7785
}

src/main/java/com/moa/moa_server/domain/group/repository/GroupMemberRepository.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import com.moa.moa_server.domain.group.entity.GroupMember;
55
import com.moa.moa_server.domain.user.entity.User;
66
import org.springframework.data.jpa.repository.JpaRepository;
7+
import org.springframework.data.jpa.repository.Modifying;
78
import org.springframework.data.jpa.repository.Query;
89
import org.springframework.data.repository.query.Param;
910

@@ -21,4 +22,12 @@ public interface GroupMemberRepository extends JpaRepository<GroupMember, Long>,
2122

2223
@Query("SELECT gm.group FROM GroupMember gm WHERE gm.user = :user AND gm.deletedAt IS NULL")
2324
List<Group> findAllActiveGroupsByUser(@Param("user") User user);
25+
26+
void deleteAllByUserId(Long userId);
27+
28+
@Modifying
29+
@Query("DELETE FROM GroupMember gm WHERE gm.user.id = :userId")
30+
void hardDeleteAllByUserId(@Param("userId") Long userId);
31+
32+
List<GroupMember> findAllByGroupOrderByJoinedAtAsc(Group group);
2433
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
package com.moa.moa_server.domain.group.repository;
22

33
import com.moa.moa_server.domain.group.entity.Group;
4+
import com.moa.moa_server.domain.user.entity.User;
45
import org.springframework.data.jpa.repository.JpaRepository;
56

7+
import java.util.List;
68
import java.util.Optional;
79

810
public interface GroupRepository extends JpaRepository<Group, Long> {
911
Optional<Group> findByInviteCode(String inviteCode);
1012
boolean existsByInviteCode(String inviteCode);
1113
boolean existsByName(String groupName);
14+
List<Group> findAllByUser(User user);
1215
}

0 commit comments

Comments
 (0)