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
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,22 @@ WHERE cm.id IN (SELECT r.target_id FROM report r WHERE r.target_type = 'CHAT')
AND cm.sender_id IN (SELECT cp.id FROM chat_participant cp WHERE cp.site_user_id IN :siteUserIds)
""", nativeQuery = true)
void bulkUpdateReportedChatMessagesIsDeleted(@Param("siteUserIds") List<Long> siteUserIds, @Param("isDeleted") boolean isDeleted);

@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query(value = """
UPDATE chat_message cm SET cm.is_deleted = false
WHERE cm.chat_room_id IN :chatRoomIds
""", nativeQuery = true)
void unmarkDeletedByChatRoomIdIn(@Param("chatRoomIds") List<Long> chatRoomIds);

void deleteAllByChatRoomIdIn(List<Long> chatRoomIds);

@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query(value = """
UPDATE chat_message cm SET cm.is_deleted = false
WHERE cm.sender_id IN :senderIds
""", nativeQuery = true)
void unmarkDeletedBySenderIdIn(@Param("senderIds") List<Long> senderIds);

void deleteAllBySenderIdIn(List<Long> senderIds);
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,17 @@ public interface ChatParticipantRepository extends JpaRepository<ChatParticipant

Optional<ChatParticipant> findByChatRoomIdAndSiteUserId(long chatRoomId, long siteUserId);

void deleteAllBySiteUserId(long siteUserId);
void deleteAllByChatRoomIdIn(List<Long> chatRoomIds);

@Query("SELECT cp.id FROM ChatParticipant cp WHERE cp.siteUserId = :siteUserId")
List<Long> findAllIdsBySiteUserId(@Param("siteUserId") long siteUserId);
@Query("""
SELECT cp.chatRoom.id FROM ChatParticipant cp
WHERE cp.siteUserId = :siteUserId AND cp.chatRoom.isGroup = :isGroup
""")
List<Long> findAllChatRoomIdsBySiteUserIdAndIsGroup(@Param("siteUserId") long siteUserId, @Param("isGroup") boolean isGroup);

@Query("""
SELECT cp.id FROM ChatParticipant cp
WHERE cp.siteUserId = :siteUserId AND cp.chatRoom.isGroup = :isGroup
""")
List<Long> findAllIdsBySiteUserIdAndIsGroup(@Param("siteUserId") long siteUserId, @Param("isGroup") boolean isGroup);
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,7 @@ INSERT INTO chat_read_status (chat_room_id, chat_participant_id, created_at, upd
""", nativeQuery = true)
void upsertReadStatus(@Param("chatRoomId") long chatRoomId, @Param("chatParticipantId") long chatParticipantId);

void deleteAllByChatRoomIdIn(List<Long> chatRoomIds);

void deleteAllByChatParticipantIdIn(List<Long> chatParticipantIds);
}
Original file line number Diff line number Diff line change
Expand Up @@ -76,5 +76,12 @@ default Post getById(Long id) {
.orElseThrow(() -> new CustomException(INVALID_POST_ID));
}

@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query(value = """
UPDATE post p SET p.is_deleted = false
WHERE p.site_user_id = :siteUserId
""", nativeQuery = true)
void unmarkDeletedBySiteUserId(@Param("siteUserId") long siteUserId);

