Skip to content
Open
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
2 changes: 2 additions & 0 deletions src/main/java/umc/cockple/demo/Application.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableJpaAuditing
@EnableCaching
@EnableScheduling
public class Application {

public static void main(String[] args) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package umc.cockple.demo.domain.bookmark.repository;

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 umc.cockple.demo.domain.bookmark.domain.ExerciseBookmark;
Expand Down Expand Up @@ -32,5 +33,7 @@ List<Long> findAllExerciseIdsByMemberIdAndExerciseIds(

Optional<ExerciseBookmark> findFirstByMemberOrderByCreatedAtAsc(Member member);


@Modifying
@Query("DELETE FROM ExerciseBookmark eb WHERE eb.member.id IN :memberIds")
void deleteByMemberIds(@Param("memberIds") List<Long> memberIds);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package umc.cockple.demo.domain.bookmark.repository;

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 umc.cockple.demo.domain.bookmark.domain.PartyBookmark;
Expand Down Expand Up @@ -37,4 +38,8 @@ public interface PartyBookmarkRepository extends JpaRepository<PartyBookmark, Lo
List<PartyBookmark> findAllByMember(Member member);

Optional<PartyBookmark> findFirstByMemberOrderByCreatedAtAsc(Member member);

@Modifying
@Query("DELETE FROM PartyBookmark pb WHERE pb.member.id IN :memberIds")
void deleteByMemberIds(@Param("memberIds") List<Long> memberIds);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import org.springframework.data.domain.Pageable;
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 umc.cockple.demo.domain.chat.domain.ChatMessage;
Expand Down Expand Up @@ -49,4 +50,9 @@ List<ChatMessage> findByRoomIdAndIdLessThanOrderByCreatedAtDesc(
Pageable pageable);

int countByChatRoomId(Long chatRoomId);

// 탈퇴 회원 하드 딜리트 전 sender_id null 처리 (메시지 보존)
@Modifying
@Query("UPDATE ChatMessage m SET m.sender = null WHERE m.sender.id IN :memberIds")
void nullifySenderByMemberIds(@Param("memberIds") List<Long> memberIds);
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ List<ChatRoomMember> findByChatRoomIdAndStatusWithMember(
@Param("status") ChatRoomMemberStatus status
);

@Modifying
@Query("DELETE FROM ChatRoomMember crm WHERE crm.member.id IN :memberIds")
void deleteByMemberIds(@Param("memberIds") List<Long> memberIds);

@Query("""
SELECT counterPart FROM ChatRoomMember counterPart
WHERE counterPart.chatRoom.type = 'DIRECT'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,8 @@ int countUnreadByMessageId(
ORDER BY mrs.chatMessageId ASC
""")
List<Long> findUnreadMessageIdsByMember(Long chatRoomId, Long memberId);

@Modifying
@Query("DELETE FROM MessageReadStatus mrs WHERE mrs.memberId IN :memberIds")
void deleteByMemberIds(@Param("memberIds") List<Long> memberIds);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package umc.cockple.demo.domain.contest.repository;

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 umc.cockple.demo.domain.contest.domain.Contest;
Expand All @@ -24,4 +25,16 @@ public interface ContestRepository extends JpaRepository<Contest, Long> {

@Query("SELECT COUNT(c) FROM Contest c WHERE c.member.id = :memberId AND c.medalType = 'BRONZE'")
int countBronzeMedalsByMemberId(@Param("memberId") Long memberId);

@Modifying
@Query("DELETE FROM ContestVideo cv WHERE cv.contest.id IN (SELECT c.id FROM Contest c WHERE c.member.id IN :memberIds)")
void deleteContestVideosByMemberIds(@Param("memberIds") List<Long> memberIds);

@Modifying
@Query("DELETE FROM ContestImg ci WHERE ci.contest.id IN (SELECT c.id FROM Contest c WHERE c.member.id IN :memberIds)")
void deleteContestImgsByMemberIds(@Param("memberIds") List<Long> memberIds);

@Modifying
@Query("DELETE FROM Contest c WHERE c.member.id IN :memberIds")
void deleteByMemberIds(@Param("memberIds") List<Long> memberIds);
}
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ public BaseResponse<String> registerMemberDetailInfo(@RequestBody @Valid MemberD

@PostMapping("/auth/refresh")
@Operation(summary = "토큰 재발급 API",
description = "액세스 토큰을 재발급 하고 리프레시 토큰 또한 만료일이 3일 이하로 남았을 경우 재발급 해주는 api입니다. 리프레시토큰은 헤더에 쿠키로 들어갑니다.")
description = "액세스 토큰과 리프레시 토큰을 모두 재발급합니다 (Refresh Token Rotation). 리프레시 토큰은 응답 쿠키로 전달됩니다.")
public ResponseEntity<TokenRefreshResponse> refresh(@CookieValue("refreshToken") String refreshToken) {

if (refreshToken == null || refreshToken.isBlank()) {
Expand All @@ -130,11 +130,13 @@ public ResponseEntity<TokenRefreshResponse> refresh(@CookieValue("refreshToken")
@PatchMapping(value = "/member")
@Operation(summary = "회원 탈퇴 API",
description = "사용자 회원 탈퇴")
public BaseResponse<String> withdraw(@AuthenticationPrincipal CustomUserDetails member) {
public BaseResponse<String> withdraw(
@AuthenticationPrincipal CustomUserDetails member,
@CookieValue(value = "refreshToken", required = false) String refreshToken) {

Long memberId = member.getMemberId();

memberCommandService.withdrawMember(memberId);
memberCommandService.withdrawMember(memberId, refreshToken);
return BaseResponse.success(CommonSuccessCode.NO_CONTENT);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import umc.cockple.demo.global.common.BaseEntity;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.Period;
import java.util.ArrayList;
import java.util.List;
Expand Down Expand Up @@ -51,7 +52,7 @@ public class Member extends BaseEntity {
@Enumerated(EnumType.STRING)
private MemberStatus isActive;

private String refreshToken;
private LocalDateTime deletedAt;

@Column(nullable = false)
private Long socialId; // 카카오에서 받아온 고유id
Expand Down Expand Up @@ -179,14 +180,10 @@ public boolean isWithdrawn() {

public void withdraw() {
this.isActive = MemberStatus.INACTIVE;
this.refreshToken = null;
this.deletedAt = LocalDateTime.now();
this.fcmToken = null;
}

public void setRefreshToken(String refreshToken) {
this.refreshToken = refreshToken;
}

// FCM 토큰 업데이트 메서드
public void updateFcmToken(String fcmToken) {
this.fcmToken = fcmToken;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
package umc.cockple.demo.domain.member.repository;

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 umc.cockple.demo.domain.member.domain.Member;
import umc.cockple.demo.domain.member.domain.MemberAddr;

import java.util.List;
import java.util.Optional;

public interface MemberAddrRepository extends JpaRepository<MemberAddr, Long> {
Optional<MemberAddr> findByMemberAndIsMain(Member member, Boolean isMain);

@Modifying
@Query("DELETE FROM MemberAddr ma WHERE ma.member.id IN :memberIds")
void deleteByMemberIds(@Param("memberIds") List<Long> memberIds);
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,8 @@ SELECT me.member.id, MAX(e.date)
List<Object[]> findLastExerciseDateByMemberIdsAndPartyId(
@Param("memberIds") List<Long> memberIds,
@Param("partyId") Long partyId);

@Modifying
@Query("DELETE FROM MemberExercise me WHERE me.member.id IN :memberIds")
void deleteByMemberIds(@Param("memberIds") List<Long> memberIds);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package umc.cockple.demo.domain.member.repository;

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 umc.cockple.demo.domain.member.domain.Member;
import umc.cockple.demo.domain.member.domain.MemberKeyword;

Expand All @@ -11,4 +14,8 @@ public interface MemberKeywordRepository extends JpaRepository<MemberKeyword, Lo
void deleteAllByMember(Member member);

List<MemberKeyword> findAllByMemberId(Long memberId);

@Modifying
@Query("DELETE FROM MemberKeyword mk WHERE mk.member.id IN :memberIds")
void deleteByMemberIds(@Param("memberIds") List<Long> memberIds);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
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 umc.cockple.demo.domain.member.domain.Member;
Expand Down Expand Up @@ -56,4 +57,8 @@ List<Long> findAllPartyIdsByMemberAndPartyIds(@Param("memberId") Long memberId,
List<MemberParty> findAllByPartyIdWithMember(@Param("partyId") Long partyId);

Optional<MemberParty> findByPartyIdAndRole(Long partyId, Role role);

@Modifying
@Query("DELETE FROM MemberParty mp WHERE mp.member.id IN :memberIds")
void deleteByMemberIds(@Param("memberIds") List<Long> memberIds);
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package umc.cockple.demo.domain.member.repository;

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 umc.cockple.demo.domain.member.domain.Member;
import umc.cockple.demo.domain.member.enums.MemberStatus;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.Optional;
Expand Down Expand Up @@ -34,9 +37,18 @@ SELECT new map(m.id as id, m.memberName as name) FROM Member m

Optional<Member> findBySocialId(Long socialId);

Optional<Member> findByRefreshToken(String refreshToken);
@Query("""
SELECT m FROM Member m
LEFT JOIN FETCH m.profileImg
WHERE m.isActive = :isActive
AND m.deletedAt < :threshold
""")
List<Member> findAllByIsActiveAndDeletedAtBefore(@Param("isActive") MemberStatus isActive, @Param("threshold") LocalDateTime threshold);

@Modifying
@Query("DELETE FROM Member m WHERE m.id IN :memberIds")
void deleteByMemberIds(@Param("memberIds") List<Long> memberIds);


@Query("""
SELECT m FROM Member m
LEFT JOIN FETCH m.addresses addr
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package umc.cockple.demo.domain.member.repository;

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 umc.cockple.demo.domain.member.domain.MemberTerms;

import java.util.List;

public interface MemberTermsRepository extends JpaRepository<MemberTerms, Long> {

@Modifying
@Query("DELETE FROM MemberTerms mt WHERE mt.member.id IN :memberIds")
void deleteByMemberIds(@Param("memberIds") List<Long> memberIds);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package umc.cockple.demo.domain.member.repository;

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 umc.cockple.demo.domain.member.domain.ProfileImg;

import java.util.List;

public interface ProfileImgRepository extends JpaRepository<ProfileImg, Long> {

@Modifying
@Query("DELETE FROM ProfileImg pi WHERE pi.member.id IN :memberIds")
void deleteByMemberIds(@Param("memberIds") List<Long> memberIds);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package umc.cockple.demo.domain.member.scheduler;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import umc.cockple.demo.domain.bookmark.repository.ExerciseBookmarkRepository;
import umc.cockple.demo.domain.bookmark.repository.PartyBookmarkRepository;
import umc.cockple.demo.domain.chat.repository.ChatMessageRepository;
import umc.cockple.demo.domain.chat.repository.ChatRoomMemberRepository;
import umc.cockple.demo.domain.chat.repository.MessageReadStatusRepository;
import umc.cockple.demo.domain.contest.repository.ContestRepository;
import umc.cockple.demo.domain.file.service.FileService;
import umc.cockple.demo.domain.member.domain.Member;
import umc.cockple.demo.domain.member.enums.MemberStatus;
import umc.cockple.demo.domain.member.repository.*;
import umc.cockple.demo.domain.notification.repository.NotificationRepository;
import umc.cockple.demo.domain.party.repository.PartyJoinRequestRepository;

import java.time.LocalDateTime;
import java.util.List;

@Component
@RequiredArgsConstructor
@Slf4j
public class WithdrawnMemberCleanupScheduler {

private final MemberRepository memberRepository;
private final ProfileImgRepository profileImgRepository;
private final MemberKeywordRepository memberKeywordRepository;
private final MemberAddrRepository memberAddrRepository;
private final MemberPartyRepository memberPartyRepository;
private final MemberExerciseRepository memberExerciseRepository;
private final MemberTermsRepository memberTermsRepository;
private final ContestRepository contestRepository;
private final NotificationRepository notificationRepository;
private final ExerciseBookmarkRepository exerciseBookmarkRepository;
private final PartyBookmarkRepository partyBookmarkRepository;
private final ChatRoomMemberRepository chatRoomMemberRepository;
private final ChatMessageRepository chatMessageRepository;
private final MessageReadStatusRepository messageReadStatusRepository;
private final PartyJoinRequestRepository partyJoinRequestRepository;
private final FileService fileService;

// 매일 새벽 3시에 탈퇴 후 14일이 지난 회원 데이터 하드 딜리트
@Scheduled(cron = "0 0 3 * * *")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이거 cron 시간 설정할 때 zone을 서울로 넣어줘야 저희가 생각하는 그 시간에 돌더라구요!

@Transactional
public void deleteExpiredWithdrawnMembers() {
LocalDateTime threshold = LocalDateTime.now().minusDays(14);
List<Member> targets = memberRepository.findAllByIsActiveAndDeletedAtBefore(
MemberStatus.INACTIVE, threshold);

if (targets.isEmpty()) {
return;
}

List<Long> memberIds = targets.stream().map(Member::getId).toList();

// S3 프로필 이미지 삭제 (DB 삭제 전에 처리)
targets.stream()
.filter(m -> m.getProfileImg() != null)
.forEach(m -> fileService.delete(m.getProfileImg().getImgKey()));

// Contest 자식 테이블 먼저 (FK: contest_id)
contestRepository.deleteContestVideosByMemberIds(memberIds);
contestRepository.deleteContestImgsByMemberIds(memberIds);
contestRepository.deleteByMemberIds(memberIds);

// sender_id null 처리 (메시지 보존)
chatMessageRepository.nullifySenderByMemberIds(memberIds);

// 나머지 자식 테이블 일괄 삭제
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

현재 자식 테이블 중 partyInvitationRepository에 대한 삭제 로직이 누락된 것 같습니다! 확인 부탁드립니다~

messageReadStatusRepository.deleteByMemberIds(memberIds);
partyJoinRequestRepository.deleteByMemberIds(memberIds);
memberTermsRepository.deleteByMemberIds(memberIds);
notificationRepository.deleteByMemberIds(memberIds);
memberKeywordRepository.deleteByMemberIds(memberIds);
memberAddrRepository.deleteByMemberIds(memberIds);
chatRoomMemberRepository.deleteByMemberIds(memberIds);
exerciseBookmarkRepository.deleteByMemberIds(memberIds);
partyBookmarkRepository.deleteByMemberIds(memberIds);
memberPartyRepository.deleteByMemberIds(memberIds);
memberExerciseRepository.deleteByMemberIds(memberIds);
profileImgRepository.deleteByMemberIds(memberIds);

// Member 일괄 삭제
memberRepository.deleteByMemberIds(memberIds);

log.info("[CLEANUP] 탈퇴 회원 하드 딜리트 완료 - {}명", memberIds.size());
}
}
Loading