From 23d2a25c1251963f94408d0d16e8ce9cb56fe9a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Tue, 2 Sep 2025 19:45:46 +0900 Subject: [PATCH 01/55] =?UTF-8?q?[refactor]=20=EC=95=88=EC=93=B0=EB=8A=94?= =?UTF-8?q?=20=EC=82=AD=EC=A0=9C=EC=9A=A9=20=EC=96=91=EB=B0=A9=ED=96=A5=20?= =?UTF-8?q?=EB=A7=A4=ED=95=91=20=EC=82=AD=EC=A0=9C=20(#176)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/comment/adapter/out/jpa/CommentJpaEntity.java | 6 ------ .../konkuk/thip/feed/adapter/out/jpa/FeedJpaEntity.java | 4 ---- .../konkuk/thip/post/adapter/out/jpa/PostJpaEntity.java | 8 -------- 3 files changed, 18 deletions(-) diff --git a/src/main/java/konkuk/thip/comment/adapter/out/jpa/CommentJpaEntity.java b/src/main/java/konkuk/thip/comment/adapter/out/jpa/CommentJpaEntity.java index 760283133..268fd9d29 100644 --- a/src/main/java/konkuk/thip/comment/adapter/out/jpa/CommentJpaEntity.java +++ b/src/main/java/konkuk/thip/comment/adapter/out/jpa/CommentJpaEntity.java @@ -38,7 +38,6 @@ public class CommentJpaEntity extends BaseJpaEntity { @Column(name = "like_count", nullable = false) private int likeCount = 0; - //TODO 상속구조 해지하면서 postType만 가질지, postId + postType가질지 논의 필요 @ManyToOne(fetch = FetchType.LAZY, optional = false) @JoinColumn(name = "post_id", nullable = false) private PostJpaEntity postJpaEntity; @@ -58,11 +57,6 @@ public class CommentJpaEntity extends BaseJpaEntity { @JoinColumn(name = "parent_id") private CommentJpaEntity parent; - // 삭제용 댓글 좋아요 양방향 매핑 관계 - @Builder.Default - @OneToMany(mappedBy = "commentJpaEntity", cascade = CascadeType.REMOVE, orphanRemoval = true) - private List commentLikeList = new ArrayList<>(); - public CommentJpaEntity updateFrom(Comment comment) { this.reportCount = comment.getReportCount(); this.likeCount = comment.getLikeCount(); diff --git a/src/main/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntity.java b/src/main/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntity.java index d2f2c8520..f9427b0a8 100644 --- a/src/main/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntity.java +++ b/src/main/java/konkuk/thip/feed/adapter/out/jpa/FeedJpaEntity.java @@ -40,10 +40,6 @@ public class FeedJpaEntity extends PostJpaEntity { @Column(name = "content_list", columnDefinition = "TEXT") private ContentList contentList = ContentList.empty(); - // 삭제용 피드 저장 양방향 매핑 관계 - @OneToMany(mappedBy = "feedJpaEntity", cascade = CascadeType.REMOVE, orphanRemoval = true) - private List savedFeeds = new ArrayList<>(); - @Column(name = "tag_list", columnDefinition = "TEXT") @Convert(converter = TagListJsonConverter.class) private TagList tagList = TagList.empty(); diff --git a/src/main/java/konkuk/thip/post/adapter/out/jpa/PostJpaEntity.java b/src/main/java/konkuk/thip/post/adapter/out/jpa/PostJpaEntity.java index b8074ca53..d542bb2eb 100644 --- a/src/main/java/konkuk/thip/post/adapter/out/jpa/PostJpaEntity.java +++ b/src/main/java/konkuk/thip/post/adapter/out/jpa/PostJpaEntity.java @@ -43,14 +43,6 @@ public abstract class PostJpaEntity extends BaseJpaEntity { @JoinColumn(name = "user_id", nullable = false) private UserJpaEntity userJpaEntity; - // 삭제용 게시물 댓글 양방향 매핑 관계 - @OneToMany(mappedBy = "postJpaEntity", cascade = CascadeType.REMOVE, orphanRemoval = true) - private List commentList = new ArrayList<>(); - - // 삭제용 게시물 좋아요 양방향 매핑 관계 - @OneToMany(mappedBy = "postJpaEntity", cascade = CascadeType.REMOVE, orphanRemoval = true) - private List postLikeList = new ArrayList<>(); - public PostJpaEntity(String content, Integer likeCount, Integer commentCount, UserJpaEntity userJpaEntity) { this.content = content; this.likeCount = likeCount; From 91a02bc8350aae3fa5763b68ec335614da0b21e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Fri, 5 Sep 2025 01:09:54 +0900 Subject: [PATCH 02/55] =?UTF-8?q?[feat]=20=ED=9A=8C=EC=9B=90=20=ED=83=88?= =?UTF-8?q?=ED=87=B4=20=EC=8B=9C=20=EC=97=B0=EA=B4=80=EB=90=9C=20=EC=98=A4?= =?UTF-8?q?=EB=8A=98=EC=9D=98=20=ED=95=9C=EB=A7=88=EB=94=94=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20(#176)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AttendanceCheckCommandPersistenceAdapter.java | 5 +++++ .../attendancecheck/AttendanceCheckJpaRepository.java | 5 +++++ .../application/port/out/AttendanceCheckCommandPort.java | 2 ++ 3 files changed, 12 insertions(+) diff --git a/src/main/java/konkuk/thip/roompost/adapter/out/persistence/AttendanceCheckCommandPersistenceAdapter.java b/src/main/java/konkuk/thip/roompost/adapter/out/persistence/AttendanceCheckCommandPersistenceAdapter.java index fa5ac8c4d..466cbceb9 100644 --- a/src/main/java/konkuk/thip/roompost/adapter/out/persistence/AttendanceCheckCommandPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/roompost/adapter/out/persistence/AttendanceCheckCommandPersistenceAdapter.java @@ -55,4 +55,9 @@ public void delete(AttendanceCheck attendanceCheck) { attendanceCheckJpaRepository.delete(attendanceCheckJpaEntity); } + + @Override + public void deleteAllByUserId(Long userId) { + attendanceCheckJpaRepository.deleteAllByUserId(userId); + } } diff --git a/src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/attendancecheck/AttendanceCheckJpaRepository.java b/src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/attendancecheck/AttendanceCheckJpaRepository.java index 4d3766ac7..c3bf53019 100644 --- a/src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/attendancecheck/AttendanceCheckJpaRepository.java +++ b/src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/attendancecheck/AttendanceCheckJpaRepository.java @@ -2,6 +2,7 @@ import konkuk.thip.roompost.adapter.out.jpa.AttendanceCheckJpaEntity; 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; @@ -22,4 +23,8 @@ public interface AttendanceCheckJpaRepository extends JpaRepository= :startOfDay " + "AND a.createdAt < :endOfDay") int countByUserIdAndRoomIdAndCreatedAtBetween(@Param("userId") Long userId, @Param("roomId") Long roomId, @Param("startOfDay") LocalDateTime startOfDay, @Param("endOfDay") LocalDateTime endOfDay); + + @Modifying(clearAutomatically = true, flushAutomatically = true) + @Query("UPDATE AttendanceCheckJpaEntity a SET a.status = 'INACTIVE' WHERE a.userJpaEntity.userId = :userId") + void deleteAllByUserId(@Param("userId") Long userId); } diff --git a/src/main/java/konkuk/thip/roompost/application/port/out/AttendanceCheckCommandPort.java b/src/main/java/konkuk/thip/roompost/application/port/out/AttendanceCheckCommandPort.java index 018d17ca0..2df49ed18 100644 --- a/src/main/java/konkuk/thip/roompost/application/port/out/AttendanceCheckCommandPort.java +++ b/src/main/java/konkuk/thip/roompost/application/port/out/AttendanceCheckCommandPort.java @@ -19,4 +19,6 @@ default AttendanceCheck getByIdOrThrow(Long id) { } void delete(AttendanceCheck attendanceCheck); + + void deleteAllByUserId(Long userId); } From 71b61424f7c97e163c23d8f890e110c02dccea2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Fri, 5 Sep 2025 01:10:05 +0900 Subject: [PATCH 03/55] =?UTF-8?q?[feat]=20=ED=9A=8C=EC=9B=90=20=ED=83=88?= =?UTF-8?q?=ED=87=B4=20=EC=8B=9C=20=EC=97=B0=EA=B4=80=EB=90=9C=20=EC=B1=85?= =?UTF-8?q?=20=EC=A0=80=EC=9E=A5=EA=B4=80=EA=B3=84=20=EC=82=AD=EC=A0=9C=20?= =?UTF-8?q?(#176)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../out/persistence/BookCommandPersistenceAdapter.java | 5 +++++ .../thip/book/application/port/out/BookCommandPort.java | 2 ++ 2 files changed, 7 insertions(+) diff --git a/src/main/java/konkuk/thip/book/adapter/out/persistence/BookCommandPersistenceAdapter.java b/src/main/java/konkuk/thip/book/adapter/out/persistence/BookCommandPersistenceAdapter.java index 4ec356b35..cf057234c 100644 --- a/src/main/java/konkuk/thip/book/adapter/out/persistence/BookCommandPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/book/adapter/out/persistence/BookCommandPersistenceAdapter.java @@ -92,4 +92,9 @@ public void deleteSavedBook(Long userId, Long bookId) { public void deleteAllByIdInBatch(Set unusedBookIds) { bookJpaRepository.deleteAllByIdInBatch(unusedBookIds); } + + @Override + public void deleteAllSavedBookByUserId(Long userId) { + savedBookJpaRepository.deleteAllByUserId(userId); + } } diff --git a/src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java b/src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java index 5c1d019db..6c6b2d60d 100644 --- a/src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java +++ b/src/main/java/konkuk/thip/book/application/port/out/BookCommandPort.java @@ -29,4 +29,6 @@ default Book getByIsbnOrThrow(String isbn){ void deleteSavedBook(Long userId, Long bookId); void deleteAllByIdInBatch(Set unusedBookIds); + + void deleteAllSavedBookByUserId(Long userId); } From 791c684c2bb212fbcd4b77c7ebd56cb42c1f5c2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Fri, 5 Sep 2025 01:11:28 +0900 Subject: [PATCH 04/55] =?UTF-8?q?[feat]=20=ED=9A=8C=EC=9B=90=20=ED=83=88?= =?UTF-8?q?=ED=87=B4=20=EC=8B=9C=20=EC=97=B0=EA=B4=80=EB=90=9C=20=EB=8C=93?= =?UTF-8?q?=EA=B8=80=20=EC=82=AD=EC=A0=9C=20=EA=B4=80=EB=A0=A8=20=EB=A9=94?= =?UTF-8?q?=EC=84=9C=EB=93=9C=20=EC=9E=91=EC=84=B1=20(#176)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CommentCommandPersistenceAdapter.java | 58 ++++++++++++++++++- .../repository/CommentJpaRepository.java | 11 ++++ .../port/out/CommentCommandPort.java | 2 + 3 files changed, 70 insertions(+), 1 deletion(-) diff --git a/src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentCommandPersistenceAdapter.java b/src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentCommandPersistenceAdapter.java index eacfca1a3..ad97e75b0 100644 --- a/src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentCommandPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentCommandPersistenceAdapter.java @@ -7,17 +7,22 @@ import konkuk.thip.comment.application.port.out.CommentCommandPort; import konkuk.thip.comment.domain.Comment; import konkuk.thip.common.exception.EntityNotFoundException; +import konkuk.thip.feed.adapter.out.jpa.FeedJpaEntity; import konkuk.thip.post.domain.PostType; import konkuk.thip.feed.adapter.out.persistence.repository.FeedJpaRepository; import konkuk.thip.post.adapter.out.jpa.PostJpaEntity; +import konkuk.thip.roompost.adapter.out.jpa.RecordJpaEntity; +import konkuk.thip.roompost.adapter.out.jpa.VoteJpaEntity; import konkuk.thip.roompost.adapter.out.persistence.repository.record.RecordJpaRepository; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; import konkuk.thip.roompost.adapter.out.persistence.repository.vote.VoteJpaRepository; import lombok.RequiredArgsConstructor; +import org.hibernate.Hibernate; import org.springframework.stereotype.Repository; -import java.util.Optional; +import java.util.*; +import java.util.stream.Collectors; import static konkuk.thip.common.exception.code.ErrorCode.*; @@ -97,4 +102,55 @@ public void softDeleteAllByPostId(Long postId) { commentJpaRepository.softDeleteAllByPostId(postId); } + @Override + public void deleteAllByUserId(Long userId) { + + // 1. 탈퇴 유저가 작성한 댓글과 연관된 게시글을 JOIN FETCH로 함께 조회 + List commentsWithPosts = commentJpaRepository.findAllCommentsWithPostsByUserId(userId); + if (commentsWithPosts == null || commentsWithPosts.isEmpty()) { + return; //early return + } + // 2. 탈퇴한 유저의 모든 댓글, 댓글의 좋아요 삭제 + commentLikeJpaRepository.deleteAllByUserId(userId); + commentJpaRepository.deleteAllByUserId(userId); + // 3. 게시글 타입별로 댓글 수 감소가 필요한 게시글 Map 생성 + Map> postsByType = new HashMap<>(); + for (CommentJpaEntity comment : commentsWithPosts) { + PostJpaEntity post = comment.getPostJpaEntity(); + post = (PostJpaEntity) Hibernate.unproxy(post); // 프록시 강제 초기화 및 타입 변경 + post.setCommentCount(Math.max(0, post.getCommentCount() - 1)); + + // 4. 엔티티에서 직접 게시글 댓글 수 감소 + PostType postType = PostType.from(post.getDtype()); + postsByType.computeIfAbsent(postType, k -> new ArrayList<>()).add(post); + } + // 5. 게시글 타입별로 저장 처리 + postsByType.forEach(this::savePostsJpaEntities); + } + + + + private void savePostsJpaEntities(PostType postType, List posts) { + switch (postType) { + case FEED: + feedJpaRepository.saveAll(posts.stream() + .filter(p -> p instanceof FeedJpaEntity) + .map(p -> (FeedJpaEntity) p) + .collect(Collectors.toList())); + break; + case RECORD: + recordJpaRepository.saveAll(posts.stream() + .filter(p -> p instanceof RecordJpaEntity) + .map(p -> (RecordJpaEntity) p) + .collect(Collectors.toList())); + break; + case VOTE: + voteJpaRepository.saveAll(posts.stream() + .filter(p -> p instanceof VoteJpaEntity) + .map(p -> (VoteJpaEntity) p) + .collect(Collectors.toList())); + voteJpaRepository.flush(); + break; + } + } } diff --git a/src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentJpaRepository.java b/src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentJpaRepository.java index aee0ebe3c..09ba881d1 100644 --- a/src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentJpaRepository.java +++ b/src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentJpaRepository.java @@ -6,6 +6,7 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import java.util.List; import java.util.Optional; public interface CommentJpaRepository extends JpaRepository, CommentQueryRepository { @@ -19,4 +20,14 @@ public interface CommentJpaRepository extends JpaRepository findAllCommentsWithPostsByUserId(@Param("userId") Long userId); + + @Modifying(clearAutomatically = true, flushAutomatically = true) + @Query("UPDATE CommentJpaEntity c SET c.status = 'INACTIVE' WHERE c.postJpaEntity.postId IN :postIds") + void softDeleteAllByPostIds(@Param("postIds") List postIds); } diff --git a/src/main/java/konkuk/thip/comment/application/port/out/CommentCommandPort.java b/src/main/java/konkuk/thip/comment/application/port/out/CommentCommandPort.java index 81eccc8ee..ac1d87bee 100644 --- a/src/main/java/konkuk/thip/comment/application/port/out/CommentCommandPort.java +++ b/src/main/java/konkuk/thip/comment/application/port/out/CommentCommandPort.java @@ -24,4 +24,6 @@ default Comment getByIdOrThrow(Long id) { void delete(Comment comment); void softDeleteAllByPostId(Long postId); + + void deleteAllByUserId(Long userId); } From 8a61d668753cf81b3535857d1ae02090e9505041 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Fri, 5 Sep 2025 01:11:45 +0900 Subject: [PATCH 05/55] =?UTF-8?q?[feat]=20=ED=9A=8C=EC=9B=90=20=ED=83=88?= =?UTF-8?q?=ED=87=B4=20=EC=8B=9C=20=EC=97=B0=EA=B4=80=EB=90=9C=20=EB=8C=93?= =?UTF-8?q?=EA=B8=80=20=EC=82=AD=EC=A0=9C=EC=8B=9C=20SETTER=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#176)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/comment/adapter/out/jpa/CommentJpaEntity.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/java/konkuk/thip/comment/adapter/out/jpa/CommentJpaEntity.java b/src/main/java/konkuk/thip/comment/adapter/out/jpa/CommentJpaEntity.java index 268fd9d29..c0cafe272 100644 --- a/src/main/java/konkuk/thip/comment/adapter/out/jpa/CommentJpaEntity.java +++ b/src/main/java/konkuk/thip/comment/adapter/out/jpa/CommentJpaEntity.java @@ -10,9 +10,6 @@ import lombok.*; import org.hibernate.annotations.SQLDelete; -import java.util.ArrayList; -import java.util.List; - @Entity @Table(name = "comments") @Getter @@ -34,6 +31,11 @@ public class CommentJpaEntity extends BaseJpaEntity { @Column(name = "report_count", nullable = false) private int reportCount = 0; + /** + * -- SETTER -- + * 회원 탈퇴용 + */ + @Setter @Builder.Default @Column(name = "like_count", nullable = false) private int likeCount = 0; From 951470a9ef0a2ef9f1611f31391db9177f3fe042 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Fri, 5 Sep 2025 01:12:12 +0900 Subject: [PATCH 06/55] =?UTF-8?q?[feat]=20=ED=9A=8C=EC=9B=90=20=ED=83=88?= =?UTF-8?q?=ED=87=B4=20=EC=8B=9C=20=EC=97=B0=EA=B4=80=EB=90=9C=20=EB=8C=93?= =?UTF-8?q?=EA=B8=80=EC=A2=8B=EC=95=84=EC=9A=94=20=EC=82=AD=EC=A0=9C=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20(#176)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CommentLikeCommandPersistenceAdapter.java | 19 +++++++++++++++ .../repository/CommentLikeJpaRepository.java | 24 +++++++++++++++++++ .../port/out/CommentLikeCommandPort.java | 1 + 3 files changed, 44 insertions(+) diff --git a/src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentLikeCommandPersistenceAdapter.java b/src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentLikeCommandPersistenceAdapter.java index 53c774908..2a446016e 100644 --- a/src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentLikeCommandPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentLikeCommandPersistenceAdapter.java @@ -11,6 +11,8 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; +import java.util.List; + import static konkuk.thip.common.exception.code.ErrorCode.*; @Repository @@ -47,4 +49,21 @@ public void deleteAllByCommentId(Long commentId) { commentLikeJpaRepository.deleteAllByCommentId(commentId); } + @Override + public void deleteAllByUserId(Long userId) { + // 1. 탈퇴 유저가 좋아요 누른 댓글 ID 리스트 조회 + List likedCommentIds = commentLikeJpaRepository.findAllCommentIdsByUserId(userId); + if (likedCommentIds == null || likedCommentIds.isEmpty()) { + return; //early return + } + // 2. 탈퇴한 유저의 모든 댓글 좋아요 관계 삭제 + commentLikeJpaRepository.deleteAllByUserId(userId); + // 3. 해당 ID들로 JPA 엔티티 직접 조회 + List commentEntities = commentJpaRepository.findAllById(likedCommentIds); + // 4. 엔티티에서 직접 좋아요 수 감소 + commentEntities.forEach(entity -> + entity.setLikeCount(Math.max(0, entity.getLikeCount() - 1))); + commentJpaRepository.saveAll(commentEntities); + } + } diff --git a/src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentLikeJpaRepository.java b/src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentLikeJpaRepository.java index 8e4d9bb35..298355cf8 100644 --- a/src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentLikeJpaRepository.java +++ b/src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentLikeJpaRepository.java @@ -40,4 +40,28 @@ WHERE cl.commentJpaEntity.commentId IN ( ) """) void deleteAllByPostId(@Param("postId") Long postId); + + @Modifying(clearAutomatically = true, flushAutomatically = true) + @Query(""" + DELETE FROM CommentLikeJpaEntity cl + WHERE cl.commentJpaEntity.commentId IN ( + SELECT c.commentId FROM CommentJpaEntity c + WHERE c.userJpaEntity.userId = :userId + ) + """) + void deleteAllByUserId(@Param("userId") Long userId); + + @Modifying(clearAutomatically = true, flushAutomatically = true) + @Query("SELECT cl.commentJpaEntity.commentId FROM CommentLikeJpaEntity cl WHERE cl.userJpaEntity.userId = :userId") + List findAllCommentIdsByUserId(@Param("userId") Long userId); + + @Modifying(clearAutomatically = true, flushAutomatically = true) + @Query(""" + DELETE FROM CommentLikeJpaEntity cl + WHERE cl.commentJpaEntity.commentId IN ( + SELECT c.commentId FROM CommentJpaEntity c + WHERE c.postJpaEntity.postId IN :postIds + ) + """) + void deleteAllByPostIds(@Param("postIds") List postIds); } diff --git a/src/main/java/konkuk/thip/comment/application/port/out/CommentLikeCommandPort.java b/src/main/java/konkuk/thip/comment/application/port/out/CommentLikeCommandPort.java index d776cef26..6303235d8 100644 --- a/src/main/java/konkuk/thip/comment/application/port/out/CommentLikeCommandPort.java +++ b/src/main/java/konkuk/thip/comment/application/port/out/CommentLikeCommandPort.java @@ -5,4 +5,5 @@ public interface CommentLikeCommandPort { void save(Long userId, Long commentId); void delete(Long userId, Long commentId); void deleteAllByCommentId(Long commentId); + void deleteAllByUserId(Long userId); } From 7357f80c2b053461d7745c03aceb154e2c0fd898 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Fri, 5 Sep 2025 01:12:26 +0900 Subject: [PATCH 07/55] =?UTF-8?q?[feat]=20=ED=9A=8C=EC=9B=90=20=ED=83=88?= =?UTF-8?q?=ED=87=B4=20=EA=B4=80=EB=A0=A8=20=EC=97=90=EB=9F=AC=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80=20(#176)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/konkuk/thip/common/exception/code/ErrorCode.java | 3 +++ .../thip/common/swagger/SwaggerResponseDescription.java | 7 ++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java b/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java index 7e340d759..67952fb2f 100644 --- a/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java +++ b/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java @@ -23,6 +23,7 @@ public enum ErrorCode implements ResponseCode { AUTH_LOGIN_FAILED(HttpStatus.UNAUTHORIZED, 40104, "로그인에 실패했습니다."), AUTH_UNSUPPORTED_SOCIAL_LOGIN(HttpStatus.UNAUTHORIZED, 40105, "지원하지 않는 소셜 로그인입니다."), AUTH_INVALID_LOGIN_TOKEN_KEY(HttpStatus.UNAUTHORIZED, 40106, "유효하지 않은 로그인 토큰 키입니다."), + AUTH_BLACKLIST_TOKEN(HttpStatus.UNAUTHORIZED, 40107, "블랙리스트에 등록된 토큰입니다."), JSON_PROCESSING_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, 50100, "JSON 직렬화/역직렬화에 실패했습니다."), AWS_BUCKET_BASE_URL_NOT_CONFIGURED(HttpStatus.INTERNAL_SERVER_ERROR, 50101, "aws s3 bucket base url 설정이 누락되었습니다."), @@ -48,6 +49,8 @@ public enum ErrorCode implements ResponseCode { USER_NICKNAME_ALREADY_EXISTS(HttpStatus.BAD_REQUEST, 70006, "다른 사용자가 이미 사용중인 닉네임입니다."), USER_ALREADY_SIGNED_UP(HttpStatus.BAD_REQUEST, 70007, "이미 가입된 사용자입니다."), USER_NOT_SIGNED_UP(HttpStatus.BAD_REQUEST, 70008, "가입되지 않은 사용자입니다."), + USER_CANNOT_DELETE_ROOM_HOST(HttpStatus.BAD_REQUEST, 70009, "모집/진행 중인 방의 host는 회원탈퇴를 할 수 없습니다."), + USER_ALREADY_DELETED(HttpStatus.BAD_REQUEST, 70010, "이미 삭제된 유저 입니다."), /** * 75000 : follow error diff --git a/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java b/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java index a8d26d610..ea51190bf 100644 --- a/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java +++ b/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java @@ -35,7 +35,11 @@ public enum SwaggerResponseDescription { USER_NICKNAME_UPDATE_TOO_FREQUENT, USER_NICKNAME_ALREADY_EXISTS ))), - + USER_DELETE(new LinkedHashSet<>(Set.of( + USER_CANNOT_DELETE_ROOM_HOST, + USER_NOT_FOUND, + USER_ALREADY_DELETED + ))), // Follow CHANGE_FOLLOW_STATE(new LinkedHashSet<>(Set.of( @@ -348,6 +352,7 @@ public enum SwaggerResponseDescription { // AUTH_TOKEN_NOT_FOUND // AUTH_LOGIN_FAILED, // AUTH_UNSUPPORTED_SOCIAL_LOGIN, +// AUTH_BLACKLIST_TOKEN, // JSON_PROCESSING_ERROR ))); From aee4840226d20dadda629a27659a4bea8bef4313 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Fri, 5 Sep 2025 01:13:52 +0900 Subject: [PATCH 08/55] =?UTF-8?q?[feat]=20=ED=9A=8C=EC=9B=90=20=ED=83=88?= =?UTF-8?q?=ED=87=B4=20=EC=8B=9C=20=EC=97=B0=EA=B4=80=EB=90=9C=20=ED=94=BC?= =?UTF-8?q?=EB=93=9C=20=EC=82=AD=EC=A0=9C=20=EA=B4=80=EB=A0=A8=20=EB=A9=94?= =?UTF-8?q?=EC=84=9C=EB=93=9C=20=EC=9E=91=EC=84=B1=20(#176)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FeedCommandPersistenceAdapter.java | 34 +++++++++++++++++++ .../repository/FeedJpaRepository.java | 10 ++++++ .../application/port/out/FeedCommandPort.java | 2 ++ 3 files changed, 46 insertions(+) diff --git a/src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedCommandPersistenceAdapter.java b/src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedCommandPersistenceAdapter.java index 792b63b55..1df36700b 100644 --- a/src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedCommandPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedCommandPersistenceAdapter.java @@ -2,6 +2,8 @@ import konkuk.thip.book.adapter.out.jpa.BookJpaEntity; import konkuk.thip.book.adapter.out.persistence.repository.BookJpaRepository; +import konkuk.thip.comment.adapter.out.persistence.repository.CommentJpaRepository; +import konkuk.thip.comment.adapter.out.persistence.repository.CommentLikeJpaRepository; import konkuk.thip.common.exception.EntityNotFoundException; import konkuk.thip.feed.adapter.out.jpa.*; import konkuk.thip.feed.adapter.out.mapper.FeedMapper; @@ -9,11 +11,13 @@ import konkuk.thip.feed.adapter.out.persistence.repository.SavedFeedJpaRepository; import konkuk.thip.feed.application.port.out.FeedCommandPort; import konkuk.thip.feed.domain.Feed; +import konkuk.thip.post.adapter.out.persistence.PostLikeJpaRepository; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; +import java.util.List; import java.util.Optional; import static konkuk.thip.common.exception.code.ErrorCode.*; @@ -27,6 +31,10 @@ public class FeedCommandPersistenceAdapter implements FeedCommandPort { private final BookJpaRepository bookJpaRepository; private final SavedFeedJpaRepository savedFeedJpaRepository; + private final CommentJpaRepository commentJpaRepository; + private final CommentLikeJpaRepository commentLikeJpaRepository; + private final PostLikeJpaRepository postLikeJpaRepository; + private final FeedMapper feedMapper; @Override @@ -80,6 +88,32 @@ public void deleteSavedFeed(Long userId, Long feedId) { savedFeedJpaRepository.deleteByUserIdAndFeedId(userId, feedId); } + @Override + public void deleteAllSavedFeedByUserId(Long userId) { + savedFeedJpaRepository.deleteAllByUserId(userId); + } + + @Override + public void deleteAllFeedByUserId(Long userId) { + + // 1. 유저가 작성한 피드 게시글 ID 리스트 조회 + List feedIds = feedJpaRepository.findFeedIdsByUserId(userId); + // 1. 유저가 작성한 피드 게시글 ID 리스트 조회 + if (feedIds == null || feedIds.isEmpty()) { + return; // early return + } + // 2-1. 댓글 좋아요 일괄 삭제 + commentLikeJpaRepository.deleteAllByPostIds(feedIds); + // 2-2. 댓글 soft delete 일괄 처리 + commentJpaRepository.softDeleteAllByPostIds(feedIds); + // 3. 게시글 좋아요 일괄 삭제 + postLikeJpaRepository.deleteAllByPostIds(feedIds); + // 4. 피드 저장 일괄 삭제 + savedFeedJpaRepository.deleteAllByFeedIds(feedIds); + // 5. 탈퇴한 유저가 작성한 피드 게시글 일괄 삭제 + feedJpaRepository.deleteAllByUserId(userId); + } + @Override public void delete(Feed feed) { FeedJpaEntity feedJpaEntity = feedJpaRepository.findByPostId(feed.getId()) diff --git a/src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedJpaRepository.java b/src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedJpaRepository.java index a46ee9d21..0c315f241 100644 --- a/src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedJpaRepository.java +++ b/src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedJpaRepository.java @@ -2,9 +2,11 @@ import konkuk.thip.feed.adapter.out.jpa.FeedJpaEntity; 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; public interface FeedJpaRepository extends JpaRepository, FeedQueryRepository { @@ -19,4 +21,12 @@ public interface FeedJpaRepository extends JpaRepository, F @Query("SELECT COUNT(f) FROM FeedJpaEntity f WHERE f.userJpaEntity.userId = :userId AND f.isPublic = TRUE") long countPublicFeedsByUserId(@Param("userId") Long userId); + + @Query("SELECT f.postId FROM FeedJpaEntity f WHERE f.userJpaEntity.userId = :userId") + List findFeedIdsByUserId(@Param("userId") Long userId); + + @Modifying(clearAutomatically = true, flushAutomatically = true) + @Query("UPDATE FeedJpaEntity f SET f.status = 'INACTIVE' WHERE f.userJpaEntity.userId = :userId") + void deleteAllByUserId(@Param("userId") Long userId); + } diff --git a/src/main/java/konkuk/thip/feed/application/port/out/FeedCommandPort.java b/src/main/java/konkuk/thip/feed/application/port/out/FeedCommandPort.java index 40dea9122..1839a4b5c 100644 --- a/src/main/java/konkuk/thip/feed/application/port/out/FeedCommandPort.java +++ b/src/main/java/konkuk/thip/feed/application/port/out/FeedCommandPort.java @@ -19,4 +19,6 @@ default Feed getByIdOrThrow(Long id) { void delete(Feed feed); void saveSavedFeed(Long userId, Long feedId); void deleteSavedFeed(Long userId, Long feedId); + void deleteAllSavedFeedByUserId(Long userId); + void deleteAllFeedByUserId(Long userId); } From 5b4d0f14b6ff0488ebc59a44dadba47ccd075b4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Fri, 5 Sep 2025 01:14:20 +0900 Subject: [PATCH 09/55] =?UTF-8?q?[feat]=20=ED=9A=8C=EC=9B=90=20=ED=83=88?= =?UTF-8?q?=ED=87=B4=20=EC=8B=9C=20=EC=97=B0=EA=B4=80=EB=90=9C=20=ED=94=BC?= =?UTF-8?q?=EB=93=9C=20=EC=82=AD=EC=A0=9C=20=EA=B4=80=EB=A0=A8=20=EB=A9=94?= =?UTF-8?q?=EC=84=9C=EB=93=9C=20=EC=9E=91=EC=84=B1=20(#176)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FollowingCommandPersistenceAdapter.java | 21 ++++++++++++++++++- .../following/FollowingJpaRepository.java | 15 +++++++++++-- .../port/out/FollowingCommandPort.java | 3 +++ 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingCommandPersistenceAdapter.java b/src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingCommandPersistenceAdapter.java index 2d7ce7cee..7f79c5fc9 100644 --- a/src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingCommandPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingCommandPersistenceAdapter.java @@ -13,7 +13,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; -import java.util.Optional; +import java.util.*; import static konkuk.thip.common.exception.code.ErrorCode.FOLLOW_NOT_FOUND; import static konkuk.thip.common.exception.code.ErrorCode.USER_NOT_FOUND; @@ -54,6 +54,25 @@ public void deleteFollowing(Following following, User targetUser) { followingJpaRepository.delete(followingJpaEntity); } + @Override + public void deleteAllByUserId(Long userId) { + + // 1. 탈퇴 유저가 팔로우 중인 유저들 ID 조회 + List targetUserIds = followingJpaRepository.findAllTargetUserIdsByUserId(userId); + // 2. 탈퇴한 유저의 모든 팔로잉 관계 삭제 + followingJpaRepository.deleteAllByUserIdOrFollowingUserId(userId); + if (targetUserIds == null || targetUserIds.isEmpty()) { + return; //early return + } + // 3. 해당 ID들로 JPA 엔티티 직접 조회 + List userEntities = userJpaRepository.findAllById(targetUserIds); + // 4. 엔티티에서 직접 팔로워 수 감소 + userEntities.forEach(entity -> + entity.setFollowerCount(Math.max(0, entity.getFollowerCount() - 1))); + userJpaRepository.saveAll(userEntities); + } + + private UserJpaEntity updateUserFollowerCount(User targetUser) { UserJpaEntity userJpaEntity = userJpaRepository.findByUserId(targetUser.getId()).orElseThrow( () -> new EntityNotFoundException(USER_NOT_FOUND) diff --git a/src/main/java/konkuk/thip/user/adapter/out/persistence/repository/following/FollowingJpaRepository.java b/src/main/java/konkuk/thip/user/adapter/out/persistence/repository/following/FollowingJpaRepository.java index 0ceae15ab..2914e0451 100644 --- a/src/main/java/konkuk/thip/user/adapter/out/persistence/repository/following/FollowingJpaRepository.java +++ b/src/main/java/konkuk/thip/user/adapter/out/persistence/repository/following/FollowingJpaRepository.java @@ -1,16 +1,27 @@ package konkuk.thip.user.adapter.out.persistence.repository.following; +import io.lettuce.core.dynamic.annotation.Param; import konkuk.thip.user.adapter.out.jpa.FollowingJpaEntity; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; +import java.util.List; + @Repository public interface FollowingJpaRepository extends JpaRepository, FollowingQueryRepository { @Query("SELECT COUNT(f) > 0 FROM FollowingJpaEntity f WHERE f.userJpaEntity.userId = :userId AND f.followingUserJpaEntity.userId = :followingUserId") - boolean existsByUserIdAndFollowingUserId(Long userId, Long followingUserId); + boolean existsByUserIdAndFollowingUserId(@Param("userId") Long userId, @Param("followingUserId") Long followingUserId); @Query("SELECT COUNT(f) FROM FollowingJpaEntity f WHERE f.userJpaEntity.userId = :userId") - int countFollowingByUserId(Long userId); + int countFollowingByUserId(@Param("userId") Long userId); + + @Modifying(clearAutomatically = true, flushAutomatically = true) + @Query("DELETE FROM FollowingJpaEntity f WHERE f.userJpaEntity.userId = :userId OR f.followingUserJpaEntity.userId = :userId") + void deleteAllByUserIdOrFollowingUserId(@Param("userId") Long userId); + + @Query("SELECT f.followingUserJpaEntity.userId FROM FollowingJpaEntity f WHERE f.userJpaEntity.userId = :userId") + List findAllTargetUserIdsByUserId(@Param("userId") Long userId); } diff --git a/src/main/java/konkuk/thip/user/application/port/out/FollowingCommandPort.java b/src/main/java/konkuk/thip/user/application/port/out/FollowingCommandPort.java index c126b73ce..0dff90fd7 100644 --- a/src/main/java/konkuk/thip/user/application/port/out/FollowingCommandPort.java +++ b/src/main/java/konkuk/thip/user/application/port/out/FollowingCommandPort.java @@ -19,4 +19,7 @@ default Following getByUserIdAndTargetUserIdOrThrow(Long userId, Long targetUser void save(Following following, User targetUser); void deleteFollowing(Following following, User targetUser); + + void deleteAllByUserId(Long userId); + } From c9c7b1f8a73cf5ea46b5ae27704f8dba716c637d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Fri, 5 Sep 2025 01:14:45 +0900 Subject: [PATCH 10/55] =?UTF-8?q?[feat]=20=ED=9A=8C=EC=9B=90=20=ED=83=88?= =?UTF-8?q?=ED=87=B4=20=EC=8B=9C=20=EC=97=B0=EA=B4=80=EB=90=9C=20=EA=B2=8C?= =?UTF-8?q?=EC=8B=9C=EB=AC=BC=20=EC=82=AD=EC=A0=9C=EC=8B=9C=20SETTER=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(#176)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/post/adapter/out/jpa/PostJpaEntity.java | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/main/java/konkuk/thip/post/adapter/out/jpa/PostJpaEntity.java b/src/main/java/konkuk/thip/post/adapter/out/jpa/PostJpaEntity.java index d542bb2eb..e463ff697 100644 --- a/src/main/java/konkuk/thip/post/adapter/out/jpa/PostJpaEntity.java +++ b/src/main/java/konkuk/thip/post/adapter/out/jpa/PostJpaEntity.java @@ -1,16 +1,13 @@ package konkuk.thip.post.adapter.out.jpa; import jakarta.persistence.*; -import konkuk.thip.comment.adapter.out.jpa.CommentJpaEntity; import konkuk.thip.common.entity.BaseJpaEntity; import konkuk.thip.common.exception.InvalidStateException; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; - -import java.util.ArrayList; -import java.util.List; +import lombok.Setter; import static konkuk.thip.common.entity.StatusType.INACTIVE; import static konkuk.thip.common.exception.code.ErrorCode.POST_ALREADY_DELETED; @@ -31,8 +28,18 @@ public abstract class PostJpaEntity extends BaseJpaEntity { @Column(length = 6100, nullable = false) protected String content; + /** + * -- SETTER -- + * 회원 탈퇴용 + */ + @Setter protected Integer likeCount = 0; + /** + * -- SETTER -- + * 회원 탈퇴용 + */ + @Setter protected Integer commentCount = 0; // type 구분을 위한 조회용 컬럼 From cdaa6fd8dd8f79b72160e8e31be5562a5a82897e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Fri, 5 Sep 2025 01:15:26 +0900 Subject: [PATCH 11/55] =?UTF-8?q?[feat]=20=ED=9A=8C=EC=9B=90=20=ED=83=88?= =?UTF-8?q?=ED=87=B4=20=EC=8B=9C=20=EC=97=B0=EA=B4=80=EB=90=9C=20=EA=B2=8C?= =?UTF-8?q?=EC=8B=9C=EB=AC=BC=20=EC=A2=8B=EC=95=84=EC=9A=94=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20=EA=B4=80=EB=A0=A8=20=EB=A9=94=EC=84=9C=EB=93=9C=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=20(#176)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PostLikeCommandPersistenceAdapter.java | 56 +++++++++++++++++++ .../persistence/PostLikeJpaRepository.java | 13 +++++ .../port/out/PostLikeCommandPort.java | 1 + 3 files changed, 70 insertions(+) diff --git a/src/main/java/konkuk/thip/post/adapter/out/persistence/PostLikeCommandPersistenceAdapter.java b/src/main/java/konkuk/thip/post/adapter/out/persistence/PostLikeCommandPersistenceAdapter.java index e4f9d2057..87eba05c5 100644 --- a/src/main/java/konkuk/thip/post/adapter/out/persistence/PostLikeCommandPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/post/adapter/out/persistence/PostLikeCommandPersistenceAdapter.java @@ -1,11 +1,14 @@ package konkuk.thip.post.adapter.out.persistence; import konkuk.thip.common.exception.EntityNotFoundException; +import konkuk.thip.feed.adapter.out.jpa.FeedJpaEntity; import konkuk.thip.post.domain.PostType; import konkuk.thip.feed.adapter.out.persistence.repository.FeedJpaRepository; import konkuk.thip.post.adapter.out.jpa.PostJpaEntity; import konkuk.thip.post.adapter.out.mapper.PostLikeMapper; import konkuk.thip.post.application.port.out.PostLikeCommandPort; +import konkuk.thip.roompost.adapter.out.jpa.RecordJpaEntity; +import konkuk.thip.roompost.adapter.out.jpa.VoteJpaEntity; import konkuk.thip.roompost.adapter.out.persistence.repository.record.RecordJpaRepository; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; @@ -13,6 +16,12 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + import static konkuk.thip.common.exception.code.ErrorCode.*; import static konkuk.thip.common.exception.code.ErrorCode.RECORD_NOT_FOUND; import static konkuk.thip.common.exception.code.ErrorCode.VOTE_NOT_FOUND; @@ -49,6 +58,30 @@ public void deleteAllByPostId(Long postId) { postLikeJpaRepository.deleteAllByPostId(postId); } + @Override + public void deleteAllByUserId(Long userId) { + + // 1. 탈퇴 유저가 좋아요한 게시글을 JOIN 조회 + List likedPosts = postLikeJpaRepository.findAllPostsWithTypeByUserId(userId); + if (likedPosts == null || likedPosts.isEmpty()) { + return; // early return + } + // 2. 탈퇴한 유저의 모든 게시글 좋아요 삭제 + postLikeJpaRepository.deleteAllByUserId(userId); + + // 3. 게시글 타입별로 좋아요 수 감소가 필요한 게시글 Map 생성 + Map> postsByType = new HashMap<>(); + + for (PostJpaEntity post : likedPosts) { + // 4. 엔티티에서 직접 게시글 좋아요 수 감소 + post.setLikeCount(Math.max(0, post.getLikeCount() - 1)); + + PostType postType = PostType.from(post.getDtype()); + postsByType.computeIfAbsent(postType, k -> new ArrayList<>()).add(post); + } + // 5. 게시글 타입별로 저장 처리 + postsByType.forEach(this::savePostsJpaEntities); + } private PostJpaEntity findPostJpaEntity(PostType postType, Long postId) { return switch (postType) { @@ -60,4 +93,27 @@ private PostJpaEntity findPostJpaEntity(PostType postType, Long postId) { .orElseThrow(() -> new EntityNotFoundException(VOTE_NOT_FOUND)); }; } + + private void savePostsJpaEntities(PostType postType, List posts) { + switch (postType) { + case FEED: + feedJpaRepository.saveAll(posts.stream() + .filter(p -> p instanceof FeedJpaEntity) + .map(p -> (FeedJpaEntity) p) + .collect(Collectors.toList())); + break; + case RECORD: + recordJpaRepository.saveAll(posts.stream() + .filter(p -> p instanceof RecordJpaEntity) + .map(p -> (RecordJpaEntity) p) + .collect(Collectors.toList())); + break; + case VOTE: + voteJpaRepository.saveAll(posts.stream() + .filter(p -> p instanceof VoteJpaEntity) + .map(p -> (VoteJpaEntity) p) + .collect(Collectors.toList())); + break; + } + } } diff --git a/src/main/java/konkuk/thip/post/adapter/out/persistence/PostLikeJpaRepository.java b/src/main/java/konkuk/thip/post/adapter/out/persistence/PostLikeJpaRepository.java index 8b291e553..6eeee5f44 100644 --- a/src/main/java/konkuk/thip/post/adapter/out/persistence/PostLikeJpaRepository.java +++ b/src/main/java/konkuk/thip/post/adapter/out/persistence/PostLikeJpaRepository.java @@ -1,5 +1,6 @@ package konkuk.thip.post.adapter.out.persistence; +import konkuk.thip.post.adapter.out.jpa.PostJpaEntity; import konkuk.thip.post.adapter.out.jpa.PostLikeJpaEntity; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; @@ -7,6 +8,7 @@ import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import java.util.List; import java.util.Set; @Repository @@ -28,4 +30,15 @@ Set findPostIdsLikedByUser(@Param("postIds") Set postIds, @Modifying(clearAutomatically = true, flushAutomatically = true) @Query("DELETE FROM PostLikeJpaEntity pl WHERE pl.postJpaEntity.postId = :postId") void deleteAllByPostId(@Param("postId") Long postId); + + @Query("SELECT pl.postJpaEntity FROM PostLikeJpaEntity pl JOIN pl.postJpaEntity WHERE pl.userJpaEntity.userId = :userId") + List findAllPostsWithTypeByUserId(@Param("userId") Long userId); + + @Modifying(clearAutomatically = true, flushAutomatically = true) + @Query("DELETE FROM PostLikeJpaEntity pl WHERE pl.userJpaEntity.userId = :userId") + void deleteAllByUserId(@Param("userId") Long userId); + + @Modifying(clearAutomatically = true, flushAutomatically = true) + @Query("DELETE FROM PostLikeJpaEntity pl WHERE pl.postJpaEntity.postId IN :postIds") + void deleteAllByPostIds(@Param("postIds") List postIds); } diff --git a/src/main/java/konkuk/thip/post/application/port/out/PostLikeCommandPort.java b/src/main/java/konkuk/thip/post/application/port/out/PostLikeCommandPort.java index 6c6421d14..28830badf 100644 --- a/src/main/java/konkuk/thip/post/application/port/out/PostLikeCommandPort.java +++ b/src/main/java/konkuk/thip/post/application/port/out/PostLikeCommandPort.java @@ -6,4 +6,5 @@ public interface PostLikeCommandPort { void save(Long userId, Long postId, PostType postType); void delete(Long userId, Long postId); void deleteAllByPostId(Long postId); + void deleteAllByUserId(Long userId); } From a294788245e4bc6ee673a4b121633374b3b76d28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Fri, 5 Sep 2025 01:15:39 +0900 Subject: [PATCH 12/55] =?UTF-8?q?[feat]=20=ED=9A=8C=EC=9B=90=20=ED=83=88?= =?UTF-8?q?=ED=87=B4=20=EC=8B=9C=20=EC=97=B0=EA=B4=80=EB=90=9C=20=EC=B5=9C?= =?UTF-8?q?=EA=B7=BC=EA=B2=80=EC=83=89=EC=96=B4=20=EC=82=AD=EC=A0=9C=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20(#176)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../persistence/RecentSearchCommandPersistenceAdapter.java | 5 +++++ .../persistence/repository/RecentSearchJpaRepository.java | 4 ++++ .../application/port/out/RecentSearchCommandPort.java | 2 ++ 3 files changed, 11 insertions(+) diff --git a/src/main/java/konkuk/thip/recentSearch/adapter/out/persistence/RecentSearchCommandPersistenceAdapter.java b/src/main/java/konkuk/thip/recentSearch/adapter/out/persistence/RecentSearchCommandPersistenceAdapter.java index d57ca22a4..e7245f24f 100644 --- a/src/main/java/konkuk/thip/recentSearch/adapter/out/persistence/RecentSearchCommandPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/recentSearch/adapter/out/persistence/RecentSearchCommandPersistenceAdapter.java @@ -56,5 +56,10 @@ public void touch(RecentSearch recentSearch) { recentSearchJpaRepository.updateModifiedAt(recentSearch.getId()); } + @Override + public void deleteAllByUserId(Long userId) { + recentSearchJpaRepository.deleteAllByUserId(userId); + } + } diff --git a/src/main/java/konkuk/thip/recentSearch/adapter/out/persistence/repository/RecentSearchJpaRepository.java b/src/main/java/konkuk/thip/recentSearch/adapter/out/persistence/repository/RecentSearchJpaRepository.java index 654b18972..6e7c23ded 100644 --- a/src/main/java/konkuk/thip/recentSearch/adapter/out/persistence/repository/RecentSearchJpaRepository.java +++ b/src/main/java/konkuk/thip/recentSearch/adapter/out/persistence/repository/RecentSearchJpaRepository.java @@ -12,4 +12,8 @@ public interface RecentSearchJpaRepository extends JpaRepository Date: Fri, 5 Sep 2025 01:16:01 +0900 Subject: [PATCH 13/55] =?UTF-8?q?[feat]=20=ED=9A=8C=EC=9B=90=20=ED=83=88?= =?UTF-8?q?=ED=87=B4=20=EC=8B=9C=20=EC=97=B0=EA=B4=80=EB=90=9C=20=EA=B8=B0?= =?UTF-8?q?=EB=A1=9D=20=EC=82=AD=EC=A0=9C=20=EA=B4=80=EB=A0=A8=20=EB=A9=94?= =?UTF-8?q?=EC=84=9C=EB=93=9C=20=EC=9E=91=EC=84=B1=20(#176)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../RecordCommandPersistenceAdapter.java | 26 +++++++++++++++++++ .../record/RecordJpaRepository.java | 11 ++++++++ .../port/out/RecordCommandPort.java | 2 ++ .../service/RecordDeleteService.java | 1 - 4 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/main/java/konkuk/thip/roompost/adapter/out/persistence/RecordCommandPersistenceAdapter.java b/src/main/java/konkuk/thip/roompost/adapter/out/persistence/RecordCommandPersistenceAdapter.java index b327f9c1a..a2ae445ba 100644 --- a/src/main/java/konkuk/thip/roompost/adapter/out/persistence/RecordCommandPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/roompost/adapter/out/persistence/RecordCommandPersistenceAdapter.java @@ -1,6 +1,9 @@ package konkuk.thip.roompost.adapter.out.persistence; +import konkuk.thip.comment.adapter.out.persistence.repository.CommentJpaRepository; +import konkuk.thip.comment.adapter.out.persistence.repository.CommentLikeJpaRepository; import konkuk.thip.common.exception.EntityNotFoundException; +import konkuk.thip.post.adapter.out.persistence.PostLikeJpaRepository; import konkuk.thip.roompost.adapter.out.jpa.RecordJpaEntity; import konkuk.thip.roompost.adapter.out.mapper.RecordMapper; import konkuk.thip.roompost.adapter.out.persistence.repository.record.RecordJpaRepository; @@ -13,6 +16,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; +import java.util.List; import java.util.Optional; import static konkuk.thip.common.exception.code.ErrorCode.*; @@ -24,6 +28,11 @@ public class RecordCommandPersistenceAdapter implements RecordCommandPort { private final RecordJpaRepository recordJpaRepository; private final UserJpaRepository userJpaRepository; private final RoomJpaRepository roomJpaRepository; + + private final CommentJpaRepository commentJpaRepository; + private final CommentLikeJpaRepository commentLikeJpaRepository; + private final PostLikeJpaRepository postLikeJpaRepository; + private final RecordMapper recordMapper; @Override @@ -57,6 +66,23 @@ public void delete(Record record) { recordJpaRepository.save(recordJpaEntity); } + @Override + public void deleteAllByUserId(Long userId) { + // 1. 유저가 작성한 피드 게시글 ID 리스트 조회 + List recordIds = recordJpaRepository.findRecordIdsByUserId(userId); + if (recordIds == null || recordIds.isEmpty()) { + return; // early return + } + // 2-1. 댓글 좋아요 일괄 삭제 + commentLikeJpaRepository.deleteAllByPostIds(recordIds); + // 2-2. 댓글 soft delete 일괄 처리 + commentJpaRepository.softDeleteAllByPostIds(recordIds); + // 3. 게시글 좋아요 일괄 삭제 + postLikeJpaRepository.deleteAllByPostIds(recordIds); + // 4. 탈퇴한 유저가 작성한 기록 게시글 일괄 삭제 + recordJpaRepository.deleteAllByUserId(userId); + } + @Override public void update(Record record) { RecordJpaEntity recordJpaEntity = recordJpaRepository.findByPostId(record.getId()).orElseThrow( diff --git a/src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/record/RecordJpaRepository.java b/src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/record/RecordJpaRepository.java index 7f684dca8..a5c45b286 100644 --- a/src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/record/RecordJpaRepository.java +++ b/src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/record/RecordJpaRepository.java @@ -2,7 +2,11 @@ import konkuk.thip.roompost.adapter.out.jpa.RecordJpaEntity; 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; public interface RecordJpaRepository extends JpaRepository, RecordQueryRepository { @@ -11,4 +15,11 @@ public interface RecordJpaRepository extends JpaRepository findByPostId(Long postId); + + @Query("SELECT r.postId FROM RecordJpaEntity r WHERE r.userJpaEntity.userId = :userId") + List findRecordIdsByUserId(@Param("userId") Long userId); + + @Modifying(clearAutomatically = true, flushAutomatically = true) + @Query("UPDATE RecordJpaEntity r SET r.status = 'INACTIVE' WHERE r.userJpaEntity.userId = :userId") + void deleteAllByUserId(@Param("userId") Long userId); } diff --git a/src/main/java/konkuk/thip/roompost/application/port/out/RecordCommandPort.java b/src/main/java/konkuk/thip/roompost/application/port/out/RecordCommandPort.java index 16b5ceb9a..505a769ce 100644 --- a/src/main/java/konkuk/thip/roompost/application/port/out/RecordCommandPort.java +++ b/src/main/java/konkuk/thip/roompost/application/port/out/RecordCommandPort.java @@ -22,4 +22,6 @@ default Record getByIdOrThrow(Long id) { } void delete(Record record); + + void deleteAllByUserId(Long userId); } diff --git a/src/main/java/konkuk/thip/roompost/application/service/RecordDeleteService.java b/src/main/java/konkuk/thip/roompost/application/service/RecordDeleteService.java index 4388472c0..f84c3f007 100644 --- a/src/main/java/konkuk/thip/roompost/application/service/RecordDeleteService.java +++ b/src/main/java/konkuk/thip/roompost/application/service/RecordDeleteService.java @@ -43,7 +43,6 @@ public Long deleteRecord(RecordDeleteCommand command) { // 3-3. 기록 삭제 recordCommandPort.delete(record); - //TODO// 4. 유저 방 진행도 업데이트 return command.roomId(); } } From c3ffa0581d1b2e036bc0a114f52a8ce98a060bb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Fri, 5 Sep 2025 01:16:15 +0900 Subject: [PATCH 14/55] =?UTF-8?q?[feat]=20=ED=9A=8C=EC=9B=90=20=ED=83=88?= =?UTF-8?q?=ED=87=B4=20=EC=8B=9C=20=EC=97=B0=EA=B4=80=EB=90=9C=20=EB=B0=A9?= =?UTF-8?q?=EC=B0=B8=EC=97=AC=EA=B4=80=EA=B3=84=20=EC=82=AD=EC=A0=9C?= =?UTF-8?q?=EC=8B=9C=20SETTER=20=EC=B6=94=EA=B0=80=20(#176)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/konkuk/thip/room/adapter/out/jpa/RoomJpaEntity.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/konkuk/thip/room/adapter/out/jpa/RoomJpaEntity.java b/src/main/java/konkuk/thip/room/adapter/out/jpa/RoomJpaEntity.java index 0d0666352..35866f880 100644 --- a/src/main/java/konkuk/thip/room/adapter/out/jpa/RoomJpaEntity.java +++ b/src/main/java/konkuk/thip/room/adapter/out/jpa/RoomJpaEntity.java @@ -46,6 +46,11 @@ public class RoomJpaEntity extends BaseJpaEntity { @Column(name = "recruit_count",nullable = false) private int recruitCount; + /** + * -- SETTER -- + * 회원 탈퇴용 + */ + @Setter @Builder.Default @Column(name = "member_count",nullable = false) private int memberCount = 1; From da887ca0e3f1fd6db5790615d27380d9b34c45e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Fri, 5 Sep 2025 01:16:46 +0900 Subject: [PATCH 15/55] =?UTF-8?q?[feat]=20=ED=9A=8C=EC=9B=90=20=ED=83=88?= =?UTF-8?q?=ED=87=B4=20=EC=8B=9C=20=EC=97=B0=EA=B4=80=EB=90=9C=20=EB=B0=A9?= =?UTF-8?q?=20=EC=B0=B8=EC=97=AC=EA=B4=80=EA=B3=84=20=EC=82=AD=EC=A0=9C=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20(#176)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...mParticipantCommandPersistenceAdapter.java | 37 +++++++++++++++++++ .../repository/RoomJpaRepository.java | 3 ++ .../RoomParticipantJpaRepository.java | 15 ++++++++ .../port/out/RoomParticipantCommandPort.java | 5 +++ 4 files changed, 60 insertions(+) diff --git a/src/main/java/konkuk/thip/room/adapter/out/persistence/RoomParticipantCommandPersistenceAdapter.java b/src/main/java/konkuk/thip/room/adapter/out/persistence/RoomParticipantCommandPersistenceAdapter.java index 620cbbaec..2dd246590 100644 --- a/src/main/java/konkuk/thip/room/adapter/out/persistence/RoomParticipantCommandPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/room/adapter/out/persistence/RoomParticipantCommandPersistenceAdapter.java @@ -70,6 +70,43 @@ public void update(RoomParticipant roomParticipant) { roomParticipantJpaRepository.save(roomParticipantJpaEntity); } + @Override + public boolean existsHostUserInActiveRoom(Long userId) { + return roomParticipantJpaRepository.existsHostUserInActiveRoom(userId); + } + + @Override + public void deleteAllByUserId(Long userId) { + + // 1. 유저가 참여한 방 ID 리스트 조회 + List roomIds = roomParticipantJpaRepository.findRoomIdsByUserId(userId); + if (roomIds.isEmpty()) { + return; // early return + } + // 2. 유저의 모든 방 참여 관계 일괄 삭제 + roomParticipantJpaRepository.deleteAllByUserId(userId); + // 3. 해당 ID들로 JPA 엔티티 직접 조회 + List roomJpaEntities = roomJpaRepository.findAllByIds(roomIds); + + // 4. 유저가 탈퇴 처리된 각 방에 대해 진행률 및 멤버 수 업데이트 + for (RoomJpaEntity room : roomJpaEntities) { + // 현재 방의 참가자 전체 조회 (탈퇴한 유저는 이미 삭제됨) + List roomParticipantEntities = roomParticipantJpaRepository.findAllByRoomId(room.getRoomId()); + + // 평균 진행률 계산 + double totalProgress = roomParticipantEntities.stream() + .mapToDouble(RoomParticipantJpaEntity::getUserPercentage) + .sum(); + double avgProgress = roomParticipantEntities.isEmpty() ? 0.0 : (totalProgress / roomParticipantEntities.size()); + + // 방 정보(진행률, 멤버수) 업데이트 + room.updateRoomPercentage(avgProgress); + room.setMemberCount(roomParticipantEntities.size()); // 남은 참가자 수로 설정 + + } + roomJpaRepository.saveAll(roomJpaEntities); + } + @Override public Optional findByUserIdAndRoomIdOptional(Long userId, Long roomId) { return roomParticipantJpaRepository.findByUserIdAndRoomId(userId, roomId) diff --git a/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomJpaRepository.java b/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomJpaRepository.java index cf88790eb..bd110e1dc 100644 --- a/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomJpaRepository.java +++ b/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomJpaRepository.java @@ -6,6 +6,7 @@ import org.springframework.data.repository.query.Param; import java.time.LocalDate; +import java.util.List; import java.util.Optional; public interface RoomJpaRepository extends JpaRepository, RoomQueryRepository { @@ -20,4 +21,6 @@ public interface RoomJpaRepository extends JpaRepository, R "AND r.startDate > :currentDate") int countActiveRoomsByBookIdAndStartDateAfter(@Param("isbn") String isbn, @Param("currentDate") LocalDate currentDate); + @Query("SELECT r FROM RoomJpaEntity r WHERE r.roomId IN :roomIds") + List findAllByIds(@Param("roomIds") List roomIds); } diff --git a/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/roomparticipant/RoomParticipantJpaRepository.java b/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/roomparticipant/RoomParticipantJpaRepository.java index 22284a4d6..cd9ef1e3a 100644 --- a/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/roomparticipant/RoomParticipantJpaRepository.java +++ b/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/roomparticipant/RoomParticipantJpaRepository.java @@ -2,6 +2,7 @@ import konkuk.thip.room.adapter.out.jpa.RoomParticipantJpaEntity; 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; @@ -30,4 +31,18 @@ public interface RoomParticipantJpaRepository extends JpaRepository 0 THEN true ELSE false END " + + "FROM RoomParticipantJpaEntity rp " + + "JOIN RoomJpaEntity r ON rp.roomJpaEntity.roomId = r.roomId " + + "WHERE rp.userJpaEntity.userId = :userId " + + "AND rp.roomParticipantRole = 'HOST' " + + "AND (r.roomStatus = 'IN_PROGRESS' OR r.roomStatus = 'RECRUITING')") + boolean existsHostUserInActiveRoom(@Param("userId") Long userId); + + @Modifying(clearAutomatically = true, flushAutomatically = true) + @Query("UPDATE RoomParticipantJpaEntity rp SET rp.status = 'INACTIVE' WHERE rp.userJpaEntity.userId = :userId") + void deleteAllByUserId(@Param("userId") Long userId); + + @Query("SELECT rp.roomJpaEntity.roomId FROM RoomParticipantJpaEntity rp WHERE rp.userJpaEntity.userId = :userId") + List findRoomIdsByUserId(@Param("userId") Long userId); } diff --git a/src/main/java/konkuk/thip/room/application/port/out/RoomParticipantCommandPort.java b/src/main/java/konkuk/thip/room/application/port/out/RoomParticipantCommandPort.java index 519a66e43..81d93696f 100644 --- a/src/main/java/konkuk/thip/room/application/port/out/RoomParticipantCommandPort.java +++ b/src/main/java/konkuk/thip/room/application/port/out/RoomParticipantCommandPort.java @@ -24,4 +24,9 @@ default RoomParticipant getByUserIdAndRoomIdOrThrow(Long userId, Long roomId) { void deleteByUserIdAndRoomId(Long userId, Long roomId); void update(RoomParticipant roomParticipant); + + boolean existsHostUserInActiveRoom(Long userId); + + void deleteAllByUserId(Long userId); + } From 186e6a05a54f6322dddce2485f43d260a1ea4713 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Fri, 5 Sep 2025 01:18:31 +0900 Subject: [PATCH 16/55] =?UTF-8?q?[refactor]=20=EA=B8=B0=EB=A1=9D=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=EC=8B=9C=20=ED=88=AC=ED=91=9C=ED=95=AD?= =?UTF-8?q?=EB=AA=A9=20=EB=93=9D=ED=91=9C=EC=88=98=20=EB=B3=B4=EC=97=AC?= =?UTF-8?q?=EC=A3=BC=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95=20(#176)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../in/web/response/RoomPostSearchResponse.java | 6 +++--- .../application/service/RoomPostSearchService.java | 11 +---------- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/src/main/java/konkuk/thip/roompost/adapter/in/web/response/RoomPostSearchResponse.java b/src/main/java/konkuk/thip/roompost/adapter/in/web/response/RoomPostSearchResponse.java index 89add3a2f..ceaab0dde 100644 --- a/src/main/java/konkuk/thip/roompost/adapter/in/web/response/RoomPostSearchResponse.java +++ b/src/main/java/konkuk/thip/roompost/adapter/in/web/response/RoomPostSearchResponse.java @@ -34,11 +34,11 @@ public record RoomPostSearchDto( public record VoteItemDto( Long voteItemId, String itemName, - int percentage, + int count, boolean isVoted ) { - public static VoteItemDto of(Long voteItemId, String itemName, int percentage, boolean isVoted) { - return new VoteItemDto(voteItemId, itemName, percentage, isVoted); + public static VoteItemDto of(Long voteItemId, String itemName, int count, boolean isVoted) { + return new VoteItemDto(voteItemId, itemName, count, isVoted); } } } diff --git a/src/main/java/konkuk/thip/roompost/application/service/RoomPostSearchService.java b/src/main/java/konkuk/thip/roompost/application/service/RoomPostSearchService.java index 8ff8e82af..2caf740a0 100644 --- a/src/main/java/konkuk/thip/roompost/application/service/RoomPostSearchService.java +++ b/src/main/java/konkuk/thip/roompost/application/service/RoomPostSearchService.java @@ -20,7 +20,6 @@ import konkuk.thip.room.domain.RoomParticipant; import konkuk.thip.roompost.application.port.out.VoteQueryPort; import konkuk.thip.roompost.application.port.out.dto.VoteItemQueryDto; -import konkuk.thip.roompost.domain.VoteItem; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -144,20 +143,12 @@ private List getVoteItemDt // VoteItemQueryDto 목록을 RecordSearchResponse.PostDto.VoteItemDto 목록으로 변환하는 메서드 private List mapToVoteItemDtos(List items, boolean isLocked) { - // voteCount를 모아 리스트로 변환 - List counts = items.stream() - .map(VoteItemQueryDto::voteCount) - .toList(); - - // 도메인에게 계산 위임 - List percentages = VoteItem.calculatePercentages(counts); - // 계산 결과를 이용해 DTO 조립 return IntStream.range(0, items.size()) .mapToObj(i -> RoomPostSearchResponse.RoomPostSearchDto.VoteItemDto.of( items.get(i).voteItemId(), isLocked ? roomPostAccessValidator.createBlurredString(items.get(i).itemName()) : items.get(i).itemName(), - percentages.get(i), + items.get(i).voteCount(), items.get(i).isVoted() )) .toList(); From 42418eaa4ac32dbefe78b5be6c37684cd8f395a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Fri, 5 Sep 2025 01:18:46 +0900 Subject: [PATCH 17/55] =?UTF-8?q?[feat]=20=ED=9A=8C=EC=9B=90=20=ED=83=88?= =?UTF-8?q?=ED=87=B4=20=EC=8B=9C=20=EC=97=B0=EA=B4=80=EB=90=9C=20=EC=B1=85?= =?UTF-8?q?=20=EC=A0=80=EC=9E=A5=EA=B4=80=EA=B3=84=20=EC=82=AD=EC=A0=9C=20?= =?UTF-8?q?=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=B6=94=EA=B0=80(#176)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../persistence/repository/SavedBookJpaRepository.java | 6 +++++- .../persistence/repository/SavedFeedJpaRepository.java | 8 ++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/main/java/konkuk/thip/book/adapter/out/persistence/repository/SavedBookJpaRepository.java b/src/main/java/konkuk/thip/book/adapter/out/persistence/repository/SavedBookJpaRepository.java index 6038ecc7d..8bd1d2415 100644 --- a/src/main/java/konkuk/thip/book/adapter/out/persistence/repository/SavedBookJpaRepository.java +++ b/src/main/java/konkuk/thip/book/adapter/out/persistence/repository/SavedBookJpaRepository.java @@ -13,5 +13,9 @@ public interface SavedBookJpaRepository extends JpaRepository 0 THEN true ELSE false END FROM SavedFeedJpaEntity s " + "WHERE s.userJpaEntity.userId = :userId AND s.feedJpaEntity.postId = :feedId") boolean existsByUserIdAndFeedId(@Param("userId") Long userId, @Param("feedId") Long feedId); + + @Modifying(clearAutomatically = true, flushAutomatically = true) + @Query("DELETE FROM SavedFeedJpaEntity sf WHERE sf.feedJpaEntity.postId IN :feedIds") + void deleteAllByFeedIds(@Param("feedIds") List feedIds); } From e5af5063345ea3100c8ebcb92d443b2f49c6dfc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Fri, 5 Sep 2025 01:19:06 +0900 Subject: [PATCH 18/55] =?UTF-8?q?[feat]=20=ED=9A=8C=EC=9B=90=20=ED=83=88?= =?UTF-8?q?=ED=87=B4=20=EC=8B=9C=20oauth2Id=EB=B3=80=EA=B2=BD=20=EB=A9=94?= =?UTF-8?q?=EC=84=9C=EB=93=9C=20=EC=9C=A0=EC=A0=80=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=EC=97=90=20=EC=B6=94=EA=B0=80=20(#176)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/konkuk/thip/user/domain/User.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/konkuk/thip/user/domain/User.java b/src/main/java/konkuk/thip/user/domain/User.java index d6df512f8..8e103d208 100644 --- a/src/main/java/konkuk/thip/user/domain/User.java +++ b/src/main/java/konkuk/thip/user/domain/User.java @@ -75,4 +75,10 @@ private void validateCanUpdateNickname(String nickname) { } } + public void markAsDeleted() { + if (this.oauth2Id != null && !this.oauth2Id.startsWith("deleted:")) { + this.oauth2Id = "deleted:" + this.oauth2Id; + } + } + } From 42b4f739f95f88cfc4a108727cc67630cd9e5f6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Fri, 5 Sep 2025 01:19:25 +0900 Subject: [PATCH 19/55] =?UTF-8?q?[feat]=20=ED=9A=8C=EC=9B=90=20=ED=83=88?= =?UTF-8?q?=ED=87=B4=20=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20(#176)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../adapter/in/web/UserCommandController.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/main/java/konkuk/thip/user/adapter/in/web/UserCommandController.java b/src/main/java/konkuk/thip/user/adapter/in/web/UserCommandController.java index ecca0b84a..e5f9044b4 100644 --- a/src/main/java/konkuk/thip/user/adapter/in/web/UserCommandController.java +++ b/src/main/java/konkuk/thip/user/adapter/in/web/UserCommandController.java @@ -5,6 +5,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import konkuk.thip.common.dto.BaseResponse; +import konkuk.thip.common.security.annotation.AuthToken; import konkuk.thip.common.security.annotation.Oauth2Id; import konkuk.thip.common.security.annotation.UserId; import konkuk.thip.common.swagger.annotation.ExceptionDescription; @@ -13,6 +14,7 @@ import konkuk.thip.user.adapter.in.web.request.UserUpdateRequest; import konkuk.thip.user.adapter.in.web.response.UserFollowResponse; import konkuk.thip.user.adapter.in.web.response.UserSignupResponse; +import konkuk.thip.user.application.port.in.UserDeleteUseCase; import konkuk.thip.user.application.port.in.UserFollowUsecase; import konkuk.thip.user.application.port.in.UserSignupUseCase; import konkuk.thip.user.application.port.in.UserUpdateUseCase; @@ -29,6 +31,7 @@ public class UserCommandController { private final UserSignupUseCase userSignupUseCase; private final UserFollowUsecase userFollowUsecase; private final UserUpdateUseCase userUpdateUseCase; + private final UserDeleteUseCase userDeleteUseCase; @Operation( @@ -74,4 +77,17 @@ public BaseResponse updateUser( userUpdateUseCase.updateUser(userUpdateRequest.toCommand(userId)); return BaseResponse.ok(null); } + + @Operation( + summary = "회원 탈퇴", + description = "사용자가 회원탈퇴를 합니다. 사용자와 관련된 정보가 모두 삭제되고," + + "회원탈퇴한 사용자의 토큰을 블랙리스트에 추가하여 서비스 재진입을 막습니다." + ) + @ExceptionDescription(USER_DELETE) + @DeleteMapping("/users") + public BaseResponse deleteUser(@Parameter(hidden = true) @UserId final Long userId, + @Parameter(hidden = true) @AuthToken final String authToken) { + userDeleteUseCase.deleteUser(userId,authToken); + return BaseResponse.ok(null); + } } From 6c85c3cebbce10a970e2beb7a9e5dd8316e26740 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Fri, 5 Sep 2025 01:19:36 +0900 Subject: [PATCH 20/55] =?UTF-8?q?[feat]=20=ED=9A=8C=EC=9B=90=20=ED=83=88?= =?UTF-8?q?=ED=87=B4=20=EC=9C=A0=EC=A6=88=EC=BC=80=EC=9D=B4=EC=8A=A4=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=20(#176)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/user/application/port/in/UserDeleteUseCase.java | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 src/main/java/konkuk/thip/user/application/port/in/UserDeleteUseCase.java diff --git a/src/main/java/konkuk/thip/user/application/port/in/UserDeleteUseCase.java b/src/main/java/konkuk/thip/user/application/port/in/UserDeleteUseCase.java new file mode 100644 index 000000000..d84641187 --- /dev/null +++ b/src/main/java/konkuk/thip/user/application/port/in/UserDeleteUseCase.java @@ -0,0 +1,5 @@ +package konkuk.thip.user.application.port.in; + +public interface UserDeleteUseCase { + void deleteUser(Long userId, String authToken); +} From 1451f164ea7ac768e80652d513bf8d7342be4454 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Fri, 5 Sep 2025 01:19:55 +0900 Subject: [PATCH 21/55] =?UTF-8?q?[feat]=20=ED=9A=8C=EC=9B=90=20=ED=83=88?= =?UTF-8?q?=ED=87=B4=20=EC=9C=A0=EC=A6=88=EC=BC=80=EC=9D=B4=EC=8A=A4=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=EC=B2=B4=20=EC=84=9C=EB=B9=84=EC=8A=A4?= =?UTF-8?q?=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=9E=91=EC=84=B1=20(#176)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/UserDeleteService.java | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 src/main/java/konkuk/thip/user/application/service/UserDeleteService.java diff --git a/src/main/java/konkuk/thip/user/application/service/UserDeleteService.java b/src/main/java/konkuk/thip/user/application/service/UserDeleteService.java new file mode 100644 index 000000000..3bfc9879a --- /dev/null +++ b/src/main/java/konkuk/thip/user/application/service/UserDeleteService.java @@ -0,0 +1,106 @@ +package konkuk.thip.user.application.service; + +import konkuk.thip.book.application.port.out.BookCommandPort; +import konkuk.thip.comment.application.port.out.CommentCommandPort; +import konkuk.thip.comment.application.port.out.CommentLikeCommandPort; +import konkuk.thip.common.exception.BusinessException; +import konkuk.thip.feed.application.port.out.FeedCommandPort; +import konkuk.thip.post.application.port.out.PostLikeCommandPort; +import konkuk.thip.recentSearch.application.port.out.RecentSearchCommandPort; +import konkuk.thip.room.application.port.out.RoomParticipantCommandPort; +import konkuk.thip.roompost.application.port.out.AttendanceCheckCommandPort; +import konkuk.thip.roompost.application.port.out.RecordCommandPort; +import konkuk.thip.roompost.application.port.out.VoteCommandPort; +import konkuk.thip.user.application.port.UserTokenBlacklistCommandPort; +import konkuk.thip.user.application.port.in.UserDeleteUseCase; +import konkuk.thip.user.application.port.out.FollowingCommandPort; +import konkuk.thip.user.application.port.out.UserCommandPort; +import konkuk.thip.user.domain.User; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import static konkuk.thip.common.exception.code.ErrorCode.USER_CANNOT_DELETE_ROOM_HOST; + +@Service +@RequiredArgsConstructor +public class UserDeleteService implements UserDeleteUseCase { + + private final UserCommandPort userCommandPort; + private final FollowingCommandPort followingCommandPort; + private final FeedCommandPort feedCommandPort; + private final BookCommandPort bookCommandPort; + private final VoteCommandPort voteCommandPort; + private final CommentCommandPort commentCommandPort; + private final PostLikeCommandPort postLikeCommandPort; + private final RecordCommandPort recordCommandPort; + private final CommentLikeCommandPort commentLikeCommandPort; + private final RecentSearchCommandPort recentSearchCommandPort; + private final AttendanceCheckCommandPort attendanceCheckCommandPort; + private final RoomParticipantCommandPort roomParticipantCommandPort; + + private final UserTokenBlacklistCommandPort userTokenBlacklistCommandPort; + + @Override + @Transactional + public void deleteUser(Long userId, String authToken) { + + // 1. 진행/모집 중인 방의 호스트일경우 회원탈퇴 불가 + boolean isHostInActiveRoom = roomParticipantCommandPort.existsHostUserInActiveRoom(userId); + if (isHostInActiveRoom) { + throw new BusinessException(USER_CANNOT_DELETE_ROOM_HOST); + } + + // 2. 유저 조회 및 검증 + User user = userCommandPort.findById(userId); + user.markAsDeleted(); + + // 3. 유저가 남긴 관련 정보들 삭제 + // 팔로잉 관계 삭제 + // 유저가 팔로워인 팔로잉 관계 -> 유저가 팔로우 중인 유저들의 팔로워 수 감소 -> 관계 삭제 + // 유저를 팔로우 하는 팔로잉 관계 -> 관계 삭제 + followingCommandPort.deleteAllByUserId(userId); + // 최근검색어 삭제 + recentSearchCommandPort.deleteAllByUserId(userId); + // 알림 삭제 // TODO 알림구현 적용되면 수정 + // notificationCommandPort.deleteAllByUserId(userId); + // 책/피드 저장 관계 삭제 + feedCommandPort.deleteAllSavedFeedByUserId(userId); + bookCommandPort.deleteAllSavedBookByUserId(userId); + // 오늘의 한마디 관계 삭제 + attendanceCheckCommandPort.deleteAllByUserId(userId); + // 투표 참여 관계 삭제 -> 투표한 항목의 득표 수 감소 + voteCommandPort.deleteAllVoteParticipantByUserId(userId); + // 댓글 좋아요 삭제 -> 댓글의 좋아요 수 감소 + commentLikeCommandPort.deleteAllByUserId(userId); + // 댓글 삭제 -> 게시글의 댓글 수 감소, 댓글의 좋아요 삭제 + commentCommandPort.deleteAllByUserId(userId); + // 게시글 좋아요 삭제 -> 게시글의 좋아요 수 감소 + postLikeCommandPort.deleteAllByUserId(userId); + + // 피드 삭제 + // 댓글 삭제 -> 댓글의 좋아요 삭제 + // 좋아요 삭제 + // 피드 저장관계 삭제 + feedCommandPort.deleteAllFeedByUserId(userId); + // 기록 삭제 + // 댓글 삭제 -> 댓글의 좋아요 삭제 + // 좋아요 삭제 + recordCommandPort.deleteAllByUserId(userId); + // 투표 삭제 + // 댓글 삭제 -> 댓글의 좋아요 삭제 + // 좋아요 삭제 + // 투표 참여 관계 일괄 삭제 -> 투표 항목 일괄 삭제 + voteCommandPort.deleteAllVoteByUserId(userId); + + // 방 참여 관계 삭제 (member는 진행/모집/만료, host는 만료) + // 방 멤버수 감소, 방 진행도 업데이트 + roomParticipantCommandPort.deleteAllByUserId(userId); + + // 유저 삭제 + userCommandPort.delete(user); + // 토큰 블랙리스트 추가 + userTokenBlacklistCommandPort.addTokenToBlacklist(authToken); + } + +} From c678717d36539ee3de233f69dde0fb4f00846c60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Fri, 5 Sep 2025 01:20:24 +0900 Subject: [PATCH 22/55] =?UTF-8?q?[feat]=20=ED=9A=8C=EC=9B=90=20=ED=83=88?= =?UTF-8?q?=ED=87=B4=20=EC=8B=9C=20softDelete=EB=A9=94=EC=84=9C=EB=93=9C?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80=20(#176)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/adapter/out/jpa/UserJpaEntity.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/main/java/konkuk/thip/user/adapter/out/jpa/UserJpaEntity.java b/src/main/java/konkuk/thip/user/adapter/out/jpa/UserJpaEntity.java index 1acef21e1..01b2941e8 100644 --- a/src/main/java/konkuk/thip/user/adapter/out/jpa/UserJpaEntity.java +++ b/src/main/java/konkuk/thip/user/adapter/out/jpa/UserJpaEntity.java @@ -3,6 +3,7 @@ import jakarta.persistence.*; import konkuk.thip.common.entity.BaseJpaEntity; +import konkuk.thip.common.exception.InvalidStateException; import konkuk.thip.user.domain.value.Alias; import konkuk.thip.user.domain.User; import lombok.*; @@ -10,6 +11,9 @@ import java.time.LocalDate; +import static konkuk.thip.common.entity.StatusType.INACTIVE; +import static konkuk.thip.common.exception.code.ErrorCode.POST_ALREADY_DELETED; + @Entity @Table(name = "users") @Getter @@ -33,6 +37,11 @@ public class UserJpaEntity extends BaseJpaEntity { @Column(name = "oauth2_id", length = 50, nullable = false) private String oauth2Id; + /** + * -- SETTER -- + * 회원 탈퇴용 + */ + @Setter @Builder.Default private Integer followerCount = 0; // 팔로워 수 @@ -58,4 +67,13 @@ public void updateFrom(User user) { this.role = UserRole.from(user.getUserRole()); this.followerCount = user.getFollowerCount(); } + + public void softDelete(User user) { + if(this.status.equals(INACTIVE)){ + throw new InvalidStateException(POST_ALREADY_DELETED); + } + this.status = INACTIVE; + this.oauth2Id = user.getOauth2Id(); + } + } From 1e8aefc25cbccdcd49696cfe983bdbe10c56c764 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Fri, 5 Sep 2025 01:20:42 +0900 Subject: [PATCH 23/55] =?UTF-8?q?[feat]=20=ED=9A=8C=EC=9B=90=20=ED=83=88?= =?UTF-8?q?=ED=87=B4=20=EC=8B=9C=20=EC=9C=A0=EC=A0=80=EC=82=AD=EC=A0=9C=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EB=A9=94=EC=84=9C=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#176)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../out/persistence/UserCommandPersistenceAdapter.java | 10 ++++++++++ .../user/application/port/out/UserCommandPort.java | 1 + 2 files changed, 11 insertions(+) diff --git a/src/main/java/konkuk/thip/user/adapter/out/persistence/UserCommandPersistenceAdapter.java b/src/main/java/konkuk/thip/user/adapter/out/persistence/UserCommandPersistenceAdapter.java index d141c6808..774ae5481 100644 --- a/src/main/java/konkuk/thip/user/adapter/out/persistence/UserCommandPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/user/adapter/out/persistence/UserCommandPersistenceAdapter.java @@ -55,4 +55,14 @@ public void update(User user) { userJpaEntity.updateIncludeAliasFrom(user); userJpaRepository.save(userJpaEntity); } + + @Override + public void delete(User user) { + UserJpaEntity userJpaEntity = userJpaRepository.findByUserId(user.getId()).orElseThrow( + () -> new EntityNotFoundException(USER_NOT_FOUND) + ); + + userJpaEntity.softDelete(user); + userJpaRepository.save(userJpaEntity); + } } diff --git a/src/main/java/konkuk/thip/user/application/port/out/UserCommandPort.java b/src/main/java/konkuk/thip/user/application/port/out/UserCommandPort.java index 3d8fd2793..ed45f3593 100644 --- a/src/main/java/konkuk/thip/user/application/port/out/UserCommandPort.java +++ b/src/main/java/konkuk/thip/user/application/port/out/UserCommandPort.java @@ -11,4 +11,5 @@ public interface UserCommandPort { User findById(Long userId); Map findByIds(List userIds); void update(User user); + void delete(User user); } From d50f730939b593bdff9614b0ed751d95b2cb7a5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Fri, 5 Sep 2025 01:21:11 +0900 Subject: [PATCH 24/55] =?UTF-8?q?[feat]=20=ED=86=A0=ED=81=B0=20=EC=B6=94?= =?UTF-8?q?=EC=B6=9C=20=EC=96=B4=EB=85=B8=ED=85=8C=EC=9D=B4=EC=85=98=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=20(#176)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/security/annotation/AuthToken.java | 10 +++++ .../AuthTokenArgumentResolver.java | 41 +++++++++++++++++++ .../security/constant/AuthParameters.java | 1 + 3 files changed, 52 insertions(+) create mode 100644 src/main/java/konkuk/thip/common/security/annotation/AuthToken.java create mode 100644 src/main/java/konkuk/thip/common/security/argument_resolver/AuthTokenArgumentResolver.java diff --git a/src/main/java/konkuk/thip/common/security/annotation/AuthToken.java b/src/main/java/konkuk/thip/common/security/annotation/AuthToken.java new file mode 100644 index 000000000..1506d1ac9 --- /dev/null +++ b/src/main/java/konkuk/thip/common/security/annotation/AuthToken.java @@ -0,0 +1,10 @@ +package konkuk.thip.common.security.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +public @interface AuthToken {} diff --git a/src/main/java/konkuk/thip/common/security/argument_resolver/AuthTokenArgumentResolver.java b/src/main/java/konkuk/thip/common/security/argument_resolver/AuthTokenArgumentResolver.java new file mode 100644 index 000000000..a9e9a767f --- /dev/null +++ b/src/main/java/konkuk/thip/common/security/argument_resolver/AuthTokenArgumentResolver.java @@ -0,0 +1,41 @@ +package konkuk.thip.common.security.argument_resolver; + +import jakarta.servlet.http.HttpServletRequest; +import konkuk.thip.common.exception.AuthException; +import konkuk.thip.common.security.annotation.AuthToken; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.MethodParameter; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +import static konkuk.thip.common.exception.code.ErrorCode.AUTH_TOKEN_NOT_FOUND; + +@Component +@Slf4j +@RequiredArgsConstructor +public class AuthTokenArgumentResolver implements HandlerMethodArgumentResolver { + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(AuthToken.class) + && parameter.getParameterType().equals(String.class); + } + + @Override + public String resolveArgument(MethodParameter parameter, + ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, + WebDataBinderFactory binderFactory) { + + Object token = ((HttpServletRequest) webRequest.getNativeRequest()).getAttribute("token"); + log.info("ArgumentResolver에서 토큰 추출: {}", token); + if (token == null) { + throw new AuthException(AUTH_TOKEN_NOT_FOUND); + } + return (String) token; + } +} diff --git a/src/main/java/konkuk/thip/common/security/constant/AuthParameters.java b/src/main/java/konkuk/thip/common/security/constant/AuthParameters.java index 397c6c576..688986eef 100644 --- a/src/main/java/konkuk/thip/common/security/constant/AuthParameters.java +++ b/src/main/java/konkuk/thip/common/security/constant/AuthParameters.java @@ -12,6 +12,7 @@ public enum AuthParameters { GOOGLE_PROVIDER_ID_KEY("sub"), JWT_ACCESS_TOKEN_KEY("userId"), JWT_SIGNUP_TOKEN_KEY("oauth2Id"), + JWT_TOKEN_ATTRIBUTE("token"), REDIRECT_SIGNUP_URL("/signup"), REDIRECT_HOME_URL("/feed"), From 6da045e3cc275e43db1a1a62d7a9c93ad7ea00d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Fri, 5 Sep 2025 01:21:31 +0900 Subject: [PATCH 25/55] =?UTF-8?q?[feat]=20=ED=95=84=ED=84=B0=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=ED=86=A0=ED=81=B0=20=EB=93=B1=EB=A1=9D/=20?= =?UTF-8?q?=EB=B8=94=EB=9E=99=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=ED=86=A0?= =?UTF-8?q?=ED=81=B0=20=EA=B2=80=EC=A6=9D=20=EB=A1=9C=EC=A7=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#176)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/security/filter/JwtAuthenticationFilter.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/main/java/konkuk/thip/common/security/filter/JwtAuthenticationFilter.java b/src/main/java/konkuk/thip/common/security/filter/JwtAuthenticationFilter.java index 32f8fd290..47cabf33b 100644 --- a/src/main/java/konkuk/thip/common/security/filter/JwtAuthenticationFilter.java +++ b/src/main/java/konkuk/thip/common/security/filter/JwtAuthenticationFilter.java @@ -8,6 +8,7 @@ import konkuk.thip.common.security.oauth2.CustomOAuth2User; import konkuk.thip.common.security.oauth2.LoginUser; import konkuk.thip.common.security.util.JwtUtil; +import konkuk.thip.user.application.port.UserTokenBlacklistQueryPort; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; @@ -27,6 +28,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { private final JwtUtil jwtUtil; + private final UserTokenBlacklistQueryPort userTokenBlacklistQueryPort; @Override protected void doFilterInternal(HttpServletRequest request, @@ -38,6 +40,10 @@ protected void doFilterInternal(HttpServletRequest request, throw new AuthException(AUTH_TOKEN_NOT_FOUND); } + if (userTokenBlacklistQueryPort.isTokenBlacklisted(token)) { + throw new AuthException(AUTH_BLACKLIST_TOKEN); + } + if (!jwtUtil.validateToken(token)) { throw new AuthException(AUTH_INVALID_TOKEN); } @@ -46,6 +52,7 @@ protected void doFilterInternal(HttpServletRequest request, throw new AuthException(AUTH_EXPIRED_TOKEN); } + request.setAttribute(JWT_TOKEN_ATTRIBUTE.getValue(), token); LoginUser loginUser = jwtUtil.getLoginUser(token); if (loginUser.userId() != null) { From 28dbba4541ae1dddef745c06ce8e89df836d485d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Fri, 5 Sep 2025 01:21:44 +0900 Subject: [PATCH 26/55] =?UTF-8?q?[feat]=20=ED=86=A0=ED=81=B0=20=EC=9C=A0?= =?UTF-8?q?=ED=9A=A8=EA=B8=B0=EA=B0=84=20=EC=B6=94=EC=B6=9C=20=EB=A9=94?= =?UTF-8?q?=EC=84=9C=EB=93=9C=20=EC=9E=91=EC=84=B1=20(#176)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/common/security/util/JwtUtil.java | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/main/java/konkuk/thip/common/security/util/JwtUtil.java b/src/main/java/konkuk/thip/common/security/util/JwtUtil.java index 90024d7a2..8be9688a2 100644 --- a/src/main/java/konkuk/thip/common/security/util/JwtUtil.java +++ b/src/main/java/konkuk/thip/common/security/util/JwtUtil.java @@ -1,9 +1,6 @@ package konkuk.thip.common.security.util; -import io.jsonwebtoken.ExpiredJwtException; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.MalformedJwtException; -import io.jsonwebtoken.UnsupportedJwtException; +import io.jsonwebtoken.*; import konkuk.thip.common.security.oauth2.LoginUser; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; @@ -86,4 +83,17 @@ public LoginUser getLoginUser(String token) { } return LoginUser.createExistingUser(oauth2Id, userId); } + + public Date getExpirationAllowExpired(String token) { + try { + Jws jwt = Jwts.parser() + .verifyWith(secretKey) + .build() + .parseSignedClaims(token); + return jwt.getPayload().getExpiration(); + } catch (ExpiredJwtException e) { + return e.getClaims().getExpiration(); + } + } + } From 4f43183b8103e303f2c125dc2aedc0cfac99d84e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Fri, 5 Sep 2025 01:22:07 +0900 Subject: [PATCH 27/55] =?UTF-8?q?[feat]=20=EB=B8=94=EB=9E=99=EB=A6=AC?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=ED=86=A0=ED=81=B0=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EB=A0=88=EB=94=94=EC=8A=A4=20=EC=96=B4=EB=8C=91=ED=84=B0=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(#176)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../UserTokenBlacklistRedisAdapter.java | 52 +++++++++++++++++++ .../port/UserTokenBlacklistCommandPort.java | 5 ++ .../port/UserTokenBlacklistQueryPort.java | 5 ++ 3 files changed, 62 insertions(+) create mode 100644 src/main/java/konkuk/thip/user/adapter/out/persistence/UserTokenBlacklistRedisAdapter.java create mode 100644 src/main/java/konkuk/thip/user/application/port/UserTokenBlacklistCommandPort.java create mode 100644 src/main/java/konkuk/thip/user/application/port/UserTokenBlacklistQueryPort.java diff --git a/src/main/java/konkuk/thip/user/adapter/out/persistence/UserTokenBlacklistRedisAdapter.java b/src/main/java/konkuk/thip/user/adapter/out/persistence/UserTokenBlacklistRedisAdapter.java new file mode 100644 index 000000000..0f2a27c32 --- /dev/null +++ b/src/main/java/konkuk/thip/user/adapter/out/persistence/UserTokenBlacklistRedisAdapter.java @@ -0,0 +1,52 @@ +package konkuk.thip.user.adapter.out.persistence; + +import konkuk.thip.common.security.util.JwtUtil; +import konkuk.thip.user.application.port.UserTokenBlacklistCommandPort; +import konkuk.thip.user.application.port.UserTokenBlacklistQueryPort; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +import java.util.Date; + +@Component +@RequiredArgsConstructor +public class UserTokenBlacklistRedisAdapter implements UserTokenBlacklistQueryPort, UserTokenBlacklistCommandPort { + + private final RedisTemplate redisTemplate; + + @Value("${app.redis.token-blacklist-prefix}") + private String blacklistPrefix; + + private final JwtUtil jwtUtil; + + /** + * 블랙리스트에 토큰 추가 (토큰 만료시간에 맞춰 자동 소멸) + */ + @Override + public void addTokenToBlacklist(String token) { + Date expiration = jwtUtil.getExpirationAllowExpired(token); + String key = makeBlacklistKey(token); + redisTemplate.opsForValue().set(key, "BLACKLISTED"); + // 토큰 만료시각으로 Redis에 expireAt 지정 (만료 이후 Redis에서 자동 삭제) + redisTemplate.expireAt(key, expiration.toInstant()); + } + + /** + * 토큰이 블랙리스트에 등록되어있는지 체크 + */ + @Override + public boolean isTokenBlacklisted(String token) { + String key = makeBlacklistKey(token); + return redisTemplate.hasKey(key); + } + + /** + * JWT 블랙리스트 Redis Key 생성 규칙 + */ + private String makeBlacklistKey(String token) { + return blacklistPrefix + ":" + token; + } + +} diff --git a/src/main/java/konkuk/thip/user/application/port/UserTokenBlacklistCommandPort.java b/src/main/java/konkuk/thip/user/application/port/UserTokenBlacklistCommandPort.java new file mode 100644 index 000000000..0105ddb1e --- /dev/null +++ b/src/main/java/konkuk/thip/user/application/port/UserTokenBlacklistCommandPort.java @@ -0,0 +1,5 @@ +package konkuk.thip.user.application.port; + +public interface UserTokenBlacklistCommandPort { + void addTokenToBlacklist(String token); +} diff --git a/src/main/java/konkuk/thip/user/application/port/UserTokenBlacklistQueryPort.java b/src/main/java/konkuk/thip/user/application/port/UserTokenBlacklistQueryPort.java new file mode 100644 index 000000000..570a69017 --- /dev/null +++ b/src/main/java/konkuk/thip/user/application/port/UserTokenBlacklistQueryPort.java @@ -0,0 +1,5 @@ +package konkuk.thip.user.application.port; + +public interface UserTokenBlacklistQueryPort { + boolean isTokenBlacklisted(String token); +} From 4b7030171af020ba08d3cdb38193cbae3da9a3a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Fri, 5 Sep 2025 01:22:34 +0900 Subject: [PATCH 28/55] =?UTF-8?q?[feat]=20=ED=9A=8C=EC=9B=90=20=ED=83=88?= =?UTF-8?q?=ED=87=B4=20=EC=8B=9C=20=EC=97=B0=EA=B4=80=EB=90=9C=20=ED=88=AC?= =?UTF-8?q?=ED=91=9C=20=EC=82=AD=EC=A0=9C=20=EA=B4=80=EB=A0=A8=20=EB=A9=94?= =?UTF-8?q?=EC=84=9C=EB=93=9C=20=EC=9E=91=EC=84=B1=20(#176)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../VoteCommandPersistenceAdapter.java | 46 +++++++++++++++++++ .../application/port/out/VoteCommandPort.java | 4 ++ .../service/VoteDeleteService.java | 1 - 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/src/main/java/konkuk/thip/roompost/adapter/out/persistence/VoteCommandPersistenceAdapter.java b/src/main/java/konkuk/thip/roompost/adapter/out/persistence/VoteCommandPersistenceAdapter.java index 3a753a395..428cc02b3 100644 --- a/src/main/java/konkuk/thip/roompost/adapter/out/persistence/VoteCommandPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/roompost/adapter/out/persistence/VoteCommandPersistenceAdapter.java @@ -1,6 +1,9 @@ package konkuk.thip.roompost.adapter.out.persistence; +import konkuk.thip.comment.adapter.out.persistence.repository.CommentJpaRepository; +import konkuk.thip.comment.adapter.out.persistence.repository.CommentLikeJpaRepository; import konkuk.thip.common.exception.EntityNotFoundException; +import konkuk.thip.post.adapter.out.persistence.PostLikeJpaRepository; import konkuk.thip.room.adapter.out.jpa.RoomJpaEntity; import konkuk.thip.room.adapter.out.persistence.repository.RoomJpaRepository; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; @@ -36,6 +39,10 @@ public class VoteCommandPersistenceAdapter implements VoteCommandPort { private final RoomJpaRepository roomJpaRepository; private final VoteParticipantJpaRepository voteParticipantJpaRepository; + private final CommentJpaRepository commentJpaRepository; + private final CommentLikeJpaRepository commentLikeJpaRepository; + private final PostLikeJpaRepository postLikeJpaRepository; + private final VoteMapper voteMapper; private final VoteItemMapper voteItemMapper; private final VoteParticipantMapper voteParticipantMapper; @@ -148,6 +155,45 @@ public void delete(Vote vote) { voteJpaRepository.save(voteJpaEntity); } + @Override + public void deleteAllVoteParticipantByUserId(Long userId) { + + // 1. 탈퇴 유저가 참여한 모든 투표 항목 ID 조회 + List voteItemIds = voteParticipantJpaRepository.findAllVoteItemIdsByUserId(userId); + if (voteItemIds == null || voteItemIds.isEmpty()) { + return; //early return + } + // 2. 투표 참여 관계 삭제 + voteParticipantJpaRepository.deleteAllByUserId(userId); + // 3. 해당 ID들로 JPA 엔티티 직접 조회 + List voteItemEntities = voteItemJpaRepository.findAllById(voteItemIds); + // 4. 엔티티에서 직접 투표 항목 수 감소 + voteItemEntities.forEach(entity -> + entity.setCount(Math.max(0, entity.getCount() - 1))); + voteItemJpaRepository.saveAll(voteItemEntities); + } + + @Override + public void deleteAllVoteByUserId(Long userId) { + // 1. 유저가 작성한 투표 게시글 ID 리스트 조회 + List voteIds = voteJpaRepository.findVoteIdsByUserId(userId); + if (voteIds == null || voteIds.isEmpty()) { + return; // early return + } + // 2-1. 댓글 좋아요 일괄 삭제 + commentLikeJpaRepository.deleteAllByPostIds(voteIds); + // 2-2. 댓글 soft delete 일괄 처리 + commentJpaRepository.softDeleteAllByPostIds(voteIds); + // 3. 게시글 좋아요 일괄 삭제 + postLikeJpaRepository.deleteAllByPostIds(voteIds); + // 4-1. 투표 참여 관계 일괄 삭제 + voteParticipantJpaRepository.deleteAllByVoteIds(voteIds); + // 4-2. 투표 항목 일괄 삭제 + voteItemJpaRepository.deleteAllByVoteIds(voteIds); + // 5. 탈퇴한 유저가 작성한 투표 게시글 일괄 삭제 + voteJpaRepository.deleteAllByUserId(userId); + } + @Override public void updateVote(Vote vote) { diff --git a/src/main/java/konkuk/thip/roompost/application/port/out/VoteCommandPort.java b/src/main/java/konkuk/thip/roompost/application/port/out/VoteCommandPort.java index 18051c628..de09c2614 100644 --- a/src/main/java/konkuk/thip/roompost/application/port/out/VoteCommandPort.java +++ b/src/main/java/konkuk/thip/roompost/application/port/out/VoteCommandPort.java @@ -46,4 +46,8 @@ default VoteItem getVoteItemByIdOrThrow(Long id) { void updateVoteItem(VoteItem voteItem); void delete(Vote vote); + + void deleteAllVoteParticipantByUserId(Long userId); + + void deleteAllVoteByUserId(Long userId); } diff --git a/src/main/java/konkuk/thip/roompost/application/service/VoteDeleteService.java b/src/main/java/konkuk/thip/roompost/application/service/VoteDeleteService.java index d0ac331d1..5fa410ef6 100644 --- a/src/main/java/konkuk/thip/roompost/application/service/VoteDeleteService.java +++ b/src/main/java/konkuk/thip/roompost/application/service/VoteDeleteService.java @@ -41,7 +41,6 @@ public Long deleteVote(VoteDeleteCommand command) { // 3-3. 투표 삭제 voteCommandPort.delete(vote); - //TODO// 4. 유저 방 진행도 업데이트 return command.roomId(); } } From 04ca0b4aae055217d20df25eea532c3bcc97a6bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Fri, 5 Sep 2025 01:22:48 +0900 Subject: [PATCH 29/55] =?UTF-8?q?[feat]=20=ED=9A=8C=EC=9B=90=20=ED=83=88?= =?UTF-8?q?=ED=87=B4=20=EC=8B=9C=20=EC=97=B0=EA=B4=80=EB=90=9C=20=ED=88=AC?= =?UTF-8?q?=ED=91=9C=20=EC=82=AD=EC=A0=9C=20=EA=B4=80=EB=A0=A8=20=EB=A9=94?= =?UTF-8?q?=EC=84=9C=EB=93=9C=20=EC=9E=91=EC=84=B1=20(#176)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vote/VoteItemJpaRepository.java | 4 ++++ .../repository/vote/VoteJpaRepository.java | 11 ++++++++++ .../vote/VoteParticipantJpaRepository.java | 20 ++++++++++++++++++- 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/vote/VoteItemJpaRepository.java b/src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/vote/VoteItemJpaRepository.java index b8640c6b2..16a56620b 100644 --- a/src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/vote/VoteItemJpaRepository.java +++ b/src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/vote/VoteItemJpaRepository.java @@ -15,4 +15,8 @@ public interface VoteItemJpaRepository extends JpaRepository voteIds); } diff --git a/src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/vote/VoteJpaRepository.java b/src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/vote/VoteJpaRepository.java index 1ec6e367e..e2dd18e29 100644 --- a/src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/vote/VoteJpaRepository.java +++ b/src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/vote/VoteJpaRepository.java @@ -1,8 +1,12 @@ package konkuk.thip.roompost.adapter.out.persistence.repository.vote; +import io.lettuce.core.dynamic.annotation.Param; import konkuk.thip.roompost.adapter.out.jpa.VoteJpaEntity; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import java.util.List; import java.util.Optional; public interface VoteJpaRepository extends JpaRepository, VoteQueryRepository { @@ -11,4 +15,11 @@ public interface VoteJpaRepository extends JpaRepository, V * 소프트 딜리트 적용 대상 entity 단건 조회 메서드 */ Optional findByPostId(Long postId); + + @Query("SELECT v.postId FROM VoteJpaEntity v WHERE v.userJpaEntity.userId = :userId") + List findVoteIdsByUserId(@Param("userId") Long userId); + + @Modifying(clearAutomatically = true, flushAutomatically = true) + @Query("UPDATE VoteJpaEntity v SET v.status = 'INACTIVE' WHERE v.userJpaEntity.userId = :userId") + void deleteAllByUserId(@Param("userId") Long userId); } diff --git a/src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/vote/VoteParticipantJpaRepository.java b/src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/vote/VoteParticipantJpaRepository.java index f92a674cc..204a04926 100644 --- a/src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/vote/VoteParticipantJpaRepository.java +++ b/src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/vote/VoteParticipantJpaRepository.java @@ -7,12 +7,13 @@ import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import java.util.List; import java.util.Optional; @Repository public interface VoteParticipantJpaRepository extends JpaRepository, VoteParticipantQueryRepository { @Query("SELECT vp FROM VoteParticipantJpaEntity vp WHERE vp.userJpaEntity.userId = :userId AND vp.voteItemJpaEntity.voteItemId = :voteItemId") - Optional findVoteParticipantByUserIdAndVoteItemId(Long userId, Long voteItemId); + Optional findVoteParticipantByUserIdAndVoteItemId(@Param("userId") Long userId,@Param("voteItemId") Long voteItemId); @Modifying(clearAutomatically = true, flushAutomatically = true) @Query(""" @@ -25,4 +26,21 @@ WHERE vp.voteItemJpaEntity.voteItemId IN ( """) void deleteAllByVoteId(@Param("voteId") Long voteId); + @Modifying(clearAutomatically = true, flushAutomatically = true) + @Query(""" + DELETE FROM VoteParticipantJpaEntity vp + WHERE vp.voteItemJpaEntity.voteItemId IN ( + SELECT vi.voteItemId + FROM VoteItemJpaEntity vi + WHERE vi.voteJpaEntity.postId IN :voteIds + ) + """) + void deleteAllByVoteIds(@Param("voteIds") List voteIds); + + @Query("SELECT vp.voteItemJpaEntity.voteItemId FROM VoteParticipantJpaEntity vp WHERE vp.userJpaEntity.userId = :userId") + List findAllVoteItemIdsByUserId(@Param("userId") Long userId); + + @Modifying(clearAutomatically = true, flushAutomatically = true) + @Query("DELETE FROM VoteParticipantJpaEntity vp WHERE vp.userJpaEntity.userId = :userId") + void deleteAllByUserId(@Param("userId") Long userId); } From 79e1fbffce84337c4cdaadad0f33da0a0d8cbd6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Fri, 5 Sep 2025 01:23:23 +0900 Subject: [PATCH 30/55] =?UTF-8?q?[feat]=20=ED=9A=8C=EC=9B=90=20=ED=83=88?= =?UTF-8?q?=ED=87=B4=20=EC=8B=9C=20=EC=97=B0=EA=B4=80=EB=90=9C=20=ED=88=AC?= =?UTF-8?q?=ED=91=9C=20=EC=82=AD=EC=A0=9C=EC=8B=9C=20SETTER=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#176)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/roompost/adapter/out/jpa/VoteItemJpaEntity.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/konkuk/thip/roompost/adapter/out/jpa/VoteItemJpaEntity.java b/src/main/java/konkuk/thip/roompost/adapter/out/jpa/VoteItemJpaEntity.java index 5312f5e70..da390d928 100644 --- a/src/main/java/konkuk/thip/roompost/adapter/out/jpa/VoteItemJpaEntity.java +++ b/src/main/java/konkuk/thip/roompost/adapter/out/jpa/VoteItemJpaEntity.java @@ -21,6 +21,11 @@ public class VoteItemJpaEntity extends BaseJpaEntity { @Column(name = "item_name",length = 70, nullable = false) private String itemName; + /** + * -- SETTER -- + * 회원 탈퇴용 + */ + @Setter @Builder.Default @Column(nullable = false) private int count = 0; From c1de20bb3b691094a7eb15a0b2887dd9e0ab053c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Fri, 5 Sep 2025 01:23:48 +0900 Subject: [PATCH 31/55] =?UTF-8?q?[refactor]=20=ED=88=AC=ED=91=9C=EC=8B=9C?= =?UTF-8?q?=20=ED=88=AC=ED=91=9C=ED=95=AD=EB=AA=A9=20=EB=93=9D=ED=91=9C?= =?UTF-8?q?=EC=88=98=20=EB=B3=B4=EC=97=AC=EC=A3=BC=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(#176)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../roompost/application/port/in/dto/vote/VoteResult.java | 6 +++--- .../thip/roompost/application/service/VoteService.java | 7 +------ 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/src/main/java/konkuk/thip/roompost/application/port/in/dto/vote/VoteResult.java b/src/main/java/konkuk/thip/roompost/application/port/in/dto/vote/VoteResult.java index 6fa404f49..e64dc3889 100644 --- a/src/main/java/konkuk/thip/roompost/application/port/in/dto/vote/VoteResult.java +++ b/src/main/java/konkuk/thip/roompost/application/port/in/dto/vote/VoteResult.java @@ -10,11 +10,11 @@ public record VoteResult( public record VoteItemDto( Long voteItemId, String itemName, - int percentage, + int count, Boolean isVoted ) { - public static VoteItemDto of(Long voteItemId, String itemName, int percentage, Boolean isVoted) { - return new VoteItemDto(voteItemId, itemName, percentage, isVoted); + public static VoteItemDto of(Long voteItemId, String itemName, int count, Boolean isVoted) { + return new VoteItemDto(voteItemId, itemName, count, isVoted); } } diff --git a/src/main/java/konkuk/thip/roompost/application/service/VoteService.java b/src/main/java/konkuk/thip/roompost/application/service/VoteService.java index 22ee0fab6..58487d2db 100644 --- a/src/main/java/konkuk/thip/roompost/application/service/VoteService.java +++ b/src/main/java/konkuk/thip/roompost/application/service/VoteService.java @@ -42,17 +42,12 @@ public VoteResult vote(VoteCommand command) { // 2. 투표 결과 반환 List voteItems = voteQueryPort.findVoteItemsByVoteId(command.voteId(), command.userId()); - List counts = voteItems.stream() - .map(VoteItemQueryDto::voteCount) - .toList(); - - List percentages = VoteItem.calculatePercentages(counts); var voteItemDtos = IntStream.range(0, voteItems.size()) .mapToObj(i -> VoteResult.VoteItemDto.of( voteItems.get(i).voteItemId(), voteItems.get(i).itemName(), - percentages.get(i), + voteItems.get(i).voteCount(), voteItems.get(i).isVoted() )) .toList(); From f4497ad6a3444caec2bfec80aaa1717d45c9c56f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Fri, 5 Sep 2025 01:23:56 +0900 Subject: [PATCH 32/55] =?UTF-8?q?[feat]=20=ED=86=A0=ED=81=B0=20=EC=B6=94?= =?UTF-8?q?=EC=B6=9C=20=EC=96=B4=EB=85=B8=ED=85=8C=EC=9D=B4=EC=85=98=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=20(#176)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/konkuk/thip/config/WebMvcConfig.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/konkuk/thip/config/WebMvcConfig.java b/src/main/java/konkuk/thip/config/WebMvcConfig.java index 12d47b9a4..44a410d9e 100644 --- a/src/main/java/konkuk/thip/config/WebMvcConfig.java +++ b/src/main/java/konkuk/thip/config/WebMvcConfig.java @@ -1,5 +1,6 @@ package konkuk.thip.config; +import konkuk.thip.common.security.argument_resolver.AuthTokenArgumentResolver; import konkuk.thip.common.security.argument_resolver.Oauth2IdArgumentResolver; import konkuk.thip.common.security.argument_resolver.UserIdArgumentResolver; import lombok.RequiredArgsConstructor; @@ -15,10 +16,12 @@ public class WebMvcConfig implements WebMvcConfigurer { private final UserIdArgumentResolver userIdArgumentResolver; private final Oauth2IdArgumentResolver oauth2IdArgumentResolver; + private final AuthTokenArgumentResolver authTokenArgumentResolver; @Override public void addArgumentResolvers(List resolvers) { resolvers.add(userIdArgumentResolver); resolvers.add(oauth2IdArgumentResolver); + resolvers.add(authTokenArgumentResolver); } } From 583c6ccb7d61284a0dd9c4c29f52973a50272bce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Fri, 5 Sep 2025 01:24:17 +0900 Subject: [PATCH 33/55] =?UTF-8?q?[test]=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=ED=8C=A9=ED=86=A0=EB=A6=AC=20=EB=A9=94=EC=84=9C=EB=93=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(#176)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../thip/common/util/TestEntityFactory.java | 46 +++++-------------- 1 file changed, 11 insertions(+), 35 deletions(-) diff --git a/src/test/java/konkuk/thip/common/util/TestEntityFactory.java b/src/test/java/konkuk/thip/common/util/TestEntityFactory.java index 697458718..003c3ebdc 100644 --- a/src/test/java/konkuk/thip/common/util/TestEntityFactory.java +++ b/src/test/java/konkuk/thip/common/util/TestEntityFactory.java @@ -12,6 +12,8 @@ import konkuk.thip.post.adapter.out.jpa.PostJpaEntity; import konkuk.thip.post.adapter.out.jpa.PostLikeJpaEntity; import konkuk.thip.post.domain.PostType; +import konkuk.thip.recentSearch.adapter.out.jpa.RecentSearchJpaEntity; +import konkuk.thip.recentSearch.adapter.out.jpa.RecentSearchType; import konkuk.thip.room.adapter.out.jpa.RoomJpaEntity; import konkuk.thip.room.adapter.out.jpa.RoomParticipantJpaEntity; import konkuk.thip.room.adapter.out.jpa.RoomParticipantRole; @@ -279,49 +281,14 @@ public static FollowingJpaEntity createFollowing(UserJpaEntity followerUser, Use * 공개/비공개 여부만을 설정하는 기본 피드 생성을 위한 팩토리 메서드 */ public static FeedJpaEntity createFeed(UserJpaEntity user, BookJpaEntity book, boolean isPublic) { - -// return FeedJpaEntity.builder() -// .content("기본 피드 본문입니다.") -// .isPublic(isPublic) -// .likeCount(0) -// .commentCount(0) -// .reportCount(0) -// .userJpaEntity(user) -// .bookJpaEntity(book) -// .contentList(ContentList.empty()) -// .build(); return createFeed(user, book, isPublic, 0, 0, Collections.emptyList(), Collections.emptyList()); } public static FeedJpaEntity createFeed(UserJpaEntity user, BookJpaEntity book, boolean isPublic, int likeCount, int commentCount, List imageUrls) { - -// FeedJpaEntity feed = FeedJpaEntity.builder() -// .content("이미지 포함 피드") -// .isPublic(isPublic) -// .likeCount(0) -// .commentCount(0) -// .reportCount(0) -// .userJpaEntity(user) -// .bookJpaEntity(book) -// .contentList(ContentList.of(imageUrls)) -// .build(); -// return createFeed(user, book, isPublic, likeCount, commentCount, imageUrls, Collections.emptyList()); } public static FeedJpaEntity createFeed(UserJpaEntity user, BookJpaEntity book, List imageUrls, boolean isPublic) { - -// FeedJpaEntity feed = FeedJpaEntity.builder() -// .content("이미지 포함 피드") -// .isPublic(isPublic) -// .likeCount(0) -// .commentCount(0) -// .reportCount(0) -// .userJpaEntity(user) -// .bookJpaEntity(book) -// .contentList(ContentList.of(imageUrls)) -// .build(); -// return createFeed(user, book, isPublic, 0, 0, imageUrls, Collections.emptyList()); } @@ -391,4 +358,13 @@ public static AttendanceCheckJpaEntity createAttendanceCheck(String todayComment .userJpaEntity(userJpaEntity) .build(); } + + public static RecentSearchJpaEntity createRecentSearch(UserJpaEntity userJpaEntity) { + return RecentSearchJpaEntity.builder() + .searchTerm("테스트검색어") + .type(RecentSearchType.BOOK_SEARCH) + .userJpaEntity(userJpaEntity) + .build(); + } + } From b8d689929751ac71287dff789212ee84235eeb33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Fri, 5 Sep 2025 01:29:17 +0900 Subject: [PATCH 34/55] =?UTF-8?q?[test]=20=ED=9A=8C=EC=9B=90=20=ED=83=88?= =?UTF-8?q?=ED=87=B4=20=ED=86=B5=ED=95=A9=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1=20(#176)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../adapter/in/web/UserDeleteApiTest.java | 468 ++++++++++++++++++ 1 file changed, 468 insertions(+) create mode 100644 src/test/java/konkuk/thip/user/adapter/in/web/UserDeleteApiTest.java diff --git a/src/test/java/konkuk/thip/user/adapter/in/web/UserDeleteApiTest.java b/src/test/java/konkuk/thip/user/adapter/in/web/UserDeleteApiTest.java new file mode 100644 index 000000000..8b715d63d --- /dev/null +++ b/src/test/java/konkuk/thip/user/adapter/in/web/UserDeleteApiTest.java @@ -0,0 +1,468 @@ +package konkuk.thip.user.adapter.in.web; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.persistence.EntityManager; +import konkuk.thip.book.adapter.out.jpa.BookJpaEntity; +import konkuk.thip.book.adapter.out.persistence.repository.BookJpaRepository; +import konkuk.thip.book.adapter.out.persistence.repository.SavedBookJpaRepository; +import konkuk.thip.comment.adapter.out.jpa.CommentJpaEntity; +import konkuk.thip.comment.adapter.out.jpa.CommentLikeJpaEntity; +import konkuk.thip.comment.adapter.out.persistence.repository.CommentJpaRepository; +import konkuk.thip.comment.adapter.out.persistence.repository.CommentLikeJpaRepository; +import konkuk.thip.common.security.util.JwtUtil; +import konkuk.thip.common.util.TestEntityFactory; +import konkuk.thip.feed.adapter.out.jpa.FeedJpaEntity; +import konkuk.thip.feed.adapter.out.persistence.repository.FeedJpaRepository; +import konkuk.thip.feed.adapter.out.persistence.repository.SavedFeedJpaRepository; +import konkuk.thip.post.adapter.out.jpa.PostLikeJpaEntity; +import konkuk.thip.post.adapter.out.persistence.PostLikeJpaRepository; +import konkuk.thip.recentSearch.adapter.out.persistence.repository.RecentSearchJpaRepository; +import konkuk.thip.room.adapter.out.jpa.RoomJpaEntity; +import konkuk.thip.room.adapter.out.jpa.RoomParticipantJpaEntity; +import konkuk.thip.room.adapter.out.jpa.RoomStatus; +import konkuk.thip.room.adapter.out.persistence.repository.RoomJpaRepository; +import konkuk.thip.room.adapter.out.persistence.repository.roomparticipant.RoomParticipantJpaRepository; +import konkuk.thip.room.domain.value.Category; +import konkuk.thip.roompost.adapter.out.jpa.*; +import konkuk.thip.roompost.adapter.out.persistence.repository.attendancecheck.AttendanceCheckJpaRepository; +import konkuk.thip.roompost.adapter.out.persistence.repository.record.RecordJpaRepository; +import konkuk.thip.roompost.adapter.out.persistence.repository.vote.VoteItemJpaRepository; +import konkuk.thip.roompost.adapter.out.persistence.repository.vote.VoteJpaRepository; +import konkuk.thip.roompost.adapter.out.persistence.repository.vote.VoteParticipantJpaRepository; +import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; +import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; +import konkuk.thip.user.adapter.out.persistence.repository.following.FollowingJpaRepository; +import konkuk.thip.user.application.port.UserTokenBlacklistQueryPort; +import konkuk.thip.user.domain.value.Alias; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; + +import java.time.LocalDate; + +import static konkuk.thip.common.entity.StatusType.INACTIVE; +import static konkuk.thip.common.exception.code.ErrorCode.USER_CANNOT_DELETE_ROOM_HOST; +import static konkuk.thip.post.domain.PostType.*; +import static konkuk.thip.room.adapter.out.jpa.RoomParticipantRole.HOST; +import static konkuk.thip.room.adapter.out.jpa.RoomParticipantRole.MEMBER; +import static konkuk.thip.room.adapter.out.jpa.RoomStatus.*; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@ActiveProfiles("test") +@AutoConfigureMockMvc +@DisplayName("[통합] 회원탈퇴 api 테스트") +public class UserDeleteApiTest { + + @Autowired private MockMvc mockMvc; + @Autowired private UserJpaRepository userJpaRepository; + @Autowired private BookJpaRepository bookJpaRepository; + @Autowired private FeedJpaRepository feedJpaRepository; + @Autowired private CommentJpaRepository commentJpaRepository; + @Autowired private CommentLikeJpaRepository commentLikeJpaRepository; + @Autowired private PostLikeJpaRepository postLikeJpaRepository; + @Autowired private SavedFeedJpaRepository savedFeedJpaRepository; + @Autowired private RoomJpaRepository roomJpaRepository; + @Autowired private RoomParticipantJpaRepository roomParticipantJpaRepository; + @Autowired private VoteJpaRepository voteJpaRepository; + @Autowired private VoteItemJpaRepository voteItemJpaRepository; + @Autowired private FollowingJpaRepository followingJpaRepository; + @Autowired private RecentSearchJpaRepository recentSearchJpaRepository; + @Autowired private SavedBookJpaRepository savedBookJpaRepository; + @Autowired private AttendanceCheckJpaRepository attendanceCheckJpaRepository; + @Autowired private VoteParticipantJpaRepository voteParticipantJpaRepository; + @Autowired private RecordJpaRepository recordJpaRepository; + @Autowired private UserTokenBlacklistQueryPort userTokenBlacklistQueryPort; + + @Autowired private JwtUtil jwtUtil; + @Autowired private EntityManager entityManager; + @Autowired private ObjectMapper objectMapper; + + @AfterEach + void tearDown() { + followingJpaRepository.deleteAllInBatch(); + recentSearchJpaRepository.deleteAllInBatch(); + savedFeedJpaRepository.deleteAllInBatch(); + savedBookJpaRepository.deleteAllInBatch(); + attendanceCheckJpaRepository.deleteAllInBatch(); + voteParticipantJpaRepository.deleteAllInBatch(); + commentLikeJpaRepository.deleteAllInBatch(); + commentJpaRepository.deleteAllInBatch(); + postLikeJpaRepository.deleteAllInBatch(); + feedJpaRepository.deleteAllInBatch(); + recordJpaRepository.deleteAllInBatch(); + voteItemJpaRepository.deleteAllInBatch(); + voteJpaRepository.deleteAllInBatch(); + roomParticipantJpaRepository.deleteAllInBatch(); + roomJpaRepository.deleteAll(); + bookJpaRepository.deleteAll(); + userJpaRepository.deleteAll(); + } + + @Test + @DisplayName("회원탈퇴 성공시 모든 연관 엔티티가 각 엔티티 삭제 전략에 맞게 삭제되고 탈퇴한 회원의 토큰이 블랙리스트에 등록된다.") + void deleteUser_success() throws Exception { + + // given + // 유저 설정 1:테스트하고자하는 유저 2:방의 호스트 유저 3:방의 멤버유저 + UserJpaEntity testUser1 = userJpaRepository.save(TestEntityFactory.createUser(Alias.ARTIST)); + UserJpaEntity otherHostUser2 = userJpaRepository.save(TestEntityFactory.createUser(Alias.ARTIST)); + UserJpaEntity otherMemberUser3 = userJpaRepository.save(TestEntityFactory.createUser(Alias.ARTIST)); + + // 팔로잉 관계 설정 + followingJpaRepository.save(TestEntityFactory.createFollowing(testUser1, otherHostUser2)); // 유저 1이 유저 2 팔로우 + followingJpaRepository.save(TestEntityFactory.createFollowing(otherMemberUser3, testUser1)); // 유저 3이 유저 1팔로우 + followingJpaRepository.save(TestEntityFactory.createFollowing(otherMemberUser3, otherHostUser2)); // 유저 3이 유저 2팔로우 + otherHostUser2.setFollowerCount(2); userJpaRepository.save(otherHostUser2); //유저 2 팔로워수 2 + testUser1.setFollowerCount(1); userJpaRepository.save(testUser1); //유저 1 팔로워수 1 + + // 최근검색어 저장 + recentSearchJpaRepository.save(TestEntityFactory.createRecentSearch(testUser1)); + + BookJpaEntity book = bookJpaRepository.save(TestEntityFactory.createBook()); + Category category = TestEntityFactory.createLiteratureCategory(); + // 방 참여 관계 설정: 모든 유저가 방에 참여해있음 + RoomJpaEntity roomExpired1 = createRoom(book,category, EXPIRED); + RoomJpaEntity roomInProgress2 = createRoom(book,category, IN_PROGRESS); + RoomJpaEntity roomRecruiting3 = createRoom(book,category, RECRUITING); + + // 방1-> 만료된 방 : 유저1이 HOST + RoomParticipantJpaEntity r1_rp1 = roomParticipantJpaRepository.save(TestEntityFactory.createRoomParticipant(roomExpired1,testUser1,HOST,50.0)); + RoomParticipantJpaEntity r1_rp2 = roomParticipantJpaRepository.save(TestEntityFactory.createRoomParticipant(roomExpired1,otherHostUser2, MEMBER,30.0)); + RoomParticipantJpaEntity r1_rp3 = roomParticipantJpaRepository.save(TestEntityFactory.createRoomParticipant(roomExpired1,otherMemberUser3,MEMBER,60.0)); + updateRoomPercentage(r1_rp1,r1_rp2,r1_rp3,roomExpired1); + // 방2-> 진행중인 방 : 유저2가 HOST + RoomParticipantJpaEntity r2_rp1 = roomParticipantJpaRepository.save(TestEntityFactory.createRoomParticipant(roomInProgress2,testUser1,MEMBER,50.0)); + RoomParticipantJpaEntity r2_rp2 = roomParticipantJpaRepository.save(TestEntityFactory.createRoomParticipant(roomInProgress2,otherHostUser2,HOST,30.0)); + RoomParticipantJpaEntity r2_rp3 = roomParticipantJpaRepository.save(TestEntityFactory.createRoomParticipant(roomInProgress2,otherMemberUser3,MEMBER,60.0)); + updateRoomPercentage(r2_rp1,r2_rp2,r2_rp3,roomInProgress2); + // 방3 -> 모집중인 방 : 유저2가 HOST + RoomParticipantJpaEntity r3_rp1 = roomParticipantJpaRepository.save(TestEntityFactory.createRoomParticipant(roomRecruiting3,testUser1,MEMBER,50.0)); + RoomParticipantJpaEntity r3_rp2 = roomParticipantJpaRepository.save(TestEntityFactory.createRoomParticipant(roomRecruiting3,otherHostUser2,HOST,30.0)); + RoomParticipantJpaEntity r3_rp3 = roomParticipantJpaRepository.save(TestEntityFactory.createRoomParticipant(roomRecruiting3,otherMemberUser3,MEMBER,60.0)); + updateRoomPercentage(r3_rp1,r3_rp2,r3_rp3,roomRecruiting3); + + // 피드/책 저장관계 설정 + FeedJpaEntity u2_f1 = feedJpaRepository.save(TestEntityFactory.createFeed(otherHostUser2,book,true)); //유저 2가 피드1 작성 + savedFeedJpaRepository.save(TestEntityFactory.createSavedFeed(testUser1,u2_f1)); //유저 1이 유저2가 작성한 피드1를 저장 + + BookJpaEntity sb = bookJpaRepository.save(TestEntityFactory.createBook()); + savedBookJpaRepository.save(TestEntityFactory.createSavedBook(testUser1,sb)); + + // 오늘의 한마디 관계 설정 + AttendanceCheckJpaEntity a = attendanceCheckJpaRepository.save(TestEntityFactory.createAttendanceCheck + ("유저1이 방2에 남긴 오늘의한마디1",roomInProgress2,testUser1)); + + //유저 2가 방2에 투표1 생성 + VoteJpaEntity u2_v1 = voteJpaRepository.save( + VoteJpaEntity.builder().content("유저2가 방2에 생성한 투표1").userJpaEntity(otherHostUser2).page(33).isOverview(true) + .commentCount(2).likeCount(1).roomJpaEntity(roomInProgress2).build()); + VoteItemJpaEntity v1_vi1 = voteItemJpaRepository.save( + VoteItemJpaEntity.builder().itemName("유저2가 방2에 생성한 투표1의 항목1").count(1).voteJpaEntity(u2_v1).build()); + VoteItemJpaEntity v1_vi2 = voteItemJpaRepository.save( + VoteItemJpaEntity.builder().itemName("유저2가 방2에 생성한 투표1의 항목2").count(0).voteJpaEntity(u2_v1).build()); + VoteParticipantJpaEntity u1_vp1 = voteParticipantJpaRepository.save(TestEntityFactory.createVoteParticipant(testUser1, v1_vi1)); //유저1이 투표1의 투표항목1에 투표 + CommentJpaEntity u2_v1_c1 = commentJpaRepository.save( + CommentJpaEntity.builder().content("유저2가 투표1에 남긴 댓글1").postJpaEntity(u2_v1).userJpaEntity(otherHostUser2) + .likeCount(1).reportCount(0).postType(VOTE).build()); //유저2가 투표1에 댓글 + // 유저1의 댓글 좋아요 관계 설정 + CommentLikeJpaEntity u1_c1_cl1 = commentLikeJpaRepository.save(TestEntityFactory.createCommentLike(u2_v1_c1, testUser1)); //유저1이 댓글1에 댓글좋아요 + + //유저2가 방2에 기록1 생성 + RecordJpaEntity u2_r1 = recordJpaRepository.save( + RecordJpaEntity.builder().content("유저2가 방2에 생성한 기록1").userJpaEntity(otherHostUser2).page(22).isOverview(false) + .commentCount(1).likeCount(1).roomJpaEntity(roomInProgress2).build()); + + // 유저1의 게시글 좋아요 관계 설정 + PostLikeJpaEntity u1_v1_pl1 = postLikeJpaRepository.save(TestEntityFactory.createPostLike(testUser1,u2_v1)); //유저1이 투표1에 좋아요 + PostLikeJpaEntity u1_f1_pl2 = postLikeJpaRepository.save(TestEntityFactory.createPostLike(testUser1,u2_f1)); //유저1이 피드1에 좋아요 + PostLikeJpaEntity u1_r1_pl3 = postLikeJpaRepository.save(TestEntityFactory.createPostLike(testUser1,u2_r1)); //유저1이 기록1에 좋아요 + + // 유저1의 댓글 저장 + CommentJpaEntity u1_v1_c2 = commentJpaRepository.save( + CommentJpaEntity.builder().content("유저1이 투표1에 남긴 댓글2").postJpaEntity(u2_v1).userJpaEntity(testUser1) + .likeCount(1).reportCount(0).postType(VOTE).build()); //유저1이 투표1에 댓글 + CommentJpaEntity u1_f1_c3 = commentJpaRepository.save( + CommentJpaEntity.builder().content("유저1이 피드1에 남긴 댓글3").postJpaEntity(u2_f1).userJpaEntity(testUser1) + .likeCount(1).reportCount(0).postType(FEED).build()); //유저1이 피드1에 댓글 + CommentJpaEntity u1_r1_c4 = commentJpaRepository.save( + CommentJpaEntity.builder().content("유저1이 기록1에 남긴 댓글4").postJpaEntity(u2_r1).userJpaEntity(testUser1) + .likeCount(1).reportCount(0).postType(RECORD).build()); //유저1이 기록1에 댓글 + + u2_f1.setLikeCount(1); //피드 1 좋아요/댓글 씽크 맞추기 + u2_f1.setCommentCount(1); + feedJpaRepository.save(u2_f1); + + // 유저1이 남긴 댓글에 대해 댓글 좋아요 관계 설정 : 유저2가 유저1이 남긴 댓글에 대해 좋아요 + CommentLikeJpaEntity u2_c2_cl = commentLikeJpaRepository.save( + TestEntityFactory.createCommentLike(u1_v1_c2, otherHostUser2)); + CommentLikeJpaEntity u2_c3_cl = commentLikeJpaRepository.save( + TestEntityFactory.createCommentLike(u1_f1_c3, otherHostUser2)); + CommentLikeJpaEntity u2_c4_cl = commentLikeJpaRepository.save( + TestEntityFactory.createCommentLike(u1_r1_c4, otherHostUser2)); + + // 유저1이 작성한 게시물 관계 설정 + // 유저 1이 피드2 작성 + FeedJpaEntity u1_f2 = feedJpaRepository.save(TestEntityFactory.createFeed(testUser1,book,true)); + CommentJpaEntity u2_f3_c4 = commentJpaRepository.save( + CommentJpaEntity.builder().content("유저2이 피드2에 남긴 댓글4").postJpaEntity(u1_f2).userJpaEntity(otherHostUser2) + .likeCount(1).reportCount(0).postType(FEED).build()); //유저2이 피드2에 댓글 + //유저3이 댓글4에 댓글좋아요 + CommentLikeJpaEntity u3_c4_cl2 = commentLikeJpaRepository.save(TestEntityFactory.createCommentLike(u2_f3_c4, otherMemberUser3)); + PostLikeJpaEntity u2_f2_pl4 = postLikeJpaRepository.save(TestEntityFactory.createPostLike(otherHostUser2, u1_f2)); //유저2가 피드2에 좋아요 + savedFeedJpaRepository.save(TestEntityFactory.createSavedFeed(otherHostUser2,u1_f2)); //유저2가 피드2를 저장 + u1_f2.setLikeCount(1); //피드 2 좋아요/댓글 씽크 맞추기 + u1_f2.setCommentCount(1); + feedJpaRepository.save(u1_f2); + + // 유저 1이 투표2 작성 + VoteJpaEntity u1_v2 = voteJpaRepository.save( + VoteJpaEntity.builder().content("유저1가 방2에 생성한 투표2").userJpaEntity(testUser1).page(33).isOverview(true) + .commentCount(1).likeCount(1).roomJpaEntity(roomInProgress2).build()); + VoteItemJpaEntity v2_vi1 = voteItemJpaRepository.save( + VoteItemJpaEntity.builder().itemName("유저1이 방2에 생성한 투표2의 항목1").count(1).voteJpaEntity(u1_v2).build()); + VoteItemJpaEntity v2_vi2 = voteItemJpaRepository.save( + VoteItemJpaEntity.builder().itemName("유저1이 방2에 생성한 투표2의 항목2").count(1).voteJpaEntity(u1_v2).build()); + VoteParticipantJpaEntity u2_vp2 = voteParticipantJpaRepository.save(TestEntityFactory.createVoteParticipant(otherHostUser2, v2_vi1)); //유저2이 투표2의 투표항목1에 투표 + VoteParticipantJpaEntity u3_vp3 = voteParticipantJpaRepository.save(TestEntityFactory.createVoteParticipant(otherMemberUser3, v2_vi2)); //유저3이 투표2의 투표항목2에 투표 + CommentJpaEntity u3_v2_c5 = commentJpaRepository.save( + CommentJpaEntity.builder().content("유저3이 투표2에 남긴 댓글5").postJpaEntity(u1_v2).userJpaEntity(otherMemberUser3) + .likeCount(1).reportCount(0).postType(VOTE).build()); //유저3이 투표2에 댓글 + //유저2이 댓글5에 댓글좋아요 + CommentLikeJpaEntity u2_c5_cl3 = commentLikeJpaRepository.save(TestEntityFactory.createCommentLike(u3_v2_c5, otherHostUser2)); + PostLikeJpaEntity u3_v2_pl5 = postLikeJpaRepository.save(TestEntityFactory.createPostLike(otherMemberUser3, u1_v2)); //유저3이 투표2에 좋아요 + + // 유저 1이 기록2 작성 + RecordJpaEntity u1_r2 = recordJpaRepository.save( + RecordJpaEntity.builder().content("유저1이 방2에 생성한 기록2").userJpaEntity(testUser1).page(22).isOverview(false) + .commentCount(1).likeCount(1).roomJpaEntity(roomInProgress2).build()); + CommentJpaEntity u2_r2_c6 = commentJpaRepository.save( + CommentJpaEntity.builder().content("유저2가 기록2에 남긴 댓글6").postJpaEntity(u1_r2).userJpaEntity(otherHostUser2) + .likeCount(1).reportCount(0).postType(RECORD).build()); //유저2가 기록2에 댓글 + //유저3이 댓글6에 댓글좋아요 + CommentLikeJpaEntity u3_c6_cl4 = commentLikeJpaRepository.save(TestEntityFactory.createCommentLike(u2_r2_c6, otherMemberUser3)); + PostLikeJpaEntity u2_r2_pl6 = postLikeJpaRepository.save(TestEntityFactory.createPostLike(otherHostUser2, u1_r2)); //유저2가 기록2에 좋아요 + + //when + String accessToken = jwtUtil.createAccessToken(testUser1.getUserId()); + mockMvc.perform(delete("/users") + .header("Authorization", "Bearer " + accessToken) //헤더 추가 + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()); + + // then: 1) 유저 팔로잉/팔로워 관계 삭제 + // 탈퇴한 유저1의 팔로잉/팔로워 관계는 모두 삭제되어야하고, 관련 없는 유저3->유저2 팔로우관계만 남아있어야함 + // 유저2의 팔로워 수가 1이어야함 + assertEquals(followingJpaRepository.findAll().get(0).getUserJpaEntity().getUserId(), otherMemberUser3.getUserId()); + assertEquals(2, (int) otherHostUser2.getFollowerCount()); + + // 2) 최근검색어 삭제 + assertTrue(recentSearchJpaRepository.findAll().isEmpty()); + + // 3) 저장한 책/파드 삭제 + assertTrue(savedFeedJpaRepository.findAllByUserId(testUser1.getUserId()).isEmpty()); + assertTrue(savedBookJpaRepository.findAll().isEmpty()); + + // 4) 오늘의 한마디 관계 soft delete (status=INACTIVE) + AttendanceCheckJpaEntity deletedAc = attendanceCheckJpaRepository.findById(a.getAttendanceCheckId()).orElse(null); + assertThat(deletedAc.getStatus()).isEqualTo(INACTIVE); + + // 5) 유저 투표 참여 관계 삭제 + // 탈퇴한 유저가 참여한 투표관계 모두 삭제 + // 탈퇴한 유저가 참여했던 투표의 득표수 감소 + assertTrue(voteParticipantJpaRepository.findById(u1_vp1.getVoteParticipantId()).isEmpty()); + assertEquals(1, v1_vi1.getCount()); + + // 6) 유저 댓글 좋아요 삭제 + // 탈퇴한 유저의 모든 댓글 좋아요 관계 삭제 + // 탈퇴한 유저가 좋아요한 댓글의 좋아요 수 감소 + assertTrue(voteParticipantJpaRepository.findById(u1_c1_cl1.getLikeId()).isEmpty()); + CommentJpaEntity updatedCm1 = commentJpaRepository.findById(u2_v1_c1.getCommentId()).orElse(null); + assertEquals(0, updatedCm1.getLikeCount()); + + // 7) 유저 댓글 soft delete (status=INACTIVE) + // 탈퇴한 유저의 모든 댓글 soft delete + // 탈퇴한 유저가 남긴 게시글의 댓글 수 감소 + // 탈퇴한 유저의 모든 댓글의 좋아요 관계 삭제 + CommentJpaEntity deletedCm1 = commentJpaRepository.findById(u1_v1_c2.getCommentId()).orElse(null); + CommentJpaEntity deletedCm2 = commentJpaRepository.findById(u1_f1_c3.getCommentId()).orElse(null); + CommentJpaEntity deletedCm3 = commentJpaRepository.findById(u1_r1_c4.getCommentId()).orElse(null); + assertThat(deletedCm1.getStatus()).isEqualTo(INACTIVE); + assertThat(deletedCm2.getStatus()).isEqualTo(INACTIVE); + assertThat(deletedCm3.getStatus()).isEqualTo(INACTIVE); + + VoteJpaEntity updateV1 = voteJpaRepository.findByPostId(u2_v1.getPostId()).orElse(null); + FeedJpaEntity updateF1 = feedJpaRepository.findByPostId(u2_f1.getPostId()).orElse(null); + RecordJpaEntity updateR1 = recordJpaRepository.findByPostId(u2_r1.getPostId()).orElse(null); + assertEquals(1, updateV1.getCommentCount()); // 유저2가 남긴 댓글은 남아있음 + assertEquals(0, updateF1.getCommentCount()); + assertEquals(0, updateR1.getCommentCount()); + + assertTrue(commentLikeJpaRepository.findById(u2_c2_cl.getLikeId()).isEmpty()); + assertTrue(commentLikeJpaRepository.findById(u2_c3_cl.getLikeId()).isEmpty()); + assertTrue(commentLikeJpaRepository.findById(u2_c4_cl.getLikeId()).isEmpty()); + + // 8) 유저 게시글 좋아요 삭제 + // 탈퇴한 유저가 남긴 모든 게시글 좋아요 삭제 + // 탈퇴한 유저가 남긴 게시글의 좋아요 수 감소 + assertTrue(postLikeJpaRepository.findById(u1_v1_pl1.getLikeId()).isEmpty()); + assertTrue(postLikeJpaRepository.findById(u1_f1_pl2.getLikeId()).isEmpty()); + assertTrue(postLikeJpaRepository.findById(u1_r1_pl3.getLikeId()).isEmpty()); + assertEquals(0, updateV1.getLikeCount()); + assertEquals(0, updateF1.getLikeCount()); + assertEquals(0, updateR1.getLikeCount()); + + // 9) 유저가 작성한 피드 soft delete (status=INACTIVE) + // 탈퇴한 유저가 작성한 모든 피드 soft delete + // 탈퇴한 유저가 작성한 모든 피드의 댓글 좋아요 삭제 + // 탈퇴한 유저가 작성한 모든 피드의 댓글 soft delete + // 탈퇴한 유저가 작성한 모든 피드 좋아요 삭제 + // 탈퇴한 유저가 작성한 피드를 저장하는 모든 관계 삭제 + FeedJpaEntity deletedF1 = feedJpaRepository.findById(u1_f2.getPostId()).orElse(null); + assertThat(deletedF1.getStatus()).isEqualTo(INACTIVE); + assertTrue(commentLikeJpaRepository.findById(u3_c4_cl2.getLikeId()).isEmpty()); + CommentJpaEntity deletedCm4 = commentJpaRepository.findById(u2_f3_c4.getCommentId()).orElse(null); + assertThat(deletedCm4.getStatus()).isEqualTo(INACTIVE); + assertTrue(postLikeJpaRepository.findById(u2_f2_pl4.getLikeId()).isEmpty()); + assertTrue(savedFeedJpaRepository.findAllByUserId(otherHostUser2.getUserId()).isEmpty()); + + // 9) 유저가 작성한 투표 soft delete (status=INACTIVE) + // 탈퇴한 유저가 작성한 모든 투표 soft delete + // 탈퇴한 유저가 작성한 모든 투표의 댓글 좋아요 삭제 + // 탈퇴한 유저가 작성한 모든 투표의 댓글 soft delete + // 탈퇴한 유저가 작성한 모든 투표 좋아요 삭제 + // 탈퇴한 유저가 작성한 투표에 참여하는 모든 관계를 삭제 + // 탈퇴한 유저가 작성한 모든 투표의 투표 항목 삭제 + VoteJpaEntity deletedV1 = voteJpaRepository.findById(u1_v2.getPostId()).orElse(null); + assertThat(deletedV1.getStatus()).isEqualTo(INACTIVE); + assertTrue(commentLikeJpaRepository.findById(u2_c5_cl3.getLikeId()).isEmpty()); + CommentJpaEntity deletedCm5 = commentJpaRepository.findById(u3_v2_c5.getCommentId()).orElse(null); + assertThat(deletedCm5.getStatus()).isEqualTo(INACTIVE); + assertTrue(postLikeJpaRepository.findById(u3_v2_pl5.getLikeId()).isEmpty()); + assertTrue(voteParticipantJpaRepository.findById(u2_vp2.getVoteParticipantId()).isEmpty()); + assertTrue(voteParticipantJpaRepository.findById(u3_vp3.getVoteParticipantId()).isEmpty()); + assertTrue(voteItemJpaRepository.findById(v2_vi1.getVoteItemId()).isEmpty()); + assertTrue(voteItemJpaRepository.findById(v2_vi2.getVoteItemId()).isEmpty()); + + // 10) 유저가 작성한 기록 soft delete (status=INACTIVE) + // 탈퇴한 유저가 작성한 모든 기록 soft delete + // 탈퇴한 유저가 작성한 모든 기록의 댓글 좋아요 삭제 + // 탈퇴한 유저가 작성한 모든 기록의 댓글 soft delete + // 탈퇴한 유저가 작성한 모든 기록 좋아요 삭제 + RecordJpaEntity deletedR1 = recordJpaRepository.findById(u1_r2.getPostId()).orElse(null); + assertThat(deletedR1.getStatus()).isEqualTo(INACTIVE); + assertTrue(commentLikeJpaRepository.findById(u3_c6_cl4.getLikeId()).isEmpty()); + CommentJpaEntity deletedCm6 = commentJpaRepository.findById(u2_r2_c6.getCommentId()).orElse(null); + assertThat(deletedCm6.getStatus()).isEqualTo(INACTIVE); + assertTrue(postLikeJpaRepository.findById(u2_r2_pl6.getLikeId()).isEmpty()); + + // 11) 유저가 참여한 방 관계 soft delete (status=INACTIVE) + // 탈퇴한 유저가 참여한 모든 방 관계 soft delete + // 탈퇴한 유저가 참여한 모든 방의 멤버수 감소 + // 탈퇴한 유저가 참여한 모든 방의 진행도 업데이트 + RoomParticipantJpaEntity deletedRp1 = roomParticipantJpaRepository. + findById(r1_rp1.getRoomParticipantId()).orElse(null); //만료된 방에서는 host가 나가도 상관없음 + RoomParticipantJpaEntity deletedRp2 = roomParticipantJpaRepository. + findById(r2_rp1.getRoomParticipantId()).orElse(null); + RoomParticipantJpaEntity deletedRp3 = roomParticipantJpaRepository. + findById(r3_rp1.getRoomParticipantId()).orElse(null); + assertThat(deletedRp1.getStatus()).isEqualTo(INACTIVE); + assertThat(deletedRp2.getStatus()).isEqualTo(INACTIVE); + assertThat(deletedRp3.getStatus()).isEqualTo(INACTIVE); + assertEquals(2, roomJpaRepository.findByRoomId(roomExpired1.getRoomId()).get().getMemberCount()); + assertEquals(45.0, roomJpaRepository.findByRoomId(roomExpired1.getRoomId()).get().getRoomPercentage()); + assertEquals(2, roomJpaRepository.findByRoomId(roomInProgress2.getRoomId()).get().getMemberCount()); + assertEquals(45.0, roomJpaRepository.findByRoomId(roomInProgress2.getRoomId()).get().getRoomPercentage()); + assertEquals(2, roomJpaRepository.findByRoomId(roomRecruiting3.getRoomId()).get().getMemberCount()); + assertEquals(45.0, roomJpaRepository.findByRoomId(roomRecruiting3.getRoomId()).get().getRoomPercentage()); + + // 12) 유저 soft delete (status=INACTIVE) + // 탈퇴한 유저의 oauth2Id는 deleted:로 시작해야함 + entityManager.clear(); + UserJpaEntity deletedUser = userJpaRepository.findById(testUser1.getUserId()).orElse(null); + assertThat(deletedUser.getStatus()).isEqualTo(INACTIVE); + assertThat(deletedUser.getOauth2Id()).startsWith("deleted:"); + + // 13) 탈퇴한 유저의 토큰을 블랙리스트 등록 검증 + assertTrue(userTokenBlacklistQueryPort.isTokenBlacklisted(accessToken)); + } + + @Test + @DisplayName("회원탈퇴 시 진행/모집 중인 방의 호스트라면 [400 에러 발생]") + void deleteUser_whenHostInActiveRoom_thenThrowBusinessException() throws Exception { + + // given + UserJpaEntity hostUser1 = userJpaRepository.save(TestEntityFactory.createUser(Alias.ARTIST)); + BookJpaEntity book = bookJpaRepository.save(TestEntityFactory.createBook()); + Category category = TestEntityFactory.createLiteratureCategory(); + RoomJpaEntity roomInProgress = createRoom(book, category, IN_PROGRESS); + roomParticipantJpaRepository.save(TestEntityFactory.createRoomParticipant(roomInProgress, hostUser1, HOST, 100.0)); + + // when + String accessToken1 = jwtUtil.createAccessToken(hostUser1.getUserId()); + // then + mockMvc.perform(delete("/users") + .header("Authorization", "Bearer " + accessToken1) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value(USER_CANNOT_DELETE_ROOM_HOST.getCode())); + + // given + UserJpaEntity hostUser2 = userJpaRepository.save(TestEntityFactory.createUser(Alias.ARTIST)); + RoomJpaEntity roomInRecruiting = createRoom(book, category, RECRUITING); + roomParticipantJpaRepository.save(TestEntityFactory.createRoomParticipant(roomInRecruiting, hostUser2, HOST, 100.0)); + + // when + String accessToken2 = jwtUtil.createAccessToken(hostUser2.getUserId()); + + // then + mockMvc.perform(delete("/users") + .header("Authorization", "Bearer " + accessToken2) + .contentType(MediaType.APPLICATION_JSON)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.code").value(USER_CANNOT_DELETE_ROOM_HOST.getCode())); + + } + + + private RoomJpaEntity createRoom(BookJpaEntity book, Category category, RoomStatus roomStatus) { + return roomJpaRepository.save(RoomJpaEntity.builder() + .title("방이름") + .description("설명") + .isPublic(true) + .startDate(LocalDate.now().minusDays(1)) + .endDate(LocalDate.now().plusDays(30)) + .recruitCount(3) + .bookJpaEntity(book) + .category(category) + .memberCount(3) // 방장과 참여자 포함 + .roomStatus(roomStatus) + .build()); + } + + private void updateRoomPercentage(RoomParticipantJpaEntity roomParticipant1, RoomParticipantJpaEntity roomParticipant2, + RoomParticipantJpaEntity roomParticipant3,RoomJpaEntity room) { + roomParticipant1.updateCurrentPage(50); // 탈퇴하는 유저의 진행도 50 + roomParticipantJpaRepository.save(roomParticipant1); + + roomParticipant2.updateCurrentPage(30); // 2번째 유저의 진행도 30; + roomParticipantJpaRepository.save(roomParticipant2); + + roomParticipant3.updateCurrentPage(60); // 3번째 유저의 진행도 60 + roomParticipantJpaRepository.save(roomParticipant3); + + room.updateRoomPercentage(46.6); // 방참여자들의 진행도 평균 46.6 --> 유저1이 탈퇴하면 진행도 평균은 45 + roomJpaRepository.save(room); + } +} From 868bc698f1cf62b86522602bee77a46496e4f3d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Fri, 5 Sep 2025 02:59:04 +0900 Subject: [PATCH 35/55] =?UTF-8?q?[refactor]=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EB=B0=98=EC=98=81=20=EC=88=98=EC=A0=95=20(#176)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../persistence/CommentCommandPersistenceAdapter.java | 2 +- .../CommentLikeCommandPersistenceAdapter.java | 10 +++------- .../persistence/repository/CommentJpaRepository.java | 6 ++++++ .../repository/CommentLikeJpaRepository.java | 7 +++++-- .../konkuk/thip/common/exception/code/ErrorCode.java | 4 ++-- .../argument_resolver/AuthTokenArgumentResolver.java | 1 - .../repository/vote/VoteItemJpaRepository.java | 2 +- .../thip/user/adapter/out/jpa/UserJpaEntity.java | 4 ++-- .../FollowingCommandPersistenceAdapter.java | 8 ++------ .../repository/following/FollowingJpaRepository.java | 9 ++++++++- .../roompost/adapter/in/web/RoomPostSearchApiTest.java | 2 +- 11 files changed, 31 insertions(+), 24 deletions(-) diff --git a/src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentCommandPersistenceAdapter.java b/src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentCommandPersistenceAdapter.java index ad97e75b0..b41d27483 100644 --- a/src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentCommandPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentCommandPersistenceAdapter.java @@ -111,7 +111,7 @@ public void deleteAllByUserId(Long userId) { return; //early return } // 2. 탈퇴한 유저의 모든 댓글, 댓글의 좋아요 삭제 - commentLikeJpaRepository.deleteAllByUserId(userId); + commentLikeJpaRepository.deleteAllByCommentAuthorUserId(userId); commentJpaRepository.deleteAllByUserId(userId); // 3. 게시글 타입별로 댓글 수 감소가 필요한 게시글 Map 생성 Map> postsByType = new HashMap<>(); diff --git a/src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentLikeCommandPersistenceAdapter.java b/src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentLikeCommandPersistenceAdapter.java index 2a446016e..ba0d47c3f 100644 --- a/src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentLikeCommandPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentLikeCommandPersistenceAdapter.java @@ -57,13 +57,9 @@ public void deleteAllByUserId(Long userId) { return; //early return } // 2. 탈퇴한 유저의 모든 댓글 좋아요 관계 삭제 - commentLikeJpaRepository.deleteAllByUserId(userId); - // 3. 해당 ID들로 JPA 엔티티 직접 조회 - List commentEntities = commentJpaRepository.findAllById(likedCommentIds); - // 4. 엔티티에서 직접 좋아요 수 감소 - commentEntities.forEach(entity -> - entity.setLikeCount(Math.max(0, entity.getLikeCount() - 1))); - commentJpaRepository.saveAll(commentEntities); + commentLikeJpaRepository.deleteAllByLikerUserId(userId); + // 3. 탈퇴 유저가 좋아요 누른 댓글의 좋아요 수 감소 + commentJpaRepository.bulkDecrementLikeCountByIds(likedCommentIds); } } diff --git a/src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentJpaRepository.java b/src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentJpaRepository.java index 09ba881d1..7d21b2508 100644 --- a/src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentJpaRepository.java +++ b/src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentJpaRepository.java @@ -30,4 +30,10 @@ public interface CommentJpaRepository extends JpaRepository postIds); + + @Modifying(clearAutomatically = true, flushAutomatically = true) + @Query("UPDATE CommentJpaEntity c " + + "SET c.likeCount = CASE WHEN c.likeCount > 0 THEN c.likeCount - 1 ELSE 0 END " + + "WHERE c.commentId IN :likedCommentIds") + void bulkDecrementLikeCountByIds(@Param("likedCommentIds") List likedCommentIds); } diff --git a/src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentLikeJpaRepository.java b/src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentLikeJpaRepository.java index 298355cf8..41bbe4ca2 100644 --- a/src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentLikeJpaRepository.java +++ b/src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentLikeJpaRepository.java @@ -49,9 +49,12 @@ WHERE cl.commentJpaEntity.commentId IN ( WHERE c.userJpaEntity.userId = :userId ) """) - void deleteAllByUserId(@Param("userId") Long userId); + void deleteAllByCommentAuthorUserId(@Param("userId") Long userId); + + @Modifying(clearAutomatically = true, flushAutomatically = true) + @Query("DELETE FROM CommentLikeJpaEntity cl WHERE cl.userJpaEntity.userId = :userId") + void deleteAllByLikerUserId(@Param("userId") Long userId); - @Modifying(clearAutomatically = true, flushAutomatically = true) @Query("SELECT cl.commentJpaEntity.commentId FROM CommentLikeJpaEntity cl WHERE cl.userJpaEntity.userId = :userId") List findAllCommentIdsByUserId(@Param("userId") Long userId); diff --git a/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java b/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java index 67952fb2f..4a2c91c59 100644 --- a/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java +++ b/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java @@ -49,8 +49,8 @@ public enum ErrorCode implements ResponseCode { USER_NICKNAME_ALREADY_EXISTS(HttpStatus.BAD_REQUEST, 70006, "다른 사용자가 이미 사용중인 닉네임입니다."), USER_ALREADY_SIGNED_UP(HttpStatus.BAD_REQUEST, 70007, "이미 가입된 사용자입니다."), USER_NOT_SIGNED_UP(HttpStatus.BAD_REQUEST, 70008, "가입되지 않은 사용자입니다."), - USER_CANNOT_DELETE_ROOM_HOST(HttpStatus.BAD_REQUEST, 70009, "모집/진행 중인 방의 host는 회원탈퇴를 할 수 없습니다."), - USER_ALREADY_DELETED(HttpStatus.BAD_REQUEST, 70010, "이미 삭제된 유저 입니다."), + USER_CANNOT_DELETE_ROOM_HOST(HttpStatus.BAD_REQUEST, 70009, "모집/진행 중인 방의 방장은 회원탈퇴를 할 수 없습니다."), + USER_ALREADY_DELETED(HttpStatus.BAD_REQUEST, 70010, "이미 삭제된 사용자 입니다."), /** * 75000 : follow error diff --git a/src/main/java/konkuk/thip/common/security/argument_resolver/AuthTokenArgumentResolver.java b/src/main/java/konkuk/thip/common/security/argument_resolver/AuthTokenArgumentResolver.java index a9e9a767f..672b12bf3 100644 --- a/src/main/java/konkuk/thip/common/security/argument_resolver/AuthTokenArgumentResolver.java +++ b/src/main/java/konkuk/thip/common/security/argument_resolver/AuthTokenArgumentResolver.java @@ -32,7 +32,6 @@ public String resolveArgument(MethodParameter parameter, WebDataBinderFactory binderFactory) { Object token = ((HttpServletRequest) webRequest.getNativeRequest()).getAttribute("token"); - log.info("ArgumentResolver에서 토큰 추출: {}", token); if (token == null) { throw new AuthException(AUTH_TOKEN_NOT_FOUND); } diff --git a/src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/vote/VoteItemJpaRepository.java b/src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/vote/VoteItemJpaRepository.java index 16a56620b..6fc4410d1 100644 --- a/src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/vote/VoteItemJpaRepository.java +++ b/src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/vote/VoteItemJpaRepository.java @@ -16,7 +16,7 @@ public interface VoteItemJpaRepository extends JpaRepository voteIds); } diff --git a/src/main/java/konkuk/thip/user/adapter/out/jpa/UserJpaEntity.java b/src/main/java/konkuk/thip/user/adapter/out/jpa/UserJpaEntity.java index 01b2941e8..603a12913 100644 --- a/src/main/java/konkuk/thip/user/adapter/out/jpa/UserJpaEntity.java +++ b/src/main/java/konkuk/thip/user/adapter/out/jpa/UserJpaEntity.java @@ -12,7 +12,7 @@ import java.time.LocalDate; import static konkuk.thip.common.entity.StatusType.INACTIVE; -import static konkuk.thip.common.exception.code.ErrorCode.POST_ALREADY_DELETED; +import static konkuk.thip.common.exception.code.ErrorCode.USER_ALREADY_DELETED; @Entity @Table(name = "users") @@ -70,7 +70,7 @@ public void updateFrom(User user) { public void softDelete(User user) { if(this.status.equals(INACTIVE)){ - throw new InvalidStateException(POST_ALREADY_DELETED); + throw new InvalidStateException(USER_ALREADY_DELETED); } this.status = INACTIVE; this.oauth2Id = user.getOauth2Id(); diff --git a/src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingCommandPersistenceAdapter.java b/src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingCommandPersistenceAdapter.java index 7f79c5fc9..49c53560a 100644 --- a/src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingCommandPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingCommandPersistenceAdapter.java @@ -64,12 +64,8 @@ public void deleteAllByUserId(Long userId) { if (targetUserIds == null || targetUserIds.isEmpty()) { return; //early return } - // 3. 해당 ID들로 JPA 엔티티 직접 조회 - List userEntities = userJpaRepository.findAllById(targetUserIds); - // 4. 엔티티에서 직접 팔로워 수 감소 - userEntities.forEach(entity -> - entity.setFollowerCount(Math.max(0, entity.getFollowerCount() - 1))); - userJpaRepository.saveAll(userEntities); + // 3. 탈퇴 유저가 팔로우 중인 유저들의 팔로워 수 감소 + followingJpaRepository.bulkDecrementFollowerCount(targetUserIds); } diff --git a/src/main/java/konkuk/thip/user/adapter/out/persistence/repository/following/FollowingJpaRepository.java b/src/main/java/konkuk/thip/user/adapter/out/persistence/repository/following/FollowingJpaRepository.java index 2914e0451..5da3b34f6 100644 --- a/src/main/java/konkuk/thip/user/adapter/out/persistence/repository/following/FollowingJpaRepository.java +++ b/src/main/java/konkuk/thip/user/adapter/out/persistence/repository/following/FollowingJpaRepository.java @@ -1,10 +1,10 @@ package konkuk.thip.user.adapter.out.persistence.repository.following; -import io.lettuce.core.dynamic.annotation.Param; import konkuk.thip.user.adapter.out.jpa.FollowingJpaEntity; 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; @@ -24,4 +24,11 @@ public interface FollowingJpaRepository extends JpaRepository findAllTargetUserIdsByUserId(@Param("userId") Long userId); + + @Modifying(clearAutomatically = true, flushAutomatically = true) + @Query("UPDATE UserJpaEntity u " + + "SET u.followerCount = CASE WHEN u.followerCount > 0 THEN u.followerCount - 1 ELSE 0 END " + + "WHERE u.userId IN :targetUserIds" + ) + void bulkDecrementFollowerCount(@Param("targetUserIds") List targetUserIds); } diff --git a/src/test/java/konkuk/thip/roompost/adapter/in/web/RoomPostSearchApiTest.java b/src/test/java/konkuk/thip/roompost/adapter/in/web/RoomPostSearchApiTest.java index c84b71227..2c1ff4f23 100644 --- a/src/test/java/konkuk/thip/roompost/adapter/in/web/RoomPostSearchApiTest.java +++ b/src/test/java/konkuk/thip/roompost/adapter/in/web/RoomPostSearchApiTest.java @@ -129,7 +129,7 @@ void searchGroupRecords_with_vote_and_record_success() throws Exception { for (JsonNode voteItem : voteItems) { assertThat(voteItem.path("voteItemId")).isNotNull(); assertThat(voteItem.path("itemName")).isNotNull(); - assertThat(voteItem.path("percentage")).isNotNull(); + assertThat(voteItem.path("count")).isNotNull(); assertThat(voteItem.path("isVoted")).isNotNull(); } } From cb584734c312bde24d0abd3f07b0a563b5410a76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Mon, 8 Sep 2025 10:06:05 +0900 Subject: [PATCH 36/55] =?UTF-8?q?[refactor]=20=EC=86=8C=ED=94=84=ED=8A=B8?= =?UTF-8?q?=20=EB=94=9C=EB=A6=AC=ED=8A=B8=20=EB=AA=85=EC=8B=9C=20(#176)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../out/persistence/FeedCommandPersistenceAdapter.java | 5 ++--- .../out/persistence/repository/FeedJpaRepository.java | 2 +- .../RoomParticipantCommandPersistenceAdapter.java | 5 +++-- .../roomparticipant/RoomParticipantJpaRepository.java | 2 +- .../AttendanceCheckCommandPersistenceAdapter.java | 2 +- .../out/persistence/RecordCommandPersistenceAdapter.java | 4 ++-- .../attendancecheck/AttendanceCheckJpaRepository.java | 2 +- .../persistence/repository/record/RecordJpaRepository.java | 2 +- 8 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedCommandPersistenceAdapter.java b/src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedCommandPersistenceAdapter.java index 1df36700b..5b9e3e7e6 100644 --- a/src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedCommandPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/feed/adapter/out/persistence/FeedCommandPersistenceAdapter.java @@ -95,7 +95,6 @@ public void deleteAllSavedFeedByUserId(Long userId) { @Override public void deleteAllFeedByUserId(Long userId) { - // 1. 유저가 작성한 피드 게시글 ID 리스트 조회 List feedIds = feedJpaRepository.findFeedIdsByUserId(userId); // 1. 유저가 작성한 피드 게시글 ID 리스트 조회 @@ -110,8 +109,8 @@ public void deleteAllFeedByUserId(Long userId) { postLikeJpaRepository.deleteAllByPostIds(feedIds); // 4. 피드 저장 일괄 삭제 savedFeedJpaRepository.deleteAllByFeedIds(feedIds); - // 5. 탈퇴한 유저가 작성한 피드 게시글 일괄 삭제 - feedJpaRepository.deleteAllByUserId(userId); + // 5. 탈퇴한 유저가 작성한 피드 게시글 soft delete 일괄 처리 + feedJpaRepository.softDeleteAllByUserId(userId); } @Override diff --git a/src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedJpaRepository.java b/src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedJpaRepository.java index 0c315f241..2c84f0a14 100644 --- a/src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedJpaRepository.java +++ b/src/main/java/konkuk/thip/feed/adapter/out/persistence/repository/FeedJpaRepository.java @@ -27,6 +27,6 @@ public interface FeedJpaRepository extends JpaRepository, F @Modifying(clearAutomatically = true, flushAutomatically = true) @Query("UPDATE FeedJpaEntity f SET f.status = 'INACTIVE' WHERE f.userJpaEntity.userId = :userId") - void deleteAllByUserId(@Param("userId") Long userId); + void softDeleteAllByUserId(@Param("userId") Long userId); } diff --git a/src/main/java/konkuk/thip/room/adapter/out/persistence/RoomParticipantCommandPersistenceAdapter.java b/src/main/java/konkuk/thip/room/adapter/out/persistence/RoomParticipantCommandPersistenceAdapter.java index 2dd246590..4eac964bf 100644 --- a/src/main/java/konkuk/thip/room/adapter/out/persistence/RoomParticipantCommandPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/room/adapter/out/persistence/RoomParticipantCommandPersistenceAdapter.java @@ -77,6 +77,8 @@ public boolean existsHostUserInActiveRoom(Long userId) { @Override public void deleteAllByUserId(Long userId) { + // 방 참여 관계 삭제 (member는 진행/모집/만료, host는 만료) + // 방 멤버수 감소, 방 진행도 업데이트 // 1. 유저가 참여한 방 ID 리스트 조회 List roomIds = roomParticipantJpaRepository.findRoomIdsByUserId(userId); @@ -84,7 +86,7 @@ public void deleteAllByUserId(Long userId) { return; // early return } // 2. 유저의 모든 방 참여 관계 일괄 삭제 - roomParticipantJpaRepository.deleteAllByUserId(userId); + roomParticipantJpaRepository.softDeleteAllByUserId(userId); // 3. 해당 ID들로 JPA 엔티티 직접 조회 List roomJpaEntities = roomJpaRepository.findAllByIds(roomIds); @@ -104,7 +106,6 @@ public void deleteAllByUserId(Long userId) { room.setMemberCount(roomParticipantEntities.size()); // 남은 참가자 수로 설정 } - roomJpaRepository.saveAll(roomJpaEntities); } @Override diff --git a/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/roomparticipant/RoomParticipantJpaRepository.java b/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/roomparticipant/RoomParticipantJpaRepository.java index cd9ef1e3a..8be3fc9e3 100644 --- a/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/roomparticipant/RoomParticipantJpaRepository.java +++ b/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/roomparticipant/RoomParticipantJpaRepository.java @@ -41,7 +41,7 @@ public interface RoomParticipantJpaRepository extends JpaRepository findRoomIdsByUserId(@Param("userId") Long userId); diff --git a/src/main/java/konkuk/thip/roompost/adapter/out/persistence/AttendanceCheckCommandPersistenceAdapter.java b/src/main/java/konkuk/thip/roompost/adapter/out/persistence/AttendanceCheckCommandPersistenceAdapter.java index 466cbceb9..e48ff3e9c 100644 --- a/src/main/java/konkuk/thip/roompost/adapter/out/persistence/AttendanceCheckCommandPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/roompost/adapter/out/persistence/AttendanceCheckCommandPersistenceAdapter.java @@ -58,6 +58,6 @@ public void delete(AttendanceCheck attendanceCheck) { @Override public void deleteAllByUserId(Long userId) { - attendanceCheckJpaRepository.deleteAllByUserId(userId); + attendanceCheckJpaRepository.softDeleteAllByUserId(userId); } } diff --git a/src/main/java/konkuk/thip/roompost/adapter/out/persistence/RecordCommandPersistenceAdapter.java b/src/main/java/konkuk/thip/roompost/adapter/out/persistence/RecordCommandPersistenceAdapter.java index a2ae445ba..840f0d8ac 100644 --- a/src/main/java/konkuk/thip/roompost/adapter/out/persistence/RecordCommandPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/roompost/adapter/out/persistence/RecordCommandPersistenceAdapter.java @@ -79,8 +79,8 @@ public void deleteAllByUserId(Long userId) { commentJpaRepository.softDeleteAllByPostIds(recordIds); // 3. 게시글 좋아요 일괄 삭제 postLikeJpaRepository.deleteAllByPostIds(recordIds); - // 4. 탈퇴한 유저가 작성한 기록 게시글 일괄 삭제 - recordJpaRepository.deleteAllByUserId(userId); + // 4. 탈퇴한 유저가 작성한 기록 soft delete 일괄 처리 + recordJpaRepository.softDeleteAllByUserId(userId); } @Override diff --git a/src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/attendancecheck/AttendanceCheckJpaRepository.java b/src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/attendancecheck/AttendanceCheckJpaRepository.java index c3bf53019..6714cf2ca 100644 --- a/src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/attendancecheck/AttendanceCheckJpaRepository.java +++ b/src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/attendancecheck/AttendanceCheckJpaRepository.java @@ -26,5 +26,5 @@ public interface AttendanceCheckJpaRepository extends JpaRepository Date: Mon, 8 Sep 2025 10:07:00 +0900 Subject: [PATCH 37/55] =?UTF-8?q?[refactor]=20=EB=8C=93=EA=B8=80=20?= =?UTF-8?q?=EC=A4=91=EB=B3=B5=20=EC=A7=91=EA=B3=84=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?(#176)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CommentCommandPersistenceAdapter.java | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentCommandPersistenceAdapter.java b/src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentCommandPersistenceAdapter.java index b41d27483..853d4f1bb 100644 --- a/src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentCommandPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentCommandPersistenceAdapter.java @@ -18,7 +18,6 @@ import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; import konkuk.thip.roompost.adapter.out.persistence.repository.vote.VoteJpaRepository; import lombok.RequiredArgsConstructor; -import org.hibernate.Hibernate; import org.springframework.stereotype.Repository; import java.util.*; @@ -112,24 +111,27 @@ public void deleteAllByUserId(Long userId) { } // 2. 탈퇴한 유저의 모든 댓글, 댓글의 좋아요 삭제 commentLikeJpaRepository.deleteAllByCommentAuthorUserId(userId); - commentJpaRepository.deleteAllByUserId(userId); - // 3. 게시글 타입별로 댓글 수 감소가 필요한 게시글 Map 생성 - Map> postsByType = new HashMap<>(); - for (CommentJpaEntity comment : commentsWithPosts) { - PostJpaEntity post = comment.getPostJpaEntity(); - post = (PostJpaEntity) Hibernate.unproxy(post); // 프록시 강제 초기화 및 타입 변경 - post.setCommentCount(Math.max(0, post.getCommentCount() - 1)); - - // 4. 엔티티에서 직접 게시글 댓글 수 감소 + commentJpaRepository.softDeleteAllByUserId(userId); + + // 3) Post 엔티티 기준으로 감소해야 할 횟수를 그룹핑하여 집계 + Map decMap = commentsWithPosts.stream() + .collect(Collectors.groupingBy(CommentJpaEntity::getPostJpaEntity, Collectors.counting())); + + // 4) 타입별로 묶어 한 번만 감소 적용 후 저장 + Map> postsByType = new EnumMap<>(PostType.class); + + decMap.forEach((post, dec) -> { + // 댓글 수를 집계만큼 한 번에 감소 + int newCount = Math.max(0, post.getCommentCount() - dec.intValue()); + post.setCommentCount(newCount); + PostType postType = PostType.from(post.getDtype()); postsByType.computeIfAbsent(postType, k -> new ArrayList<>()).add(post); - } + }); // 5. 게시글 타입별로 저장 처리 postsByType.forEach(this::savePostsJpaEntities); } - - private void savePostsJpaEntities(PostType postType, List posts) { switch (postType) { case FEED: From 98fa05c4eb9e6a159056dd651222963c39942518 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Mon, 8 Sep 2025 10:07:18 +0900 Subject: [PATCH 38/55] =?UTF-8?q?[refactor]=20=EB=8C=93=EA=B8=80=20?= =?UTF-8?q?=EC=86=8C=ED=94=84=ED=8A=B8=20=EB=94=9C=EB=A7=85=ED=8A=B8=20?= =?UTF-8?q?=EB=AA=85=EC=8B=9C=20=EB=B0=8F=20=EB=B3=80=ED=99=98=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=20=EB=AA=85=EC=8B=9C=20(#176)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../out/persistence/repository/CommentJpaRepository.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentJpaRepository.java b/src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentJpaRepository.java index 7d21b2508..ca6721276 100644 --- a/src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentJpaRepository.java +++ b/src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentJpaRepository.java @@ -22,9 +22,11 @@ public interface CommentJpaRepository extends JpaRepository findAllCommentsWithPostsByUserId(@Param("userId") Long userId); @Modifying(clearAutomatically = true, flushAutomatically = true) From 5dd094c86b73b9e512e5681035172e0bc0b0ecf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Mon, 8 Sep 2025 10:07:28 +0900 Subject: [PATCH 39/55] =?UTF-8?q?[refactor]=20=EC=97=90=EB=9F=AC=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80=20(#176)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/konkuk/thip/common/exception/code/ErrorCode.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java b/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java index 4a2c91c59..98c8308f3 100644 --- a/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java +++ b/src/main/java/konkuk/thip/common/exception/code/ErrorCode.java @@ -51,6 +51,7 @@ public enum ErrorCode implements ResponseCode { USER_NOT_SIGNED_UP(HttpStatus.BAD_REQUEST, 70008, "가입되지 않은 사용자입니다."), USER_CANNOT_DELETE_ROOM_HOST(HttpStatus.BAD_REQUEST, 70009, "모집/진행 중인 방의 방장은 회원탈퇴를 할 수 없습니다."), USER_ALREADY_DELETED(HttpStatus.BAD_REQUEST, 70010, "이미 삭제된 사용자 입니다."), + USER_OAUTH2ID_CANNOT_BE_NULL(HttpStatus.INTERNAL_SERVER_ERROR, 70011, "유저의 OAuth2Id 값이 null일 수 없습니다."), /** * 75000 : follow error From 3666ce778316d3052a74198b987bba84a786b7f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Mon, 8 Sep 2025 10:07:57 +0900 Subject: [PATCH 40/55] =?UTF-8?q?[refactor]=20=EC=9C=A0=EC=A0=80=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20UserJpaRepository=EC=97=90=EC=84=9C=20=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95=20(#176)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../out/persistence/FollowingCommandPersistenceAdapter.java | 4 +++- .../out/persistence/repository/UserJpaRepository.java | 6 ++++++ .../repository/following/FollowingJpaRepository.java | 3 --- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingCommandPersistenceAdapter.java b/src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingCommandPersistenceAdapter.java index 49c53560a..b2b6cd4b6 100644 --- a/src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingCommandPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/user/adapter/out/persistence/FollowingCommandPersistenceAdapter.java @@ -56,9 +56,11 @@ public void deleteFollowing(Following following, User targetUser) { @Override public void deleteAllByUserId(Long userId) { + // 유저가 팔로워인 팔로잉 관계 -> 유저가 팔로우 중인 유저들의 팔로워 수 감소 -> 관계 삭제 + // 유저를 팔로우 하는 팔로잉 관계 -> 관계 삭제 // 1. 탈퇴 유저가 팔로우 중인 유저들 ID 조회 - List targetUserIds = followingJpaRepository.findAllTargetUserIdsByUserId(userId); + List targetUserIds = userJpaRepository.findAllTargetUserIdsByUserId(userId); // 2. 탈퇴한 유저의 모든 팔로잉 관계 삭제 followingJpaRepository.deleteAllByUserIdOrFollowingUserId(userId); if (targetUserIds == null || targetUserIds.isEmpty()) { diff --git a/src/main/java/konkuk/thip/user/adapter/out/persistence/repository/UserJpaRepository.java b/src/main/java/konkuk/thip/user/adapter/out/persistence/repository/UserJpaRepository.java index 214ea6009..497296710 100644 --- a/src/main/java/konkuk/thip/user/adapter/out/persistence/repository/UserJpaRepository.java +++ b/src/main/java/konkuk/thip/user/adapter/out/persistence/repository/UserJpaRepository.java @@ -2,7 +2,10 @@ import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import java.util.List; import java.util.Optional; public interface UserJpaRepository extends JpaRepository, UserQueryRepository { @@ -19,4 +22,7 @@ public interface UserJpaRepository extends JpaRepository, U boolean existsByNicknameAndUserIdNot(String nickname, Long userId); boolean existsByOauth2Id(String oauth2Id); + + @Query("SELECT f.followingUserJpaEntity.userId FROM FollowingJpaEntity f WHERE f.userJpaEntity.userId = :userId") + List findAllTargetUserIdsByUserId(@Param("userId") Long userId); } diff --git a/src/main/java/konkuk/thip/user/adapter/out/persistence/repository/following/FollowingJpaRepository.java b/src/main/java/konkuk/thip/user/adapter/out/persistence/repository/following/FollowingJpaRepository.java index 5da3b34f6..0ddc40bdf 100644 --- a/src/main/java/konkuk/thip/user/adapter/out/persistence/repository/following/FollowingJpaRepository.java +++ b/src/main/java/konkuk/thip/user/adapter/out/persistence/repository/following/FollowingJpaRepository.java @@ -22,9 +22,6 @@ public interface FollowingJpaRepository extends JpaRepository findAllTargetUserIdsByUserId(@Param("userId") Long userId); - @Modifying(clearAutomatically = true, flushAutomatically = true) @Query("UPDATE UserJpaEntity u " + "SET u.followerCount = CASE WHEN u.followerCount > 0 THEN u.followerCount - 1 ELSE 0 END " + From 22723243d7d6fbf5341b7b17c2e23e8e5f50f878 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Mon, 8 Sep 2025 10:08:07 +0900 Subject: [PATCH 41/55] =?UTF-8?q?[refactor]=20=EC=97=90=EB=9F=AC=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20=EC=B6=94=EA=B0=80=20(#176)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/konkuk/thip/user/domain/User.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/main/java/konkuk/thip/user/domain/User.java b/src/main/java/konkuk/thip/user/domain/User.java index 8e103d208..ca2b08f28 100644 --- a/src/main/java/konkuk/thip/user/domain/User.java +++ b/src/main/java/konkuk/thip/user/domain/User.java @@ -9,6 +9,9 @@ import java.time.LocalDate; +import static konkuk.thip.common.exception.code.ErrorCode.USER_ALREADY_DELETED; +import static konkuk.thip.common.exception.code.ErrorCode.USER_OAUTH2ID_CANNOT_BE_NULL; + @Getter @SuperBuilder public class User extends BaseDomainEntity { @@ -76,9 +79,13 @@ private void validateCanUpdateNickname(String nickname) { } public void markAsDeleted() { - if (this.oauth2Id != null && !this.oauth2Id.startsWith("deleted:")) { - this.oauth2Id = "deleted:" + this.oauth2Id; + if (this.oauth2Id == null) { + throw new InvalidStateException(USER_OAUTH2ID_CANNOT_BE_NULL); + } + if (this.oauth2Id.startsWith("deleted:")) { + throw new InvalidStateException(USER_ALREADY_DELETED); } + this.oauth2Id = "deleted:" + this.oauth2Id; } } From 3745996269e5f1785fff5b6f70efbf052051194b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Mon, 8 Sep 2025 10:08:18 +0900 Subject: [PATCH 42/55] =?UTF-8?q?[refactor]=20=EC=A3=BC=EC=84=9D=EC=A0=95?= =?UTF-8?q?=EB=A6=AC=20(#176)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../application/service/UserDeleteService.java | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/src/main/java/konkuk/thip/user/application/service/UserDeleteService.java b/src/main/java/konkuk/thip/user/application/service/UserDeleteService.java index 3bfc9879a..842f91dd3 100644 --- a/src/main/java/konkuk/thip/user/application/service/UserDeleteService.java +++ b/src/main/java/konkuk/thip/user/application/service/UserDeleteService.java @@ -57,13 +57,11 @@ public void deleteUser(Long userId, String authToken) { // 3. 유저가 남긴 관련 정보들 삭제 // 팔로잉 관계 삭제 - // 유저가 팔로워인 팔로잉 관계 -> 유저가 팔로우 중인 유저들의 팔로워 수 감소 -> 관계 삭제 - // 유저를 팔로우 하는 팔로잉 관계 -> 관계 삭제 followingCommandPort.deleteAllByUserId(userId); // 최근검색어 삭제 recentSearchCommandPort.deleteAllByUserId(userId); // 알림 삭제 // TODO 알림구현 적용되면 수정 - // notificationCommandPort.deleteAllByUserId(userId); + // notificationCommandPort.softDeleteAllByUserId(userId); // 책/피드 저장 관계 삭제 feedCommandPort.deleteAllSavedFeedByUserId(userId); bookCommandPort.deleteAllSavedBookByUserId(userId); @@ -79,24 +77,14 @@ public void deleteUser(Long userId, String authToken) { postLikeCommandPort.deleteAllByUserId(userId); // 피드 삭제 - // 댓글 삭제 -> 댓글의 좋아요 삭제 - // 좋아요 삭제 - // 피드 저장관계 삭제 feedCommandPort.deleteAllFeedByUserId(userId); // 기록 삭제 - // 댓글 삭제 -> 댓글의 좋아요 삭제 - // 좋아요 삭제 recordCommandPort.deleteAllByUserId(userId); // 투표 삭제 - // 댓글 삭제 -> 댓글의 좋아요 삭제 - // 좋아요 삭제 - // 투표 참여 관계 일괄 삭제 -> 투표 항목 일괄 삭제 voteCommandPort.deleteAllVoteByUserId(userId); - // 방 참여 관계 삭제 (member는 진행/모집/만료, host는 만료) - // 방 멤버수 감소, 방 진행도 업데이트 + // 방 참여 관계 삭제 roomParticipantCommandPort.deleteAllByUserId(userId); - // 유저 삭제 userCommandPort.delete(user); // 토큰 블랙리스트 추가 From bdffe2e79c9c576772c9478939e66473e288d05c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Mon, 8 Sep 2025 10:08:34 +0900 Subject: [PATCH 43/55] =?UTF-8?q?[refactor]=20=EB=93=9D=ED=91=9C=20?= =?UTF-8?q?=EC=88=98=20=EB=B2=8C=ED=81=AC=EC=BF=BC=EB=A6=AC=20=20(#176)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../persistence/VoteCommandPersistenceAdapter.java | 12 ++++-------- .../repository/vote/VoteItemJpaRepository.java | 6 ++++++ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/main/java/konkuk/thip/roompost/adapter/out/persistence/VoteCommandPersistenceAdapter.java b/src/main/java/konkuk/thip/roompost/adapter/out/persistence/VoteCommandPersistenceAdapter.java index 428cc02b3..8db4e64ae 100644 --- a/src/main/java/konkuk/thip/roompost/adapter/out/persistence/VoteCommandPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/roompost/adapter/out/persistence/VoteCommandPersistenceAdapter.java @@ -165,12 +165,8 @@ public void deleteAllVoteParticipantByUserId(Long userId) { } // 2. 투표 참여 관계 삭제 voteParticipantJpaRepository.deleteAllByUserId(userId); - // 3. 해당 ID들로 JPA 엔티티 직접 조회 - List voteItemEntities = voteItemJpaRepository.findAllById(voteItemIds); - // 4. 엔티티에서 직접 투표 항목 수 감소 - voteItemEntities.forEach(entity -> - entity.setCount(Math.max(0, entity.getCount() - 1))); - voteItemJpaRepository.saveAll(voteItemEntities); + // 3. 탈퇴 유저가 투표 했던 투표 항목들의 득표 수 감소 + voteItemJpaRepository.bulkDecrementLikeCount(voteItemIds); } @Override @@ -190,8 +186,8 @@ public void deleteAllVoteByUserId(Long userId) { voteParticipantJpaRepository.deleteAllByVoteIds(voteIds); // 4-2. 투표 항목 일괄 삭제 voteItemJpaRepository.deleteAllByVoteIds(voteIds); - // 5. 탈퇴한 유저가 작성한 투표 게시글 일괄 삭제 - voteJpaRepository.deleteAllByUserId(userId); + // 5. 탈퇴한 유저가 작성한 투표 soft delete 일괄 처리 + voteJpaRepository.softDeleteAllByUserId(userId); } diff --git a/src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/vote/VoteItemJpaRepository.java b/src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/vote/VoteItemJpaRepository.java index 6fc4410d1..5c07e4ac0 100644 --- a/src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/vote/VoteItemJpaRepository.java +++ b/src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/vote/VoteItemJpaRepository.java @@ -19,4 +19,10 @@ public interface VoteItemJpaRepository extends JpaRepository voteIds); + + @Modifying(clearAutomatically = true, flushAutomatically = true) + @Query("UPDATE VoteItemJpaEntity vi " + + "SET vi.count = CASE WHEN vi.count > 0 THEN vi.count - 1 ELSE 0 END " + + "WHERE vi.voteItemId IN :voteItemIds") + void bulkDecrementLikeCount(@Param("voteItemIds") List voteItemIds); } From 05d2aded54934e898b79a7afeb4530d667fd6ddb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Mon, 8 Sep 2025 10:08:41 +0900 Subject: [PATCH 44/55] =?UTF-8?q?[refactor]=20=ED=88=AC=ED=91=9C=20?= =?UTF-8?q?=EC=86=8C=ED=94=84=ED=8A=B8=20=EB=94=9C=EB=A6=AC=ED=8A=B8=20?= =?UTF-8?q?=EB=AA=85=EC=8B=9C=20(#176)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../out/persistence/repository/vote/VoteJpaRepository.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/vote/VoteJpaRepository.java b/src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/vote/VoteJpaRepository.java index e2dd18e29..90251801d 100644 --- a/src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/vote/VoteJpaRepository.java +++ b/src/main/java/konkuk/thip/roompost/adapter/out/persistence/repository/vote/VoteJpaRepository.java @@ -21,5 +21,5 @@ public interface VoteJpaRepository extends JpaRepository, V @Modifying(clearAutomatically = true, flushAutomatically = true) @Query("UPDATE VoteJpaEntity v SET v.status = 'INACTIVE' WHERE v.userJpaEntity.userId = :userId") - void deleteAllByUserId(@Param("userId") Long userId); + void softDeleteAllByUserId(@Param("userId") Long userId); } From 720ec4f49b7f0aa91aa50be7ddeb74e1a96b9b5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Mon, 8 Sep 2025 10:09:00 +0900 Subject: [PATCH 45/55] =?UTF-8?q?[refactor]=20=EB=B8=94=EB=9E=99=EB=A6=AC?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=ED=86=A0=ED=81=B0=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=EC=8B=9C=20=EA=B4=80=EB=A0=A8=20=EC=A0=95=EB=B3=B4=20=ED=95=A8?= =?UTF-8?q?=EA=BB=98=20json=ED=98=95=ED=83=9C=EB=A1=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#176)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../UserTokenBlacklistRedisAdapter.java | 47 +++++++++++++++---- 1 file changed, 37 insertions(+), 10 deletions(-) diff --git a/src/main/java/konkuk/thip/user/adapter/out/persistence/UserTokenBlacklistRedisAdapter.java b/src/main/java/konkuk/thip/user/adapter/out/persistence/UserTokenBlacklistRedisAdapter.java index 0f2a27c32..48f1de97d 100644 --- a/src/main/java/konkuk/thip/user/adapter/out/persistence/UserTokenBlacklistRedisAdapter.java +++ b/src/main/java/konkuk/thip/user/adapter/out/persistence/UserTokenBlacklistRedisAdapter.java @@ -1,15 +1,28 @@ package konkuk.thip.user.adapter.out.persistence; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import konkuk.thip.common.exception.ExternalApiException; +import konkuk.thip.common.security.oauth2.LoginUser; import konkuk.thip.common.security.util.JwtUtil; import konkuk.thip.user.application.port.UserTokenBlacklistCommandPort; import konkuk.thip.user.application.port.UserTokenBlacklistQueryPort; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; +import java.time.LocalDateTime; import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import static konkuk.thip.common.exception.code.ErrorCode.JSON_PROCESSING_ERROR; + +@Slf4j @Component @RequiredArgsConstructor public class UserTokenBlacklistRedisAdapter implements UserTokenBlacklistQueryPort, UserTokenBlacklistCommandPort { @@ -21,30 +34,44 @@ public class UserTokenBlacklistRedisAdapter implements UserTokenBlacklistQueryPo private final JwtUtil jwtUtil; - /** - * 블랙리스트에 토큰 추가 (토큰 만료시간에 맞춰 자동 소멸) - */ + //블랙리스트에 토큰 추가 (토큰 만료시간에 맞춰 자동 소멸) @Override public void addTokenToBlacklist(String token) { Date expiration = jwtUtil.getExpirationAllowExpired(token); + LoginUser loginUser = jwtUtil.getLoginUser(token); + LocalDateTime withdrawalTime = LocalDateTime.now(); String key = makeBlacklistKey(token); - redisTemplate.opsForValue().set(key, "BLACKLISTED"); + + Map valueMap = new HashMap<>(); + valueMap.put("userId", loginUser.userId()); + valueMap.put("withdrawalTime", withdrawalTime); + String valueJson = null; + try { + ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(new JavaTimeModule()); + mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + valueJson = mapper.writeValueAsString(valueMap); + } catch (JsonProcessingException e) { + throw new ExternalApiException(JSON_PROCESSING_ERROR); + } + redisTemplate.opsForValue().set(key, valueJson); + log.info("Add token to blacklist - userId: {}, withdrawalTime: {}, expiration: {}", + loginUser.userId(), + withdrawalTime, + expiration + ); // 토큰 만료시각으로 Redis에 expireAt 지정 (만료 이후 Redis에서 자동 삭제) redisTemplate.expireAt(key, expiration.toInstant()); } - /** - * 토큰이 블랙리스트에 등록되어있는지 체크 - */ + // 토큰이 블랙리스트에 등록되어있는지 체크 @Override public boolean isTokenBlacklisted(String token) { String key = makeBlacklistKey(token); return redisTemplate.hasKey(key); } - /** - * JWT 블랙리스트 Redis Key 생성 규칙 - */ + // JWT 블랙리스트 Redis Key 생성 규칙 private String makeBlacklistKey(String token) { return blacklistPrefix + ":" + token; } From 3c530d532bee517d9c0c9d110dbedc2ca978e011 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Mon, 8 Sep 2025 10:09:07 +0900 Subject: [PATCH 46/55] =?UTF-8?q?[refactor]=20=EC=8A=A4=EC=9B=A8=EA=B1=B0?= =?UTF-8?q?=20=EC=97=90=EB=9F=AC=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?(#176)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../konkuk/thip/common/swagger/SwaggerResponseDescription.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java b/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java index ea51190bf..3482586cc 100644 --- a/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java +++ b/src/main/java/konkuk/thip/common/swagger/SwaggerResponseDescription.java @@ -38,7 +38,8 @@ public enum SwaggerResponseDescription { USER_DELETE(new LinkedHashSet<>(Set.of( USER_CANNOT_DELETE_ROOM_HOST, USER_NOT_FOUND, - USER_ALREADY_DELETED + USER_ALREADY_DELETED, + USER_OAUTH2ID_CANNOT_BE_NULL ))), // Follow From 7ea4c22c1fad886833ffd4f569dc1c6dadef52d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Mon, 8 Sep 2025 10:09:27 +0900 Subject: [PATCH 47/55] =?UTF-8?q?[refactor]=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95=20(#176)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../adapter/in/web/UserDeleteApiTest.java | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/test/java/konkuk/thip/user/adapter/in/web/UserDeleteApiTest.java b/src/test/java/konkuk/thip/user/adapter/in/web/UserDeleteApiTest.java index 8b715d63d..461ee8305 100644 --- a/src/test/java/konkuk/thip/user/adapter/in/web/UserDeleteApiTest.java +++ b/src/test/java/konkuk/thip/user/adapter/in/web/UserDeleteApiTest.java @@ -29,6 +29,7 @@ import konkuk.thip.roompost.adapter.out.persistence.repository.vote.VoteItemJpaRepository; import konkuk.thip.roompost.adapter.out.persistence.repository.vote.VoteJpaRepository; import konkuk.thip.roompost.adapter.out.persistence.repository.vote.VoteParticipantJpaRepository; +import konkuk.thip.user.adapter.out.jpa.FollowingJpaEntity; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; import konkuk.thip.user.adapter.out.persistence.repository.following.FollowingJpaRepository; @@ -43,6 +44,7 @@ import org.springframework.http.MediaType; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; import java.time.LocalDate; @@ -87,7 +89,6 @@ public class UserDeleteApiTest { @Autowired private JwtUtil jwtUtil; @Autowired private EntityManager entityManager; - @Autowired private ObjectMapper objectMapper; @AfterEach void tearDown() { @@ -121,8 +122,8 @@ void deleteUser_success() throws Exception { UserJpaEntity otherMemberUser3 = userJpaRepository.save(TestEntityFactory.createUser(Alias.ARTIST)); // 팔로잉 관계 설정 - followingJpaRepository.save(TestEntityFactory.createFollowing(testUser1, otherHostUser2)); // 유저 1이 유저 2 팔로우 - followingJpaRepository.save(TestEntityFactory.createFollowing(otherMemberUser3, testUser1)); // 유저 3이 유저 1팔로우 + FollowingJpaEntity u1_u2_f1 = followingJpaRepository.save(TestEntityFactory.createFollowing(testUser1, otherHostUser2)); // 유저 1이 유저 2 팔로우 + FollowingJpaEntity u3_u1_f2 = followingJpaRepository.save(TestEntityFactory.createFollowing(otherMemberUser3, testUser1)); // 유저 3이 유저 1팔로우 followingJpaRepository.save(TestEntityFactory.createFollowing(otherMemberUser3, otherHostUser2)); // 유저 3이 유저 2팔로우 otherHostUser2.setFollowerCount(2); userJpaRepository.save(otherHostUser2); //유저 2 팔로워수 2 testUser1.setFollowerCount(1); userJpaRepository.save(testUser1); //유저 1 팔로워수 1 @@ -264,9 +265,10 @@ void deleteUser_success() throws Exception { // then: 1) 유저 팔로잉/팔로워 관계 삭제 // 탈퇴한 유저1의 팔로잉/팔로워 관계는 모두 삭제되어야하고, 관련 없는 유저3->유저2 팔로우관계만 남아있어야함 // 유저2의 팔로워 수가 1이어야함 - assertEquals(followingJpaRepository.findAll().get(0).getUserJpaEntity().getUserId(), otherMemberUser3.getUserId()); - assertEquals(2, (int) otherHostUser2.getFollowerCount()); - + assertTrue(followingJpaRepository.findById(u1_u2_f1.getFollowingId()).isEmpty()); + assertTrue(followingJpaRepository.findById(u3_u1_f2.getFollowingId()).isEmpty()); + UserJpaEntity updateUser2 = userJpaRepository.findById(otherHostUser2.getUserId()).orElse(null); + assertEquals(1, updateUser2.getFollowerCount()); // 2) 최근검색어 삭제 assertTrue(recentSearchJpaRepository.findAll().isEmpty()); @@ -282,12 +284,13 @@ void deleteUser_success() throws Exception { // 탈퇴한 유저가 참여한 투표관계 모두 삭제 // 탈퇴한 유저가 참여했던 투표의 득표수 감소 assertTrue(voteParticipantJpaRepository.findById(u1_vp1.getVoteParticipantId()).isEmpty()); - assertEquals(1, v1_vi1.getCount()); + VoteItemJpaEntity updateVi1 = voteItemJpaRepository.findById(v1_vi1.getVoteItemId()).orElse(null); + assertEquals(0, updateVi1.getCount()); // 6) 유저 댓글 좋아요 삭제 // 탈퇴한 유저의 모든 댓글 좋아요 관계 삭제 // 탈퇴한 유저가 좋아요한 댓글의 좋아요 수 감소 - assertTrue(voteParticipantJpaRepository.findById(u1_c1_cl1.getLikeId()).isEmpty()); + assertTrue(commentLikeJpaRepository.findById(u1_c1_cl1.getLikeId()).isEmpty()); CommentJpaEntity updatedCm1 = commentJpaRepository.findById(u2_v1_c1.getCommentId()).orElse(null); assertEquals(0, updatedCm1.getLikeCount()); From 15e0a8a6db5574dd5835c9cf389be9ade056e678 Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Mon, 8 Sep 2025 17:42:41 +0900 Subject: [PATCH 48/55] =?UTF-8?q?[refactor]=20=ED=83=88=ED=87=B4=ED=95=A0?= =?UTF-8?q?=20=EC=9C=A0=EC=A0=80=EA=B0=80=20=EC=9E=91=EC=84=B1=ED=95=9C=20?= =?UTF-8?q?=EB=8C=93=EA=B8=80=20=EC=82=AD=EC=A0=9C=EC=99=80=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=EB=90=9C=20=EC=98=81=EC=86=8D=EC=84=B1=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=88=98=EC=A0=95=20(#176)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CommentJpaRepository 에서 유저가 작성한 댓글과 연관되는 게시글을 fetch join 으로 가져올 때, 불필요한 type 조건 삭제 - 영속성 어댑터에서 posts의 commentCount 먼저 업데이트 한 후, comments 의 soft delete(벌크 jpql 및 em flush) 하도록 순서 변경 --- .../CommentCommandPersistenceAdapter.java | 56 +++++++++++++------ .../repository/CommentJpaRepository.java | 8 ++- 2 files changed, 45 insertions(+), 19 deletions(-) diff --git a/src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentCommandPersistenceAdapter.java b/src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentCommandPersistenceAdapter.java index 853d4f1bb..d24a0cfa1 100644 --- a/src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentCommandPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentCommandPersistenceAdapter.java @@ -101,35 +101,57 @@ public void softDeleteAllByPostId(Long postId) { commentJpaRepository.softDeleteAllByPostId(postId); } +// @Override +// public void deleteAllByUserId(Long userId) { +// +// // 1. 탈퇴 유저가 작성한 댓글과 연관된 게시글을 JOIN FETCH로 함께 조회 +// List commentsWithPosts = commentJpaRepository.findAllCommentsWithPostsByUserId(userId); +// if (commentsWithPosts == null || commentsWithPosts.isEmpty()) { +// return; //early return +// } +// // 2. 탈퇴한 유저의 모든 댓글, 댓글의 좋아요 삭제 +// commentLikeJpaRepository.deleteAllByCommentAuthorUserId(userId); +// commentJpaRepository.softDeleteAllByUserId(userId); +// +// // 3) Post 엔티티 기준으로 감소해야 할 횟수를 그룹핑하여 집계 +// Map decMap = commentsWithPosts.stream() +// .collect(Collectors.groupingBy(CommentJpaEntity::getPostJpaEntity, Collectors.counting())); +// +// // 4) 타입별로 묶어 한 번만 감소 적용 후 저장 +// Map> postsByType = new EnumMap<>(PostType.class); +// +// decMap.forEach((post, dec) -> { +// // 댓글 수를 집계만큼 한 번에 감소 +// int newCount = Math.max(0, post.getCommentCount() - dec.intValue()); +// post.setCommentCount(newCount); +// +// PostType postType = PostType.from(post.getDtype()); +// postsByType.computeIfAbsent(postType, k -> new ArrayList<>()).add(post); +// }); +// // 5. 게시글 타입별로 저장 처리 +// postsByType.forEach(this::savePostsJpaEntities); +// } + @Override public void deleteAllByUserId(Long userId) { - // 1. 탈퇴 유저가 작성한 댓글과 연관된 게시글을 JOIN FETCH로 함께 조회 List commentsWithPosts = commentJpaRepository.findAllCommentsWithPostsByUserId(userId); if (commentsWithPosts == null || commentsWithPosts.isEmpty()) { return; //early return } - // 2. 탈퇴한 유저의 모든 댓글, 댓글의 좋아요 삭제 - commentLikeJpaRepository.deleteAllByCommentAuthorUserId(userId); - commentJpaRepository.softDeleteAllByUserId(userId); - // 3) Post 엔티티 기준으로 감소해야 할 횟수를 그룹핑하여 집계 + // 2. 삭제될 댓글이 어느 Post에 몇 개씩 붙어있는지 집계 (postId 기준 추천) Map decMap = commentsWithPosts.stream() .collect(Collectors.groupingBy(CommentJpaEntity::getPostJpaEntity, Collectors.counting())); - // 4) 타입별로 묶어 한 번만 감소 적용 후 저장 - Map> postsByType = new EnumMap<>(PostType.class); - - decMap.forEach((post, dec) -> { - // 댓글 수를 집계만큼 한 번에 감소 - int newCount = Math.max(0, post.getCommentCount() - dec.intValue()); - post.setCommentCount(newCount); + for (PostJpaEntity p : decMap.keySet()) { + long dec = decMap.getOrDefault(p, 0L); + p.setCommentCount(Math.max(0, p.getCommentCount() - (int) dec)); + } - PostType postType = PostType.from(post.getDtype()); - postsByType.computeIfAbsent(postType, k -> new ArrayList<>()).add(post); - }); - // 5. 게시글 타입별로 저장 처리 - postsByType.forEach(this::savePostsJpaEntities); + // 3. 탈퇴한 유저의 모든 댓글, 댓글의 좋아요 삭제 + commentLikeJpaRepository.deleteAllByCommentAuthorUserId(userId); + commentJpaRepository.softDeleteAllByUserId(userId); } private void savePostsJpaEntities(PostType postType, List posts) { diff --git a/src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentJpaRepository.java b/src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentJpaRepository.java index ca6721276..98b313186 100644 --- a/src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentJpaRepository.java +++ b/src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentJpaRepository.java @@ -24,9 +24,13 @@ public interface CommentJpaRepository extends JpaRepository findAllCommentsWithPostsByUserId(@Param("userId") Long userId); + @Query("SELECT c FROM CommentJpaEntity c JOIN FETCH c.postJpaEntity p " + - "WHERE (TYPE(p) = FeedJpaEntity OR TYPE(p) = RecordJpaEntity OR TYPE(p) = VoteJpaEntity) " + - "AND c.userJpaEntity.userId = :userId") + "WHERE c.userJpaEntity.userId = :userId") List findAllCommentsWithPostsByUserId(@Param("userId") Long userId); @Modifying(clearAutomatically = true, flushAutomatically = true) From bcf15c52ccefd75b25e9358d5bd946d0e25fe98f Mon Sep 17 00:00:00 2001 From: seongjunnoh Date: Mon, 8 Sep 2025 18:16:32 +0900 Subject: [PATCH 49/55] =?UTF-8?q?[test]=20CommentCommandPersistenceAdapter?= =?UTF-8?q?=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#176)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 수정된 deleteAllByUserId 메서드가 제대로 동작함을 보여주기 위해 테스트 코드 작성 --- .../CommentCommandPersistenceAdapterTest.java | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 src/test/java/konkuk/thip/comment/adapter/out/persistence/CommentCommandPersistenceAdapterTest.java diff --git a/src/test/java/konkuk/thip/comment/adapter/out/persistence/CommentCommandPersistenceAdapterTest.java b/src/test/java/konkuk/thip/comment/adapter/out/persistence/CommentCommandPersistenceAdapterTest.java new file mode 100644 index 000000000..875360766 --- /dev/null +++ b/src/test/java/konkuk/thip/comment/adapter/out/persistence/CommentCommandPersistenceAdapterTest.java @@ -0,0 +1,94 @@ +package konkuk.thip.comment.adapter.out.persistence; + +import jakarta.persistence.EntityManager; +import konkuk.thip.book.adapter.out.jpa.BookJpaEntity; +import konkuk.thip.book.adapter.out.persistence.repository.BookJpaRepository; +import konkuk.thip.comment.adapter.out.jpa.CommentJpaEntity; +import konkuk.thip.comment.adapter.out.mapper.CommentMapper; +import konkuk.thip.comment.adapter.out.persistence.repository.CommentJpaRepository; +import konkuk.thip.comment.adapter.out.persistence.repository.CommentLikeJpaRepository; +import konkuk.thip.common.entity.StatusType; +import konkuk.thip.common.util.TestEntityFactory; +import konkuk.thip.config.TestQuerydslConfig; +import konkuk.thip.feed.adapter.out.jpa.FeedJpaEntity; +import konkuk.thip.feed.adapter.out.persistence.repository.FeedJpaRepository; +import konkuk.thip.post.domain.PostType; +import konkuk.thip.roompost.adapter.out.persistence.repository.record.RecordJpaRepository; +import konkuk.thip.roompost.adapter.out.persistence.repository.vote.VoteJpaRepository; +import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; +import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; +import konkuk.thip.user.domain.value.Alias; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.bean.override.mockito.MockitoBean; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@DataJpaTest +@ActiveProfiles("test") +@Import({TestQuerydslConfig.class, CommentCommandPersistenceAdapter.class}) +class CommentCommandPersistenceAdapterTest { + + @Autowired CommentCommandPersistenceAdapter adapter; // repository 인터페이스가 아니므로 자동 스캔 X -> import 해줘야함 + @Autowired CommentJpaRepository commentJpaRepository; + @Autowired CommentLikeJpaRepository commentLikeJpaRepository; + @Autowired BookJpaRepository bookJpaRepository; + @Autowired FeedJpaRepository feedJpaRepository; + @Autowired UserJpaRepository userJpaRepository; + @Autowired RecordJpaRepository recordJpaRepository; + @Autowired VoteJpaRepository voteJpaRepository; + + @MockitoBean CommentMapper commentMapper; // Mock bean 으로 설정 + + @Autowired EntityManager em; + + @Test + @DisplayName("deleteAllByUserId: Post.commentCount는 targetUser의 댓글 수만큼 감소하고, 해당 댓글들은 INACTIVE 처리된다.") + void deleteAllByUserId_updatesPostCount_andSoftDeletesComments() { + // given + UserJpaEntity targetUser = userJpaRepository.save(TestEntityFactory.createUser(Alias.WRITER)); + UserJpaEntity otherUser = userJpaRepository.save(TestEntityFactory.createUser(Alias.WRITER)); + + BookJpaEntity bookJpaEntity = bookJpaRepository.save(TestEntityFactory.createBook()); + FeedJpaEntity feedJpaEntity = feedJpaRepository.save(TestEntityFactory.createFeed(otherUser, bookJpaEntity, true)); + + CommentJpaEntity c1 = commentJpaRepository.save(TestEntityFactory.createComment(feedJpaEntity, targetUser, PostType.FEED)); + CommentJpaEntity c2 = commentJpaRepository.save(TestEntityFactory.createComment(feedJpaEntity, targetUser, PostType.FEED)); + CommentJpaEntity c3 = commentJpaRepository.save(TestEntityFactory.createComment(feedJpaEntity, otherUser, PostType.FEED)); + + // 피드의 commentCount update + feedJpaEntity.setCommentCount(3); + + // when + adapter.deleteAllByUserId(targetUser.getUserId()); + + // then + em.flush(); // 더티체킹 → DB 반영 + em.clear(); + + // 1) 게시글의 commentCount가 3 -> 1 로 감소했는지 확인 (targetUser 댓글 2개 삭제) + FeedJpaEntity updated = feedJpaRepository.findByPostId(feedJpaEntity.getPostId()).orElseThrow(); + assertThat(updated.getCommentCount()).isEqualTo(1); + + // 2) targetUser의 댓글은 INACTIVE, otherUser의 댓글은 ACTIVE + List all = commentJpaRepository.findAll(); // 상태 필터링 AOP 없다면 전부 보임 + long targetInactive = all.stream() + .filter(c -> c.getUserJpaEntity().getUserId().equals(targetUser.getUserId())) + .filter(c -> StatusType.INACTIVE.name().equals(c.getStatus().name())) // BaseJpaEntity.status 타입에 맞게 비교 + .count(); + + long otherActive = all.stream() + .filter(c -> c.getUserJpaEntity().getUserId().equals(otherUser.getUserId())) + .filter(c -> StatusType.ACTIVE.name().equals(c.getStatus().name())) + .count(); + + assertThat(targetInactive).isEqualTo(2); // targetUser가 단 댓글 2개 모두 INACTIVE + assertThat(otherActive).isEqualTo(1); // otherUser가 단 댓글 1개는 그대로 ACTIVE + } +} From 3bac26ced36c3b37dee1b593e9d03586868433ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Mon, 8 Sep 2025 21:19:40 +0900 Subject: [PATCH 50/55] =?UTF-8?q?[refactor]=20=EB=8C=93=EA=B8=88=20?= =?UTF-8?q?=EA=B0=90=EC=86=8C=20=EB=A1=9C=EC=A7=81=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=88=9C=EC=84=9C=20=EC=88=98=EC=A0=95=20=20(#176)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CommentCommandPersistenceAdapter.java | 79 +++---------------- .../repository/CommentJpaRepository.java | 5 -- 2 files changed, 11 insertions(+), 73 deletions(-) diff --git a/src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentCommandPersistenceAdapter.java b/src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentCommandPersistenceAdapter.java index d24a0cfa1..5c31fc429 100644 --- a/src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentCommandPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/comment/adapter/out/persistence/CommentCommandPersistenceAdapter.java @@ -7,12 +7,9 @@ import konkuk.thip.comment.application.port.out.CommentCommandPort; import konkuk.thip.comment.domain.Comment; import konkuk.thip.common.exception.EntityNotFoundException; -import konkuk.thip.feed.adapter.out.jpa.FeedJpaEntity; import konkuk.thip.post.domain.PostType; import konkuk.thip.feed.adapter.out.persistence.repository.FeedJpaRepository; import konkuk.thip.post.adapter.out.jpa.PostJpaEntity; -import konkuk.thip.roompost.adapter.out.jpa.RecordJpaEntity; -import konkuk.thip.roompost.adapter.out.jpa.VoteJpaEntity; import konkuk.thip.roompost.adapter.out.persistence.repository.record.RecordJpaRepository; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; @@ -61,17 +58,6 @@ public Long save(Comment comment) { ).getCommentId(); } - private PostJpaEntity findPostJpaEntity(PostType postType, Long postId) { - return switch (postType) { - case FEED -> feedJpaRepository.findByPostId(postId) - .orElseThrow(() -> new EntityNotFoundException(FEED_NOT_FOUND)); - case RECORD -> recordJpaRepository.findByPostId(postId) - .orElseThrow(() -> new EntityNotFoundException(RECORD_NOT_FOUND)); - case VOTE -> voteJpaRepository.findByPostId(postId) - .orElseThrow(() -> new EntityNotFoundException(VOTE_NOT_FOUND)); - }; - } - @Override public Optional findById(Long id) { return commentJpaRepository.findByCommentId(id) @@ -101,37 +87,6 @@ public void softDeleteAllByPostId(Long postId) { commentJpaRepository.softDeleteAllByPostId(postId); } -// @Override -// public void deleteAllByUserId(Long userId) { -// -// // 1. 탈퇴 유저가 작성한 댓글과 연관된 게시글을 JOIN FETCH로 함께 조회 -// List commentsWithPosts = commentJpaRepository.findAllCommentsWithPostsByUserId(userId); -// if (commentsWithPosts == null || commentsWithPosts.isEmpty()) { -// return; //early return -// } -// // 2. 탈퇴한 유저의 모든 댓글, 댓글의 좋아요 삭제 -// commentLikeJpaRepository.deleteAllByCommentAuthorUserId(userId); -// commentJpaRepository.softDeleteAllByUserId(userId); -// -// // 3) Post 엔티티 기준으로 감소해야 할 횟수를 그룹핑하여 집계 -// Map decMap = commentsWithPosts.stream() -// .collect(Collectors.groupingBy(CommentJpaEntity::getPostJpaEntity, Collectors.counting())); -// -// // 4) 타입별로 묶어 한 번만 감소 적용 후 저장 -// Map> postsByType = new EnumMap<>(PostType.class); -// -// decMap.forEach((post, dec) -> { -// // 댓글 수를 집계만큼 한 번에 감소 -// int newCount = Math.max(0, post.getCommentCount() - dec.intValue()); -// post.setCommentCount(newCount); -// -// PostType postType = PostType.from(post.getDtype()); -// postsByType.computeIfAbsent(postType, k -> new ArrayList<>()).add(post); -// }); -// // 5. 게시글 타입별로 저장 처리 -// postsByType.forEach(this::savePostsJpaEntities); -// } - @Override public void deleteAllByUserId(Long userId) { // 1. 탈퇴 유저가 작성한 댓글과 연관된 게시글을 JOIN FETCH로 함께 조회 @@ -144,37 +99,25 @@ public void deleteAllByUserId(Long userId) { Map decMap = commentsWithPosts.stream() .collect(Collectors.groupingBy(CommentJpaEntity::getPostJpaEntity, Collectors.counting())); + // 3. 댓글 수를 집계만큼 한 번에 감소 for (PostJpaEntity p : decMap.keySet()) { long dec = decMap.getOrDefault(p, 0L); p.setCommentCount(Math.max(0, p.getCommentCount() - (int) dec)); } - // 3. 탈퇴한 유저의 모든 댓글, 댓글의 좋아요 삭제 + // 4. 탈퇴한 유저의 모든 댓글, 댓글의 좋아요 삭제 commentLikeJpaRepository.deleteAllByCommentAuthorUserId(userId); commentJpaRepository.softDeleteAllByUserId(userId); } - private void savePostsJpaEntities(PostType postType, List posts) { - switch (postType) { - case FEED: - feedJpaRepository.saveAll(posts.stream() - .filter(p -> p instanceof FeedJpaEntity) - .map(p -> (FeedJpaEntity) p) - .collect(Collectors.toList())); - break; - case RECORD: - recordJpaRepository.saveAll(posts.stream() - .filter(p -> p instanceof RecordJpaEntity) - .map(p -> (RecordJpaEntity) p) - .collect(Collectors.toList())); - break; - case VOTE: - voteJpaRepository.saveAll(posts.stream() - .filter(p -> p instanceof VoteJpaEntity) - .map(p -> (VoteJpaEntity) p) - .collect(Collectors.toList())); - voteJpaRepository.flush(); - break; - } + private PostJpaEntity findPostJpaEntity(PostType postType, Long postId) { + return switch (postType) { + case FEED -> feedJpaRepository.findByPostId(postId) + .orElseThrow(() -> new EntityNotFoundException(FEED_NOT_FOUND)); + case RECORD -> recordJpaRepository.findByPostId(postId) + .orElseThrow(() -> new EntityNotFoundException(RECORD_NOT_FOUND)); + case VOTE -> voteJpaRepository.findByPostId(postId) + .orElseThrow(() -> new EntityNotFoundException(VOTE_NOT_FOUND)); + }; } } diff --git a/src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentJpaRepository.java b/src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentJpaRepository.java index 98b313186..98048b3ac 100644 --- a/src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentJpaRepository.java +++ b/src/main/java/konkuk/thip/comment/adapter/out/persistence/repository/CommentJpaRepository.java @@ -24,11 +24,6 @@ public interface CommentJpaRepository extends JpaRepository findAllCommentsWithPostsByUserId(@Param("userId") Long userId); - @Query("SELECT c FROM CommentJpaEntity c JOIN FETCH c.postJpaEntity p " + "WHERE c.userJpaEntity.userId = :userId") List findAllCommentsWithPostsByUserId(@Param("userId") Long userId); From be383b0c23d1c5945a076b6985eda4f9fcc8c0c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Mon, 8 Sep 2025 21:19:58 +0900 Subject: [PATCH 51/55] =?UTF-8?q?[refactor]=20=EA=B2=8C=EC=8B=9C=EA=B8=80?= =?UTF-8?q?=20=EC=A2=8B=EC=95=84=EC=9A=94=20=EA=B0=90=EC=86=8C=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=BD=94=EB=93=9C=20=EC=88=9C=EC=84=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20=20(#176)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PostLikeCommandPersistenceAdapter.java | 45 +++---------------- 1 file changed, 7 insertions(+), 38 deletions(-) diff --git a/src/main/java/konkuk/thip/post/adapter/out/persistence/PostLikeCommandPersistenceAdapter.java b/src/main/java/konkuk/thip/post/adapter/out/persistence/PostLikeCommandPersistenceAdapter.java index 87eba05c5..b7d61e7d8 100644 --- a/src/main/java/konkuk/thip/post/adapter/out/persistence/PostLikeCommandPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/post/adapter/out/persistence/PostLikeCommandPersistenceAdapter.java @@ -60,60 +60,29 @@ public void deleteAllByPostId(Long postId) { @Override public void deleteAllByUserId(Long userId) { - // 1. 탈퇴 유저가 좋아요한 게시글을 JOIN 조회 List likedPosts = postLikeJpaRepository.findAllPostsWithTypeByUserId(userId); if (likedPosts == null || likedPosts.isEmpty()) { return; // early return } - // 2. 탈퇴한 유저의 모든 게시글 좋아요 삭제 - postLikeJpaRepository.deleteAllByUserId(userId); - - // 3. 게시글 타입별로 좋아요 수 감소가 필요한 게시글 Map 생성 - Map> postsByType = new HashMap<>(); + // 2. 게시글 좋아요 수 감소 for (PostJpaEntity post : likedPosts) { - // 4. 엔티티에서 직접 게시글 좋아요 수 감소 post.setLikeCount(Math.max(0, post.getLikeCount() - 1)); - - PostType postType = PostType.from(post.getDtype()); - postsByType.computeIfAbsent(postType, k -> new ArrayList<>()).add(post); } - // 5. 게시글 타입별로 저장 처리 - postsByType.forEach(this::savePostsJpaEntities); + + // 3. 탈퇴한 유저의 모든 게시글 좋아요 삭제 + postLikeJpaRepository.deleteAllByUserId(userId); } private PostJpaEntity findPostJpaEntity(PostType postType, Long postId) { return switch (postType) { - case FEED -> feedJpaRepository.findById(postId) + case FEED -> feedJpaRepository.findByPostId(postId) .orElseThrow(() -> new EntityNotFoundException(FEED_NOT_FOUND)); - case RECORD -> recordJpaRepository.findById(postId) + case RECORD -> recordJpaRepository.findByPostId(postId) .orElseThrow(() -> new EntityNotFoundException(RECORD_NOT_FOUND)); - case VOTE -> voteJpaRepository.findById(postId) + case VOTE -> voteJpaRepository.findByPostId(postId) .orElseThrow(() -> new EntityNotFoundException(VOTE_NOT_FOUND)); }; } - - private void savePostsJpaEntities(PostType postType, List posts) { - switch (postType) { - case FEED: - feedJpaRepository.saveAll(posts.stream() - .filter(p -> p instanceof FeedJpaEntity) - .map(p -> (FeedJpaEntity) p) - .collect(Collectors.toList())); - break; - case RECORD: - recordJpaRepository.saveAll(posts.stream() - .filter(p -> p instanceof RecordJpaEntity) - .map(p -> (RecordJpaEntity) p) - .collect(Collectors.toList())); - break; - case VOTE: - voteJpaRepository.saveAll(posts.stream() - .filter(p -> p instanceof VoteJpaEntity) - .map(p -> (VoteJpaEntity) p) - .collect(Collectors.toList())); - break; - } - } } From 63ffe7bdfa73b28d84769079745e892c6f5dfce1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Mon, 8 Sep 2025 21:20:48 +0900 Subject: [PATCH 52/55] =?UTF-8?q?[refactor]=20=EB=B0=A9=20=EB=A9=A4?= =?UTF-8?q?=EB=B2=84=EC=88=98/=EC=A7=84=ED=96=89=EB=8F=84=20=EC=97=85?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=8A=B8=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=EC=85=98=20=EC=82=AC=EC=9A=A9=ED=95=98=EC=97=AC=20=EC=A7=91?= =?UTF-8?q?=EA=B3=84=EC=BF=BC=EB=A6=AC=EB=A1=9C=20=EC=88=98=EC=A0=95=20(#1?= =?UTF-8?q?76)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...mParticipantCommandPersistenceAdapter.java | 26 +++++++------------ .../persistence/projection/RoomStatsRow.java | 7 +++++ .../repository/RoomJpaRepository.java | 12 ++++++--- .../RoomParticipantJpaRepository.java | 12 +++++++++ 4 files changed, 38 insertions(+), 19 deletions(-) create mode 100644 src/main/java/konkuk/thip/room/adapter/out/persistence/projection/RoomStatsRow.java diff --git a/src/main/java/konkuk/thip/room/adapter/out/persistence/RoomParticipantCommandPersistenceAdapter.java b/src/main/java/konkuk/thip/room/adapter/out/persistence/RoomParticipantCommandPersistenceAdapter.java index 4eac964bf..e7df0aba5 100644 --- a/src/main/java/konkuk/thip/room/adapter/out/persistence/RoomParticipantCommandPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/room/adapter/out/persistence/RoomParticipantCommandPersistenceAdapter.java @@ -8,6 +8,7 @@ import konkuk.thip.room.adapter.out.persistence.repository.RoomJpaRepository; import konkuk.thip.room.adapter.out.persistence.repository.roomparticipant.RoomParticipantJpaRepository; import konkuk.thip.room.application.port.out.RoomParticipantCommandPort; +import konkuk.thip.room.adapter.out.persistence.projection.RoomStatsRow; import konkuk.thip.room.domain.RoomParticipant; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; @@ -87,24 +88,17 @@ public void deleteAllByUserId(Long userId) { } // 2. 유저의 모든 방 참여 관계 일괄 삭제 roomParticipantJpaRepository.softDeleteAllByUserId(userId); - // 3. 해당 ID들로 JPA 엔티티 직접 조회 - List roomJpaEntities = roomJpaRepository.findAllByIds(roomIds); - // 4. 유저가 탈퇴 처리된 각 방에 대해 진행률 및 멤버 수 업데이트 - for (RoomJpaEntity room : roomJpaEntities) { - // 현재 방의 참가자 전체 조회 (탈퇴한 유저는 이미 삭제됨) - List roomParticipantEntities = roomParticipantJpaRepository.findAllByRoomId(room.getRoomId()); - - // 평균 진행률 계산 - double totalProgress = roomParticipantEntities.stream() - .mapToDouble(RoomParticipantJpaEntity::getUserPercentage) - .sum(); - double avgProgress = roomParticipantEntities.isEmpty() ? 0.0 : (totalProgress / roomParticipantEntities.size()); - - // 방 정보(진행률, 멤버수) 업데이트 - room.updateRoomPercentage(avgProgress); - room.setMemberCount(roomParticipantEntities.size()); // 남은 참가자 수로 설정 + // 3. 남은 ACTIVE 참여자 기준 방별 평균/인원 집계 + List stats = roomParticipantJpaRepository.aggregateStatsByRoomIds(roomIds); + // 4. 방 정보(진행률, 멤버수) 업데이트 + for (RoomStatsRow row : stats) { + roomJpaRepository.updateRoomStats( + row.getRoomId(), + row.getAvgPercentage() == null ? 0.0 : row.getAvgPercentage(), + row.getMemberCount().intValue() + ); } } diff --git a/src/main/java/konkuk/thip/room/adapter/out/persistence/projection/RoomStatsRow.java b/src/main/java/konkuk/thip/room/adapter/out/persistence/projection/RoomStatsRow.java new file mode 100644 index 000000000..199d41b15 --- /dev/null +++ b/src/main/java/konkuk/thip/room/adapter/out/persistence/projection/RoomStatsRow.java @@ -0,0 +1,7 @@ +package konkuk.thip.room.adapter.out.persistence.projection; + +public interface RoomStatsRow { + Long getRoomId(); + Double getAvgPercentage(); + Long getMemberCount(); +} \ No newline at end of file diff --git a/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomJpaRepository.java b/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomJpaRepository.java index bd110e1dc..c58658c15 100644 --- a/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomJpaRepository.java +++ b/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/RoomJpaRepository.java @@ -2,11 +2,11 @@ import konkuk.thip.room.adapter.out.jpa.RoomJpaEntity; 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.time.LocalDate; -import java.util.List; import java.util.Optional; public interface RoomJpaRepository extends JpaRepository, RoomQueryRepository { @@ -21,6 +21,12 @@ public interface RoomJpaRepository extends JpaRepository, R "AND r.startDate > :currentDate") int countActiveRoomsByBookIdAndStartDateAfter(@Param("isbn") String isbn, @Param("currentDate") LocalDate currentDate); - @Query("SELECT r FROM RoomJpaEntity r WHERE r.roomId IN :roomIds") - List findAllByIds(@Param("roomIds") List roomIds); + @Modifying(clearAutomatically = true, flushAutomatically = true) + @Query(""" + update RoomJpaEntity r + set r.roomPercentage = :avg, + r.memberCount = :count + where r.roomId = :roomId + """) + void updateRoomStats(@Param("roomId") Long roomId, @Param("avg") double avg, @Param("count") int count); } diff --git a/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/roomparticipant/RoomParticipantJpaRepository.java b/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/roomparticipant/RoomParticipantJpaRepository.java index 8be3fc9e3..f5712a660 100644 --- a/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/roomparticipant/RoomParticipantJpaRepository.java +++ b/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/roomparticipant/RoomParticipantJpaRepository.java @@ -1,6 +1,7 @@ package konkuk.thip.room.adapter.out.persistence.repository.roomparticipant; import konkuk.thip.room.adapter.out.jpa.RoomParticipantJpaEntity; +import konkuk.thip.room.adapter.out.persistence.projection.RoomStatsRow; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; @@ -45,4 +46,15 @@ public interface RoomParticipantJpaRepository extends JpaRepository findRoomIdsByUserId(@Param("userId") Long userId); + + + @Query(""" + select rp.roomJpaEntity.roomId as roomId, + avg(rp.userPercentage) as avgPercentage, + count(rp) as memberCount + from RoomParticipantJpaEntity rp + where rp.roomJpaEntity.roomId in :roomIds + group by rp.roomJpaEntity.roomId + """) + List aggregateStatsByRoomIds(@Param("roomIds") List roomIds); } From 7914efcd949b9167720d5e070e3e3c088a8607fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Mon, 8 Sep 2025 21:20:58 +0900 Subject: [PATCH 53/55] =?UTF-8?q?[refactor]=20=EB=B8=94=EB=9E=99=EB=A6=AC?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=ED=86=A0=ED=81=B0=20=EB=A1=9C=EA=B9=85=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(#176)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../adapter/out/persistence/UserTokenBlacklistRedisAdapter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/konkuk/thip/user/adapter/out/persistence/UserTokenBlacklistRedisAdapter.java b/src/main/java/konkuk/thip/user/adapter/out/persistence/UserTokenBlacklistRedisAdapter.java index 48f1de97d..0c2a72949 100644 --- a/src/main/java/konkuk/thip/user/adapter/out/persistence/UserTokenBlacklistRedisAdapter.java +++ b/src/main/java/konkuk/thip/user/adapter/out/persistence/UserTokenBlacklistRedisAdapter.java @@ -55,7 +55,7 @@ public void addTokenToBlacklist(String token) { throw new ExternalApiException(JSON_PROCESSING_ERROR); } redisTemplate.opsForValue().set(key, valueJson); - log.info("Add token to blacklist - userId: {}, withdrawalTime: {}, expiration: {}", + log.info("블랙리스트에 탈퇴한 회원 토큰 및 관련 정보 추가 - userId: {}, withdrawalTime: {}, expiration: {}", loginUser.userId(), withdrawalTime, expiration From d53c26518ac794cbb31b36a814d308b7ecc19cc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Mon, 8 Sep 2025 21:21:10 +0900 Subject: [PATCH 54/55] =?UTF-8?q?[refactor]=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EC=BC=80=EC=9D=B4=EC=8A=A4=20=EC=9E=84=ED=8F=AC=ED=8A=B8?= =?UTF-8?q?=EB=AC=B8=20=EC=88=98=EC=A0=95=20(#176)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/konkuk/thip/user/adapter/in/web/UserDeleteApiTest.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/test/java/konkuk/thip/user/adapter/in/web/UserDeleteApiTest.java b/src/test/java/konkuk/thip/user/adapter/in/web/UserDeleteApiTest.java index 461ee8305..f3e66fe8c 100644 --- a/src/test/java/konkuk/thip/user/adapter/in/web/UserDeleteApiTest.java +++ b/src/test/java/konkuk/thip/user/adapter/in/web/UserDeleteApiTest.java @@ -1,6 +1,5 @@ package konkuk.thip.user.adapter.in.web; -import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.persistence.EntityManager; import konkuk.thip.book.adapter.out.jpa.BookJpaEntity; import konkuk.thip.book.adapter.out.persistence.repository.BookJpaRepository; @@ -44,7 +43,6 @@ import org.springframework.http.MediaType; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.web.servlet.MockMvc; -import org.springframework.transaction.annotation.Transactional; import java.time.LocalDate; From 6f5b2fe4c623acbfeb7810b58281f69fe91b2c6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=ED=9D=AC=EC=A7=84?= Date: Tue, 9 Sep 2025 01:30:21 +0900 Subject: [PATCH 55/55] =?UTF-8?q?[refactor]=20RoomAggregateProjection?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20dto=20=EB=84=A4=EC=9D=B4=EB=B0=8D=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20(#176)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../RoomParticipantCommandPersistenceAdapter.java | 6 +++--- .../{RoomStatsRow.java => RoomAggregateProjection.java} | 2 +- .../roomparticipant/RoomParticipantJpaRepository.java | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) rename src/main/java/konkuk/thip/room/adapter/out/persistence/projection/{RoomStatsRow.java => RoomAggregateProjection.java} (76%) diff --git a/src/main/java/konkuk/thip/room/adapter/out/persistence/RoomParticipantCommandPersistenceAdapter.java b/src/main/java/konkuk/thip/room/adapter/out/persistence/RoomParticipantCommandPersistenceAdapter.java index e7df0aba5..d43d06b5f 100644 --- a/src/main/java/konkuk/thip/room/adapter/out/persistence/RoomParticipantCommandPersistenceAdapter.java +++ b/src/main/java/konkuk/thip/room/adapter/out/persistence/RoomParticipantCommandPersistenceAdapter.java @@ -8,7 +8,7 @@ import konkuk.thip.room.adapter.out.persistence.repository.RoomJpaRepository; import konkuk.thip.room.adapter.out.persistence.repository.roomparticipant.RoomParticipantJpaRepository; import konkuk.thip.room.application.port.out.RoomParticipantCommandPort; -import konkuk.thip.room.adapter.out.persistence.projection.RoomStatsRow; +import konkuk.thip.room.adapter.out.persistence.projection.RoomAggregateProjection; import konkuk.thip.room.domain.RoomParticipant; import konkuk.thip.user.adapter.out.jpa.UserJpaEntity; import konkuk.thip.user.adapter.out.persistence.repository.UserJpaRepository; @@ -90,10 +90,10 @@ public void deleteAllByUserId(Long userId) { roomParticipantJpaRepository.softDeleteAllByUserId(userId); // 3. 남은 ACTIVE 참여자 기준 방별 평균/인원 집계 - List stats = roomParticipantJpaRepository.aggregateStatsByRoomIds(roomIds); + List stats = roomParticipantJpaRepository.aggregateStatsByRoomIds(roomIds); // 4. 방 정보(진행률, 멤버수) 업데이트 - for (RoomStatsRow row : stats) { + for (RoomAggregateProjection row : stats) { roomJpaRepository.updateRoomStats( row.getRoomId(), row.getAvgPercentage() == null ? 0.0 : row.getAvgPercentage(), diff --git a/src/main/java/konkuk/thip/room/adapter/out/persistence/projection/RoomStatsRow.java b/src/main/java/konkuk/thip/room/adapter/out/persistence/projection/RoomAggregateProjection.java similarity index 76% rename from src/main/java/konkuk/thip/room/adapter/out/persistence/projection/RoomStatsRow.java rename to src/main/java/konkuk/thip/room/adapter/out/persistence/projection/RoomAggregateProjection.java index 199d41b15..34670215d 100644 --- a/src/main/java/konkuk/thip/room/adapter/out/persistence/projection/RoomStatsRow.java +++ b/src/main/java/konkuk/thip/room/adapter/out/persistence/projection/RoomAggregateProjection.java @@ -1,6 +1,6 @@ package konkuk.thip.room.adapter.out.persistence.projection; -public interface RoomStatsRow { +public interface RoomAggregateProjection { Long getRoomId(); Double getAvgPercentage(); Long getMemberCount(); diff --git a/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/roomparticipant/RoomParticipantJpaRepository.java b/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/roomparticipant/RoomParticipantJpaRepository.java index f5712a660..ef8120a16 100644 --- a/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/roomparticipant/RoomParticipantJpaRepository.java +++ b/src/main/java/konkuk/thip/room/adapter/out/persistence/repository/roomparticipant/RoomParticipantJpaRepository.java @@ -1,7 +1,7 @@ package konkuk.thip.room.adapter.out.persistence.repository.roomparticipant; import konkuk.thip.room.adapter.out.jpa.RoomParticipantJpaEntity; -import konkuk.thip.room.adapter.out.persistence.projection.RoomStatsRow; +import konkuk.thip.room.adapter.out.persistence.projection.RoomAggregateProjection; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; @@ -56,5 +56,5 @@ public interface RoomParticipantJpaRepository extends JpaRepository aggregateStatsByRoomIds(@Param("roomIds") List roomIds); + List aggregateStatsByRoomIds(@Param("roomIds") List roomIds); }