void deleteAllBySiteUserId(long siteUserId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,8 @@ public interface MentorRepository extends JpaRepository<Mentor, Long> {

List<Mentor> findAllBySiteUserIdIn(Set<Long> siteUserIds);

@Query("SELECT m.id FROM Mentor m WHERE m.siteUserId = :siteUserId")
List<Long> findAllIdsBySiteUserId(@Param("siteUserId") long siteUserId);

void deleteAllBySiteUserId(long siteUserId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,6 @@ public interface MentoringRepository extends JpaRepository<Mentoring, Long> {
Slice<Mentoring> findApprovedMentoringsByMenteeId(long menteeId, @Param("verifyStatus") VerifyStatus verifyStatus, Pageable pageable);

void deleteAllByMenteeId(long menteeId);

void deleteAllByMentorIdIn(List<Long> mentorIds);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.example.solidconnection.news.repository;

import com.example.solidconnection.news.domain.LikedNews;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;

Expand All @@ -11,4 +12,6 @@ public interface LikedNewsRepository extends JpaRepository<LikedNews, Long> {
Optional<LikedNews> findByNewsIdAndSiteUserId(long newsId, long siteUserId);

void deleteAllBySiteUserId(long siteUserId);

void deleteAllByNewsIdIn(List<Long> newsIds);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,17 @@
import com.example.solidconnection.news.repository.custom.NewsCustomRepository;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

public interface NewsRepository extends JpaRepository<News, Long>, NewsCustomRepository {

List<News> findAllByOrderByUpdatedAtDesc();

List<News> findAllBySiteUserIdOrderByUpdatedAtDesc(long siteUserId);

@Query("SELECT n.id FROM News n WHERE n.siteUserId = :siteUserId")
List<Long> findAllIdsBySiteUserId(@Param("siteUserId") long siteUserId);

void deleteAllBySiteUserId(long siteUserId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,9 @@ public void deleteExProfile(long siteUserId) {
SiteUser siteUser = siteUserRepository.findById(siteUserId)
.orElseThrow(() -> new CustomException(USER_NOT_FOUND));
String key = siteUser.getProfileImageUrl();
if (key == null || key.isBlank()) {
return;
}
deleteFile(key);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package com.example.solidconnection.scheduler;

import com.example.solidconnection.application.repository.ApplicationRepository;
import com.example.solidconnection.chat.repository.ChatMessageRepository;
import com.example.solidconnection.chat.repository.ChatParticipantRepository;
import com.example.solidconnection.chat.repository.ChatReadStatusRepository;
import com.example.solidconnection.chat.repository.ChatRoomRepository;
import com.example.solidconnection.community.comment.repository.CommentRepository;
import com.example.solidconnection.community.post.repository.PostLikeRepository;
import com.example.solidconnection.community.post.repository.PostRepository;
Expand All @@ -19,6 +21,7 @@
import com.example.solidconnection.score.repository.LanguageTestScoreRepository;
import com.example.solidconnection.siteuser.domain.SiteUser;
import com.example.solidconnection.siteuser.repository.SiteUserRepository;
import com.example.solidconnection.siteuser.repository.UserBanRepository;
import com.example.solidconnection.siteuser.repository.UserBlockRepository;
import com.example.solidconnection.university.repository.LikedUnivApplyInfoRepository;
import java.time.LocalDate;
Expand Down Expand Up @@ -49,10 +52,13 @@ public class UserRemovalScheduler {
private final MentoringRepository mentoringRepository;
private final NewsRepository newsRepository;
private final LikedNewsRepository likedNewsRepository;
private final ChatRoomRepository chatRoomRepository;
private final ChatParticipantRepository chatParticipantRepository;
private final ChatMessageRepository chatMessageRepository;
private final ChatReadStatusRepository chatReadStatusRepository;
private final ReportRepository reportRepository;
private final UserBlockRepository userBlockRepository;
private final UserBanRepository userBanRepository;
private final MentorApplicationRepository mentorApplicationRepository;
private final S3Service s3Service;

Expand All @@ -72,21 +78,19 @@ private void deleteUserAndRelatedData(SiteUser user) {
long siteUserId = user.getId();

likedNewsRepository.deleteAllBySiteUserId(siteUserId);
newsRepository.deleteAllBySiteUserId(siteUserId);
deleteNews(siteUserId);

postLikeRepository.deleteAllBySiteUserId(siteUserId);
commentRepository.deleteAllBySiteUserId(siteUserId);
postRepository.deleteAllBySiteUserId(siteUserId);
deletePosts(siteUserId);

mentoringRepository.deleteAllByMenteeId(siteUserId);
mentorRepository.deleteAllBySiteUserId(siteUserId);
deleteChatData(siteUserId);
deleteMentorings(siteUserId);
mentorApplicationRepository.deleteAllBySiteUserId(siteUserId);

List<Long> chatParticipantIds = chatParticipantRepository.findAllIdsBySiteUserId(siteUserId);
chatReadStatusRepository.deleteAllByChatParticipantIdIn(chatParticipantIds);
chatParticipantRepository.deleteAllBySiteUserId(siteUserId);
reportRepository.deleteAllByReporterId(siteUserId);

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.

deleteAllByReporterId는 이 사용자가 "신고한" 기록만 지우는데, 이 사용자가 "신고당한" 기록(report.reported_id = siteUserId)이나 이 사용자의 게시글/채팅메시지를 대상(target_id)으로 한 신고 기록은 그대로 남아 고아 데이터가 발생할 수 있을 것 같습니다!

target_id/reported_id에는 FK가 없어서(ReportRepository, V25__create_report_table.sql 확인) 삭제 자체가 실패하지는 않지만, 대상이 사라진 report 행이 고아 데이터로 계속 쌓이게 됩니다. 이번 PR 범위 밖일 수 있지만 후속으로 reportedId/targetId 기준 정리도 필요해 보입니다.

userBlockRepository.deleteAllByBlockerIdOrBlockedId(siteUserId, siteUserId);
deleteUserBans(siteUserId);

applicationRepository.deleteAllBySiteUserId(siteUserId);
gpaScoreRepository.deleteAllBySiteUserId(siteUserId);
Expand All @@ -99,4 +103,83 @@ private void deleteUserAndRelatedData(SiteUser user) {

siteUserRepository.delete(user);
}

/*
* 다른 사용자가 누른 좋아요가 남아 있으면 뉴스를 삭제할 수 없으므로 함께 삭제한다.
* */
private void deleteNews(long siteUserId) {
List<Long> newsIds = newsRepository.findAllIdsBySiteUserId(siteUserId);
if (!newsIds.isEmpty()) {
likedNewsRepository.deleteAllByNewsIdIn(newsIds);
}
newsRepository.deleteAllBySiteUserId(siteUserId);
}

/*
* 신고로 가려진 게시글은 조회 필터에 걸려 삭제되지 않으므로, 플래그를 되돌린 뒤 삭제한다.
* */
private void deletePosts(long siteUserId) {
postRepository.unmarkDeletedBySiteUserId(siteUserId);
postRepository.deleteAllBySiteUserId(siteUserId);
}

/*
* 신고로 가려진 메시지는 조회 필터에 걸리므로, 플래그를 되돌린 뒤 삭제한다.
* 채팅방이 멘토링을 참조하므로 멘토링보다 먼저 삭제한다.
* */
private void deleteChatData(long siteUserId) {
deleteOneOnOneChatRooms(siteUserId);
deleteGroupChatParticipation(siteUserId);
}

/*
* 1:1 채팅방은 참여자 한 명이 사라지면 유지될 수 없으므로 방 전체를 삭제한다.
* */
private void deleteOneOnOneChatRooms(long siteUserId) {
List<Long> chatRoomIds = chatParticipantRepository.findAllChatRoomIdsBySiteUserIdAndIsGroup(siteUserId, false);
if (chatRoomIds.isEmpty()) {
return;
}
chatMessageRepository.unmarkDeletedByChatRoomIdIn(chatRoomIds);
chatMessageRepository.deleteAllByChatRoomIdIn(chatRoomIds);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Delete chat attachment objects before dropping their rows

When a deleted room contains files uploaded through the chat upload endpoint, this call cascades removal of the ChatAttachment database rows but never deletes the objects referenced by ChatAttachment.url from S3. Those keys become unreachable after the rows are removed and accumulate permanently; collect and delete the attachment objects before deleting the messages.

Useful? React with 👍 / 👎.

chatReadStatusRepository.deleteAllByChatRoomIdIn(chatRoomIds);
chatParticipantRepository.deleteAllByChatRoomIdIn(chatRoomIds);
chatRoomRepository.deleteAllById(chatRoomIds);
Comment thread
whqtker marked this conversation as resolved.
}

/*
* 그룹 채팅방은 남은 참여자들의 대화가 유지되어야 하므로 탈퇴자의 참여 기록만 삭제한다.
* */
private void deleteGroupChatParticipation(long siteUserId) {
List<Long> chatParticipantIds = chatParticipantRepository.findAllIdsBySiteUserIdAndIsGroup(siteUserId, true);
if (chatParticipantIds.isEmpty()) {
return;
}
chatMessageRepository.unmarkDeletedBySenderIdIn(chatParticipantIds);
chatMessageRepository.deleteAllBySenderIdIn(chatParticipantIds);
chatReadStatusRepository.deleteAllByChatParticipantIdIn(chatParticipantIds);
chatParticipantRepository.deleteAllById(chatParticipantIds);
}

/*
* 멘티로 참여한 멘토링과 멘토로 참여한 멘토링을 모두 삭제한 뒤 멘토를 삭제한다.
* */
private void deleteMentorings(long siteUserId) {
mentoringRepository.deleteAllByMenteeId(siteUserId);
List<Long> mentorIds = mentorRepository.findAllIdsBySiteUserId(siteUserId);
if (!mentorIds.isEmpty()) {
mentoringRepository.deleteAllByMentorIdIn(mentorIds);
}
mentorRepository.deleteAllBySiteUserId(siteUserId);
}

/*
* 탈퇴자를 대상으로 한 정지 기록은 삭제한다.
* 탈퇴자가 집행한 정지 기록은 다른 사용자의 이력이므로, 집행자 참조만 해제하고 보존한다.
* */
private void deleteUserBans(long siteUserId) {
userBanRepository.deleteAllByBannedUserId(siteUserId);
userBanRepository.clearBannedBy(siteUserId);
userBanRepository.clearUnbannedBy(siteUserId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public class UserBan extends BaseEntity {
@Column(name = "banned_user_id", nullable = false)
private Long bannedUserId;

@Column(name = "banned_by", nullable = false)
@Column(name = "banned_by")
Comment thread
whqtker marked this conversation as resolved.
private Long bannedBy;

@Column(name = "duration", nullable = false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,14 @@ public interface UserBanRepository extends JpaRepository<UserBan, Long> {
@Modifying
@Query("UPDATE UserBan ub SET ub.isExpired = true WHERE ub.isExpired = false AND ub.expiredAt < :current")
void bulkExpireUserBans(@Param("current") ZonedDateTime current);

void deleteAllByBannedUserId(long bannedUserId);

@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("UPDATE UserBan ub SET ub.bannedBy = null WHERE ub.bannedBy = :siteUserId")
void clearBannedBy(@Param("siteUserId") long siteUserId);

@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("UPDATE UserBan ub SET ub.unbannedBy = null WHERE ub.unbannedBy = :siteUserId")
void clearUnbannedBy(@Param("siteUserId") long siteUserId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE user_ban
MODIFY COLUMN banned_by BIGINT NULL;
Loading