Skip to content

Commit 5fb21e8

Browse files
committed
fix[member]: 회원 탈퇴 시 연관 데이터 삭제 로직 추가
1 parent a863c2a commit 5fb21e8

File tree

8 files changed

+152
-13
lines changed

8 files changed

+152
-13
lines changed

backend/src/main/java/com/ai/lawyer/domain/chatbot/entity/History.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public class History {
2525
private Long historyId;
2626

2727
@ManyToOne
28-
@JoinColumn(name = "member_id")
28+
@JoinColumn(name = "member_id", foreignKey = @ForeignKey(name = "FK_HISTORY_MEMBER"))
2929
private Member memberId;
3030

3131
@OneToMany(mappedBy = "historyId", cascade = CascadeType.ALL, orphanRemoval = true)

backend/src/main/java/com/ai/lawyer/domain/chatbot/repository/HistoryRepository.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
import com.ai.lawyer.domain.chatbot.entity.History;
44
import com.ai.lawyer.domain.member.entity.Member;
55
import org.springframework.data.jpa.repository.JpaRepository;
6+
import org.springframework.data.jpa.repository.Modifying;
7+
import org.springframework.data.jpa.repository.Query;
8+
import org.springframework.data.repository.query.Param;
69
import org.springframework.stereotype.Repository;
710

811
import java.util.List;
@@ -14,4 +17,12 @@ public interface HistoryRepository extends JpaRepository<History, Long> {
1417

1518
History findByHistoryIdAndMemberId(Long roomId, Member memberId);
1619

20+
/**
21+
* member_id로 채팅 히스토리 삭제 (회원 탈퇴 시 사용)
22+
* Member와 OAuth2Member 모두 같은 member_id 공간을 사용하므로 Long 타입으로 삭제
23+
*/
24+
@Modifying
25+
@Query("DELETE FROM History h WHERE h.memberId.memberId = :memberId")
26+
void deleteByMemberIdValue(@Param("memberId") Long memberId);
27+
1728
}

backend/src/main/java/com/ai/lawyer/domain/member/entity/Member.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
package com.ai.lawyer.domain.member.entity;
22

3+
import com.ai.lawyer.domain.post.entity.Post;
4+
import com.ai.lawyer.domain.poll.entity.PollVote;
5+
import com.ai.lawyer.domain.chatbot.entity.History;
36
import jakarta.persistence.*;
47
import jakarta.validation.constraints.*;
58
import lombok.*;
69
import org.hibernate.annotations.CreationTimestamp;
710
import org.hibernate.annotations.UpdateTimestamp;
811

912
import java.time.LocalDateTime;
13+
import java.util.ArrayList;
14+
import java.util.List;
1015

1116
@Entity
1217
@Table(name = "member",
@@ -63,6 +68,19 @@ public class Member implements MemberAdapter {
6368
@Column(name = "updated_at")
6469
private LocalDateTime updatedAt;
6570

71+
// 연관 관계: 회원 탈퇴 시 cascade 삭제
72+
@OneToMany(mappedBy = "member", cascade = CascadeType.ALL, orphanRemoval = true)
73+
@Builder.Default
74+
private List<Post> posts = new ArrayList<>();
75+
76+
@OneToMany(mappedBy = "member", cascade = CascadeType.ALL, orphanRemoval = true)
77+
@Builder.Default
78+
private List<PollVote> pollVotes = new ArrayList<>();
79+
80+
@OneToMany(mappedBy = "memberId", cascade = CascadeType.ALL, orphanRemoval = true)
81+
@Builder.Default
82+
private List<History> histories = new ArrayList<>();
83+
6684
@Getter
6785
public enum Gender {
6886
MALE("남성"), FEMALE("여성"), OTHER("기타");

backend/src/main/java/com/ai/lawyer/domain/member/entity/OAuth2Member.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
package com.ai.lawyer.domain.member.entity;
22

3+
import com.ai.lawyer.domain.post.entity.Post;
4+
import com.ai.lawyer.domain.poll.entity.PollVote;
5+
import com.ai.lawyer.domain.chatbot.entity.History;
36
import jakarta.persistence.*;
47
import jakarta.validation.constraints.*;
58
import lombok.*;
69
import org.hibernate.annotations.CreationTimestamp;
710
import org.hibernate.annotations.UpdateTimestamp;
811

912
import java.time.LocalDateTime;
13+
import java.util.ArrayList;
14+
import java.util.List;
1015

1116
@Entity
1217
@Table(name = "oauth2_member",
@@ -74,6 +79,19 @@ public class OAuth2Member implements MemberAdapter {
7479
@Column(name = "updated_at")
7580
private LocalDateTime updatedAt;
7681

82+
// 연관 관계: 회원 탈퇴 시 cascade 삭제
83+
@OneToMany(mappedBy = "member", cascade = CascadeType.ALL, orphanRemoval = true)
84+
@Builder.Default
85+
private List<Post> posts = new ArrayList<>();
86+
87+
@OneToMany(mappedBy = "member", cascade = CascadeType.ALL, orphanRemoval = true)
88+
@Builder.Default
89+
private List<PollVote> pollVotes = new ArrayList<>();
90+
91+
@OneToMany(mappedBy = "memberId", cascade = CascadeType.ALL, orphanRemoval = true)
92+
@Builder.Default
93+
private List<History> histories = new ArrayList<>();
94+
7795
@Getter
7896
public enum Provider {
7997
KAKAO("카카오"), NAVER("네이버");

backend/src/main/java/com/ai/lawyer/domain/member/service/MemberService.java

Lines changed: 80 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
import com.ai.lawyer.domain.member.entity.OAuth2Member;
66
import com.ai.lawyer.domain.member.repositories.MemberRepository;
77
import com.ai.lawyer.domain.member.repositories.OAuth2MemberRepository;
8+
import com.ai.lawyer.domain.post.repository.PostRepository;
9+
import com.ai.lawyer.domain.poll.repository.PollVoteRepository;
10+
import com.ai.lawyer.domain.chatbot.repository.HistoryRepository;
811
import com.ai.lawyer.global.jwt.TokenProvider;
912
import com.ai.lawyer.global.jwt.CookieUtil;
1013
import com.ai.lawyer.global.email.service.EmailService;
@@ -27,20 +30,29 @@ public class MemberService {
2730
private final CookieUtil cookieUtil;
2831
private final EmailService emailService;
2932
private final EmailAuthService emailAuthService;
33+
private final PostRepository postRepository;
34+
private final PollVoteRepository pollVoteRepository;
35+
private final HistoryRepository historyRepository;
3036

3137
public MemberService(
3238
MemberRepository memberRepository,
3339
PasswordEncoder passwordEncoder,
3440
TokenProvider tokenProvider,
3541
CookieUtil cookieUtil,
3642
EmailService emailService,
37-
EmailAuthService emailAuthService) {
43+
EmailAuthService emailAuthService,
44+
PostRepository postRepository,
45+
PollVoteRepository pollVoteRepository,
46+
HistoryRepository historyRepository) {
3847
this.memberRepository = memberRepository;
3948
this.passwordEncoder = passwordEncoder;
4049
this.tokenProvider = tokenProvider;
4150
this.cookieUtil = cookieUtil;
4251
this.emailService = emailService;
4352
this.emailAuthService = emailAuthService;
53+
this.postRepository = postRepository;
54+
this.pollVoteRepository = pollVoteRepository;
55+
this.historyRepository = historyRepository;
4456
}
4557

4658
@org.springframework.beans.factory.annotation.Autowired(required = false)
@@ -186,24 +198,81 @@ public String getLoginIdByMemberId(Long memberId) {
186198

187199
@Transactional
188200
public void deleteMember(String loginId) {
189-
// Member 또는 OAuth2Member 삭제
201+
log.info("회원 탈퇴 시작: loginId={}", loginId);
202+
203+
// 1. Member 또는 OAuth2Member 조회하여 memberId 가져오기
204+
Long memberId = null;
205+
boolean isRegularMember = false;
206+
190207
java.util.Optional<Member> regularMember = memberRepository.findByLoginId(loginId);
191208
if (regularMember.isPresent()) {
192-
memberRepository.delete(regularMember.get());
193-
log.info("일반 회원 삭제 완료: loginId={}", loginId);
209+
memberId = regularMember.get().getMemberId();
210+
isRegularMember = true;
211+
log.info("일반 회원 찾음: loginId={}, memberId={}", loginId, memberId);
212+
} else if (oauth2MemberRepository != null) {
213+
java.util.Optional<OAuth2Member> oauth2Member = oauth2MemberRepository.findByLoginId(loginId);
214+
if (oauth2Member.isPresent()) {
215+
memberId = oauth2Member.get().getMemberId();
216+
log.info("OAuth2 회원 찾음: loginId={}, memberId={}", loginId, memberId);
217+
}
218+
}
219+
220+
if (memberId == null) {
221+
log.warn("삭제할 회원을 찾을 수 없습니다: loginId={}", loginId);
194222
return;
195223
}
196224

197-
if (oauth2MemberRepository != null) {
225+
// 2. 연관된 데이터 명시적 삭제 (순서 중요: FK 제약조건 고려)
226+
log.info("연관 데이터 삭제 시작: memberId={}", memberId);
227+
228+
// 2-1. 채팅 히스토리 삭제 (Chat 엔티티도 cascade로 함께 삭제됨)
229+
try {
230+
historyRepository.deleteByMemberIdValue(memberId);
231+
log.info("채팅 히스토리 삭제 완료: memberId={}", memberId);
232+
} catch (Exception e) {
233+
log.error("채팅 히스토리 삭제 실패: memberId={}, error={}", memberId, e.getMessage());
234+
}
235+
236+
// 2-2. 투표 내역 삭제
237+
try {
238+
pollVoteRepository.deleteByMemberIdValue(memberId);
239+
log.info("투표 내역 삭제 완료: memberId={}", memberId);
240+
} catch (Exception e) {
241+
log.error("투표 내역 삭제 실패: memberId={}, error={}", memberId, e.getMessage());
242+
}
243+
244+
// 2-3. 게시글 삭제 (Poll 엔티티도 cascade로 함께 삭제됨)
245+
try {
246+
postRepository.deleteByMemberIdValue(memberId);
247+
log.info("게시글 삭제 완료: memberId={}", memberId);
248+
} catch (Exception e) {
249+
log.error("게시글 삭제 실패: memberId={}, error={}", memberId, e.getMessage());
250+
}
251+
252+
// 3. Redis 토큰 삭제
253+
try {
254+
tokenProvider.deleteAllTokens(loginId);
255+
log.info("Redis 토큰 삭제 완료: loginId={}", loginId);
256+
} catch (Exception e) {
257+
log.error("Redis 토큰 삭제 실패: loginId={}, error={}", loginId, e.getMessage());
258+
}
259+
260+
// 4. 회원 정보 삭제
261+
final Long finalMemberId = memberId;
262+
if (isRegularMember) {
263+
regularMember.ifPresent(member -> {
264+
memberRepository.delete(member);
265+
log.info("일반 회원 삭제 완료: loginId={}, memberId={}", loginId, finalMemberId);
266+
});
267+
} else if (oauth2MemberRepository != null) {
198268
java.util.Optional<OAuth2Member> oauth2Member = oauth2MemberRepository.findByLoginId(loginId);
199-
if (oauth2Member.isPresent()) {
200-
oauth2MemberRepository.delete(oauth2Member.get());
201-
log.info("OAuth2 회원 삭제 완료: loginId={}", loginId);
202-
return;
203-
}
269+
oauth2Member.ifPresent(member -> {
270+
oauth2MemberRepository.delete(member);
271+
log.info("OAuth2 회원 삭제 완료: loginId={}, memberId={}", loginId, finalMemberId);
272+
});
204273
}
205274

206-
log.warn("삭제할 회원을 찾을 수 없습니다: loginId={}", loginId);
275+
log.info("회원 탈퇴 완료: loginId={}, memberId={}", loginId, finalMemberId);
207276
}
208277

209278
public void sendCodeToEmailByLoginId(String loginId) {

backend/src/main/java/com/ai/lawyer/domain/poll/repository/PollVoteRepository.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
import com.ai.lawyer.domain.poll.entity.PollVote;
44
import org.springframework.data.jpa.repository.JpaRepository;
5+
import org.springframework.data.jpa.repository.Modifying;
6+
import org.springframework.data.jpa.repository.Query;
7+
import org.springframework.data.repository.query.Param;
58

69
import java.util.List;
710
import java.util.Optional;
@@ -11,4 +14,12 @@ public interface PollVoteRepository extends JpaRepository<PollVote, Long>, PollV
1114
void deleteByMember_MemberIdAndPoll_PollId(Long memberId, Long pollId);
1215
Optional<PollVote> findByMember_MemberIdAndPollOptions_PollItemsId(Long memberId, Long pollItemsId);
1316
List<PollVote> findByMember_MemberId(Long memberId);
17+
18+
/**
19+
* member_id로 투표 내역 삭제 (회원 탈퇴 시 사용)
20+
* Member와 OAuth2Member 모두 같은 member_id 공간을 사용하므로 Long 타입으로 삭제
21+
*/
22+
@Modifying
23+
@Query("DELETE FROM PollVote pv WHERE pv.member.memberId = :memberId")
24+
void deleteByMemberIdValue(@Param("memberId") Long memberId);
1425
}

backend/src/main/java/com/ai/lawyer/domain/post/repository/PostRepository.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,22 @@
33
import com.ai.lawyer.domain.member.entity.Member;
44
import com.ai.lawyer.domain.post.entity.Post;
55
import org.springframework.data.jpa.repository.JpaRepository;
6+
import org.springframework.data.jpa.repository.Modifying;
7+
import org.springframework.data.jpa.repository.Query;
8+
import org.springframework.data.repository.query.Param;
69
import org.springframework.stereotype.Repository;
710

811
import java.util.List;
912

1013
@Repository
1114
public interface PostRepository extends JpaRepository<Post, Long> {
1215
List<Post> findByMember(Member member);
16+
17+
/**
18+
* member_id로 게시글 삭제 (회원 탈퇴 시 사용)
19+
* Member와 OAuth2Member 모두 같은 member_id 공간을 사용하므로 Long 타입으로 삭제
20+
*/
21+
@Modifying
22+
@Query("DELETE FROM Post p WHERE p.member.memberId = :memberId")
23+
void deleteByMemberIdValue(@Param("memberId") Long memberId);
1324
}

backend/src/test/java/com/ai/lawyer/domain/member/service/MemberServiceTest.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -300,8 +300,9 @@ void withdraw_Success() {
300300
memberService.deleteMember(loginId);
301301

302302
// then
303+
verify(tokenProvider).deleteAllTokens(loginId); // Redis 토큰 삭제
303304
verify(memberRepository).findByLoginId(loginId);
304-
verify(memberRepository).delete(member);
305+
verify(memberRepository).delete(member); // 회원 삭제 (cascade로 연관 데이터 자동 삭제)
305306
}
306307

307308
@Test

0 commit comments

Comments
 (0)