From 13ff14054f82aef249ea9030cd299b0aed0aff77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Sun, 22 Mar 2026 17:21:14 +0900 Subject: [PATCH 1/4] =?UTF-8?q?refactor:=20=EA=B4=80=EB=A6=AC=EC=9E=90=201?= =?UTF-8?q?:1=20=EC=B1=84=ED=8C=85=EB=B0=A9=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EC=84=B1=EB=8A=A5=20=EC=B5=9C=EC=A0=81=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/dto/AdminChatRoomProjection.java | 18 ++++++ .../chat/repository/ChatRoomRepository.java | 53 ++++++++++++++++ .../domain/chat/service/ChatService.java | 60 ++++--------------- 3 files changed, 83 insertions(+), 48 deletions(-) create mode 100644 src/main/java/gg/agit/konect/domain/chat/dto/AdminChatRoomProjection.java 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 new file mode 100644 index 000000000..d65913962 --- /dev/null +++ b/src/main/java/gg/agit/konect/domain/chat/dto/AdminChatRoomProjection.java @@ -0,0 +1,18 @@ +package gg.agit.konect.domain.chat.dto; + +import java.time.LocalDateTime; + +/** + * 관리자용 1:1 채팅방 목록 조회를 위한 Projection DTO + * 필드 순서와 타입이 JPQL SELECT 절과 정확히 일치해야 합니다. + */ +public record AdminChatRoomProjection( + Integer roomId, + String lastMessage, + LocalDateTime lastSentAt, + Integer nonAdminUserId, + String nonAdminUserName, + String nonAdminImageUrl, + Long unreadCount +) { +} 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 c1a308912..d464fc963 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 @@ -7,6 +7,7 @@ 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.user.enums.UserRole; @@ -114,4 +115,56 @@ List findAllSystemAdminDirectRooms( @Param("systemAdminId") Integer systemAdminId, @Param("adminRole") UserRole adminRole ); + + /** + * 관리자용 1:1 채팅방 목록을 Projection DTO로 최적화 조회 + *

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

+ */ + @Query(""" + SELECT new gg.agit.konect.domain.chat.dto.AdminChatRoomProjection( + cr.id, + cr.lastMessageContent, + cr.lastMessageSentAt, + 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 + LEFT JOIN ChatMessage cm ON cm.chatRoom.id = cr.id + AND cm.sender.id <> :systemAdminId + AND cm.createdAt > ( + SELECT MAX(crm2.lastReadAt) + FROM ChatRoomMember crm2 + WHERE crm2.id.chatRoomId = cr.id + AND crm2.id.userId = :systemAdminId + ) + WHERE cr.club IS NULL + AND EXISTS ( + SELECT 1 FROM ChatRoomMember systemAdminCrm + WHERE systemAdminCrm.id.chatRoomId = cr.id + AND systemAdminCrm.id.userId = :systemAdminId + ) + AND u.role != :adminRole + AND EXISTS ( + SELECT 1 FROM ChatMessage replyMsg + WHERE replyMsg.chatRoom.id = cr.id + AND replyMsg.sender.id = :systemAdminId + ) + GROUP BY cr.id, cr.lastMessageContent, cr.lastMessageSentAt, u.id, u.name, u.imageUrl + ORDER BY cr.lastMessageSentAt DESC NULLS LAST, cr.id + """) + List findAdminChatRoomsOptimized( + @Param("systemAdminId") Integer systemAdminId, + @Param("adminRole") UserRole adminRole + ); } 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 21f9b463b..b9c4e37b6 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 @@ -30,6 +30,7 @@ import gg.agit.konect.domain.chat.dto.ChatRoomResponse; import gg.agit.konect.domain.chat.dto.ChatRoomSummaryResponse; import gg.agit.konect.domain.chat.dto.ChatRoomsSummaryResponse; +import gg.agit.konect.domain.chat.dto.AdminChatRoomProjection; import gg.agit.konect.domain.chat.dto.UnreadMessageCount; import gg.agit.konect.domain.chat.enums.ChatType; import gg.agit.konect.domain.chat.event.AdminChatReceivedEvent; @@ -277,59 +278,22 @@ private List getDirectChatRooms(Integer userId) { } private List getAdminDirectChatRooms() { - List roomSummaries = new ArrayList<>(); - - List adminUserRooms = chatRoomRepository.findAllSystemAdminDirectRooms( + List projections = chatRoomRepository.findAdminChatRoomsOptimized( SYSTEM_ADMIN_ID, UserRole.ADMIN ); - List roomIds = extractChatRoomIds(adminUserRooms); - Map> roomMemberInfoMap = getRoomMemberInfoMap(adminUserRooms); - Map adminUnreadCountMap = getAdminUnreadCountMap(roomIds); - Set repliedRoomIds = roomIds.isEmpty() - ? Set.of() - : new HashSet<>(chatMessageRepository.findRoomIdsWithUserReplyByRoomIds(roomIds, UserRole.ADMIN)); - - List allUserIds = roomMemberInfoMap.values().stream() - .flatMap(List::stream) - .map(MemberInfo::userId) - .distinct() - .toList(); - - Map userMap = allUserIds.isEmpty() - ? Map.of() - : userRepository.findAllByIdIn(allUserIds).stream() - .collect(Collectors.toMap(User::getId, user -> user)); - for (ChatRoom chatRoom : adminUserRooms) { - List memberInfos = roomMemberInfoMap.getOrDefault(chatRoom.getId(), List.of()); - User nonAdminUser = findNonAdminUserFromMemberInfo(memberInfos, userMap); - if (nonAdminUser == null) { - continue; - } - if (!repliedRoomIds.contains(chatRoom.getId())) { - continue; - } - - roomSummaries.add(new ChatRoomSummaryResponse( - chatRoom.getId(), + return projections.stream() + .map(projection -> new ChatRoomSummaryResponse( + projection.roomId(), ChatType.DIRECT, - nonAdminUser.getName(), - nonAdminUser.getImageUrl(), - chatRoom.getLastMessageContent(), - chatRoom.getLastMessageSentAt(), - adminUnreadCountMap.getOrDefault(chatRoom.getId(), 0), + projection.nonAdminUserName(), + projection.nonAdminImageUrl(), + projection.lastMessage(), + projection.lastSentAt(), + projection.unreadCount().intValue(), false - )); - } - - roomSummaries.sort(Comparator - .comparing( - ChatRoomSummaryResponse::lastSentAt, - Comparator.nullsLast(Comparator.reverseOrder()) - ) - .thenComparing(ChatRoomSummaryResponse::roomId)); - - return roomSummaries; + )) + .toList(); } private ChatMessagePageResponse getDirectChatRoomMessages( From d6df13aa695faa22b14c9b5fa74555b55054ae84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Sun, 22 Mar 2026 18:18:05 +0900 Subject: [PATCH 2/4] =?UTF-8?q?refactor:=20=EC=84=9C=EB=B8=8C=EC=BF=BC?= =?UTF-8?q?=EB=A6=AC=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chat/repository/ChatRoomRepository.java | 21 +++++++------------ 1 file changed, 7 insertions(+), 14 deletions(-) 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 d464fc963..6cc9f7f02 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 @@ -140,25 +140,18 @@ List findAllSystemAdminDirectRooms( FROM ChatRoom cr JOIN ChatRoomMember crm ON crm.id.chatRoomId = cr.id JOIN User u ON u.id = crm.id.userId + JOIN ChatRoomMember adminCrm ON adminCrm.id.chatRoomId = cr.id + AND adminCrm.id.userId = :systemAdminId LEFT JOIN ChatMessage cm ON cm.chatRoom.id = cr.id AND cm.sender.id <> :systemAdminId - AND cm.createdAt > ( - SELECT MAX(crm2.lastReadAt) - FROM ChatRoomMember crm2 - WHERE crm2.id.chatRoomId = cr.id - AND crm2.id.userId = :systemAdminId - ) + AND cm.createdAt > adminCrm.lastReadAt WHERE cr.club IS NULL - AND EXISTS ( - SELECT 1 FROM ChatRoomMember systemAdminCrm - WHERE systemAdminCrm.id.chatRoomId = cr.id - AND systemAdminCrm.id.userId = :systemAdminId - ) AND u.role != :adminRole AND EXISTS ( - SELECT 1 FROM ChatMessage replyMsg - WHERE replyMsg.chatRoom.id = cr.id - AND replyMsg.sender.id = :systemAdminId + 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, u.id, u.name, u.imageUrl ORDER BY cr.lastMessageSentAt DESC NULLS LAST, cr.id From f379297a4883808d743db2256418f015e68346c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Sun, 22 Mar 2026 18:39:05 +0900 Subject: [PATCH 3/4] =?UTF-8?q?feat:=20=EC=9D=B8=EB=8D=B1=EC=8A=A4=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../db/migration/V57__add_chat_admin_query_indexes.sql | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 src/main/resources/db/migration/V57__add_chat_admin_query_indexes.sql diff --git a/src/main/resources/db/migration/V57__add_chat_admin_query_indexes.sql b/src/main/resources/db/migration/V57__add_chat_admin_query_indexes.sql new file mode 100644 index 000000000..ae235662c --- /dev/null +++ b/src/main/resources/db/migration/V57__add_chat_admin_query_indexes.sql @@ -0,0 +1,10 @@ +-- 관리자 1:1 채팅방 조회 쿼리 최적화를 위한 인덱스 추가 +-- findAdminChatRoomsOptimized() 메소드 성능 개선 + +-- 1. 관리자 응답 여부 확인 및 unread count 계산용 +CREATE INDEX idx_chat_message_room_sender + ON chat_message (chat_room_id, sender_id); + +-- 2. last_message_sent_at 정렬 최적화 +CREATE INDEX idx_chat_room_last_message + ON chat_room (club_id, last_message_sent_at DESC); From 5e0c6c2aaf476fd100b21a30bd5ad45e0ba0ccf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EB=8F=99=ED=9B=88?= <2dh2@naver.com> Date: Sun, 22 Mar 2026 18:39:14 +0900 Subject: [PATCH 4/4] =?UTF-8?q?chore:=20=EC=BD=94=EB=93=9C=20=ED=8F=AC?= =?UTF-8?q?=EB=A7=B7=ED=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/gg/agit/konect/domain/chat/service/ChatService.java | 1 - 1 file changed, 1 deletion(-) 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 b9c4e37b6..da1b7cbc3 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.ArrayList; import java.util.Comparator; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects;