diff --git a/src/main/java/com/dokdok/gathering/repository/GatheringCountProjection.java b/src/main/java/com/dokdok/gathering/repository/GatheringCountProjection.java new file mode 100644 index 00000000..eb612eeb --- /dev/null +++ b/src/main/java/com/dokdok/gathering/repository/GatheringCountProjection.java @@ -0,0 +1,6 @@ +package com.dokdok.gathering.repository; + +public interface GatheringCountProjection { + Long getGatheringId(); + Long getCount(); +} diff --git a/src/main/java/com/dokdok/gathering/repository/GatheringMemberRepository.java b/src/main/java/com/dokdok/gathering/repository/GatheringMemberRepository.java index 08701544..57881a71 100644 --- a/src/main/java/com/dokdok/gathering/repository/GatheringMemberRepository.java +++ b/src/main/java/com/dokdok/gathering/repository/GatheringMemberRepository.java @@ -168,4 +168,17 @@ int countMembersByStatus( @Param("gatheringId") Long gatheringId, @Param("status") GatheringMemberStatus status ); + + /** + * 여러 모임의 ACTIVE 멤버 수를 한번에 조회 + */ + @Query("SELECT gm.gathering.id AS gatheringId, COUNT(gm) AS count " + + "FROM GatheringMember gm " + + "WHERE gm.gathering.id IN :gatheringIds " + + "AND gm.memberStatus = 'ACTIVE' " + + "AND gm.removedAt IS NULL " + + "GROUP BY gm.gathering.id") + List countActiveMembersByGatherings( + @Param("gatheringIds") List gatheringIds + ); } diff --git a/src/main/java/com/dokdok/gathering/service/GatheringService.java b/src/main/java/com/dokdok/gathering/service/GatheringService.java index 945282a7..da93528e 100644 --- a/src/main/java/com/dokdok/gathering/service/GatheringService.java +++ b/src/main/java/com/dokdok/gathering/service/GatheringService.java @@ -8,6 +8,7 @@ import com.dokdok.gathering.entity.*; import com.dokdok.gathering.exception.GatheringErrorCode; import com.dokdok.gathering.exception.GatheringException; +import com.dokdok.gathering.repository.GatheringCountProjection; import com.dokdok.gathering.repository.GatheringMemberRepository; import com.dokdok.gathering.repository.GatheringRepository; import com.dokdok.global.response.CursorResponse; @@ -129,12 +130,19 @@ public FavoriteGatheringListResponse getFavoriteGatherings() { List favoriteMembers = gatheringMemberRepository.findFavoriteGatheringsByUserId(userId); + List gatheringIds = favoriteMembers.stream() + .map(gm -> gm.getGathering().getId()) + .toList(); + + Map memberCountMap = getActiveMemberCountMap(gatheringIds); + Map meetingCountMap = getMeetingCountMap(gatheringIds); + List gatheringResponses = favoriteMembers.stream() - .map(gatheringMember -> { - Gathering gathering = gatheringMember.getGathering(); - int totalMembers = getActiveMemberCount(gathering.getId()); - int totalMeetings = getMeetingCount(gathering.getId()); - return GatheringListItemResponse.from(gatheringMember, totalMembers, totalMeetings, gatheringMember.getRole()); + .map(gm -> { + Long gatheringId = gm.getGathering().getId(); + int totalMembers = memberCountMap.getOrDefault(gatheringId, 0); + int totalMeetings = meetingCountMap.getOrDefault(gatheringId, 0); + return GatheringListItemResponse.from(gm, totalMembers, totalMeetings, gm.getRole()); }) .toList(); @@ -163,12 +171,20 @@ public CursorResponse getMyGatheri boolean hasNext = members.size() > pageSize; List pageMembers = hasNext ? members.subList(0, pageSize) : members; + List gatheringIds = pageMembers.stream() + .map(gm -> gm.getGathering().getId()) + .toList(); + + Map memberCountMap = getActiveMemberCountMap(gatheringIds); + Map meetingCountMap = getMeetingCountMap(gatheringIds); + List items = pageMembers.stream() - .map(gm -> GatheringListItemResponse.from( - gm, - getActiveMemberCount(gm.getGathering().getId()), - getMeetingCount(gm.getGathering().getId()), - gm.getRole())) + .map(gm ->{ + Long gatheringId = gm.getGathering().getId(); + int totalMembers = memberCountMap.getOrDefault(gatheringId, 0); + int totalMeetings = meetingCountMap.getOrDefault(gatheringId, 0); + return GatheringListItemResponse.from(gm, totalMembers, totalMeetings, gm.getRole()); + }) .toList(); GatheringMember lastMember = pageMembers.isEmpty() ? null : pageMembers.get(pageMembers.size() - 1); @@ -419,4 +435,33 @@ private int getMeetingCount(Long gatheringId) { return meetingRepository.countByGatheringIdAndMeetingStatus(gatheringId, MeetingStatus.DONE); } + /** + * 여러 모임의 활성 멤버 수를 Map으로 조회 + */ + private Map getActiveMemberCountMap(List gatheringIds) { + if(gatheringIds.isEmpty()) { + return Map.of(); + } + return gatheringMemberRepository.countActiveMembersByGatherings(gatheringIds) + .stream() + .collect(Collectors.toMap( + GatheringCountProjection::getGatheringId, + p -> p.getCount().intValue() + )); + } + + /** + * 여러 모임의 완료된 미팅 수를 Map으로 조회 + */ + private Map getMeetingCountMap(List gatheringIds) { + if (gatheringIds.isEmpty()) { + return Map.of(); + } + return meetingRepository.countByGatheringIdsAndStatus(gatheringIds, MeetingStatus.DONE) + .stream() + .collect(Collectors.toMap( + GatheringCountProjection::getGatheringId, + p -> p.getCount().intValue() + )); + } } diff --git a/src/main/java/com/dokdok/meeting/repository/MeetingRepository.java b/src/main/java/com/dokdok/meeting/repository/MeetingRepository.java index f3753528..84a8a16b 100644 --- a/src/main/java/com/dokdok/meeting/repository/MeetingRepository.java +++ b/src/main/java/com/dokdok/meeting/repository/MeetingRepository.java @@ -1,5 +1,6 @@ package com.dokdok.meeting.repository; +import com.dokdok.gathering.repository.GatheringCountProjection; import com.dokdok.meeting.entity.Meeting; import com.dokdok.meeting.entity.MeetingStatus; import org.springframework.data.domain.Page; @@ -113,4 +114,14 @@ Optional findTopByGatheringIdAndBookIdAndMeetingStatusOrderByMeetingSta """) List findByIdInWithGathering(@Param("meetingIds") List meetingIds); + /** + * 여러 모임의 완료된 미팅 수를 한번에 조회 + */ + @Query("SELECT m.gathering.id AS gatheringId, COUNT (m) AS count " + + "FROM Meeting m " + + "WHERE m.gathering.id IN :gatheringIds " + + "AND m.meetingStatus = :status " + + "GROUP BY m.gathering.id") + List countByGatheringIdsAndStatus(@Param("gatheringIds") List gatheringIds, @Param("status") MeetingStatus status); + } diff --git a/src/test/java/com/dokdok/gathering/service/GatheringServiceTest.java b/src/test/java/com/dokdok/gathering/service/GatheringServiceTest.java index a90066a5..faf39584 100644 --- a/src/test/java/com/dokdok/gathering/service/GatheringServiceTest.java +++ b/src/test/java/com/dokdok/gathering/service/GatheringServiceTest.java @@ -24,6 +24,7 @@ import com.dokdok.gathering.entity.GatheringStatus; import com.dokdok.gathering.exception.GatheringErrorCode; import com.dokdok.gathering.exception.GatheringException; +import com.dokdok.gathering.repository.GatheringCountProjection; import com.dokdok.gathering.repository.GatheringMemberRepository; import com.dokdok.gathering.repository.GatheringRepository; import com.dokdok.gathering.util.InvitationCodeGenerator; @@ -308,10 +309,16 @@ void getFavoriteGatherings_Success() { List favoriteMembers = List.of(member1, member2); given(gatheringMemberRepository.findFavoriteGatheringsByUserId(userId)).willReturn(favoriteMembers); - given(gatheringMemberRepository.countActiveMembersByStatus(1L)).willReturn(1); - given(gatheringMemberRepository.countActiveMembersByStatus(2L)).willReturn(1); - given(meetingRepository.countByGatheringIdAndMeetingStatus(1L, MeetingStatus.DONE)).willReturn(3); - given(meetingRepository.countByGatheringIdAndMeetingStatus(2L, MeetingStatus.DONE)).willReturn(5); + given(gatheringMemberRepository.countActiveMembersByGatherings(List.of(1L, 2L))) + .willReturn(List.of( + createCountProjection(1L, 1L), + createCountProjection(2L, 1L) + )); + given(meetingRepository.countByGatheringIdsAndStatus(List.of(1L, 2L), MeetingStatus.DONE)) + .willReturn(List.of( + createCountProjection(1L, 3L), + createCountProjection(2L, 5L) + )); // when FavoriteGatheringListResponse response = gatheringService.getFavoriteGatherings(); @@ -341,10 +348,17 @@ void getFavoriteGatherings_Success() { securityUtilMock.verify(SecurityUtil::getCurrentUserId, times(1)); verify(gatheringMemberRepository, times(1)).findFavoriteGatheringsByUserId(eq(userId)); - verify(gatheringMemberRepository, times(1)).countActiveMembersByStatus(1L); - verify(gatheringMemberRepository, times(1)).countActiveMembersByStatus(2L); - verify(meetingRepository, times(1)).countByGatheringIdAndMeetingStatus(1L, MeetingStatus.DONE); - verify(meetingRepository, times(1)).countByGatheringIdAndMeetingStatus(2L, MeetingStatus.DONE); + verify(gatheringMemberRepository, times(1)).countActiveMembersByGatherings(List.of(1L, 2L)); + verify(meetingRepository, times(1)).countByGatheringIdsAndStatus(List.of(1L, 2L), MeetingStatus.DONE); + } + + private GatheringCountProjection createCountProjection(Long gatheringId, Long count) { + return new GatheringCountProjection() { + @Override + public Long getGatheringId() { return gatheringId; } + @Override + public Long getCount() { return count; } + }; } @Test @@ -366,8 +380,8 @@ void getFavoriteGatherings_EmptyList() { securityUtilMock.verify(SecurityUtil::getCurrentUserId, times(1)); verify(gatheringMemberRepository, times(1)).findFavoriteGatheringsByUserId(eq(userId)); - verify(gatheringMemberRepository, times(0)).countActiveMembersByStatus(any()); - verify(meetingRepository, times(0)).countByGatheringIdAndMeetingStatus(any(), any()); + verify(gatheringMemberRepository, times(0)).countActiveMembersByGatherings(any()); + verify(meetingRepository, times(0)).countByGatheringIdsAndStatus(any(), any()); } @Test @@ -401,10 +415,17 @@ void getMyGatherings_Success_FirstPage() { given(gatheringMemberRepository.findMyGatheringsFirstPage(eq(userId), any(Pageable.class))) .willReturn(members); - given(gatheringMemberRepository.countActiveMembersByStatus(1L)).willReturn(1); - given(gatheringMemberRepository.countActiveMembersByStatus(2L)).willReturn(1); - given(meetingRepository.countByGatheringIdAndMeetingStatus(1L, MeetingStatus.DONE)).willReturn(3); - given(meetingRepository.countByGatheringIdAndMeetingStatus(2L, MeetingStatus.DONE)).willReturn(5); + given(gatheringMemberRepository.countMyGatherings(userId)).willReturn(2); + given(gatheringMemberRepository.countActiveMembersByGatherings(List.of(1L, 2L))) + .willReturn(List.of( + createCountProjection(1L, 1L), + createCountProjection(2L, 1L) + )); + given(meetingRepository.countByGatheringIdsAndStatus(List.of(1L, 2L), MeetingStatus.DONE)) + .willReturn(List.of( + createCountProjection(1L, 3L), + createCountProjection(2L, 5L) + )); // when CursorResponse response = @@ -418,6 +439,8 @@ void getMyGatherings_Success_FirstPage() { assertThat(response.nextCursor()).isNull(); verify(gatheringMemberRepository).findMyGatheringsFirstPage(eq(userId), any(Pageable.class)); + verify(gatheringMemberRepository).countActiveMembersByGatherings(List.of(1L, 2L)); + verify(meetingRepository).countByGatheringIdsAndStatus(List.of(1L, 2L), MeetingStatus.DONE); } @Test @@ -452,8 +475,12 @@ void getMyGatherings_Success_HasNextPage() { given(gatheringMemberRepository.findMyGatheringsFirstPage(eq(userId), any(Pageable.class))) .willReturn(members); - given(gatheringMemberRepository.countActiveMembersByStatus(any())).willReturn(1); - given(meetingRepository.countByGatheringIdAndMeetingStatus(any(), eq(MeetingStatus.DONE))).willReturn(0); + given(gatheringMemberRepository.countMyGatherings(userId)).willReturn(2); + // pageSize=1이므로 pageMembers는 member1만 포함, gatheringIds = List.of(1L) + given(gatheringMemberRepository.countActiveMembersByGatherings(List.of(1L))) + .willReturn(List.of(createCountProjection(1L, 1L))); + given(meetingRepository.countByGatheringIdsAndStatus(List.of(1L), MeetingStatus.DONE)) + .willReturn(List.of(createCountProjection(1L, 0L))); // when CursorResponse response =