From f2778a280f3ea384d7f58e0a7c2a75b3e1b2bbdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Mon, 27 Apr 2026 11:17:51 +0900 Subject: [PATCH 1/6] =?UTF-8?q?refactor:=20=EC=B1=84=ED=8C=85=EB=B0=A9=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=ED=95=A9=EC=84=B1=20=EC=B1=85=EC=9E=84=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 채팅방 목록 요약 분리 작업을 작은 PR 단위로 진행하기 위해 이름과 뮤트 설정 합성만 먼저 분리 - 목록 조회와 검색 조회가 같은 설정 적용 규칙을 쓰도록 ChatRoomSettingsService로 책임을 모음 - custom room name과 mute 상태가 기존 응답 필드에 동일하게 반영되는지 단위 테스트로 고정 --- .../chat/service/ChatRoomSettingsService.java | 87 ++++++++++++++ .../domain/chat/service/ChatService.java | 107 ++---------------- .../service/ChatRoomSettingsServiceTest.java | 94 +++++++++++++++ .../domain/chat/service/ChatServiceTest.java | 4 + 4 files changed, 193 insertions(+), 99 deletions(-) create mode 100644 src/main/java/gg/agit/konect/domain/chat/service/ChatRoomSettingsService.java create mode 100644 src/test/java/gg/agit/konect/unit/domain/chat/service/ChatRoomSettingsServiceTest.java diff --git a/src/main/java/gg/agit/konect/domain/chat/service/ChatRoomSettingsService.java b/src/main/java/gg/agit/konect/domain/chat/service/ChatRoomSettingsService.java new file mode 100644 index 000000000..69dd33f46 --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/chat/service/ChatRoomSettingsService.java @@ -0,0 +1,87 @@ +package gg.agit.konect.domain.chat.service; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; + +import gg.agit.konect.domain.chat.dto.ChatRoomSummaryResponse; +import gg.agit.konect.domain.chat.model.ChatRoomMember; +import gg.agit.konect.domain.chat.repository.ChatRoomMemberRepository; +import gg.agit.konect.domain.notification.enums.NotificationTargetType; +import gg.agit.konect.domain.notification.model.NotificationMuteSetting; +import gg.agit.konect.domain.notification.repository.NotificationMuteSettingRepository; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class ChatRoomSettingsService { + + private final NotificationMuteSettingRepository notificationMuteSettingRepository; + private final ChatRoomMemberRepository chatRoomMemberRepository; + + public List applyUserSettings( + List rooms, + Integer userId + ) { + List roomIds = rooms.stream() + .map(ChatRoomSummaryResponse::roomId) + .toList(); + Map muteMap = getMuteMap(roomIds, userId); + Map customRoomNameMap = getCustomRoomNameMap(roomIds, userId); + + return rooms.stream() + .map(room -> applyRoomSettings(room, muteMap, customRoomNameMap)) + .toList(); + } + + private ChatRoomSummaryResponse applyRoomSettings( + ChatRoomSummaryResponse room, + Map muteMap, + Map customRoomNameMap + ) { + return new ChatRoomSummaryResponse( + room.roomId(), + room.chatType(), + customRoomNameMap.getOrDefault(room.roomId(), room.roomName()), + room.roomImageUrl(), + room.lastMessage(), + room.lastSentAt(), + room.createdAt(), + room.unreadCount(), + muteMap.getOrDefault(room.roomId(), false) + ); + } + + private Map getMuteMap(List roomIds, Integer userId) { + if (roomIds.isEmpty()) { + return Map.of(); + } + + List settings = notificationMuteSettingRepository + .findByTargetTypeAndTargetIdsAndUserId(NotificationTargetType.CHAT_ROOM, roomIds, userId); + + Map muteMap = new HashMap<>(); + for (NotificationMuteSetting setting : settings) { + Integer targetId = setting.getTargetId(); + if (targetId != null) { + muteMap.put(targetId, setting.getIsMuted()); + } + } + + return muteMap; + } + + private Map getCustomRoomNameMap(List roomIds, Integer userId) { + if (roomIds.isEmpty()) { + return Map.of(); + } + + return chatRoomMemberRepository.findByChatRoomIdsAndUserId(roomIds, userId).stream() + .filter(member -> StringUtils.hasText(member.getCustomRoomName())) + .collect(Collectors.toMap(ChatRoomMember::getChatRoomId, ChatRoomMember::getCustomRoomName)); + } +} diff --git a/src/main/java/gg/agit/konect/domain/chat/service/ChatService.java b/src/main/java/gg/agit/konect/domain/chat/service/ChatService.java index 8b43f73ba..561b3ca38 100644 --- a/src/main/java/gg/agit/konect/domain/chat/service/ChatService.java +++ b/src/main/java/gg/agit/konect/domain/chat/service/ChatService.java @@ -82,6 +82,7 @@ public class ChatService { private final UserRepository userRepository; private final ChatPresenceService chatPresenceService; private final ChatRoomMembershipService chatRoomMembershipService; + private final ChatRoomSettingsService chatRoomSettingsService; private final NotificationService notificationService; private final ApplicationEventPublisher eventPublisher; @@ -212,50 +213,11 @@ public ChatRoomsSummaryResponse getChatRooms(Integer userId) { List clubRooms = getClubChatRooms(userId); List groupRooms = getGroupChatRooms(userId); - List roomIds = new ArrayList<>(); - roomIds.addAll(directRooms.stream().map(ChatRoomSummaryResponse::roomId).toList()); - roomIds.addAll(clubRooms.stream().map(ChatRoomSummaryResponse::roomId).toList()); - roomIds.addAll(groupRooms.stream().map(ChatRoomSummaryResponse::roomId).toList()); - - Map muteMap = getMuteMap(roomIds, userId); - Map customRoomNameMap = getCustomRoomNameMap(roomIds, userId); List rooms = new ArrayList<>(); - - directRooms.forEach(room -> rooms.add(new ChatRoomSummaryResponse( - room.roomId(), - room.chatType(), - resolveRoomName(room.roomId(), room.roomName(), customRoomNameMap), - room.roomImageUrl(), - room.lastMessage(), - room.lastSentAt(), - room.createdAt(), - room.unreadCount(), - muteMap.getOrDefault(room.roomId(), false) - ))); - - clubRooms.forEach(room -> rooms.add(new ChatRoomSummaryResponse( - room.roomId(), - room.chatType(), - resolveRoomName(room.roomId(), room.roomName(), customRoomNameMap), - room.roomImageUrl(), - room.lastMessage(), - room.lastSentAt(), - room.createdAt(), - room.unreadCount(), - muteMap.getOrDefault(room.roomId(), false) - ))); - - groupRooms.forEach(room -> rooms.add(new ChatRoomSummaryResponse( - room.roomId(), - room.chatType(), - resolveRoomName(room.roomId(), room.roomName(), customRoomNameMap), - room.roomImageUrl(), - room.lastMessage(), - room.lastSentAt(), - room.createdAt(), - room.unreadCount(), - muteMap.getOrDefault(room.roomId(), false) - ))); + rooms.addAll(directRooms); + rooms.addAll(clubRooms); + rooms.addAll(groupRooms); + rooms = new ArrayList<>(chatRoomSettingsService.applyUserSettings(rooms, userId)); rooms.sort( Comparator.comparing( @@ -948,12 +910,11 @@ private AccessibleChatRooms getAccessibleChatRooms(Integer userId) { roomIds.addAll(directRooms.stream().map(ChatRoomSummaryResponse::roomId).toList()); roomIds.addAll(clubRooms.stream().map(ChatRoomSummaryResponse::roomId).toList()); - Map muteMap = getMuteMap(roomIds, userId); - Map customRoomNameMap = getCustomRoomNameMap(roomIds, userId); Map defaultRoomNameMap = getDefaultRoomNameMap(directRooms, clubRooms); List rooms = new ArrayList<>(); - directRooms.forEach(room -> rooms.add(applyRoomSettings(room, muteMap, customRoomNameMap))); - clubRooms.forEach(room -> rooms.add(applyRoomSettings(room, muteMap, customRoomNameMap))); + rooms.addAll(directRooms); + rooms.addAll(clubRooms); + rooms = new ArrayList<>(chatRoomSettingsService.applyUserSettings(rooms, userId)); rooms.sort( Comparator.comparing(ChatRoomSummaryResponse::lastSentAt, @@ -963,24 +924,6 @@ private AccessibleChatRooms getAccessibleChatRooms(Integer userId) { return new AccessibleChatRooms(rooms, defaultRoomNameMap); } - private ChatRoomSummaryResponse applyRoomSettings( - ChatRoomSummaryResponse room, - Map muteMap, - Map customRoomNameMap - ) { - return new ChatRoomSummaryResponse( - room.roomId(), - room.chatType(), - resolveRoomName(room.roomId(), room.roomName(), customRoomNameMap), - room.roomImageUrl(), - room.lastMessage(), - room.lastSentAt(), - room.createdAt(), - room.unreadCount(), - muteMap.getOrDefault(room.roomId(), false) - ); - } - private ChatRoomMatchesResponse searchRoomsByName( AccessibleChatRooms accessibleChatRooms, String keyword, @@ -1057,25 +1000,6 @@ private Page emptyPage(Integer page, Integer limit) { return new PageImpl<>(List.of(), PageRequest.of(page - 1, limit), 0); } - private Map getMuteMap(List roomIds, Integer userId) { - if (roomIds.isEmpty()) { - return Map.of(); - } - - List settings = notificationMuteSettingRepository - .findByTargetTypeAndTargetIdsAndUserId(NotificationTargetType.CHAT_ROOM, roomIds, userId); - - Map muteMap = new HashMap<>(); - for (NotificationMuteSetting setting : settings) { - Integer targetId = setting.getTargetId(); - if (targetId != null) { - muteMap.put(targetId, setting.getIsMuted()); - } - } - - return muteMap; - } - private Map getVisibleMessageFromMap(List roomIds, Integer userId) { if (roomIds.isEmpty()) { return Map.of(); @@ -1098,21 +1022,6 @@ private Map getDefaultRoomNameMap( return defaultRoomNameMap; } - private Map getCustomRoomNameMap(List roomIds, Integer userId) { - if (roomIds.isEmpty()) { - return Map.of(); - } - - return chatRoomMemberRepository.findByChatRoomIdsAndUserId(roomIds, userId).stream() - .filter(member -> StringUtils.hasText(member.getCustomRoomName())) - .collect(Collectors.toMap(ChatRoomMember::getChatRoomId, ChatRoomMember::getCustomRoomName)); - } - - private String resolveRoomName(Integer roomId, String - defaultRoomName, Map customRoomNameMap) { - return customRoomNameMap.getOrDefault(roomId, defaultRoomName); - } - private boolean matchesRoomName( ChatRoomSummaryResponse room, String keyword, diff --git a/src/test/java/gg/agit/konect/unit/domain/chat/service/ChatRoomSettingsServiceTest.java b/src/test/java/gg/agit/konect/unit/domain/chat/service/ChatRoomSettingsServiceTest.java new file mode 100644 index 000000000..1bec16abd --- /dev/null +++ b/src/test/java/gg/agit/konect/unit/domain/chat/service/ChatRoomSettingsServiceTest.java @@ -0,0 +1,94 @@ +package gg.agit.konect.unit.domain.chat.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verifyNoInteractions; + +import java.time.LocalDateTime; +import java.util.List; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import gg.agit.konect.domain.chat.dto.ChatRoomSummaryResponse; +import gg.agit.konect.domain.chat.enums.ChatType; +import gg.agit.konect.domain.chat.model.ChatRoomMember; +import gg.agit.konect.domain.chat.repository.ChatRoomMemberRepository; +import gg.agit.konect.domain.chat.service.ChatRoomSettingsService; +import gg.agit.konect.domain.notification.enums.NotificationTargetType; +import gg.agit.konect.domain.notification.model.NotificationMuteSetting; +import gg.agit.konect.domain.notification.repository.NotificationMuteSettingRepository; +import gg.agit.konect.domain.user.model.User; +import gg.agit.konect.support.ServiceTestSupport; + +class ChatRoomSettingsServiceTest extends ServiceTestSupport { + + @Mock + private NotificationMuteSettingRepository notificationMuteSettingRepository; + + @Mock + private ChatRoomMemberRepository chatRoomMemberRepository; + + @InjectMocks + private ChatRoomSettingsService chatRoomSettingsService; + + @Test + @DisplayName("applyUserSettings는 커스텀 방 이름과 뮤트 설정을 목록 응답에 합성한다") + void applyUserSettingsAppliesCustomNameAndMute() { + // given + Integer userId = 10; + ChatRoomSummaryResponse room = createRoomSummary(1, "기본 이름"); + ChatRoomMember member = mock(ChatRoomMember.class); + given(member.getChatRoomId()).willReturn(room.roomId()); + given(member.getCustomRoomName()).willReturn("내 방 이름"); + given(notificationMuteSettingRepository.findByTargetTypeAndTargetIdsAndUserId( + NotificationTargetType.CHAT_ROOM, + List.of(room.roomId()), + userId + )).willReturn(List.of(NotificationMuteSetting.of( + NotificationTargetType.CHAT_ROOM, + room.roomId(), + mock(User.class), + true + ))); + given(chatRoomMemberRepository.findByChatRoomIdsAndUserId(List.of(room.roomId()), userId)) + .willReturn(List.of(member)); + + // when + List result = chatRoomSettingsService.applyUserSettings(List.of(room), userId); + + // then + assertThat(result).hasSize(1); + assertThat(result.get(0).roomName()).isEqualTo("내 방 이름"); + assertThat(result.get(0).isMuted()).isTrue(); + assertThat(result.get(0).lastMessage()).isEqualTo(room.lastMessage()); + } + + @Test + @DisplayName("applyUserSettings는 빈 목록이면 설정 조회를 생략한다") + void applyUserSettingsSkipsLookupForEmptyRooms() { + // when + List result = chatRoomSettingsService.applyUserSettings(List.of(), 10); + + // then + assertThat(result).isEmpty(); + verifyNoInteractions(notificationMuteSettingRepository, chatRoomMemberRepository); + } + + private ChatRoomSummaryResponse createRoomSummary(Integer roomId, String roomName) { + return new ChatRoomSummaryResponse( + roomId, + ChatType.DIRECT, + roomName, + "https://example.com/image.png", + "마지막 메시지", + LocalDateTime.of(2026, 4, 27, 11, 0), + LocalDateTime.of(2026, 4, 27, 10, 0), + 3, + false + ); + } +} diff --git a/src/test/java/gg/agit/konect/unit/domain/chat/service/ChatServiceTest.java b/src/test/java/gg/agit/konect/unit/domain/chat/service/ChatServiceTest.java index e28c3856e..71e05479f 100644 --- a/src/test/java/gg/agit/konect/unit/domain/chat/service/ChatServiceTest.java +++ b/src/test/java/gg/agit/konect/unit/domain/chat/service/ChatServiceTest.java @@ -52,6 +52,7 @@ import gg.agit.konect.domain.chat.repository.ChatRoomRepository; import gg.agit.konect.domain.chat.service.ChatPresenceService; import gg.agit.konect.domain.chat.service.ChatRoomMembershipService; +import gg.agit.konect.domain.chat.service.ChatRoomSettingsService; import gg.agit.konect.domain.chat.service.ChatService; import gg.agit.konect.domain.club.model.Club; import gg.agit.konect.domain.club.model.ClubMember; @@ -100,6 +101,9 @@ class ChatServiceTest extends ServiceTestSupport { @Mock private ChatRoomMembershipService chatRoomMembershipService; + @Mock + private ChatRoomSettingsService chatRoomSettingsService; + @Mock private NotificationService notificationService; From 288f8bfda9652ab28a0b628f1328b35aa7b22474 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Mon, 27 Apr 2026 11:21:51 +0900 Subject: [PATCH 2/6] =?UTF-8?q?refactor:=20=EC=B1=84=ED=8C=85=EB=B0=A9=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=EC=9A=94=EC=95=BD=20=EC=A1=B0=ED=95=A9=20?= =?UTF-8?q?=EC=B1=85=EC=9E=84=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 채팅방 목록 요약 책임을 단계적으로 분리하기 위해 방 타입별 목록을 합치고 정렬하는 흐름을 ChatRoomSummaryService로 이동 - 목록 조회와 검색 조회가 각각 기존 정렬 기준을 유지하도록 분리된 메서드로 보존 - 검색에서 커스텀 이름과 기본 이름을 함께 비교할 수 있도록 기본 방 이름 맵 생성을 명시적으로 테스트 --- .../chat/service/ChatRoomSummaryService.java | 70 +++++++++++++ .../domain/chat/service/ChatService.java | 47 +++------ .../service/ChatRoomSummaryServiceTest.java | 97 +++++++++++++++++++ .../domain/chat/service/ChatServiceTest.java | 4 +- 4 files changed, 183 insertions(+), 35 deletions(-) create mode 100644 src/main/java/gg/agit/konect/domain/chat/service/ChatRoomSummaryService.java create mode 100644 src/test/java/gg/agit/konect/unit/domain/chat/service/ChatRoomSummaryServiceTest.java diff --git a/src/main/java/gg/agit/konect/domain/chat/service/ChatRoomSummaryService.java b/src/main/java/gg/agit/konect/domain/chat/service/ChatRoomSummaryService.java new file mode 100644 index 000000000..023096768 --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/chat/service/ChatRoomSummaryService.java @@ -0,0 +1,70 @@ +package gg.agit.konect.domain.chat.service; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.stereotype.Service; + +import gg.agit.konect.domain.chat.dto.ChatRoomSummaryResponse; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class ChatRoomSummaryService { + + private final ChatRoomSettingsService chatRoomSettingsService; + + public List summarizeChatRooms( + Integer userId, + List directRooms, + List clubRooms, + List groupRooms + ) { + List rooms = new ArrayList<>(); + rooms.addAll(directRooms); + rooms.addAll(clubRooms); + rooms.addAll(groupRooms); + + rooms = new ArrayList<>(chatRoomSettingsService.applyUserSettings(rooms, userId)); + rooms.sort(Comparator + .comparing( + (ChatRoomSummaryResponse room) -> + room.lastSentAt() != null ? room.lastSentAt() : room.createdAt(), + Comparator.reverseOrder() + )); + + return rooms; + } + + public List summarizeSearchableRooms( + Integer userId, + List directRooms, + List clubRooms + ) { + List rooms = new ArrayList<>(); + rooms.addAll(directRooms); + rooms.addAll(clubRooms); + + rooms = new ArrayList<>(chatRoomSettingsService.applyUserSettings(rooms, userId)); + rooms.sort( + Comparator.comparing(ChatRoomSummaryResponse::lastSentAt, + Comparator.nullsLast(Comparator.reverseOrder())) + .thenComparing(ChatRoomSummaryResponse::roomId) + ); + + return rooms; + } + + public Map getDefaultRoomNameMap( + List directRooms, + List clubRooms + ) { + Map defaultRoomNameMap = new HashMap<>(); + directRooms.forEach(room -> defaultRoomNameMap.put(room.roomId(), room.roomName())); + clubRooms.forEach(room -> defaultRoomNameMap.put(room.roomId(), room.roomName())); + return defaultRoomNameMap; + } +} diff --git a/src/main/java/gg/agit/konect/domain/chat/service/ChatService.java b/src/main/java/gg/agit/konect/domain/chat/service/ChatService.java index 561b3ca38..3ef9fc618 100644 --- a/src/main/java/gg/agit/konect/domain/chat/service/ChatService.java +++ b/src/main/java/gg/agit/konect/domain/chat/service/ChatService.java @@ -82,7 +82,7 @@ public class ChatService { private final UserRepository userRepository; private final ChatPresenceService chatPresenceService; private final ChatRoomMembershipService chatRoomMembershipService; - private final ChatRoomSettingsService chatRoomSettingsService; + private final ChatRoomSummaryService chatRoomSummaryService; private final NotificationService notificationService; private final ApplicationEventPublisher eventPublisher; @@ -213,18 +213,11 @@ public ChatRoomsSummaryResponse getChatRooms(Integer userId) { List clubRooms = getClubChatRooms(userId); List groupRooms = getGroupChatRooms(userId); - List rooms = new ArrayList<>(); - rooms.addAll(directRooms); - rooms.addAll(clubRooms); - rooms.addAll(groupRooms); - rooms = new ArrayList<>(chatRoomSettingsService.applyUserSettings(rooms, userId)); - - rooms.sort( - Comparator.comparing( - (ChatRoomSummaryResponse room) -> - room.lastSentAt() != null ? room.lastSentAt() : room.createdAt(), - Comparator.reverseOrder() - ) + List rooms = chatRoomSummaryService.summarizeChatRooms( + userId, + directRooms, + clubRooms, + groupRooms ); return new ChatRoomsSummaryResponse(rooms); @@ -910,16 +903,14 @@ private AccessibleChatRooms getAccessibleChatRooms(Integer userId) { roomIds.addAll(directRooms.stream().map(ChatRoomSummaryResponse::roomId).toList()); roomIds.addAll(clubRooms.stream().map(ChatRoomSummaryResponse::roomId).toList()); - Map defaultRoomNameMap = getDefaultRoomNameMap(directRooms, clubRooms); - List rooms = new ArrayList<>(); - rooms.addAll(directRooms); - rooms.addAll(clubRooms); - rooms = new ArrayList<>(chatRoomSettingsService.applyUserSettings(rooms, userId)); - - rooms.sort( - Comparator.comparing(ChatRoomSummaryResponse::lastSentAt, - Comparator.nullsLast(Comparator.reverseOrder())) - .thenComparing(ChatRoomSummaryResponse::roomId) + Map defaultRoomNameMap = chatRoomSummaryService.getDefaultRoomNameMap( + directRooms, + clubRooms + ); + List rooms = chatRoomSummaryService.summarizeSearchableRooms( + userId, + directRooms, + clubRooms ); return new AccessibleChatRooms(rooms, defaultRoomNameMap); } @@ -1012,16 +1003,6 @@ private Map getVisibleMessageFromMap(List roomI return visibleMessageFromMap; } - private Map getDefaultRoomNameMap( - List directRooms, - List clubRooms - ) { - Map defaultRoomNameMap = new HashMap<>(); - directRooms.forEach(room -> defaultRoomNameMap.put(room.roomId(), room.roomName())); - clubRooms.forEach(room -> defaultRoomNameMap.put(room.roomId(), room.roomName())); - return defaultRoomNameMap; - } - private boolean matchesRoomName( ChatRoomSummaryResponse room, String keyword, diff --git a/src/test/java/gg/agit/konect/unit/domain/chat/service/ChatRoomSummaryServiceTest.java b/src/test/java/gg/agit/konect/unit/domain/chat/service/ChatRoomSummaryServiceTest.java new file mode 100644 index 000000000..61484b78f --- /dev/null +++ b/src/test/java/gg/agit/konect/unit/domain/chat/service/ChatRoomSummaryServiceTest.java @@ -0,0 +1,97 @@ +package gg.agit.konect.unit.domain.chat.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import gg.agit.konect.domain.chat.dto.ChatRoomSummaryResponse; +import gg.agit.konect.domain.chat.enums.ChatType; +import gg.agit.konect.domain.chat.service.ChatRoomSettingsService; +import gg.agit.konect.domain.chat.service.ChatRoomSummaryService; +import gg.agit.konect.support.ServiceTestSupport; + +class ChatRoomSummaryServiceTest extends ServiceTestSupport { + + @Mock + private ChatRoomSettingsService chatRoomSettingsService; + + @InjectMocks + private ChatRoomSummaryService chatRoomSummaryService; + + @Test + @DisplayName("summarizeChatRooms는 사용자 설정을 적용한 뒤 최신 대화 순으로 정렬한다") + void summarizeChatRoomsAppliesSettingsAndSortsByRecentActivity() { + // given + Integer userId = 10; + ChatRoomSummaryResponse olderRoom = createRoom(1, ChatType.DIRECT, "오래된 방", + LocalDateTime.of(2026, 4, 27, 9, 0), LocalDateTime.of(2026, 4, 27, 8, 0)); + ChatRoomSummaryResponse emptyNewRoom = createRoom(2, ChatType.GROUP, "새 빈 방", + null, LocalDateTime.of(2026, 4, 27, 11, 0)); + ChatRoomSummaryResponse newestRoom = createRoom(3, ChatType.CLUB_GROUP, "최신 방", + LocalDateTime.of(2026, 4, 27, 12, 0), LocalDateTime.of(2026, 4, 27, 7, 0)); + List combinedRooms = List.of(olderRoom, newestRoom, emptyNewRoom); + + given(chatRoomSettingsService.applyUserSettings(combinedRooms, userId)) + .willReturn(combinedRooms); + + // when + List result = chatRoomSummaryService.summarizeChatRooms( + userId, + List.of(olderRoom), + List.of(newestRoom), + List.of(emptyNewRoom) + ); + + // then + assertThat(result).extracting(ChatRoomSummaryResponse::roomId) + .containsExactly(3, 2, 1); + } + + @Test + @DisplayName("getDefaultRoomNameMap은 검색용 기본 방 이름을 보존한다") + void getDefaultRoomNameMapKeepsOriginalRoomNames() { + // given + ChatRoomSummaryResponse directRoom = createRoom(1, ChatType.DIRECT, "상대방", + LocalDateTime.of(2026, 4, 27, 9, 0), LocalDateTime.of(2026, 4, 27, 8, 0)); + ChatRoomSummaryResponse clubRoom = createRoom(2, ChatType.CLUB_GROUP, "동아리", + LocalDateTime.of(2026, 4, 27, 10, 0), LocalDateTime.of(2026, 4, 27, 8, 0)); + + // when + Map result = chatRoomSummaryService.getDefaultRoomNameMap( + List.of(directRoom), + List.of(clubRoom) + ); + + // then + assertThat(result).containsEntry(1, "상대방") + .containsEntry(2, "동아리"); + } + + private ChatRoomSummaryResponse createRoom( + Integer roomId, + ChatType chatType, + String roomName, + LocalDateTime lastSentAt, + LocalDateTime createdAt + ) { + return new ChatRoomSummaryResponse( + roomId, + chatType, + roomName, + null, + null, + lastSentAt, + createdAt, + 0, + false + ); + } +} diff --git a/src/test/java/gg/agit/konect/unit/domain/chat/service/ChatServiceTest.java b/src/test/java/gg/agit/konect/unit/domain/chat/service/ChatServiceTest.java index 71e05479f..6df2bd4e1 100644 --- a/src/test/java/gg/agit/konect/unit/domain/chat/service/ChatServiceTest.java +++ b/src/test/java/gg/agit/konect/unit/domain/chat/service/ChatServiceTest.java @@ -52,7 +52,7 @@ import gg.agit.konect.domain.chat.repository.ChatRoomRepository; import gg.agit.konect.domain.chat.service.ChatPresenceService; import gg.agit.konect.domain.chat.service.ChatRoomMembershipService; -import gg.agit.konect.domain.chat.service.ChatRoomSettingsService; +import gg.agit.konect.domain.chat.service.ChatRoomSummaryService; import gg.agit.konect.domain.chat.service.ChatService; import gg.agit.konect.domain.club.model.Club; import gg.agit.konect.domain.club.model.ClubMember; @@ -102,7 +102,7 @@ class ChatServiceTest extends ServiceTestSupport { private ChatRoomMembershipService chatRoomMembershipService; @Mock - private ChatRoomSettingsService chatRoomSettingsService; + private ChatRoomSummaryService chatRoomSummaryService; @Mock private NotificationService notificationService; From 82edb33716e0e110fb91c58a3663e639a700596f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Mon, 27 Apr 2026 13:15:38 +0900 Subject: [PATCH 3/6] =?UTF-8?q?refactor:=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EB=A7=88=EC=A7=80=EB=A7=89=20=EB=A9=94=EC=8B=9C?= =?UTF-8?q?=EC=A7=80=20=EB=A9=94=ED=83=80=EB=8D=B0=EC=9D=B4=ED=84=B0=20?= =?UTF-8?q?=ED=99=9C=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 채팅방 목록 응답에서 club/group 방의 마지막 메시지를 이미 유지되는 chat_room 메타데이터로 읽도록 변경 - 목록 조회마다 chat_message에서 최신 메시지를 다시 조회하던 배치 쿼리를 제거해 반복 조회 비용을 줄임 - direct 방은 나간 뒤 가시성 정책이 별도로 필요하므로 기존 visibleMessageFrom 기반 처리를 유지 --- .../repository/ChatMessageRepository.java | 13 ---- .../domain/chat/service/ChatService.java | 61 +++++++------------ 2 files changed, 22 insertions(+), 52 deletions(-) diff --git a/src/main/java/gg/agit/konect/domain/chat/repository/ChatMessageRepository.java b/src/main/java/gg/agit/konect/domain/chat/repository/ChatMessageRepository.java index 921513d14..928579af6 100644 --- a/src/main/java/gg/agit/konect/domain/chat/repository/ChatMessageRepository.java +++ b/src/main/java/gg/agit/konect/domain/chat/repository/ChatMessageRepository.java @@ -109,19 +109,6 @@ List findRoomIdsWithUserReplyByRoomIds( @Param("adminRole") UserRole adminRole ); - @Query(""" - SELECT m - FROM ChatMessage m - JOIN FETCH m.sender - WHERE m.id IN ( - SELECT MAX(m2.id) - FROM ChatMessage m2 - WHERE m2.chatRoom.id IN :roomIds - GROUP BY m2.chatRoom.id - ) - """) - List findLatestMessagesByRoomIds(@Param("roomIds") List roomIds); - @Query( value = """ SELECT cm diff --git a/src/main/java/gg/agit/konect/domain/chat/service/ChatService.java b/src/main/java/gg/agit/konect/domain/chat/service/ChatService.java index 3ef9fc618..f0d4d24e4 100644 --- a/src/main/java/gg/agit/konect/domain/chat/service/ChatService.java +++ b/src/main/java/gg/agit/konect/domain/chat/service/ChatService.java @@ -538,24 +538,20 @@ private List getClubChatRooms(Integer userId) { .toList(); List roomIds = rooms.stream().map(ChatRoom::getId).toList(); - Map lastMessageMap = getLastMessageMap(roomIds); Map unreadCountMap = getRoomUnreadCountMap(roomIds, userId); return rooms.stream() - .map(room -> { - ChatMessage lastMessage = lastMessageMap.get(room.getId()); - return new ChatRoomSummaryResponse( - room.getId(), - ChatType.CLUB_GROUP, - room.getClub().getName(), - room.getClub().getImageUrl(), - lastMessage != null ? lastMessage.getContent() : null, - lastMessage != null ? lastMessage.getCreatedAt() : null, - room.getCreatedAt(), - unreadCountMap.getOrDefault(room.getId(), 0), - false - ); - }) + .map(room -> new ChatRoomSummaryResponse( + room.getId(), + ChatType.CLUB_GROUP, + room.getClub().getName(), + room.getClub().getImageUrl(), + room.getLastMessageContent(), + room.getLastMessageSentAt(), + room.getCreatedAt(), + unreadCountMap.getOrDefault(room.getId(), 0), + false + )) .toList(); } @@ -566,24 +562,20 @@ private List getGroupChatRooms(Integer userId) { } List roomIds = rooms.stream().map(ChatRoom::getId).toList(); - Map lastMessageMap = getLastMessageMap(roomIds); Map unreadCountMap = getRoomUnreadCountMap(roomIds, userId); return rooms.stream() - .map(room -> { - ChatMessage lastMessage = lastMessageMap.get(room.getId()); - return new ChatRoomSummaryResponse( - room.getId(), - ChatType.GROUP, - DEFAULT_GROUP_ROOM_NAME, - null, - lastMessage != null ? lastMessage.getContent() : null, - lastMessage != null ? lastMessage.getCreatedAt() : null, - room.getCreatedAt(), - unreadCountMap.getOrDefault(room.getId(), 0), - false - ); - }) + .map(room -> new ChatRoomSummaryResponse( + room.getId(), + ChatType.GROUP, + DEFAULT_GROUP_ROOM_NAME, + null, + room.getLastMessageContent(), + room.getLastMessageSentAt(), + room.getCreatedAt(), + unreadCountMap.getOrDefault(room.getId(), 0), + false + )) .toList(); } @@ -1257,15 +1249,6 @@ private int countUnreadSince(LocalDateTime messageCreatedAt, List return left; } - private Map getLastMessageMap(List roomIds) { - if (roomIds.isEmpty()) { - return Map.of(); - } - - return chatMessageRepository.findLatestMessagesByRoomIds(roomIds).stream() - .collect(Collectors.toMap(message -> message.getChatRoom().getId(), message -> message)); - } - private Map getRoomUnreadCountMap(List roomIds, Integer userId) { if (roomIds.isEmpty()) { return Map.of(); From 6327f1893ffb3f221b4d49a2cc44db21b6d7755c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Mon, 27 Apr 2026 11:21:51 +0900 Subject: [PATCH 4/6] =?UTF-8?q?refactor:=20=EC=B1=84=ED=8C=85=EB=B0=A9=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=EC=9A=94=EC=95=BD=20=EC=A1=B0=ED=95=A9=20?= =?UTF-8?q?=EC=B1=85=EC=9E=84=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 채팅방 목록 요약 책임을 단계적으로 분리하기 위해 방 타입별 목록을 합치고 정렬하는 흐름을 ChatRoomSummaryService로 이동 - 목록 조회와 검색 조회가 각각 기존 정렬 기준을 유지하도록 분리된 메서드로 보존 - 검색에서 커스텀 이름과 기본 이름을 함께 비교할 수 있도록 기본 방 이름 맵 생성을 명시적으로 테스트 --- .../chat/service/ChatRoomSummaryService.java | 70 +++++++++++++ .../domain/chat/service/ChatService.java | 47 +++------ .../service/ChatRoomSummaryServiceTest.java | 97 +++++++++++++++++++ .../domain/chat/service/ChatServiceTest.java | 4 +- 4 files changed, 183 insertions(+), 35 deletions(-) create mode 100644 src/main/java/gg/agit/konect/domain/chat/service/ChatRoomSummaryService.java create mode 100644 src/test/java/gg/agit/konect/unit/domain/chat/service/ChatRoomSummaryServiceTest.java diff --git a/src/main/java/gg/agit/konect/domain/chat/service/ChatRoomSummaryService.java b/src/main/java/gg/agit/konect/domain/chat/service/ChatRoomSummaryService.java new file mode 100644 index 000000000..023096768 --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/chat/service/ChatRoomSummaryService.java @@ -0,0 +1,70 @@ +package gg.agit.konect.domain.chat.service; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.stereotype.Service; + +import gg.agit.konect.domain.chat.dto.ChatRoomSummaryResponse; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class ChatRoomSummaryService { + + private final ChatRoomSettingsService chatRoomSettingsService; + + public List summarizeChatRooms( + Integer userId, + List directRooms, + List clubRooms, + List groupRooms + ) { + List rooms = new ArrayList<>(); + rooms.addAll(directRooms); + rooms.addAll(clubRooms); + rooms.addAll(groupRooms); + + rooms = new ArrayList<>(chatRoomSettingsService.applyUserSettings(rooms, userId)); + rooms.sort(Comparator + .comparing( + (ChatRoomSummaryResponse room) -> + room.lastSentAt() != null ? room.lastSentAt() : room.createdAt(), + Comparator.reverseOrder() + )); + + return rooms; + } + + public List summarizeSearchableRooms( + Integer userId, + List directRooms, + List clubRooms + ) { + List rooms = new ArrayList<>(); + rooms.addAll(directRooms); + rooms.addAll(clubRooms); + + rooms = new ArrayList<>(chatRoomSettingsService.applyUserSettings(rooms, userId)); + rooms.sort( + Comparator.comparing(ChatRoomSummaryResponse::lastSentAt, + Comparator.nullsLast(Comparator.reverseOrder())) + .thenComparing(ChatRoomSummaryResponse::roomId) + ); + + return rooms; + } + + public Map getDefaultRoomNameMap( + List directRooms, + List clubRooms + ) { + Map defaultRoomNameMap = new HashMap<>(); + directRooms.forEach(room -> defaultRoomNameMap.put(room.roomId(), room.roomName())); + clubRooms.forEach(room -> defaultRoomNameMap.put(room.roomId(), room.roomName())); + return defaultRoomNameMap; + } +} diff --git a/src/main/java/gg/agit/konect/domain/chat/service/ChatService.java b/src/main/java/gg/agit/konect/domain/chat/service/ChatService.java index 561b3ca38..3ef9fc618 100644 --- a/src/main/java/gg/agit/konect/domain/chat/service/ChatService.java +++ b/src/main/java/gg/agit/konect/domain/chat/service/ChatService.java @@ -82,7 +82,7 @@ public class ChatService { private final UserRepository userRepository; private final ChatPresenceService chatPresenceService; private final ChatRoomMembershipService chatRoomMembershipService; - private final ChatRoomSettingsService chatRoomSettingsService; + private final ChatRoomSummaryService chatRoomSummaryService; private final NotificationService notificationService; private final ApplicationEventPublisher eventPublisher; @@ -213,18 +213,11 @@ public ChatRoomsSummaryResponse getChatRooms(Integer userId) { List clubRooms = getClubChatRooms(userId); List groupRooms = getGroupChatRooms(userId); - List rooms = new ArrayList<>(); - rooms.addAll(directRooms); - rooms.addAll(clubRooms); - rooms.addAll(groupRooms); - rooms = new ArrayList<>(chatRoomSettingsService.applyUserSettings(rooms, userId)); - - rooms.sort( - Comparator.comparing( - (ChatRoomSummaryResponse room) -> - room.lastSentAt() != null ? room.lastSentAt() : room.createdAt(), - Comparator.reverseOrder() - ) + List rooms = chatRoomSummaryService.summarizeChatRooms( + userId, + directRooms, + clubRooms, + groupRooms ); return new ChatRoomsSummaryResponse(rooms); @@ -910,16 +903,14 @@ private AccessibleChatRooms getAccessibleChatRooms(Integer userId) { roomIds.addAll(directRooms.stream().map(ChatRoomSummaryResponse::roomId).toList()); roomIds.addAll(clubRooms.stream().map(ChatRoomSummaryResponse::roomId).toList()); - Map defaultRoomNameMap = getDefaultRoomNameMap(directRooms, clubRooms); - List rooms = new ArrayList<>(); - rooms.addAll(directRooms); - rooms.addAll(clubRooms); - rooms = new ArrayList<>(chatRoomSettingsService.applyUserSettings(rooms, userId)); - - rooms.sort( - Comparator.comparing(ChatRoomSummaryResponse::lastSentAt, - Comparator.nullsLast(Comparator.reverseOrder())) - .thenComparing(ChatRoomSummaryResponse::roomId) + Map defaultRoomNameMap = chatRoomSummaryService.getDefaultRoomNameMap( + directRooms, + clubRooms + ); + List rooms = chatRoomSummaryService.summarizeSearchableRooms( + userId, + directRooms, + clubRooms ); return new AccessibleChatRooms(rooms, defaultRoomNameMap); } @@ -1012,16 +1003,6 @@ private Map getVisibleMessageFromMap(List roomI return visibleMessageFromMap; } - private Map getDefaultRoomNameMap( - List directRooms, - List clubRooms - ) { - Map defaultRoomNameMap = new HashMap<>(); - directRooms.forEach(room -> defaultRoomNameMap.put(room.roomId(), room.roomName())); - clubRooms.forEach(room -> defaultRoomNameMap.put(room.roomId(), room.roomName())); - return defaultRoomNameMap; - } - private boolean matchesRoomName( ChatRoomSummaryResponse room, String keyword, diff --git a/src/test/java/gg/agit/konect/unit/domain/chat/service/ChatRoomSummaryServiceTest.java b/src/test/java/gg/agit/konect/unit/domain/chat/service/ChatRoomSummaryServiceTest.java new file mode 100644 index 000000000..61484b78f --- /dev/null +++ b/src/test/java/gg/agit/konect/unit/domain/chat/service/ChatRoomSummaryServiceTest.java @@ -0,0 +1,97 @@ +package gg.agit.konect.unit.domain.chat.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import gg.agit.konect.domain.chat.dto.ChatRoomSummaryResponse; +import gg.agit.konect.domain.chat.enums.ChatType; +import gg.agit.konect.domain.chat.service.ChatRoomSettingsService; +import gg.agit.konect.domain.chat.service.ChatRoomSummaryService; +import gg.agit.konect.support.ServiceTestSupport; + +class ChatRoomSummaryServiceTest extends ServiceTestSupport { + + @Mock + private ChatRoomSettingsService chatRoomSettingsService; + + @InjectMocks + private ChatRoomSummaryService chatRoomSummaryService; + + @Test + @DisplayName("summarizeChatRooms는 사용자 설정을 적용한 뒤 최신 대화 순으로 정렬한다") + void summarizeChatRoomsAppliesSettingsAndSortsByRecentActivity() { + // given + Integer userId = 10; + ChatRoomSummaryResponse olderRoom = createRoom(1, ChatType.DIRECT, "오래된 방", + LocalDateTime.of(2026, 4, 27, 9, 0), LocalDateTime.of(2026, 4, 27, 8, 0)); + ChatRoomSummaryResponse emptyNewRoom = createRoom(2, ChatType.GROUP, "새 빈 방", + null, LocalDateTime.of(2026, 4, 27, 11, 0)); + ChatRoomSummaryResponse newestRoom = createRoom(3, ChatType.CLUB_GROUP, "최신 방", + LocalDateTime.of(2026, 4, 27, 12, 0), LocalDateTime.of(2026, 4, 27, 7, 0)); + List combinedRooms = List.of(olderRoom, newestRoom, emptyNewRoom); + + given(chatRoomSettingsService.applyUserSettings(combinedRooms, userId)) + .willReturn(combinedRooms); + + // when + List result = chatRoomSummaryService.summarizeChatRooms( + userId, + List.of(olderRoom), + List.of(newestRoom), + List.of(emptyNewRoom) + ); + + // then + assertThat(result).extracting(ChatRoomSummaryResponse::roomId) + .containsExactly(3, 2, 1); + } + + @Test + @DisplayName("getDefaultRoomNameMap은 검색용 기본 방 이름을 보존한다") + void getDefaultRoomNameMapKeepsOriginalRoomNames() { + // given + ChatRoomSummaryResponse directRoom = createRoom(1, ChatType.DIRECT, "상대방", + LocalDateTime.of(2026, 4, 27, 9, 0), LocalDateTime.of(2026, 4, 27, 8, 0)); + ChatRoomSummaryResponse clubRoom = createRoom(2, ChatType.CLUB_GROUP, "동아리", + LocalDateTime.of(2026, 4, 27, 10, 0), LocalDateTime.of(2026, 4, 27, 8, 0)); + + // when + Map result = chatRoomSummaryService.getDefaultRoomNameMap( + List.of(directRoom), + List.of(clubRoom) + ); + + // then + assertThat(result).containsEntry(1, "상대방") + .containsEntry(2, "동아리"); + } + + private ChatRoomSummaryResponse createRoom( + Integer roomId, + ChatType chatType, + String roomName, + LocalDateTime lastSentAt, + LocalDateTime createdAt + ) { + return new ChatRoomSummaryResponse( + roomId, + chatType, + roomName, + null, + null, + lastSentAt, + createdAt, + 0, + false + ); + } +} diff --git a/src/test/java/gg/agit/konect/unit/domain/chat/service/ChatServiceTest.java b/src/test/java/gg/agit/konect/unit/domain/chat/service/ChatServiceTest.java index 71e05479f..6df2bd4e1 100644 --- a/src/test/java/gg/agit/konect/unit/domain/chat/service/ChatServiceTest.java +++ b/src/test/java/gg/agit/konect/unit/domain/chat/service/ChatServiceTest.java @@ -52,7 +52,7 @@ import gg.agit.konect.domain.chat.repository.ChatRoomRepository; import gg.agit.konect.domain.chat.service.ChatPresenceService; import gg.agit.konect.domain.chat.service.ChatRoomMembershipService; -import gg.agit.konect.domain.chat.service.ChatRoomSettingsService; +import gg.agit.konect.domain.chat.service.ChatRoomSummaryService; import gg.agit.konect.domain.chat.service.ChatService; import gg.agit.konect.domain.club.model.Club; import gg.agit.konect.domain.club.model.ClubMember; @@ -102,7 +102,7 @@ class ChatServiceTest extends ServiceTestSupport { private ChatRoomMembershipService chatRoomMembershipService; @Mock - private ChatRoomSettingsService chatRoomSettingsService; + private ChatRoomSummaryService chatRoomSummaryService; @Mock private NotificationService notificationService; From 08da04a8277dbd43cacc93fca3627205babcca30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Mon, 27 Apr 2026 13:23:53 +0900 Subject: [PATCH 5/6] =?UTF-8?q?refactor:=20=EA=B2=80=EC=83=89=20=EA=B0=80?= =?UTF-8?q?=EB=8A=A5=20=EB=B0=A9=20=EB=AA=A9=EB=A1=9D=20=EC=9E=94=EC=97=AC?= =?UTF-8?q?=20=EC=BD=94=EB=93=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 채팅방 요약 책임 분리 후 더 이상 사용하지 않는 roomIds 생성 코드를 제거 - 검색 가능한 방 목록 조합 흐름에서 실제로 필요한 기본 방 이름 맵과 요약 목록만 남김 - 미사용 변수가 남아 이후 조회 로직으로 오해되는 상황을 방지 --- .../java/gg/agit/konect/domain/chat/service/ChatService.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/main/java/gg/agit/konect/domain/chat/service/ChatService.java b/src/main/java/gg/agit/konect/domain/chat/service/ChatService.java index 3ef9fc618..5b9e59f85 100644 --- a/src/main/java/gg/agit/konect/domain/chat/service/ChatService.java +++ b/src/main/java/gg/agit/konect/domain/chat/service/ChatService.java @@ -899,10 +899,6 @@ private AccessibleChatRooms getAccessibleChatRooms(Integer userId) { List directRooms = getDirectChatRooms(userId); List clubRooms = getClubChatRooms(userId); - List roomIds = new ArrayList<>(); - roomIds.addAll(directRooms.stream().map(ChatRoomSummaryResponse::roomId).toList()); - roomIds.addAll(clubRooms.stream().map(ChatRoomSummaryResponse::roomId).toList()); - Map defaultRoomNameMap = chatRoomSummaryService.getDefaultRoomNameMap( directRooms, clubRooms From c8c82a6094c7496741c91475ca375c6b7b346ac9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Mon, 27 Apr 2026 11:39:36 +0900 Subject: [PATCH 6/6] =?UTF-8?q?refactor:=20=EC=B1=84=ED=8C=85=20=EA=B2=80?= =?UTF-8?q?=EC=83=89=20=EC=9D=91=EB=8B=B5=20=EC=A1=B0=EB=A6=BD=20=EC=B1=85?= =?UTF-8?q?=EC=9E=84=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 채팅 검색 책임 분리 작업을 작은 단위로 진행하기 위해 방 이름 검색과 메시지 내용 검색 조립 흐름을 ChatSearchService로 이동 - ChatService는 접근 가능한 방 목록을 준비한 뒤 검색 서비스에 위임하도록 축소 - direct visibleMessageFrom 필터링과 기본 방 이름 검색 정책은 기존 검색 통합 테스트로 유지 --- .../chat/service/ChatSearchService.java | 167 ++++++++++++++++++ .../domain/chat/service/ChatService.java | 132 +------------- 2 files changed, 170 insertions(+), 129 deletions(-) create mode 100644 src/main/java/gg/agit/konect/domain/chat/service/ChatSearchService.java diff --git a/src/main/java/gg/agit/konect/domain/chat/service/ChatSearchService.java b/src/main/java/gg/agit/konect/domain/chat/service/ChatSearchService.java new file mode 100644 index 000000000..69c5774c1 --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/chat/service/ChatSearchService.java @@ -0,0 +1,167 @@ +package gg.agit.konect.domain.chat.service; + +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.stream.Collectors; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; +import org.springframework.stereotype.Service; + +import gg.agit.konect.domain.chat.dto.ChatMessageMatchResult; +import gg.agit.konect.domain.chat.dto.ChatMessageMatchesResponse; +import gg.agit.konect.domain.chat.dto.ChatRoomMatchesResponse; +import gg.agit.konect.domain.chat.dto.ChatRoomSummaryResponse; +import gg.agit.konect.domain.chat.dto.ChatSearchResponse; +import gg.agit.konect.domain.chat.enums.ChatType; +import gg.agit.konect.domain.chat.model.ChatMessage; +import gg.agit.konect.domain.chat.model.ChatRoomMember; +import gg.agit.konect.domain.chat.repository.ChatMessageRepository; +import gg.agit.konect.domain.chat.repository.ChatRoomMemberRepository; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class ChatSearchService { + + private final ChatMessageRepository chatMessageRepository; + private final ChatRoomMemberRepository chatRoomMemberRepository; + + public ChatSearchResponse search( + Integer userId, + String keyword, + List accessibleRooms, + Map defaultRoomNameMap, + Integer page, + Integer limit + ) { + String normalizedKeyword = normalizeKeyword(keyword); + ChatRoomMatchesResponse roomMatches = searchRoomsByName( + accessibleRooms, + defaultRoomNameMap, + normalizedKeyword, + page, + limit + ); + ChatMessageMatchesResponse messageMatches = searchByMessageContent( + userId, + accessibleRooms, + normalizedKeyword, + page, + limit + ); + + return new ChatSearchResponse(roomMatches, messageMatches); + } + + private ChatRoomMatchesResponse searchRoomsByName( + List accessibleRooms, + Map defaultRoomNameMap, + String keyword, + Integer page, + Integer limit + ) { + List matchedRooms = accessibleRooms.stream() + .filter(room -> matchesRoomName(room, keyword, defaultRoomNameMap)) + .toList(); + + return ChatRoomMatchesResponse.from(toPage(matchedRooms, page, limit)); + } + + private ChatMessageMatchesResponse searchByMessageContent( + Integer userId, + List accessibleRooms, + String keyword, + Integer page, + Integer limit + ) { + if (accessibleRooms.isEmpty() || keyword.isBlank()) { + return ChatMessageMatchesResponse.from(emptyPage(page, limit)); + } + + Map roomMap = accessibleRooms.stream() + .collect(Collectors.toMap(ChatRoomSummaryResponse::roomId, room -> room)); + List roomIds = accessibleRooms.stream() + .map(ChatRoomSummaryResponse::roomId) + .toList(); + List directRoomIds = accessibleRooms.stream() + .filter(room -> room.chatType() == ChatType.DIRECT) + .map(ChatRoomSummaryResponse::roomId) + .toList(); + Map visibleMessageFromMap = getVisibleMessageFromMap(directRoomIds, userId); + + List matchedMessages = chatMessageRepository + .searchLatestMatchingMessagesByChatRoomIds(roomIds, keyword) + .stream() + .filter(message -> isVisibleMessageMatch(message, roomMap, visibleMessageFromMap)) + .map(message -> ChatMessageMatchResult.from(roomMap.get(message.getChatRoom().getId()), message)) + .toList(); + + return ChatMessageMatchesResponse.from(toPage(matchedMessages, page, limit)); + } + + private String normalizeKeyword(String keyword) { + return keyword == null ? "" : keyword.trim(); + } + + private boolean matchesRoomName( + ChatRoomSummaryResponse room, + String keyword, + Map defaultRoomNameMap + ) { + return containsKeyword(room.roomName(), keyword) + || containsKeyword(defaultRoomNameMap.get(room.roomId()), keyword); + } + + private boolean containsKeyword(String text, String keyword) { + return text != null + && !keyword.isBlank() + && text.toLowerCase(Locale.ROOT).contains(keyword.toLowerCase(Locale.ROOT)); + } + + private Map getVisibleMessageFromMap(List roomIds, Integer userId) { + if (roomIds.isEmpty()) { + return Map.of(); + } + + Map visibleMessageFromMap = new HashMap<>(); + for (ChatRoomMember roomMember : chatRoomMemberRepository.findByChatRoomIdsAndUserId(roomIds, userId)) { + visibleMessageFromMap.put(roomMember.getChatRoomId(), roomMember.getVisibleMessageFrom()); + } + return visibleMessageFromMap; + } + + private boolean isVisibleMessageMatch( + ChatMessage message, + Map roomMap, + Map visibleMessageFromMap + ) { + ChatRoomSummaryResponse room = roomMap.get(message.getChatRoom().getId()); + if (room == null || room.chatType() != ChatType.DIRECT) { + return true; + } + + LocalDateTime visibleMessageFrom = visibleMessageFromMap.get(room.roomId()); + return visibleMessageFrom == null || message.getCreatedAt().isAfter(visibleMessageFrom); + } + + private Page toPage(List items, Integer page, Integer limit) { + PageRequest pageable = PageRequest.of(page - 1, limit); + long offset = (long)(page - 1) * limit; + if (offset >= items.size()) { + return new PageImpl<>(List.of(), pageable, items.size()); + } + + int fromIndex = (int)offset; + int toIndex = Math.min(fromIndex + limit, items.size()); + return new PageImpl<>(items.subList(fromIndex, toIndex), pageable, items.size()); + } + + private Page emptyPage(Integer page, Integer limit) { + return new PageImpl<>(List.of(), PageRequest.of(page - 1, limit), 0); + } +} diff --git a/src/main/java/gg/agit/konect/domain/chat/service/ChatService.java b/src/main/java/gg/agit/konect/domain/chat/service/ChatService.java index 5b9e59f85..4d3a2b62f 100644 --- a/src/main/java/gg/agit/konect/domain/chat/service/ChatService.java +++ b/src/main/java/gg/agit/konect/domain/chat/service/ChatService.java @@ -9,7 +9,6 @@ import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; -import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Set; @@ -26,13 +25,10 @@ import gg.agit.konect.domain.chat.dto.AdminChatRoomProjection; import gg.agit.konect.domain.chat.dto.ChatInvitableUsersResponse; import gg.agit.konect.domain.chat.dto.ChatMessageDetailResponse; -import gg.agit.konect.domain.chat.dto.ChatMessageMatchResult; -import gg.agit.konect.domain.chat.dto.ChatMessageMatchesResponse; import gg.agit.konect.domain.chat.dto.ChatMessagePageResponse; import gg.agit.konect.domain.chat.dto.ChatMessageSendRequest; import gg.agit.konect.domain.chat.dto.ChatMuteResponse; import gg.agit.konect.domain.chat.dto.ChatRoomCreateRequest; -import gg.agit.konect.domain.chat.dto.ChatRoomMatchesResponse; import gg.agit.konect.domain.chat.dto.ChatRoomNameUpdateRequest; import gg.agit.konect.domain.chat.dto.ChatRoomResponse; import gg.agit.konect.domain.chat.dto.ChatRoomSummaryResponse; @@ -83,6 +79,7 @@ public class ChatService { private final ChatPresenceService chatPresenceService; private final ChatRoomMembershipService chatRoomMembershipService; private final ChatRoomSummaryService chatRoomSummaryService; + private final ChatSearchService chatSearchService; private final NotificationService notificationService; private final ApplicationEventPublisher eventPublisher; @@ -224,18 +221,9 @@ public ChatRoomsSummaryResponse getChatRooms(Integer userId) { } public ChatSearchResponse searchChats(Integer userId, String keyword, Integer page, Integer limit) { - String normalizedKeyword = normalizeKeyword(keyword); AccessibleChatRooms accessibleChatRooms = getAccessibleChatRooms(userId); - ChatRoomMatchesResponse roomMatches = searchRoomsByName(accessibleChatRooms, normalizedKeyword, page, limit); - ChatMessageMatchesResponse messageMatches = searchByMessageContent( - userId, - accessibleChatRooms.rooms(), - normalizedKeyword, - page, - limit - ); - - return new ChatSearchResponse(roomMatches, messageMatches); + return chatSearchService.search(userId, keyword, accessibleChatRooms.rooms(), + accessibleChatRooms.defaultRoomNameMap(), page, limit); } public ChatInvitableUsersResponse getInvitableUsers( @@ -911,120 +899,6 @@ private AccessibleChatRooms getAccessibleChatRooms(Integer userId) { return new AccessibleChatRooms(rooms, defaultRoomNameMap); } - private ChatRoomMatchesResponse searchRoomsByName( - AccessibleChatRooms accessibleChatRooms, - String keyword, - Integer page, - Integer limit - ) { - List matchedRooms = accessibleChatRooms.rooms().stream() - .filter(room -> matchesRoomName(room, keyword, accessibleChatRooms.defaultRoomNameMap())) - .toList(); - - return ChatRoomMatchesResponse.from(toPage(matchedRooms, page, limit)); - } - - private ChatMessageMatchesResponse searchByMessageContent( - Integer userId, - List accessibleRooms, - String keyword, - Integer page, - Integer limit - ) { - if (accessibleRooms.isEmpty() || keyword.isBlank()) { - return ChatMessageMatchesResponse.from(emptyPage(page, limit)); - } - - Map roomMap = accessibleRooms.stream() - .collect(Collectors.toMap(ChatRoomSummaryResponse::roomId, room -> room)); - List roomIds = accessibleRooms.stream() - .map(ChatRoomSummaryResponse::roomId) - .toList(); - List directRoomIds = accessibleRooms.stream() - .filter(room -> room.chatType() == ChatType.DIRECT) - .map(ChatRoomSummaryResponse::roomId) - .toList(); - Map visibleMessageFromMap = getVisibleMessageFromMap(directRoomIds, userId); - - List matchedMessages = chatMessageRepository - .searchLatestMatchingMessagesByChatRoomIds(roomIds, keyword) - .stream() - .filter(message -> isVisibleMessageMatch(message, roomMap, visibleMessageFromMap)) - .map(message -> ChatMessageMatchResult.from(roomMap.get(message.getChatRoom().getId()), message)) - .toList(); - - return ChatMessageMatchesResponse.from(toPage(matchedMessages, page, limit)); - } - - private String normalizeKeyword(String keyword) { - if (keyword == null) { - return ""; - } - return keyword.trim(); - } - - private boolean containsKeyword(String text, String keyword) { - if (text == null || keyword.isBlank()) { - return false; - } - - return text.toLowerCase(Locale.ROOT).contains(keyword.toLowerCase(Locale.ROOT)); - } - - private Page toPage(List items, Integer page, Integer limit) { - PageRequest pageable = PageRequest.of(page - 1, limit); - long offset = (long)(page - 1) * limit; - if (offset >= items.size()) { - return new PageImpl<>(List.of(), pageable, items.size()); - } - - int fromIndex = (int)offset; - int toIndex = Math.min(fromIndex + limit, items.size()); - return new PageImpl<>(items.subList(fromIndex, toIndex), pageable, items.size()); - } - - private Page emptyPage(Integer page, Integer limit) { - return new PageImpl<>(List.of(), PageRequest.of(page - 1, limit), 0); - } - - private Map getVisibleMessageFromMap(List roomIds, Integer userId) { - if (roomIds.isEmpty()) { - return Map.of(); - } - - Map visibleMessageFromMap = new HashMap<>(); - for (ChatRoomMember roomMember : chatRoomMemberRepository.findByChatRoomIdsAndUserId(roomIds, userId)) { - visibleMessageFromMap.put(roomMember.getChatRoomId(), roomMember.getVisibleMessageFrom()); - } - return visibleMessageFromMap; - } - - private boolean matchesRoomName( - ChatRoomSummaryResponse room, - String keyword, - Map defaultRoomNameMap - ) { - if (containsKeyword(room.roomName(), keyword)) { - return true; - } - - return containsKeyword(defaultRoomNameMap.get(room.roomId()), keyword); - } - - private boolean isVisibleMessageMatch( - ChatMessage message, - Map roomMap, - Map visibleMessageFromMap - ) { - ChatRoomSummaryResponse room = roomMap.get(message.getChatRoom().getId()); - if (room == null || room.chatType() != ChatType.DIRECT) { - return true; - } - - LocalDateTime visibleMessageFrom = visibleMessageFromMap.get(room.roomId()); - return visibleMessageFrom == null || message.getCreatedAt().isAfter(visibleMessageFrom); - } - private ChatRoom getDirectRoom(Integer roomId) { ChatRoom chatRoom = chatRoomRepository.findById(roomId) .orElseThrow(() -> CustomException.of(NOT_FOUND_CHAT_ROOM));