diff --git a/src/main/java/gg/agit/konect/domain/chat/service/ChatMessageReadService.java b/src/main/java/gg/agit/konect/domain/chat/service/ChatMessageReadService.java new file mode 100644 index 00000000..2d1eab08 --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/chat/service/ChatMessageReadService.java @@ -0,0 +1,286 @@ +package gg.agit.konect.domain.chat.service; + +import static gg.agit.konect.domain.chat.service.ChatRoomMembershipService.SYSTEM_ADMIN_ID; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import gg.agit.konect.domain.chat.dto.ChatMessageDetailResponse; +import gg.agit.konect.domain.chat.dto.ChatMessagePageResponse; +import gg.agit.konect.domain.chat.model.ChatMessage; +import gg.agit.konect.domain.chat.model.ChatRoom; +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 gg.agit.konect.domain.user.model.User; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class ChatMessageReadService { + + private final ChatMessageRepository chatMessageRepository; + private final ChatRoomMemberRepository chatRoomMemberRepository; + private final ChatRoomSystemAdminService chatRoomSystemAdminService; + private final ChatDirectRoomAccessService chatDirectRoomAccessService; + + @Transactional + public ChatMessagePageResponse getDirectChatRoomMessages( + User user, + ChatRoom chatRoom, + Integer page, + Integer limit, + LocalDateTime readAt + ) { + Integer roomId = chatRoom.getId(); + List members = chatRoomMemberRepository.findByChatRoomId(roomId); + LocalDateTime visibleMessageFrom = + chatDirectRoomAccessService.prepareAccessAndGetVisibleMessageFrom(chatRoom, user); + + List sortedReadBaselines = toSortedReadBaselines(members); + Integer maskedAdminId = getMaskedAdminId(user, members); + + return buildDirectChatRoomMessages(user, roomId, page, limit, readAt, + visibleMessageFrom, sortedReadBaselines, maskedAdminId); + } + + public ChatMessagePageResponse getAdminSystemDirectChatRoomMessages( + User user, + ChatRoom chatRoom, + Integer page, + Integer limit, + LocalDateTime readAt + ) { + Integer roomId = chatRoom.getId(); + List members = chatRoomMemberRepository.findByChatRoomId(roomId); + LocalDateTime visibleMessageFrom = resolveAdminSystemRoomVisibleMessageFrom(members); + + List sortedReadBaselines = toAdminChatReadBaselines(members); + Integer maskedAdminId = getMaskedAdminId(user, members); + + return buildDirectChatRoomMessages(user, roomId, page, limit, readAt, + visibleMessageFrom, sortedReadBaselines, maskedAdminId); + } + + public ChatMessagePageResponse getClubMessagesByRoom( + ChatRoom room, + Integer userId, + Integer page, + Integer limit + ) { + Integer roomId = room.getId(); + PageRequest pageable = PageRequest.of(page - 1, limit); + long totalCount = chatMessageRepository.countByChatRoomId(roomId, null); + Page messagePage = chatMessageRepository.findByChatRoomId(roomId, null, pageable); + List messages = messagePage.getContent(); + List members = chatRoomMemberRepository.findByChatRoomId(roomId); + List sortedReadBaselines = toSortedReadBaselines(members); + + List responseMessages = messages.stream() + .map(message -> { + int unreadCount = countUnreadSince(message.getCreatedAt(), sortedReadBaselines); + return new ChatMessageDetailResponse( + message.getId(), + message.getSender().getId(), + message.getSender().getName(), + message.getContent(), + message.getCreatedAt(), + null, + unreadCount, + message.isSentBy(userId) + ); + }) + .toList(); + + int totalPage = limit > 0 ? (int)Math.ceil((double)totalCount / (double)limit) : 0; + return new ChatMessagePageResponse( + totalCount, + responseMessages.size(), + totalPage, + page, + room.getClub().getId(), + responseMessages + ); + } + + public ChatMessagePageResponse getGroupMessagesByRoom( + Integer roomId, + Integer userId, + Integer page, + Integer limit + ) { + PageRequest pageable = PageRequest.of(page - 1, limit); + long totalCount = chatMessageRepository.countByChatRoomId(roomId, null); + Page messagePage = chatMessageRepository.findByChatRoomId(roomId, null, pageable); + List messages = messagePage.getContent(); + List members = chatRoomMemberRepository.findByChatRoomId(roomId); + List sortedReadBaselines = toSortedReadBaselines(members); + + List responseMessages = messages.stream() + .map(message -> { + int unreadCount = countUnreadSince(message.getCreatedAt(), sortedReadBaselines); + return new ChatMessageDetailResponse( + message.getId(), + message.getSender().getId(), + message.getSender().getName(), + message.getContent(), + message.getCreatedAt(), + null, + unreadCount, + message.isSentBy(userId) + ); + }) + .toList(); + + int totalPage = limit > 0 ? (int)Math.ceil((double)totalCount / (double)limit) : 0; + return new ChatMessagePageResponse( + totalCount, + responseMessages.size(), + totalPage, + page, + null, + responseMessages + ); + } + + private ChatMessagePageResponse buildDirectChatRoomMessages( + User user, + Integer roomId, + Integer page, + Integer limit, + LocalDateTime readAt, + LocalDateTime visibleMessageFrom, + List sortedReadBaselines, + Integer maskedAdminId + ) { + PageRequest pageable = PageRequest.of(page - 1, limit); + Page messages = chatMessageRepository.findByChatRoomId(roomId, visibleMessageFrom, pageable); + + List responseMessages = messages.getContent().stream() + .map(message -> { + Integer senderId = maskedAdminId != null + ? resolveDirectSenderId(message, maskedAdminId) + : message.getSender().getId(); + boolean isMine = message.isSentBy(user.getId()); + boolean isRead = isMine || !message.getCreatedAt().isAfter(readAt); + int unreadCount = countUnreadSince(message.getCreatedAt(), sortedReadBaselines); + return new ChatMessageDetailResponse( + message.getId(), + senderId, + null, + message.getContent(), + message.getCreatedAt(), + isRead, + unreadCount, + isMine + ); + }) + .toList(); + + return new ChatMessagePageResponse( + messages.getTotalElements(), + messages.getNumberOfElements(), + messages.getTotalPages(), + messages.getNumber() + 1, + null, + responseMessages + ); + } + + private List toSortedReadBaselines(List members) { + return members.stream() + .map(this::resolveUnreadBaseline) + .sorted() + .toList(); + } + + private List toAdminChatReadBaselines(List members) { + LocalDateTime adminLastReadAt = null; + LocalDateTime userLastReadAt = null; + + for (ChatRoomMember member : members) { + LocalDateTime unreadBaseline = resolveUnreadBaseline(member); + if (member.getUser().isAdmin()) { + if (adminLastReadAt == null || unreadBaseline.isAfter(adminLastReadAt)) { + adminLastReadAt = unreadBaseline; + } + } else { + userLastReadAt = unreadBaseline; + } + } + + List baselines = new ArrayList<>(); + if (adminLastReadAt != null) { + baselines.add(adminLastReadAt); + } + if (userLastReadAt != null) { + baselines.add(userLastReadAt); + } + baselines.sort(Comparator.naturalOrder()); + return baselines; + } + + private LocalDateTime resolveUnreadBaseline(ChatRoomMember member) { + LocalDateTime lastReadAt = member.getLastReadAt(); + LocalDateTime visibleMessageFrom = member.getVisibleMessageFrom(); + + // direct 방에서 다시 보이기 시작한 시각 이전 메시지는 unreadCount에도 포함하지 않는다. + if (visibleMessageFrom == null) { + return lastReadAt; + } + if (lastReadAt == null) { + return visibleMessageFrom; + } + return lastReadAt.isAfter(visibleMessageFrom) ? lastReadAt : visibleMessageFrom; + } + + private int countUnreadSince(LocalDateTime messageCreatedAt, List sortedReadBaselines) { + int left = 0; + int right = sortedReadBaselines.size(); + + while (left < right) { + int mid = (left + right) >>> 1; + LocalDateTime baseline = sortedReadBaselines.get(mid); + + if (baseline.isBefore(messageCreatedAt)) { + left = mid + 1; + } else { + right = mid; + } + } + + return left; + } + + private LocalDateTime resolveAdminSystemRoomVisibleMessageFrom(List members) { + ChatRoomMember systemAdminMember = chatRoomSystemAdminService.findSystemAdminMember(members); + return systemAdminMember != null ? systemAdminMember.getVisibleMessageFrom() : null; + } + + private Integer resolveDirectSenderId(ChatMessage message, Integer maskedAdminId) { + if (maskedAdminId != null && message.getSender().isAdmin()) { + return maskedAdminId; + } + return message.getSender().getId(); + } + + private Integer getMaskedAdminId(User user, List members) { + if (user.isAdmin()) { + return null; + } + + boolean hasSystemAdmin = members.stream() + .map(ChatRoomMember::getUserId) + .anyMatch(memberUserId -> memberUserId.equals(SYSTEM_ADMIN_ID)); + + return hasSystemAdmin ? SYSTEM_ADMIN_ID : null; + } +} 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 4d1b3b2b..8b61d4e5 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 @@ -13,8 +13,6 @@ import java.util.Set; import java.util.stream.Collectors; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; @@ -34,7 +32,6 @@ import gg.agit.konect.domain.chat.dto.UnreadMessageCount; import gg.agit.konect.domain.chat.enums.ChatInviteSortBy; import gg.agit.konect.domain.chat.enums.ChatType; -import gg.agit.konect.domain.chat.model.ChatMessage; import gg.agit.konect.domain.chat.model.ChatRoom; import gg.agit.konect.domain.chat.model.ChatRoomMember; import gg.agit.konect.domain.chat.repository.ChatMessageRepository; @@ -75,6 +72,7 @@ public class ChatService { private final ChatRoomSummaryService chatRoomSummaryService; private final ChatSearchService chatSearchService; private final ChatInviteService chatInviteService; + private final ChatMessageReadService chatMessageReadService; private final ChatMessagePageResolver chatMessagePageResolver; private final ChatRoomSystemAdminService chatRoomSystemAdminService; private final ChatDirectRoomAccessService chatDirectRoomAccessService; @@ -258,25 +256,25 @@ public ChatMessagePageResponse getMessages( if (isAdminViewingSystemRoom) { chatRoomMembershipService.updateLastReadAt(roomId, SYSTEM_ADMIN_ID, readAt); recordPresenceSafely(roomId, userId); - return getAdminSystemDirectChatRoomMessages(user, room, roomId, page, limit, readAt); + return chatMessageReadService.getAdminSystemDirectChatRoomMessages(user, room, page, limit, readAt); } chatRoomMembershipService.updateDirectRoomLastReadAt(roomId, user, readAt, room); recordPresenceSafely(roomId, userId); - return getDirectChatRoomMessages(userId, roomId, page, limit, readAt); + return chatMessageReadService.getDirectChatRoomMessages(user, room, page, limit, readAt); } if (room.isClubGroupRoom()) { chatRoomMembershipService.ensureClubRoomMember(roomId, userId); chatRoomMembershipService.updateLastReadAt(roomId, userId, readAt); recordPresenceSafely(roomId, userId); - return getClubMessagesByRoomId(roomId, userId, page, limit); + return chatMessageReadService.getClubMessagesByRoom(room, userId, page, limit); } getAccessibleRoomMember(room, userId); chatRoomMembershipService.updateLastReadAt(roomId, userId, readAt); recordPresenceSafely(roomId, userId); - return getGroupMessagesByRoomId(roomId, userId, page, limit); + return chatMessageReadService.getGroupMessagesByRoom(roomId, userId, page, limit); } @Transactional @@ -459,173 +457,6 @@ private List getGroupChatRooms(Integer userId) { .toList(); } - private ChatMessagePageResponse buildDirectChatRoomMessages( - User user, - Integer roomId, - Integer page, - Integer limit, - LocalDateTime readAt, - LocalDateTime visibleMessageFrom, - List sortedReadBaselines, - Integer maskedAdminId - ) { - PageRequest pageable = PageRequest.of(page - 1, limit); - Page messages = chatMessageRepository.findByChatRoomId(roomId, visibleMessageFrom, pageable); - - List responseMessages = messages.getContent().stream() - .map(message -> { - Integer senderId = maskedAdminId != null - ? resolveDirectSenderId(message, maskedAdminId) - : message.getSender().getId(); - boolean isMine = maskedAdminId != null - ? shouldDisplayAsOwnMessage(user, message, true) - : message.isSentBy(user.getId()); - boolean isRead = isMine || !message.getCreatedAt().isAfter(readAt); - int unreadCount = countUnreadSince(message.getCreatedAt(), sortedReadBaselines); - return new ChatMessageDetailResponse( - message.getId(), - senderId, - null, - message.getContent(), - message.getCreatedAt(), - isRead, - unreadCount, - isMine - ); - }) - .toList(); - - return new ChatMessagePageResponse( - messages.getTotalElements(), - messages.getNumberOfElements(), - messages.getTotalPages(), - messages.getNumber() + 1, - null, - responseMessages - ); - } - - private ChatMessagePageResponse getDirectChatRoomMessages( - Integer userId, - Integer roomId, - Integer page, - Integer limit, - LocalDateTime readAt - ) { - ChatRoom chatRoom = getDirectRoom(roomId); - User user = userRepository.getById(userId); - List members = chatRoomMemberRepository.findByChatRoomId(roomId); - LocalDateTime visibleMessageFrom = - chatDirectRoomAccessService.prepareAccessAndGetVisibleMessageFrom(chatRoom, user); - - List sortedReadBaselines = toSortedReadBaselines(members); - - return buildDirectChatRoomMessages(user, roomId, page, limit, readAt, - visibleMessageFrom, sortedReadBaselines, null); - } - - private ChatMessagePageResponse getAdminSystemDirectChatRoomMessages( - User user, - ChatRoom chatRoom, - Integer roomId, - Integer page, - Integer limit, - LocalDateTime readAt - ) { - List members = chatRoomMemberRepository.findByChatRoomId(roomId); - LocalDateTime visibleMessageFrom = resolveAdminSystemRoomVisibleMessageFrom(members); - - List sortedReadBaselines = toAdminChatReadBaselines(members); - Integer maskedAdminId = getMaskedAdminId(user, chatRoom); - - return buildDirectChatRoomMessages(user, roomId, page, limit, readAt, - visibleMessageFrom, sortedReadBaselines, maskedAdminId); - } - - private ChatMessagePageResponse getClubMessagesByRoomId( - Integer roomId, - Integer userId, - Integer page, - Integer limit - ) { - ChatRoom room = getClubRoom(roomId); - - PageRequest pageable = PageRequest.of(page - 1, limit); - long totalCount = chatMessageRepository.countByChatRoomId(roomId, null); - Page messagePage = chatMessageRepository.findByChatRoomId(roomId, null, pageable); - List messages = messagePage.getContent(); - List members = chatRoomMemberRepository.findByChatRoomId(roomId); - List sortedReadBaselines = toSortedReadBaselines(members); - - List responseMessages = messages.stream() - .map(message -> { - int unreadCount = countUnreadSince(message.getCreatedAt(), sortedReadBaselines); - return new ChatMessageDetailResponse( - message.getId(), - message.getSender().getId(), - message.getSender().getName(), - message.getContent(), - message.getCreatedAt(), - null, - unreadCount, - message.isSentBy(userId) - ); - }) - .toList(); - - int totalPage = limit > 0 ? (int)Math.ceil((double)totalCount / (double)limit) : 0; - return new ChatMessagePageResponse( - totalCount, - responseMessages.size(), - totalPage, - page, - room.getClub().getId(), - responseMessages - ); - } - - private ChatMessagePageResponse getGroupMessagesByRoomId( - Integer roomId, - Integer userId, - Integer page, - Integer limit - ) { - chatRoomRepository.getById(roomId); - - PageRequest pageable = PageRequest.of(page - 1, limit); - long totalCount = chatMessageRepository.countByChatRoomId(roomId, null); - Page messagePage = chatMessageRepository.findByChatRoomId(roomId, null, pageable); - List messages = messagePage.getContent(); - List members = chatRoomMemberRepository.findByChatRoomId(roomId); - List sortedReadBaselines = toSortedReadBaselines(members); - - List responseMessages = messages.stream() - .map(message -> { - int unreadCount = countUnreadSince(message.getCreatedAt(), sortedReadBaselines); - return new ChatMessageDetailResponse( - message.getId(), - message.getSender().getId(), - message.getSender().getName(), - message.getContent(), - message.getCreatedAt(), - null, - unreadCount, - message.isSentBy(userId) - ); - }) - .toList(); - - int totalPage = limit > 0 ? (int)Math.ceil((double)totalCount / (double)limit) : 0; - return new ChatMessagePageResponse( - totalCount, - responseMessages.size(), - totalPage, - page, - null, - responseMessages - ); - } - private AccessibleChatRooms getAccessibleChatRooms(Integer userId) { List directRooms = getDirectChatRooms(userId); List clubRooms = getClubChatRooms(userId); @@ -642,26 +473,6 @@ private AccessibleChatRooms getAccessibleChatRooms(Integer userId) { return new AccessibleChatRooms(rooms, defaultRoomNameMap); } - private ChatRoom getDirectRoom(Integer roomId) { - ChatRoom chatRoom = chatRoomRepository.findById(roomId) - .orElseThrow(() -> CustomException.of(NOT_FOUND_CHAT_ROOM)); - - if (!chatRoom.isDirectRoom()) { - throw CustomException.of(NOT_FOUND_CHAT_ROOM); - } - - return chatRoom; - } - - private ChatRoom getClubRoom(Integer roomId) { - ChatRoom room = chatRoomRepository.findById(roomId) - .orElseThrow(() -> CustomException.of(ApiResponseCode.NOT_FOUND_CHAT_ROOM)); - if (!room.isClubGroupRoom()) { - throw CustomException.of(ApiResponseCode.NOT_FOUND_GROUP_CHAT_ROOM); - } - return room; - } - private List extractChatRoomIds(List chatRooms) { return chatRooms.stream() .map(ChatRoom::getId) @@ -685,28 +496,6 @@ private Map getUnreadCountMap(List chatRoomIds, Integ )); } - private Integer getMaskedAdminId(User user, ChatRoom chatRoom) { - if (user.isAdmin()) { - return null; - } - - List memberResults = chatRoomMemberRepository.findRoomMemberIdsByChatRoomIds( - List.of(chatRoom.getId()) - ); - List memberInfos = memberResults.stream() - .map(row -> new MemberInfo((Integer)row[1], (LocalDateTime)row[2])) - .toList(); - - boolean hasSystemAdmin = memberInfos.stream() - .anyMatch(info -> info.userId().equals(SYSTEM_ADMIN_ID)); - - if (hasSystemAdmin) { - return SYSTEM_ADMIN_ID; - } - - return null; - } - private ChatRoomMember getRoomMember(Integer roomId, Integer userId) { return chatRoomMemberRepository.findByChatRoomIdAndUserId(roomId, userId) .orElseThrow(() -> CustomException.of(FORBIDDEN_CHAT_ROOM_ACCESS)); @@ -776,56 +565,6 @@ private String normalizeCustomRoomName(String roomName) { return roomName.trim(); } - private List toSortedReadBaselines(List members) { - return members.stream() - .map(ChatRoomMember::getLastReadAt) - .sorted() - .toList(); - } - - private List toAdminChatReadBaselines(List members) { - LocalDateTime adminLastReadAt = null; - LocalDateTime userLastReadAt = null; - - for (ChatRoomMember member : members) { - if (member.getUser().isAdmin()) { - if (adminLastReadAt == null || member.getLastReadAt().isAfter(adminLastReadAt)) { - adminLastReadAt = member.getLastReadAt(); - } - } else { - userLastReadAt = member.getLastReadAt(); - } - } - - List baselines = new ArrayList<>(); - if (adminLastReadAt != null) { - baselines.add(adminLastReadAt); - } - if (userLastReadAt != null) { - baselines.add(userLastReadAt); - } - baselines.sort(Comparator.naturalOrder()); - return baselines; - } - - private int countUnreadSince(LocalDateTime messageCreatedAt, List sortedReadBaselines) { - int left = 0; - int right = sortedReadBaselines.size(); - - while (left < right) { - int mid = (left + right) >>> 1; - LocalDateTime baseline = sortedReadBaselines.get(mid); - - if (baseline.isBefore(messageCreatedAt)) { - left = mid + 1; - } else { - right = mid; - } - } - - return left; - } - private Map getRoomUnreadCountMap(List roomIds, Integer userId) { if (roomIds.isEmpty()) { return Map.of(); @@ -850,29 +589,6 @@ private Map getRoomUnreadCountMap(List roomIds, Integ return unreadCountMap; } - private LocalDateTime resolveAdminSystemRoomVisibleMessageFrom(List members) { - ChatRoomMember systemAdminMember = chatRoomSystemAdminService.findSystemAdminMember(members); - return systemAdminMember != null ? systemAdminMember.getVisibleMessageFrom() : null; - } - - private boolean shouldDisplayAsOwnMessage( - User currentUser, - ChatMessage message, - boolean isAdminViewingSystemRoom - ) { - if (isAdminViewingSystemRoom) { - return message.getSender().isAdmin(); - } - return message.isSentBy(currentUser.getId()); - } - - private Integer resolveDirectSenderId(ChatMessage message, Integer maskedAdminId) { - if (maskedAdminId != null && message.getSender().isAdmin()) { - return maskedAdminId; - } - return message.getSender().getId(); - } - private ChatRoomMember findRoomMember(List members, Integer userId) { return members.stream() .filter(member -> member.getUserId().equals(userId)) 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 9f7380bb..176e4ba7 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 @@ -12,6 +12,7 @@ import static gg.agit.konect.global.code.ApiResponseCode.NOT_FOUND_USER; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.tuple; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.nullable; @@ -53,6 +54,7 @@ import gg.agit.konect.domain.chat.repository.ChatRoomRepository; import gg.agit.konect.domain.chat.service.ChatDirectRoomAccessService; import gg.agit.konect.domain.chat.service.ChatInviteService; +import gg.agit.konect.domain.chat.service.ChatMessageReadService; import gg.agit.konect.domain.chat.service.ChatMessageSendService; import gg.agit.konect.domain.chat.service.ChatMessagePageResolver; import gg.agit.konect.domain.chat.service.ChatPresenceService; @@ -149,6 +151,12 @@ void setUp() { notificationService, eventPublisher ); + ChatMessageReadService chatMessageReadService = new ChatMessageReadService( + chatMessageRepository, + chatRoomMemberRepository, + chatRoomSystemAdminService, + chatDirectRoomAccessService + ); chatService = new ChatService( chatRoomRepository, chatRoomQueryRepository, @@ -162,6 +170,7 @@ void setUp() { chatRoomSummaryService, chatSearchService, chatInviteService, + chatMessageReadService, chatMessagePageResolver, chatRoomSystemAdminService, chatDirectRoomAccessService, @@ -820,8 +829,16 @@ void getMessagesReturnsAdminSystemRoomMessages() { LocalDateTime.of(2026, 4, 11, 10, 0)); ChatRoomMember targetMember = createRoomMember(systemAdminRoom, targetUser, false, LocalDateTime.of(2026, 4, 11, 10, 0)); - ChatMessage message = createMessage(100, systemAdminRoom, admin, "문의", - LocalDateTime.of(2026, 4, 11, 10, 1)); + ReflectionTestUtils.setField(systemAdminMember, "visibleMessageFrom", + LocalDateTime.of(2026, 4, 11, 10, 5)); + ReflectionTestUtils.setField(targetMember, "visibleMessageFrom", + LocalDateTime.of(2026, 4, 11, 10, 7)); + systemAdminMember.updateLastReadAt(LocalDateTime.of(2026, 4, 11, 10, 7)); + targetMember.updateLastReadAt(LocalDateTime.of(2026, 4, 11, 10, 3)); + ChatMessage adminMessage = createMessage(100, systemAdminRoom, admin, "관리자 답변", + LocalDateTime.of(2026, 4, 11, 10, 6)); + ChatMessage userMessage = createMessage(101, systemAdminRoom, targetUser, "사용자 문의", + LocalDateTime.of(2026, 4, 11, 10, 8)); given(chatRoomRepository.findById(systemAdminRoom.getId())).willReturn(Optional.of(systemAdminRoom)); given(userRepository.getById(adminId)).willReturn(admin); @@ -830,20 +847,86 @@ void getMessagesReturnsAdminSystemRoomMessages() { .willReturn(systemAdminMember); given(chatRoomMemberRepository.findByChatRoomId(systemAdminRoom.getId())) .willReturn(List.of(systemAdminMember, targetMember)); - given(chatMessageRepository.findByChatRoomId(eq(systemAdminRoom.getId()), nullable(LocalDateTime.class), + given(chatMessageRepository.findByChatRoomId(eq(systemAdminRoom.getId()), + eq(systemAdminMember.getVisibleMessageFrom()), eq(PageRequest.of(0, 20)))) - .willReturn(new PageImpl<>(List.of(message), PageRequest.of(0, 20), 1)); + .willReturn(new PageImpl<>(List.of(adminMessage, userMessage), PageRequest.of(0, 20), 2)); // when ChatMessagePageResponse response = chatService.getMessages(adminId, systemAdminRoom.getId(), 1, 20); // then - assertThat(response.messages()).hasSize(1); + assertThat(response.messages()) + .extracting( + ChatMessageDetailResponse::senderId, + ChatMessageDetailResponse::content, + ChatMessageDetailResponse::unreadCount, + ChatMessageDetailResponse::isMine + ) + .containsExactly( + tuple(adminId, "관리자 답변", 0, true), + tuple(targetUser.getId(), "사용자 문의", 2, false) + ); verify(chatRoomMembershipService).updateLastReadAt(eq(systemAdminRoom.getId()), eq(SYSTEM_ADMIN_ID), any(LocalDateTime.class)); verify(chatPresenceService).recordPresence(systemAdminRoom.getId(), adminId); } + @Test + @DisplayName("getMessages는 일반 사용자의 system admin 방 조회에서 가시 범위와 sender masking을 적용한다") + void getMessagesAppliesVisibilityAndSenderMaskingForUserInSystemAdminRoom() { + // given + Integer userId = 20; + Integer adminId = 99; + User user = createUser(userId, "사용자", UserRole.USER); + User admin = createUser(adminId, "관리자", UserRole.ADMIN); + User systemAdmin = createUser(SYSTEM_ADMIN_ID, "시스템관리자", UserRole.ADMIN); + ChatRoom systemAdminRoom = createRoom(1, ChatType.DIRECT, LocalDateTime.of(2026, 4, 11, 10, 0)); + ChatRoomMember userMember = createRoomMember(systemAdminRoom, user, false, + LocalDateTime.of(2026, 4, 11, 10, 0)); + ChatRoomMember systemAdminMember = createRoomMember(systemAdminRoom, systemAdmin, false, + LocalDateTime.of(2026, 4, 11, 10, 0)); + ReflectionTestUtils.setField(userMember, "visibleMessageFrom", LocalDateTime.of(2026, 4, 11, 10, 5)); + ReflectionTestUtils.setField(systemAdminMember, "visibleMessageFrom", + LocalDateTime.of(2026, 4, 11, 10, 7)); + userMember.updateLastReadAt(LocalDateTime.of(2026, 4, 11, 10, 7)); + systemAdminMember.updateLastReadAt(LocalDateTime.of(2026, 4, 11, 10, 3)); + ChatMessage adminMessage = createMessage(100, systemAdminRoom, admin, "관리자 답변", + LocalDateTime.of(2026, 4, 11, 10, 6)); + ChatMessage userMessage = createMessage(101, systemAdminRoom, user, "사용자 문의", + LocalDateTime.of(2026, 4, 11, 10, 8)); + + given(chatRoomRepository.findById(systemAdminRoom.getId())).willReturn(Optional.of(systemAdminRoom)); + given(userRepository.getById(userId)).willReturn(user); + given(chatRoomMemberRepository.findByChatRoomIdAndUserId(systemAdminRoom.getId(), userId)) + .willReturn(Optional.of(userMember)); + given(chatRoomMemberRepository.findByChatRoomId(systemAdminRoom.getId())) + .willReturn(List.of(systemAdminMember, userMember)); + given(chatMessageRepository.findByChatRoomId(eq(systemAdminRoom.getId()), + eq(userMember.getVisibleMessageFrom()), + eq(PageRequest.of(0, 20)))) + .willReturn(new PageImpl<>(List.of(adminMessage, userMessage), PageRequest.of(0, 20), 2)); + + // when + ChatMessagePageResponse response = chatService.getMessages(userId, systemAdminRoom.getId(), 1, 20); + + // then + assertThat(response.messages()) + .extracting( + ChatMessageDetailResponse::senderId, + ChatMessageDetailResponse::content, + ChatMessageDetailResponse::unreadCount, + ChatMessageDetailResponse::isMine + ) + .containsExactly( + tuple(SYSTEM_ADMIN_ID, "관리자 답변", 0, false), + tuple(userId, "사용자 문의", 2, true) + ); + verify(chatRoomMembershipService).updateDirectRoomLastReadAt(eq(systemAdminRoom.getId()), eq(user), + any(LocalDateTime.class), eq(systemAdminRoom)); + verify(chatPresenceService).recordPresence(systemAdminRoom.getId(), userId); + } + // ===== sendMessage ===== @Test