Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public class History {
private Long historyId;

@ManyToOne
@JoinColumn(name = "member_id")
@JoinColumn(name = "member_id", foreignKey = @ForeignKey(name = "FK_HISTORY_MEMBER"))
private Member memberId;

@OneToMany(mappedBy = "historyId", cascade = CascadeType.ALL, orphanRemoval = true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
import com.ai.lawyer.domain.chatbot.entity.History;
import com.ai.lawyer.domain.member.entity.Member;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

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

History findByHistoryIdAndMemberId(Long roomId, Member memberId);

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

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
import com.ai.lawyer.domain.member.entity.OAuth2Member;
import com.ai.lawyer.domain.member.repositories.MemberRepository;
import com.ai.lawyer.domain.member.repositories.OAuth2MemberRepository;
import com.ai.lawyer.domain.post.repository.PostRepository;
import com.ai.lawyer.domain.poll.repository.PollVoteRepository;
import com.ai.lawyer.domain.chatbot.repository.HistoryRepository;
import com.ai.lawyer.global.jwt.TokenProvider;
import com.ai.lawyer.global.jwt.CookieUtil;
import com.ai.lawyer.global.email.service.EmailService;
Expand All @@ -27,20 +30,29 @@ public class MemberService {
private final CookieUtil cookieUtil;
private final EmailService emailService;
private final EmailAuthService emailAuthService;
private final PostRepository postRepository;
private final PollVoteRepository pollVoteRepository;
private final HistoryRepository historyRepository;

public MemberService(
MemberRepository memberRepository,
PasswordEncoder passwordEncoder,
TokenProvider tokenProvider,
CookieUtil cookieUtil,
EmailService emailService,
EmailAuthService emailAuthService) {
EmailAuthService emailAuthService,
PostRepository postRepository,
PollVoteRepository pollVoteRepository,
HistoryRepository historyRepository) {
this.memberRepository = memberRepository;
this.passwordEncoder = passwordEncoder;
this.tokenProvider = tokenProvider;
this.cookieUtil = cookieUtil;
this.emailService = emailService;
this.emailAuthService = emailAuthService;
this.postRepository = postRepository;
this.pollVoteRepository = pollVoteRepository;
this.historyRepository = historyRepository;
}

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

@Transactional
public void deleteMember(String loginId) {
// Member 또는 OAuth2Member 삭제
log.info("회원 탈퇴 시작: loginId={}", loginId);

// 1. Member 또는 OAuth2Member 조회하여 memberId 가져오기
Long memberId = null;
boolean isRegularMember = false;

java.util.Optional<Member> regularMember = memberRepository.findByLoginId(loginId);
if (regularMember.isPresent()) {
memberRepository.delete(regularMember.get());
log.info("일반 회원 삭제 완료: loginId={}", loginId);
memberId = regularMember.get().getMemberId();
isRegularMember = true;
log.info("일반 회원 찾음: loginId={}, memberId={}", loginId, memberId);
} else if (oauth2MemberRepository != null) {
java.util.Optional<OAuth2Member> oauth2Member = oauth2MemberRepository.findByLoginId(loginId);
if (oauth2Member.isPresent()) {
memberId = oauth2Member.get().getMemberId();
log.info("OAuth2 회원 찾음: loginId={}, memberId={}", loginId, memberId);
}
}

if (memberId == null) {
log.warn("삭제할 회원을 찾을 수 없습니다: loginId={}", loginId);
return;
}

if (oauth2MemberRepository != null) {
// 2. 연관된 데이터 명시적 삭제 (순서 중요: FK 제약조건 고려)
log.info("연관 데이터 삭제 시작: memberId={}", memberId);

// 2-1. 채팅 히스토리 삭제 (Chat 엔티티도 cascade로 함께 삭제됨)
try {
historyRepository.deleteByMemberIdValue(memberId);
log.info("채팅 히스토리 삭제 완료: memberId={}", memberId);
} catch (Exception e) {
log.error("채팅 히스토리 삭제 실패: memberId={}, error={}", memberId, e.getMessage());
}

// 2-2. 투표 내역 삭제
try {
pollVoteRepository.deleteByMemberIdValue(memberId);
log.info("투표 내역 삭제 완료: memberId={}", memberId);
} catch (Exception e) {
log.error("투표 내역 삭제 실패: memberId={}, error={}", memberId, e.getMessage());
}

// 2-3. 게시글 삭제 (Poll 엔티티도 cascade로 함께 삭제됨)
try {
postRepository.deleteByMemberIdValue(memberId);
log.info("게시글 삭제 완료: memberId={}", memberId);
} catch (Exception e) {
log.error("게시글 삭제 실패: memberId={}, error={}", memberId, e.getMessage());
}

// 3. Redis 토큰 삭제
try {
tokenProvider.deleteAllTokens(loginId);
log.info("Redis 토큰 삭제 완료: loginId={}", loginId);
} catch (Exception e) {
log.error("Redis 토큰 삭제 실패: loginId={}, error={}", loginId, e.getMessage());
}

// 4. 회원 정보 삭제
final Long finalMemberId = memberId;
if (isRegularMember) {
regularMember.ifPresent(member -> {
memberRepository.delete(member);
log.info("일반 회원 삭제 완료: loginId={}, memberId={}", loginId, finalMemberId);
});
} else if (oauth2MemberRepository != null) {
java.util.Optional<OAuth2Member> oauth2Member = oauth2MemberRepository.findByLoginId(loginId);
if (oauth2Member.isPresent()) {
oauth2MemberRepository.delete(oauth2Member.get());
log.info("OAuth2 회원 삭제 완료: loginId={}", loginId);
return;
}
oauth2Member.ifPresent(member -> {
oauth2MemberRepository.delete(member);
log.info("OAuth2 회원 삭제 완료: loginId={}, memberId={}", loginId, finalMemberId);
});
}

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

public void sendCodeToEmailByLoginId(String loginId) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

import com.ai.lawyer.domain.poll.entity.PollVote;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.util.List;
import java.util.Optional;
Expand All @@ -11,4 +14,12 @@ public interface PollVoteRepository extends JpaRepository<PollVote, Long>, PollV
void deleteByMember_MemberIdAndPoll_PollId(Long memberId, Long pollId);
Optional<PollVote> findByMember_MemberIdAndPollOptions_PollItemsId(Long memberId, Long pollItemsId);
List<PollVote> findByMember_MemberId(Long memberId);

/**
* member_id로 투표 내역 삭제 (회원 탈퇴 시 사용)
* Member와 OAuth2Member 모두 같은 member_id 공간을 사용하므로 Long 타입으로 삭제
*/
@Modifying
@Query("DELETE FROM PollVote pv WHERE pv.member.memberId = :memberId")
void deleteByMemberIdValue(@Param("memberId") Long memberId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,22 @@
import com.ai.lawyer.domain.member.entity.Member;
import com.ai.lawyer.domain.post.entity.Post;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface PostRepository extends JpaRepository<Post, Long> {
List<Post> findByMember(Member member);

/**
* member_id로 게시글 삭제 (회원 탈퇴 시 사용)
* Member와 OAuth2Member 모두 같은 member_id 공간을 사용하므로 Long 타입으로 삭제
*/
@Modifying
@Query("DELETE FROM Post p WHERE p.member.memberId = :memberId")
void deleteByMemberIdValue(@Param("memberId") Long memberId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,15 @@ class MemberServiceOAuth2Test {
@Mock
private EmailAuthService emailAuthService;

@Mock
private com.ai.lawyer.domain.post.repository.PostRepository postRepository;

@Mock
private com.ai.lawyer.domain.poll.repository.PollVoteRepository pollVoteRepository;

@Mock
private com.ai.lawyer.domain.chatbot.repository.HistoryRepository historyRepository;

@Mock
private HttpServletResponse response;

Expand All @@ -74,7 +83,10 @@ void setUp() {
tokenProvider,
cookieUtil,
emailService,
emailAuthService
emailAuthService,
postRepository,
pollVoteRepository,
historyRepository
);
memberService.setOauth2MemberRepository(oauth2MemberRepository);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,15 @@ class MemberServiceTest {
@Mock
private EmailAuthService emailAuthService;

@Mock
private com.ai.lawyer.domain.post.repository.PostRepository postRepository;

@Mock
private com.ai.lawyer.domain.poll.repository.PollVoteRepository pollVoteRepository;

@Mock
private com.ai.lawyer.domain.chatbot.repository.HistoryRepository historyRepository;

@Mock
private HttpServletResponse response;

Expand All @@ -73,7 +82,10 @@ void setUp() {
tokenProvider,
cookieUtil,
emailService,
emailAuthService
emailAuthService,
postRepository,
pollVoteRepository,
historyRepository
);
memberService.setOauth2MemberRepository(oauth2MemberRepository);

Expand Down Expand Up @@ -300,7 +312,18 @@ void withdraw_Success() {
memberService.deleteMember(loginId);

// then
// 1. 회원 조회
verify(memberRepository).findByLoginId(loginId);

// 2. 연관 데이터 명시적 삭제 (순서 중요)
verify(historyRepository).deleteByMemberIdValue(member.getMemberId());
verify(pollVoteRepository).deleteByMemberIdValue(member.getMemberId());
verify(postRepository).deleteByMemberIdValue(member.getMemberId());

// 3. Redis 토큰 삭제
verify(tokenProvider).deleteAllTokens(loginId);

// 4. 회원 삭제
verify(memberRepository).delete(member);
}

Expand Down