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,13 @@ 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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ 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")
List<Long> findAllChatRoomIdsBySiteUserId(@Param("siteUserId") long siteUserId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,5 @@ 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 deleteAllByChatParticipantIdIn(List<Long> chatParticipantIds);
void deleteAllByChatRoomIdIn(List<Long> chatRoomIds);
}
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);
deleteChatRooms(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,62 @@ 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);
}

/*
* 1:1 채팅방은 참여자 한 명이 사라지면 유지될 수 없으므로 방 전체를 삭제한다.
* - 신고로 가려진 메시지는 조회 필터에 걸리므로, 플래그를 되돌린 뒤 삭제한다.
* - 채팅방이 멘토링을 참조하므로 멘토링보다 먼저 삭제한다.
* */
private void deleteChatRooms(long siteUserId) {
List<Long> chatRoomIds = chatParticipantRepository.findAllChatRoomIdsBySiteUserId(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.

현재 ChatRoom의 스키마 상 isGroup 컬럼이 존재합니다! 하지만 현재 findAllChatRoomIdsBySiteUserId가 isGroup 여부를 구분하지 않아서, 탈퇴자가 속한 채팅방은 1:1이든 그룹이든 전부 여기서 삭제 대상이 됩니다.

PR 설명에는 "1:1 채팅방은 참여자가 사라지면 유지될 수 없으므로 삭제"라고 되어 있는데 코드에는 그 조건이 명시돼 있지 않네요. 지금은 그룹 채팅방을 생성하는 경로가 없어 실제 영향은 없지만, ChatRoom.isGroup이 이미 존재하고 ChatService에서도 분기 처리하는 걸 보면 나중에 그룹 채팅에 대해서도 대비가 되게 구현하는 게 좋아보입니다.

그때 멤버 한 명 탈퇴로 다른 멤버들의 방 전체가 사라지는 회귀를 막기 위해, isGroup = false인 방만 대상으로 삼도록 조건을 걸거나 최소한 의도를 주석/TODO로 남겨두면 좋을 것 같습니다!

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);

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.

1:1 채팅방을 통째로 삭제하면, 탈퇴하지 않은 상대방 입장에서는 아무 통보 없이 대화 기록이 사라지게 됩니다!

참여자 한 명이 빠지면 방을 유지하기 어렵다는 제약 자체는 이해되지만, 이게 기획 쪽과 합의된 정책인지 확인이 필요해 보입니다. 필요하면 상대방 메시지는 보존하고 탈퇴한 참여자 정보만 "알 수 없음" 등으로 치환하는 방식도 고려해볼 수 있을 것 같습니다.

}

/*
* 멘티로 참여한 멘토링과 멘토로 참여한 멘토링을 모두 삭제한 뒤 멘토를 삭제한다.
* */
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")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Declare the nullable mapping explicitly

Because this change intentionally makes banned_by nullable, declare that contract as @Column(name = "banned_by", nullable = true) rather than relying on the annotation default. The repository convention requires entity columns to state nullability explicitly, so the current mapping no longer documents the schema change consistently.

AGENTS.md reference: AGENTS.md:L301-L305

Useful? React with 👍 / 👎.

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