[Feat] 회원 탈퇴 구현 및 투표 조회시 투표 득표수 보여주도록 수정#292
Merged
Merged
Conversation
Contributor
리뷰 답글에 말한 것처럼 방 진행도를 계산하는 로직을 RoomProgressManager쪽에다가 모으는 것도 좋을 것 같습니다~!
저희가 adapter가 그렇게 많은 건 아니라서 크게 문제되진 않아보입니다! 네이밍도 좋습니다! |
- CommentJpaRepository 에서 유저가 작성한 댓글과 연관되는 게시글을 fetch join 으로 가져올 때, 불필요한 type 조건 삭제 - 영속성 어댑터에서 posts의 commentCount 먼저 업데이트 한 후, comments 의 soft delete(벌크 jpql 및 em flush) 하도록 순서 변경
- 수정된 deleteAllByUserId 메서드가 제대로 동작함을 보여주기 위해 테스트 코드 작성
buzz0331
previously approved these changes
Sep 8, 2025
Comment on lines
+90
to
+111
| @Override | ||
| public void deleteAllByUserId(Long userId) { | ||
|
|
||
| // 1. 탈퇴 유저가 작성한 댓글과 연관된 게시글을 JOIN FETCH로 함께 조회 | ||
| List<CommentJpaEntity> commentsWithPosts = commentJpaRepository.findAllCommentsWithPostsByUserId(userId); | ||
| if (commentsWithPosts == null || commentsWithPosts.isEmpty()) { | ||
| return; //early return | ||
| } | ||
| // 2. 탈퇴한 유저의 모든 댓글, 댓글의 좋아요 삭제 | ||
| commentLikeJpaRepository.deleteAllByCommentAuthorUserId(userId); | ||
| commentJpaRepository.deleteAllByUserId(userId); | ||
| // 3. 게시글 타입별로 댓글 수 감소가 필요한 게시글 Map 생성 | ||
| Map<PostType, List<PostJpaEntity>> 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); | ||
| } | ||
|
|
||
| // 2. 삭제될 댓글이 어느 Post에 몇 개씩 붙어있는지 집계 (postId 기준 추천) | ||
| Map<PostJpaEntity, Long> decMap = commentsWithPosts.stream() | ||
| .collect(Collectors.groupingBy(CommentJpaEntity::getPostJpaEntity, Collectors.counting())); | ||
|
|
||
|
|
||
| private void savePostsJpaEntities(PostType postType, List<PostJpaEntity> 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; | ||
| // 3. 댓글 수를 집계만큼 한 번에 감소 | ||
| for (PostJpaEntity p : decMap.keySet()) { | ||
| long dec = decMap.getOrDefault(p, 0L); | ||
| p.setCommentCount(Math.max(0, p.getCommentCount() - (int) dec)); | ||
| } | ||
|
|
||
| // 4. 탈퇴한 유저의 모든 댓글, 댓글의 좋아요 삭제 | ||
| commentLikeJpaRepository.deleteAllByCommentAuthorUserId(userId); | ||
| commentJpaRepository.softDeleteAllByUserId(userId); | ||
| } |
Comment on lines
+3
to
+7
| public interface RoomStatsRow { | ||
| Long getRoomId(); | ||
| Double getAvgPercentage(); | ||
| Long getMemberCount(); | ||
| } No newline at end of file |
Contributor
There was a problem hiding this comment.
p3: 리뷰 답글에도 남겨두긴했는데 네이밍이 조금 애매해서 RoomAggregateProjection 같이 바꿔보는거 어떨까요?
Comment on lines
+79
to
+101
| @Override | ||
| public void deleteAllByUserId(Long userId) { | ||
| // 방 참여 관계 삭제 (member는 진행/모집/만료, host는 만료) | ||
| // 방 멤버수 감소, 방 진행도 업데이트 | ||
|
|
||
| // 1. 유저가 참여한 방 ID 리스트 조회 | ||
| List<Long> roomIds = roomParticipantJpaRepository.findRoomIdsByUserId(userId); | ||
| if (roomIds.isEmpty()) { | ||
| return; // early return | ||
| } | ||
| // 2. 유저의 모든 방 참여 관계 일괄 삭제 | ||
| roomParticipantJpaRepository.deleteAllByUserId(userId); | ||
| // 3. 해당 ID들로 JPA 엔티티 직접 조회 | ||
| List<RoomJpaEntity> roomJpaEntities = roomJpaRepository.findAllByIds(roomIds); | ||
|
|
||
| // 4. 유저가 탈퇴 처리된 각 방에 대해 진행률 및 멤버 수 업데이트 | ||
| for (RoomJpaEntity room : roomJpaEntities) { | ||
| // 현재 방의 참가자 전체 조회 (탈퇴한 유저는 이미 삭제됨) | ||
| List<RoomParticipantJpaEntity> 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()); // 남은 참가자 수로 설정 | ||
|
|
||
| roomParticipantJpaRepository.softDeleteAllByUserId(userId); | ||
|
|
||
| // 3. 남은 ACTIVE 참여자 기준 방별 평균/인원 집계 | ||
| List<RoomStatsRow> stats = roomParticipantJpaRepository.aggregateStatsByRoomIds(roomIds); | ||
|
|
||
| // 4. 방 정보(진행률, 멤버수) 업데이트 | ||
| for (RoomStatsRow row : stats) { | ||
| roomJpaRepository.updateRoomStats( | ||
| row.getRoomId(), | ||
| row.getAvgPercentage() == null ? 0.0 : row.getAvgPercentage(), | ||
| row.getMemberCount().intValue() | ||
| ); |
Comment on lines
+41
to
+62
| LoginUser loginUser = jwtUtil.getLoginUser(token); | ||
| LocalDateTime withdrawalTime = LocalDateTime.now(); | ||
| String key = makeBlacklistKey(token); | ||
| redisTemplate.opsForValue().set(key, "BLACKLISTED"); | ||
|
|
||
| Map<String, Object> 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("블랙리스트에 탈퇴한 회원 토큰 및 관련 정보 추가 - userId: {}, withdrawalTime: {}, expiration: {}", | ||
| loginUser.userId(), | ||
| withdrawalTime, | ||
| expiration | ||
| ); |
seongjunnoh
previously approved these changes
Sep 8, 2025
Comment on lines
-88
to
+80
| case FEED -> feedJpaRepository.findById(postId) | ||
| case FEED -> feedJpaRepository.findByPostId(postId) |
Collaborator
There was a problem hiding this comment.
LGTM!! 이 부분 챙겨주셨네요! 좋습니다
This was referenced Sep 12, 2025
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
#️⃣ 연관된 이슈
📝 작업 내용
--> 회원탈퇴를 하는 도중에 토큰이 만료되어 다시 그 유저를 로그인으로 리다이렉트 시키는것보다 위와 같이 구현하는게 더 낫다고 판단했습니다.
📸 스크린샷
💬 리뷰 요구사항
1.최대한 join을 사용하지 않으려고 엔티티에 직접 수를 가지고있는 관련 엔티티들을 삭제할때(팔로워 수/투표 수 etc...) 해당 엔티티 id들을 1차적으로 조회 -> 얼리 리턴 -> id들가지고 해당 엔티티들 재조회 와 같이 해당 방식으로 구현했습니다.
UserTokenBlacklistRedisAdapter로 작성하고 패키지도 user아래 위치해있는데 이건 auth역할이 더 강하다고 생각하긴하는데 auth에 어댑터라는 패키지가 없다보니.. 이름이랑 패키지 구조 추천받습니다 허헣📌 PR 진행 시 이러한 점들을 참고해 주세요
Summary by CodeRabbit
New Features
Security
Documentation
Tests