diff --git a/src/main/java/gg/agit/konect/domain/chat/service/ChatMessageSendService.java b/src/main/java/gg/agit/konect/domain/chat/service/ChatMessageSendService.java new file mode 100644 index 000000000..f44d13f49 --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/chat/service/ChatMessageSendService.java @@ -0,0 +1,327 @@ +package gg.agit.konect.domain.chat.service; + +import static gg.agit.konect.domain.chat.service.ChatRoomMembershipService.SYSTEM_ADMIN_ID; +import static gg.agit.konect.global.code.ApiResponseCode.FORBIDDEN_CHAT_ROOM_ACCESS; +import static gg.agit.konect.global.code.ApiResponseCode.NOT_FOUND_CHAT_ROOM; + +import java.time.LocalDateTime; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.springframework.context.ApplicationEventPublisher; +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.ChatMessageSendRequest; +import gg.agit.konect.domain.chat.event.AdminChatReceivedEvent; +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.chat.repository.ChatRoomRepository; +import gg.agit.konect.domain.club.model.ClubMember; +import gg.agit.konect.domain.club.repository.ClubMemberRepository; +import gg.agit.konect.domain.notification.service.NotificationService; +import gg.agit.konect.domain.user.model.User; +import gg.agit.konect.domain.user.repository.UserRepository; +import gg.agit.konect.global.exception.CustomException; +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +@Transactional +public class ChatMessageSendService { + + private static final String DEFAULT_GROUP_ROOM_NAME = "그룹 채팅"; + + private final ChatRoomRepository chatRoomRepository; + private final ChatMessageRepository chatMessageRepository; + private final ChatRoomMemberRepository chatRoomMemberRepository; + private final ClubMemberRepository clubMemberRepository; + private final UserRepository userRepository; + private final ChatRoomSystemAdminService chatRoomSystemAdminService; + private final ChatDirectRoomAccessService chatDirectRoomAccessService; + private final NotificationService notificationService; + private final ApplicationEventPublisher eventPublisher; + + public ChatMessageDetailResponse sendMessage(Integer userId, Integer roomId, ChatMessageSendRequest request) { + ChatRoom room = chatRoomRepository.findById(roomId) + .orElseThrow(() -> CustomException.of(NOT_FOUND_CHAT_ROOM)); + + if (room.isDirectRoom()) { + return sendDirectMessage(userId, room, request); + } + + if (room.isClubGroupRoom()) { + return sendClubMessageByRoomId(room, userId, request.content()); + } + + return sendGroupMessageByRoomId(room, userId, request.content()); + } + + private ChatMessageDetailResponse sendDirectMessage( + Integer userId, + ChatRoom chatRoom, + ChatMessageSendRequest request + ) { + Integer roomId = chatRoom.getId(); + User sender = userRepository.getById(userId); + + // 어드민이 SYSTEM_ADMIN 방에 메시지를 보내는 경우 + boolean isAdminSendingToSystemAdminRoom = sender.isAdmin() + && chatRoomSystemAdminService.isSystemAdminRoom(chatRoom.getId()); + + if (!isAdminSendingToSystemAdminRoom) { + chatDirectRoomAccessService.getAccessibleMember(chatRoom, sender); + } + + List members = chatRoomMemberRepository.findByChatRoomId(roomId); + User receiver = resolveDirectMessageReceiver(members, sender); + + ChatMessage chatMessage = chatMessageRepository.save( + ChatMessage.of(chatRoom, sender, request.content()) + ); + + syncLastMessage(chatRoom, chatMessage); + members.stream() + .filter(member -> !member.getUserId().equals(userId)) + .filter(ChatRoomMember::hasLeft) + .forEach(member -> member.restoreDirectRoomFromIncomingMessage(chatMessage.getCreatedAt())); + + // 어드민이 보낸 경우는 lastReadAt 업데이트하지 않음 (멤버가 아니므로) + if (!isAdminSendingToSystemAdminRoom) { + updateLastReadAtOrEnsureMember(roomId, userId, chatMessage.getCreatedAt()); + } + + List sortedReadBaselines = toSortedReadBaselines(members); + + notificationService.sendChatNotification(receiver.getId(), roomId, sender.getName(), request.content()); + + boolean isSystemAdminRoom = members.stream() + .map(ChatRoomMember::getUserId) + .anyMatch(memberUserId -> memberUserId.equals(SYSTEM_ADMIN_ID)); + publishAdminChatEventIfNeeded(isSystemAdminRoom, sender, request.content()); + + return new ChatMessageDetailResponse( + chatMessage.getId(), + chatMessage.getSender().getId(), + null, + chatMessage.getContent(), + chatMessage.getCreatedAt(), + true, + countUnreadSince(chatMessage.getCreatedAt(), sortedReadBaselines), + true + ); + } + + private ChatMessageDetailResponse sendClubMessageByRoomId(ChatRoom room, Integer userId, String content) { + Integer roomId = room.getId(); + ClubMember member = clubMemberRepository.getByClubIdAndUserId(room.getClub().getId(), userId); + User sender = member.getUser(); + + ensureRoomMember(room, sender, member.getCreatedAt()); + + ChatMessage message = chatMessageRepository.save(ChatMessage.of(room, sender, content)); + syncLastMessage(room, message); + updateLastReadAtOrEnsureMember(roomId, userId, message.getCreatedAt()); + + List members = chatRoomMemberRepository.findByChatRoomId(roomId); + List recipientUserIds = members.stream().map(ChatRoomMember::getUserId).toList(); + List sortedReadBaselines = toSortedReadBaselines(members); + + notificationService.sendGroupChatNotification( + roomId, + sender.getId(), + room.getClub().getName(), + sender.getName(), + message.getContent(), + recipientUserIds + ); + + return new ChatMessageDetailResponse( + message.getId(), + sender.getId(), + sender.getName(), + message.getContent(), + message.getCreatedAt(), + null, + countUnreadSince(message.getCreatedAt(), sortedReadBaselines), + true + ); + } + + private ChatMessageDetailResponse sendGroupMessageByRoomId(ChatRoom room, Integer userId, String content) { + Integer roomId = room.getId(); + User sender = userRepository.getById(userId); + + ChatRoomMember senderMember = getRoomMember(roomId, userId); + if (senderMember.hasLeft()) { + throw CustomException.of(FORBIDDEN_CHAT_ROOM_ACCESS); + } + + ChatMessage message = chatMessageRepository.save(ChatMessage.of(room, sender, content)); + syncLastMessage(room, message); + updateLastReadAt(roomId, userId, message.getCreatedAt()); + + List members = chatRoomMemberRepository.findByChatRoomId(roomId); + List recipientUserIds = members.stream() + .map(ChatRoomMember::getUserId) + .filter(id -> !id.equals(userId)) + .toList(); + List sortedReadBaselines = toSortedReadBaselines(members); + + notificationService.sendGroupChatNotification( + roomId, + sender.getId(), + DEFAULT_GROUP_ROOM_NAME, + sender.getName(), + message.getContent(), + recipientUserIds + ); + + return new ChatMessageDetailResponse( + message.getId(), + sender.getId(), + sender.getName(), + message.getContent(), + message.getCreatedAt(), + null, + countUnreadSince(message.getCreatedAt(), sortedReadBaselines), + true + ); + } + + private ChatRoomMember getRoomMember(Integer roomId, Integer userId) { + return chatRoomMemberRepository.findByChatRoomIdAndUserId(roomId, userId) + .orElseThrow(() -> CustomException.of(FORBIDDEN_CHAT_ROOM_ACCESS)); + } + + private void ensureRoomMember(ChatRoom room, User user, LocalDateTime joinedAt) { + chatRoomMemberRepository.findByChatRoomIdAndUserId(room.getId(), user.getId()) + .ifPresentOrElse(member -> { + LocalDateTime lastReadAt = member.getLastReadAt(); + if (lastReadAt == null || lastReadAt.isBefore(joinedAt)) { + member.updateLastReadAt(joinedAt); + } + }, () -> chatRoomMemberRepository.save(ChatRoomMember.of(room, user, joinedAt))); + } + + private void updateLastReadAtOrEnsureMember(Integer roomId, Integer userId, LocalDateTime lastReadAt) { + int updated = chatRoomMemberRepository.updateLastReadAtIfOlder(roomId, userId, lastReadAt); + if (updated == 0) { + ChatRoom room = chatRoomRepository.findById(roomId) + .orElseThrow(() -> CustomException.of(NOT_FOUND_CHAT_ROOM)); + User user = userRepository.getById(userId); + ensureRoomMember(room, user, lastReadAt); + } + } + + private void updateLastReadAt(Integer roomId, Integer userId, LocalDateTime lastReadAt) { + chatRoomMemberRepository.updateLastReadAtIfOlder(roomId, userId, lastReadAt); + } + + private List toSortedReadBaselines(List members) { + return members.stream() + .map(ChatRoomMember::getLastReadAt) + .sorted() + .toList(); + } + + 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 void syncLastMessage(ChatRoom room, ChatMessage message) { + // 채팅방 목록은 chat_room.last_message_*를 직접 조회하므로 + // 동시 전송에서도 가장 최신 메시지만 메타데이터를 덮어쓰도록 DB 조건을 같이 건다. + int updated = chatRoomRepository.updateLastMessageIfLatest( + room.getId(), + message.getId(), + message.getContent(), + message.getCreatedAt() + ); + if (updated > 0) { + room.updateLastMessage(message.getContent(), message.getCreatedAt()); + } + } + + private void publishAdminChatEventIfNeeded(boolean isSystemAdminRoom, User sender, String content) { + if (isSystemAdminRoom && !sender.isAdmin()) { + eventPublisher.publishEvent(AdminChatReceivedEvent.of(sender.getId(), sender.getName(), content)); + } + } + + private User resolveDirectMessageReceiver(List members, User sender) { + Map userMap = members.stream() + .collect(Collectors.toMap( + ChatRoomMember::getUserId, + ChatRoomMember::getUser, + (existing, replacement) -> existing + )); + List memberInfos = members.stream() + .map(member -> new MemberInfo(member.getUserId(), member.getCreatedAt())) + .toList(); + return resolveMessageReceiverFromMemberInfo(sender, memberInfos, userMap); + } + + private User findDirectPartnerFromMemberInfo( + List memberInfos, + Integer userId, + Map userMap + ) { + return memberInfos.stream() + .filter(info -> !info.userId().equals(userId)) + .min(Comparator.comparing(MemberInfo::createdAt)) + .map(info -> userMap.get(info.userId())) + .orElse(null); + } + + private User findNonAdminUserFromMemberInfo(List memberInfos, Map userMap) { + return memberInfos.stream() + .sorted(Comparator.comparing(MemberInfo::createdAt)) + .map(info -> userMap.get(info.userId())) + .filter(user -> user != null && !user.isAdmin()) + .findFirst() + .orElse(null); + } + + private User resolveMessageReceiverFromMemberInfo( + User sender, + List memberInfos, + Map userMap + ) { + if (sender.isAdmin()) { + User nonAdminUser = findNonAdminUserFromMemberInfo(memberInfos, userMap); + if (nonAdminUser != null) { + return nonAdminUser; + } + } + + User partner = findDirectPartnerFromMemberInfo(memberInfos, sender.getId(), userMap); + if (partner == null) { + throw CustomException.of(FORBIDDEN_CHAT_ROOM_ACCESS); + } + return partner; + } + + private record MemberInfo(Integer userId, LocalDateTime createdAt) { + } +} 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 b57c96976..4d1b3b2be 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,7 +13,6 @@ import java.util.Set; import java.util.stream.Collectors; -import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; @@ -35,7 +34,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.event.AdminChatReceivedEvent; import gg.agit.konect.domain.chat.model.ChatMessage; import gg.agit.konect.domain.chat.model.ChatRoom; import gg.agit.konect.domain.chat.model.ChatRoomMember; @@ -49,7 +47,6 @@ 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.notification.service.NotificationService; import gg.agit.konect.domain.user.enums.UserRole; import gg.agit.konect.domain.user.model.User; import gg.agit.konect.domain.user.repository.UserRepository; @@ -81,8 +78,7 @@ public class ChatService { private final ChatMessagePageResolver chatMessagePageResolver; private final ChatRoomSystemAdminService chatRoomSystemAdminService; private final ChatDirectRoomAccessService chatDirectRoomAccessService; - private final NotificationService notificationService; - private final ApplicationEventPublisher eventPublisher; + private final ChatMessageSendService chatMessageSendService; @Transactional public ChatRoomResponse createOrGetChatRoom(Integer currentUserId, ChatRoomCreateRequest request) { @@ -285,18 +281,7 @@ public ChatMessagePageResponse getMessages( @Transactional public ChatMessageDetailResponse sendMessage(Integer userId, Integer roomId, ChatMessageSendRequest request) { - ChatRoom room = chatRoomRepository.findById(roomId) - .orElseThrow(() -> CustomException.of(NOT_FOUND_CHAT_ROOM)); - - if (room.isDirectRoom()) { - return sendDirectMessage(userId, roomId, request); - } - - if (room.isClubGroupRoom()) { - return sendClubMessageByRoomId(roomId, userId, request.content()); - } - - return sendGroupMessageByRoomId(roomId, userId, request.content()); + return chatMessageSendService.sendMessage(userId, roomId, request); } @Transactional @@ -557,61 +542,6 @@ private ChatMessagePageResponse getAdminSystemDirectChatRoomMessages( visibleMessageFrom, sortedReadBaselines, maskedAdminId); } - private ChatMessageDetailResponse sendDirectMessage( - Integer userId, - Integer roomId, - ChatMessageSendRequest request - ) { - ChatRoom chatRoom = getDirectRoom(roomId); - User sender = userRepository.getById(userId); - - // 어드민이 SYSTEM_ADMIN 방에 메시지를 보내는 경우 - boolean isAdminSendingToSystemAdminRoom = sender.isAdmin() - && chatRoomSystemAdminService.isSystemAdminRoom(chatRoom.getId()); - - if (!isAdminSendingToSystemAdminRoom) { - chatDirectRoomAccessService.getAccessibleMember(chatRoom, sender); - } - - List members = chatRoomMemberRepository.findByChatRoomId(roomId); - User receiver = resolveDirectMessageReceiver(members, sender); - - ChatMessage chatMessage = chatMessageRepository.save( - ChatMessage.of(chatRoom, sender, request.content()) - ); - - syncLastMessage(chatRoom, chatMessage); - members.stream() - .filter(member -> !member.getUserId().equals(userId)) - .filter(ChatRoomMember::hasLeft) - .forEach(member -> member.restoreDirectRoomFromIncomingMessage(chatMessage.getCreatedAt())); - - // 어드민이 보낸 경우는 lastReadAt 업데이트하지 않음 (멤버가 아니므로) - if (!isAdminSendingToSystemAdminRoom) { - updateMemberLastReadAt(roomId, userId, chatMessage.getCreatedAt()); - } - - List sortedReadBaselines = toSortedReadBaselines(members); - - notificationService.sendChatNotification(receiver.getId(), roomId, sender.getName(), request.content()); - - boolean isSystemAdminRoom = members.stream() - .map(ChatRoomMember::getUserId) - .anyMatch(memberUserId -> memberUserId.equals(SYSTEM_ADMIN_ID)); - publishAdminChatEventIfNeeded(isSystemAdminRoom, sender, request.content()); - - return new ChatMessageDetailResponse( - chatMessage.getId(), - chatMessage.getSender().getId(), - null, - chatMessage.getContent(), - chatMessage.getCreatedAt(), - true, - countUnreadSince(chatMessage.getCreatedAt(), sortedReadBaselines), - true - ); - } - private ChatMessagePageResponse getClubMessagesByRoomId( Integer roomId, Integer userId, @@ -654,42 +584,6 @@ private ChatMessagePageResponse getClubMessagesByRoomId( ); } - private ChatMessageDetailResponse sendClubMessageByRoomId(Integer roomId, Integer userId, String content) { - ChatRoom room = getClubRoom(roomId); - ClubMember member = clubMemberRepository.getByClubIdAndUserId(room.getClub().getId(), userId); - User sender = member.getUser(); - - ensureRoomMember(room, sender, member.getCreatedAt()); - - ChatMessage message = chatMessageRepository.save(ChatMessage.of(room, sender, content)); - syncLastMessage(room, message); - updateClubMessageLastReadAt(roomId, userId, message.getCreatedAt()); - - List members = chatRoomMemberRepository.findByChatRoomId(roomId); - List recipientUserIds = members.stream().map(ChatRoomMember::getUserId).toList(); - List sortedReadBaselines = toSortedReadBaselines(members); - - notificationService.sendGroupChatNotification( - roomId, - sender.getId(), - room.getClub().getName(), - sender.getName(), - message.getContent(), - recipientUserIds - ); - - return new ChatMessageDetailResponse( - message.getId(), - sender.getId(), - sender.getName(), - message.getContent(), - message.getCreatedAt(), - null, - countUnreadSince(message.getCreatedAt(), sortedReadBaselines), - true - ); - } - private ChatMessagePageResponse getGroupMessagesByRoomId( Integer roomId, Integer userId, @@ -732,48 +626,6 @@ private ChatMessagePageResponse getGroupMessagesByRoomId( ); } - private ChatMessageDetailResponse sendGroupMessageByRoomId(Integer roomId, Integer userId, String content) { - ChatRoom room = chatRoomRepository.findById(roomId) - .orElseThrow(() -> CustomException.of(NOT_FOUND_CHAT_ROOM)); - User sender = userRepository.getById(userId); - - ChatRoomMember senderMember = getRoomMember(roomId, userId); - if (senderMember.hasLeft()) { - throw CustomException.of(FORBIDDEN_CHAT_ROOM_ACCESS); - } - - ChatMessage message = chatMessageRepository.save(ChatMessage.of(room, sender, content)); - syncLastMessage(room, message); - updateLastReadAt(roomId, userId, message.getCreatedAt()); - - List members = chatRoomMemberRepository.findByChatRoomId(roomId); - List recipientUserIds = members.stream() - .map(ChatRoomMember::getUserId) - .filter(id -> !id.equals(userId)) - .toList(); - List sortedReadBaselines = toSortedReadBaselines(members); - - notificationService.sendGroupChatNotification( - roomId, - sender.getId(), - DEFAULT_GROUP_ROOM_NAME, - sender.getName(), - message.getContent(), - recipientUserIds - ); - - return new ChatMessageDetailResponse( - message.getId(), - sender.getId(), - sender.getName(), - message.getContent(), - message.getCreatedAt(), - null, - countUnreadSince(message.getCreatedAt(), sortedReadBaselines), - true - ); - } - private AccessibleChatRooms getAccessibleChatRooms(Integer userId) { List directRooms = getDirectChatRooms(userId); List clubRooms = getClubChatRooms(userId); @@ -855,26 +707,6 @@ private Integer getMaskedAdminId(User user, ChatRoom chatRoom) { return null; } - private void publishAdminChatEventIfNeeded(boolean isSystemAdminRoom, User sender, String content) { - if (isSystemAdminRoom && !sender.isAdmin()) { - eventPublisher.publishEvent(AdminChatReceivedEvent.of(sender.getId(), sender.getName(), content)); - } - } - - private void syncLastMessage(ChatRoom room, ChatMessage message) { - // 채팅방 목록은 chat_room.last_message_*를 직접 조회하므로 - // 동시 전송에서도 가장 최신 메시지만 메타데이터를 덮어쓰도록 DB 조건을 같이 건다. - int updated = chatRoomRepository.updateLastMessageIfLatest( - room.getId(), - message.getId(), - message.getContent(), - message.getCreatedAt() - ); - if (updated > 0) { - room.updateLastMessage(message.getContent(), message.getCreatedAt()); - } - } - private ChatRoomMember getRoomMember(Integer roomId, Integer userId) { return chatRoomMemberRepository.findByChatRoomIdAndUserId(roomId, userId) .orElseThrow(() -> CustomException.of(FORBIDDEN_CHAT_ROOM_ACCESS)); @@ -944,30 +776,6 @@ private String normalizeCustomRoomName(String roomName) { return roomName.trim(); } - private void updateMemberLastReadAt(Integer roomId, Integer userId, LocalDateTime lastReadAt) { - int updated = chatRoomMemberRepository.updateLastReadAtIfOlder(roomId, userId, lastReadAt); - if (updated == 0) { - ChatRoom room = chatRoomRepository.findById(roomId) - .orElseThrow(() -> CustomException.of(NOT_FOUND_CHAT_ROOM)); - User user = userRepository.getById(userId); - ensureRoomMember(room, user, lastReadAt); - } - } - - private void updateLastReadAt(Integer roomId, Integer userId, LocalDateTime lastReadAt) { - chatRoomMemberRepository.updateLastReadAtIfOlder(roomId, userId, lastReadAt); - } - - private void updateClubMessageLastReadAt(Integer roomId, Integer userId, LocalDateTime lastReadAt) { - int updated = chatRoomMemberRepository.updateLastReadAtIfOlder(roomId, userId, lastReadAt); - if (updated == 0) { - ChatRoom room = chatRoomRepository.findById(roomId) - .orElseThrow(() -> CustomException.of(NOT_FOUND_CHAT_ROOM)); - User user = userRepository.getById(userId); - ensureRoomMember(room, user, lastReadAt); - } - } - private List toSortedReadBaselines(List members) { return members.stream() .map(ChatRoomMember::getLastReadAt) @@ -1143,32 +951,6 @@ private User resolveDirectChatPartner(List members, Integer user return findDirectPartner(members, userId); } - private User findNonAdminUser(List members) { - Map userMap = members.stream() - .collect(Collectors.toMap( - ChatRoomMember::getUserId, - ChatRoomMember::getUser, - (existing, replacement) -> existing - )); - List memberInfos = members.stream() - .map(m -> new MemberInfo(m.getUserId(), m.getCreatedAt())) - .toList(); - return findNonAdminUserFromMemberInfo(memberInfos, userMap); - } - - private User resolveDirectMessageReceiver(List members, User sender) { - Map userMap = members.stream() - .collect(Collectors.toMap( - ChatRoomMember::getUserId, - ChatRoomMember::getUser, - (existing, replacement) -> existing - )); - List memberInfos = members.stream() - .map(m -> new MemberInfo(m.getUserId(), m.getCreatedAt())) - .toList(); - return resolveMessageReceiverFromMemberInfo(sender, memberInfos, userMap); - } - private User findDirectPartnerFromMemberInfo( List memberInfos, Integer userId, @@ -1196,35 +978,6 @@ private User resolveDirectChatPartner( return findDirectPartnerFromMemberInfo(memberInfos, userId, userMap); } - private User findNonAdminUserFromMemberInfo(List memberInfos, Map userMap) { - return memberInfos.stream() - .sorted(Comparator.comparing(MemberInfo::createdAt)) - .map(info -> userMap.get(info.userId())) - .filter(Objects::nonNull) - .filter(user -> !user.isAdmin()) - .findFirst() - .orElse(null); - } - - private User resolveMessageReceiverFromMemberInfo( - User sender, - List memberInfos, - Map userMap - ) { - if (sender.isAdmin()) { - User nonAdminUser = findNonAdminUserFromMemberInfo(memberInfos, userMap); - if (nonAdminUser != null) { - return nonAdminUser; - } - } - - User partner = findDirectPartnerFromMemberInfo(memberInfos, sender.getId(), userMap); - if (partner == null) { - throw CustomException.of(FORBIDDEN_CHAT_ROOM_ACCESS); - } - return partner; - } - private void validateGroupRoomForKick(ChatRoom room) { if (!room.isGroupRoom() || room.isClubGroupRoom()) { throw CustomException.of(CANNOT_KICK_IN_NON_GROUP_ROOM); 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 63285edb5..9f7380bb8 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 @@ -43,6 +43,7 @@ import gg.agit.konect.domain.chat.dto.ChatRoomNameUpdateRequest; import gg.agit.konect.domain.chat.dto.ChatRoomResponse; import gg.agit.konect.domain.chat.enums.ChatType; +import gg.agit.konect.domain.chat.event.AdminChatReceivedEvent; import gg.agit.konect.domain.chat.model.ChatMessage; import gg.agit.konect.domain.chat.model.ChatRoom; import gg.agit.konect.domain.chat.model.ChatRoomMember; @@ -52,6 +53,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.ChatMessageSendService; import gg.agit.konect.domain.chat.service.ChatMessagePageResolver; import gg.agit.konect.domain.chat.service.ChatPresenceService; import gg.agit.konect.domain.chat.service.ChatRoomMembershipService; @@ -136,6 +138,17 @@ void setUp() { clubMemberRepository, chatRoomSystemAdminService ); + ChatMessageSendService chatMessageSendService = new ChatMessageSendService( + chatRoomRepository, + chatMessageRepository, + chatRoomMemberRepository, + clubMemberRepository, + userRepository, + chatRoomSystemAdminService, + chatDirectRoomAccessService, + notificationService, + eventPublisher + ); chatService = new ChatService( chatRoomRepository, chatRoomQueryRepository, @@ -152,8 +165,7 @@ void setUp() { chatMessagePageResolver, chatRoomSystemAdminService, chatDirectRoomAccessService, - notificationService, - eventPublisher + chatMessageSendService ); } @@ -1034,6 +1046,51 @@ void sendMessageInClubRoomSavesMessageAndSendsGroupNotification() { ); } + @Test + @DisplayName("sendMessage는 일반 사용자가 SYSTEM_ADMIN 방에 보내면 관리자 문의 이벤트를 발행한다") + void sendMessageByUserInSystemAdminRoomPublishesAdminChatEvent() { + // given + Integer senderId = 20; + String content = "문의합니다"; + User sender = createUser(senderId, "사용자", UserRole.USER); + User systemAdmin = createUser(SYSTEM_ADMIN_ID, "시스템관리자", UserRole.ADMIN); + ChatRoom systemAdminRoom = createRoom(1, ChatType.DIRECT, LocalDateTime.of(2026, 4, 11, 10, 0)); + ChatRoomMember senderMember = createRoomMember(systemAdminRoom, sender, false, + LocalDateTime.of(2026, 4, 11, 10, 0)); + ChatRoomMember systemAdminMember = createRoomMember(systemAdminRoom, systemAdmin, false, + LocalDateTime.of(2026, 4, 11, 10, 0)); + ChatMessage savedMessage = createMessage(100, systemAdminRoom, sender, content, + LocalDateTime.of(2026, 4, 11, 10, 1)); + + given(chatRoomRepository.findById(systemAdminRoom.getId())).willReturn(Optional.of(systemAdminRoom)); + given(userRepository.getById(senderId)).willReturn(sender); + given(chatRoomMemberRepository.findByChatRoomIdAndUserId(systemAdminRoom.getId(), senderId)) + .willReturn(Optional.of(senderMember)); + given(chatRoomMemberRepository.findByChatRoomId(systemAdminRoom.getId())) + .willReturn(List.of(systemAdminMember, senderMember)); + given(chatMessageRepository.save(any(ChatMessage.class))).willReturn(savedMessage); + given(chatRoomRepository.updateLastMessageIfLatest( + systemAdminRoom.getId(), savedMessage.getId(), savedMessage.getContent(), savedMessage.getCreatedAt() + )).willReturn(1); + given(chatRoomMemberRepository.updateLastReadAtIfOlder(eq(systemAdminRoom.getId()), eq(senderId), + any(LocalDateTime.class))) + .willReturn(1); + + // when + chatService.sendMessage(senderId, systemAdminRoom.getId(), new ChatMessageSendRequest(content)); + + // then + ArgumentCaptor eventCaptor = ArgumentCaptor.forClass(AdminChatReceivedEvent.class); + verify(eventPublisher).publishEvent(eventCaptor.capture()); + assertThat(eventCaptor.getValue()) + .extracting( + AdminChatReceivedEvent::senderId, + AdminChatReceivedEvent::senderName, + AdminChatReceivedEvent::content + ) + .containsExactly(senderId, sender.getName(), content); + } + @Test @DisplayName("sendMessage는 admin이 system admin room에 보내면 멤버십 체크를 건너뛰고 lastReadAt 업데이트도 하지 않는다") void sendMessageAdminBypassesMembershipInSystemAdminRoom() { @@ -1076,6 +1133,7 @@ void sendMessageAdminBypassesMembershipInSystemAdminRoom() { // 비관리자에게 알림이 전송되어야 한다 verify(notificationService).sendChatNotification(eq(targetUserId), eq(systemAdminRoom.getId()), eq("관리자"), eq("문의")); + verify(eventPublisher, never()).publishEvent(any(AdminChatReceivedEvent.class)); } // ===== toggleMute additional =====