Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -114,4 +115,49 @@ List<ChatRoom> findAllSystemAdminDirectRooms(
@Param("systemAdminId") Integer systemAdminId,
@Param("adminRole") UserRole adminRole
);

/**
* 관리자용 1:1 채팅방 목록을 Projection DTO로 최적화 조회
* <p>
* 사용자가 응답한 채팅방만 필터링하고, 필요한 필드만 한 번에 조회합니다.
* 이 메소드는 다음과 같은 최적화를 제공합니다:
* <ul>
* <li>ChatRoom 엔티티 전체 로딩 대신 필요한 필드만 Projection</li>
* <li>읽지 않은 메시지 수를 DB에서 직접 계산 (COUNT 서브쿼리)</li>
* <li>상대방 사용자 정보를 JOIN으로 한 번에 조회</li>
* </ul>
*/
@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
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 > adminCrm.lastReadAt
WHERE cr.club IS NULL
AND u.role != :adminRole
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, u.id, u.name, u.imageUrl
ORDER BY cr.lastMessageSentAt DESC NULLS LAST, cr.id
""")
List<AdminChatRoomProjection> findAdminChatRoomsOptimized(
@Param("systemAdminId") Integer systemAdminId,
@Param("adminRole") UserRole adminRole
);
}
61 changes: 12 additions & 49 deletions src/main/java/gg/agit/konect/domain/chat/service/ChatService.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -30,6 +29,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;
Expand Down Expand Up @@ -277,59 +277,22 @@ private List<ChatRoomSummaryResponse> getDirectChatRooms(Integer userId) {
}

private List<ChatRoomSummaryResponse> getAdminDirectChatRooms() {
List<ChatRoomSummaryResponse> roomSummaries = new ArrayList<>();

List<ChatRoom> adminUserRooms = chatRoomRepository.findAllSystemAdminDirectRooms(
List<AdminChatRoomProjection> projections = chatRoomRepository.findAdminChatRoomsOptimized(
SYSTEM_ADMIN_ID, UserRole.ADMIN
);
List<Integer> roomIds = extractChatRoomIds(adminUserRooms);
Map<Integer, List<MemberInfo>> roomMemberInfoMap = getRoomMemberInfoMap(adminUserRooms);
Map<Integer, Integer> adminUnreadCountMap = getAdminUnreadCountMap(roomIds);
Set<Integer> repliedRoomIds = roomIds.isEmpty()
? Set.of()
: new HashSet<>(chatMessageRepository.findRoomIdsWithUserReplyByRoomIds(roomIds, UserRole.ADMIN));

List<Integer> allUserIds = roomMemberInfoMap.values().stream()
.flatMap(List::stream)
.map(MemberInfo::userId)
.distinct()
.toList();

Map<Integer, User> userMap = allUserIds.isEmpty()
? Map.of()
: userRepository.findAllByIdIn(allUserIds).stream()
.collect(Collectors.toMap(User::getId, user -> user));

for (ChatRoom chatRoom : adminUserRooms) {
List<MemberInfo> 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(
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
Loading