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/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/ChatSearchService.java b/src/main/java/gg/agit/konect/domain/chat/service/ChatSearchService.java new file mode 100644 index 000000000..1b8ef0650 --- /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.ChatMessageQueryRepository; +import gg.agit.konect.domain.chat.repository.ChatRoomMemberRepository; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class ChatSearchService { + + private final ChatMessageQueryRepository chatMessageQueryRepository; + 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 = chatMessageQueryRepository + .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 039a652eb..e46f44067 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; @@ -46,7 +42,6 @@ import gg.agit.konect.domain.chat.model.ChatRoom; import gg.agit.konect.domain.chat.model.ChatRoomMember; import gg.agit.konect.domain.chat.repository.ChatInviteQueryRepository; -import gg.agit.konect.domain.chat.repository.ChatMessageQueryRepository; import gg.agit.konect.domain.chat.repository.ChatMessageRepository; import gg.agit.konect.domain.chat.repository.ChatRoomMemberRepository; import gg.agit.konect.domain.chat.repository.ChatRoomQueryRepository; @@ -82,10 +77,11 @@ public class ChatService { private final NotificationMuteSettingRepository notificationMuteSettingRepository; private final ClubMemberRepository clubMemberRepository; private final ChatInviteQueryRepository chatInviteQueryRepository; - private final ChatMessageQueryRepository chatMessageQueryRepository; private final UserRepository userRepository; private final ChatPresenceService chatPresenceService; private final ChatRoomMembershipService chatRoomMembershipService; + private final ChatRoomSummaryService chatRoomSummaryService; + private final ChatSearchService chatSearchService; private final NotificationService notificationService; private final ApplicationEventPublisher eventPublisher; @@ -216,75 +212,20 @@ 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.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); } 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( @@ -948,199 +889,16 @@ 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 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.sort( - Comparator.comparing(ChatRoomSummaryResponse::lastSentAt, - Comparator.nullsLast(Comparator.reverseOrder())) - .thenComparing(ChatRoomSummaryResponse::roomId) + Map defaultRoomNameMap = chatRoomSummaryService.getDefaultRoomNameMap( + directRooms, + clubRooms ); - 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) + List rooms = chatRoomSummaryService.summarizeSearchableRooms( + userId, + directRooms, + clubRooms ); - } - - 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 = chatMessageQueryRepository - .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 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(); - } - - Map visibleMessageFromMap = new HashMap<>(); - for (ChatRoomMember roomMember : chatRoomMemberRepository.findByChatRoomIdsAndUserId(roomIds, userId)) { - visibleMessageFromMap.put(roomMember.getChatRoomId(), roomMember.getVisibleMessageFrom()); - } - 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 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, - 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); + return new AccessibleChatRooms(rooms, defaultRoomNameMap); } private ChatRoom getDirectRoom(Integer roomId) { 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/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 01d8af408..d465f26bb 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 @@ -47,13 +47,14 @@ import gg.agit.konect.domain.chat.model.ChatRoom; import gg.agit.konect.domain.chat.model.ChatRoomMember; import gg.agit.konect.domain.chat.repository.ChatInviteQueryRepository; -import gg.agit.konect.domain.chat.repository.ChatMessageQueryRepository; import gg.agit.konect.domain.chat.repository.ChatMessageRepository; import gg.agit.konect.domain.chat.repository.ChatRoomMemberRepository; import gg.agit.konect.domain.chat.repository.ChatRoomQueryRepository; 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.ChatRoomSummaryService; +import gg.agit.konect.domain.chat.service.ChatSearchService; import gg.agit.konect.domain.chat.service.ChatService; import gg.agit.konect.domain.club.model.Club; import gg.agit.konect.domain.club.model.ClubMember; @@ -96,9 +97,6 @@ class ChatServiceTest extends ServiceTestSupport { @Mock private ChatInviteQueryRepository chatInviteQueryRepository; - @Mock - private ChatMessageQueryRepository chatMessageQueryRepository; - @Mock private UserRepository userRepository; @@ -108,6 +106,12 @@ class ChatServiceTest extends ServiceTestSupport { @Mock private ChatRoomMembershipService chatRoomMembershipService; + @Mock + private ChatRoomSummaryService chatRoomSummaryService; + + @Mock + private ChatSearchService chatSearchService; + @Mock private NotificationService notificationService;