diff --git a/src/main/java/gg/agit/konect/domain/chat/dto/AdminChatRoomProjection.java b/src/main/java/gg/agit/konect/domain/chat/dto/AdminChatRoomProjection.java index a277a19a6..96e871757 100644 --- a/src/main/java/gg/agit/konect/domain/chat/dto/AdminChatRoomProjection.java +++ b/src/main/java/gg/agit/konect/domain/chat/dto/AdminChatRoomProjection.java @@ -4,7 +4,7 @@ /** * 관리자용 1:1 채팅방 목록 조회를 위한 Projection DTO - * 필드 순서와 타입이 JPQL SELECT 절과 정확히 일치해야 합니다. + * 필드 순서와 타입이 조회 쿼리의 constructor projection과 정확히 일치해야 합니다. */ public record AdminChatRoomProjection( Integer roomId, diff --git a/src/main/java/gg/agit/konect/domain/chat/repository/ChatRoomQueryRepository.java b/src/main/java/gg/agit/konect/domain/chat/repository/ChatRoomQueryRepository.java new file mode 100644 index 000000000..36e3e6d73 --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/chat/repository/ChatRoomQueryRepository.java @@ -0,0 +1,98 @@ +package gg.agit.konect.domain.chat.repository; + +import static gg.agit.konect.domain.chat.model.QChatMessage.chatMessage; +import static gg.agit.konect.domain.chat.model.QChatRoom.chatRoom; + +import java.util.List; + +import org.springframework.stereotype.Repository; + +import com.querydsl.core.types.Projections; +import com.querydsl.jpa.JPAExpressions; +import com.querydsl.jpa.impl.JPAQueryFactory; + +import gg.agit.konect.domain.chat.dto.AdminChatRoomProjection; +import gg.agit.konect.domain.chat.enums.ChatType; +import gg.agit.konect.domain.chat.model.QChatMessage; +import gg.agit.konect.domain.chat.model.QChatRoomMember; +import gg.agit.konect.domain.user.enums.UserRole; +import gg.agit.konect.domain.user.model.QUser; +import lombok.RequiredArgsConstructor; + +@Repository +@RequiredArgsConstructor +public class ChatRoomQueryRepository { + + private final JPAQueryFactory jpaQueryFactory; + + public List findAdminChatRoomsOptimized( + Integer systemAdminId, + Integer viewerAdminId, + UserRole adminRole, + ChatType roomType + ) { + QChatRoomMember roomMember = new QChatRoomMember("roomMember"); + QChatRoomMember systemAdminMember = new QChatRoomMember("systemAdminMember"); + QChatRoomMember viewerAdminMember = new QChatRoomMember("viewerAdminMember"); + QUser nonAdminUser = new QUser("nonAdminUser"); + QChatMessage userReply = new QChatMessage("userReply"); + QUser userReplySender = new QUser("userReplySender"); + + return jpaQueryFactory + .select(Projections.constructor( + AdminChatRoomProjection.class, + chatRoom.id, + chatRoom.lastMessageContent, + chatRoom.lastMessageSentAt, + chatRoom.createdAt, + nonAdminUser.id, + nonAdminUser.name, + nonAdminUser.imageUrl, + chatMessage.count() + )) + .from(chatRoom) + .join(roomMember).on(roomMember.id.chatRoomId.eq(chatRoom.id)) + .join(roomMember.user, nonAdminUser) + .join(systemAdminMember).on( + systemAdminMember.id.chatRoomId.eq(chatRoom.id), + systemAdminMember.id.userId.eq(systemAdminId) + ) + .leftJoin(viewerAdminMember).on( + viewerAdminMember.id.chatRoomId.eq(chatRoom.id), + viewerAdminMember.id.userId.eq(viewerAdminId) + ) + .leftJoin(chatMessage).on( + chatMessage.chatRoom.id.eq(chatRoom.id), + chatMessage.sender.id.ne(systemAdminId), + chatMessage.createdAt.gt(systemAdminMember.lastReadAt) + ) + .where( + chatRoom.roomType.eq(roomType), + nonAdminUser.role.ne(adminRole), + nonAdminUser.deletedAt.isNull(), + // 관리자는 문의방 멤버가 아니거나 나갔어도 새 메시지가 있으면 목록에서 다시 볼 수 있다. + viewerAdminMember.leftAt.isNull() + .or(chatRoom.lastMessageSentAt.gt(viewerAdminMember.visibleMessageFrom)), + JPAExpressions + .selectOne() + .from(userReply) + .join(userReply.sender, userReplySender) + .where( + userReply.chatRoom.id.eq(chatRoom.id), + userReplySender.role.ne(adminRole) + ) + .exists() + ) + .groupBy( + chatRoom.id, + chatRoom.lastMessageContent, + chatRoom.lastMessageSentAt, + chatRoom.createdAt, + nonAdminUser.id, + nonAdminUser.name, + nonAdminUser.imageUrl + ) + .orderBy(chatRoom.lastMessageSentAt.coalesce(chatRoom.createdAt).desc()) + .fetch(); + } +} diff --git a/src/main/java/gg/agit/konect/domain/chat/repository/ChatRoomRepository.java b/src/main/java/gg/agit/konect/domain/chat/repository/ChatRoomRepository.java index f2789c553..078ad36a3 100644 --- a/src/main/java/gg/agit/konect/domain/chat/repository/ChatRoomRepository.java +++ b/src/main/java/gg/agit/konect/domain/chat/repository/ChatRoomRepository.java @@ -11,7 +11,6 @@ import org.springframework.data.repository.Repository; import org.springframework.data.repository.query.Param; -import gg.agit.konect.domain.chat.dto.AdminChatRoomProjection; import gg.agit.konect.domain.chat.model.ChatRoom; import gg.agit.konect.domain.chat.enums.ChatType; import gg.agit.konect.domain.user.enums.UserRole; @@ -171,61 +170,4 @@ List findAllSystemAdminDirectRooms( @Param("roomType") ChatType roomType ); - /** - * 관리자용 1:1 채팅방 목록을 Projection DTO로 최적화 조회 - *

- * 사용자가 응답한 채팅방만 필터링하고, 필요한 필드만 한 번에 조회합니다. - * 이 메소드는 다음과 같은 최적화를 제공합니다: - *

- */ - @Query(""" - SELECT new gg.agit.konect.domain.chat.dto.AdminChatRoomProjection( - cr.id, - cr.lastMessageContent, - cr.lastMessageSentAt, - cr.createdAt, - u.id, - u.name, - u.imageUrl, - COUNT(cm) - ) - FROM ChatRoom cr - JOIN ChatRoomMember crm ON crm.id.chatRoomId = cr.id - JOIN User u ON u.id = crm.id.userId - JOIN ChatRoomMember systemAdminCrm ON systemAdminCrm.id.chatRoomId = cr.id - AND systemAdminCrm.id.userId = :systemAdminId - LEFT JOIN ChatRoomMember viewerAdminCrm ON viewerAdminCrm.id.chatRoomId = cr.id - AND viewerAdminCrm.id.userId = :viewerAdminId - LEFT JOIN ChatMessage cm ON cm.chatRoom.id = cr.id - AND cm.sender.id <> :systemAdminId - AND cm.createdAt > systemAdminCrm.lastReadAt - WHERE cr.roomType = :roomType - AND u.role != :adminRole - AND ( - viewerAdminCrm.leftAt IS NULL - OR viewerAdminCrm.id.userId IS NULL - OR ( - viewerAdminCrm.leftAt IS NOT NULL - AND cr.lastMessageSentAt > viewerAdminCrm.visibleMessageFrom - ) - ) - AND EXISTS ( - SELECT 1 FROM ChatMessage userReply - JOIN userReply.sender userSender - WHERE userReply.chatRoom.id = cr.id - AND userSender.role != :adminRole - ) - GROUP BY cr.id, cr.lastMessageContent, cr.lastMessageSentAt, cr.createdAt, u.id, u.name, u.imageUrl - ORDER BY COALESCE(cr.lastMessageSentAt, cr.createdAt) DESC - """) - List findAdminChatRoomsOptimized( - @Param("systemAdminId") Integer systemAdminId, - @Param("viewerAdminId") Integer viewerAdminId, - @Param("adminRole") UserRole adminRole, - @Param("roomType") ChatType roomType - ); } 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..67ce16210 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 @@ -48,6 +48,7 @@ import gg.agit.konect.domain.chat.repository.ChatInviteQueryRepository; 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.repository.RoomUnreadCountProjection; import gg.agit.konect.domain.club.model.ClubMember; @@ -74,6 +75,7 @@ public class ChatService { private static final String DEFAULT_GROUP_ROOM_NAME = "그룹 채팅"; private final ChatRoomRepository chatRoomRepository; + private final ChatRoomQueryRepository chatRoomQueryRepository; private final ChatMessageRepository chatMessageRepository; private final ChatRoomMemberRepository chatRoomMemberRepository; private final NotificationMuteSettingRepository notificationMuteSettingRepository; @@ -548,7 +550,7 @@ private List getDirectChatRooms(Integer userId) { } private List getAdminDirectChatRooms(Integer adminUserId) { - List projections = chatRoomRepository.findAdminChatRoomsOptimized( + List projections = chatRoomQueryRepository.findAdminChatRoomsOptimized( SYSTEM_ADMIN_ID, adminUserId, UserRole.ADMIN, ChatType.DIRECT ); 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..c38801a99 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 @@ -49,6 +49,7 @@ import gg.agit.konect.domain.chat.repository.ChatInviteQueryRepository; 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; @@ -76,6 +77,9 @@ class ChatServiceTest extends ServiceTestSupport { @Mock private ChatRoomRepository chatRoomRepository; + @Mock + private ChatRoomQueryRepository chatRoomQueryRepository; + @Mock private ChatMessageRepository chatMessageRepository;