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
Expand Up @@ -2,9 +2,11 @@

import static gg.agit.konect.global.code.ApiResponseCode.NOT_FOUND_CHAT_ROOM;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;

import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.query.Param;
Expand All @@ -19,6 +21,30 @@ public interface ChatRoomRepository extends Repository<ChatRoom, Integer> {

ChatRoom save(ChatRoom chatRoom);

@Modifying(flushAutomatically = true)
@Query("""
UPDATE ChatRoom cr
SET cr.lastMessageContent = :content,
cr.lastMessageSentAt = :sentAt
WHERE cr.id = :roomId
AND NOT EXISTS (
SELECT 1
FROM ChatMessage cm
WHERE cm.chatRoom.id = :roomId
AND cm.id <> :messageId
AND (
cm.createdAt > :sentAt
OR (cm.createdAt = :sentAt AND cm.id > :messageId)
)
)
""")
int updateLastMessageIfLatest(
@Param("roomId") Integer roomId,
@Param("messageId") Integer messageId,
@Param("content") String content,
@Param("sentAt") LocalDateTime sentAt
);

@Query("""
SELECT DISTINCT cr
FROM ChatRoom cr
Expand Down
20 changes: 17 additions & 3 deletions src/main/java/gg/agit/konect/domain/chat/service/ChatService.java
Original file line number Diff line number Diff line change
Expand Up @@ -746,7 +746,7 @@ private ChatMessageDetailResponse sendDirectMessage(
senderMember.restoreDirectRoom();
}

chatRoom.updateLastMessage(chatMessage.getContent(), chatMessage.getCreatedAt());
syncLastMessage(chatRoom, chatMessage);
members.stream()
.filter(member -> !member.getUserId().equals(userId))
.filter(ChatRoomMember::hasLeft)
Expand Down Expand Up @@ -828,7 +828,7 @@ private ChatMessageDetailResponse sendClubMessageByRoomId(Integer roomId, Intege
ensureRoomMember(room, sender, member.getCreatedAt());

ChatMessage message = chatMessageRepository.save(ChatMessage.of(room, sender, content));
room.updateLastMessage(message.getContent(), message.getCreatedAt());
syncLastMessage(room, message);
updateClubMessageLastReadAt(roomId, userId, message.getCreatedAt());

List<ChatRoomMember> members = chatRoomMemberRepository.findByChatRoomId(roomId);
Expand Down Expand Up @@ -909,7 +909,7 @@ private ChatMessageDetailResponse sendGroupMessageByRoomId(Integer roomId, Integ
}

ChatMessage message = chatMessageRepository.save(ChatMessage.of(room, sender, content));
room.updateLastMessage(message.getContent(), message.getCreatedAt());
syncLastMessage(room, message);
updateLastReadAt(roomId, userId, message.getCreatedAt());

List<ChatRoomMember> members = chatRoomMemberRepository.findByChatRoomId(roomId);
Expand Down Expand Up @@ -1210,6 +1210,20 @@ private void publishAdminChatEventIfNeeded(boolean isSystemAdminRoom, User sende
}
}

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());
}
}
Comment thread
dh2906 marked this conversation as resolved.

private ChatRoomMember getRoomMember(Integer roomId, Integer userId) {
return chatRoomMemberRepository.findByChatRoomIdAndUserId(roomId, userId)
.orElseThrow(() -> CustomException.of(FORBIDDEN_CHAT_ROOM_ACCESS));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,12 +131,24 @@ private void sendWelcomeMessage(User newUser) {
ChatMessage chatMessage = chatMessageRepository.save(
ChatMessage.of(chatRoom, operator, DEFAULT_WELCOME_MESSAGE)
);
chatRoom.updateLastMessage(chatMessage.getContent(), chatMessage.getCreatedAt());
syncLastMessage(chatRoom, chatMessage);
} catch (Exception e) {
log.warn("회원가입 환영 메시지 전송 실패. userId={}", newUser.getId(), e);
}
}

private void syncLastMessage(ChatRoom chatRoom, ChatMessage chatMessage) {
int updated = chatRoomRepository.updateLastMessageIfLatest(
chatRoom.getId(),
chatMessage.getId(),
chatMessage.getContent(),
chatMessage.getCreatedAt()
);
if (updated > 0) {
chatRoom.updateLastMessage(chatMessage.getContent(), chatMessage.getCreatedAt());
}
}

private UnRegisteredUser findUnregisteredUser(String email, String providerId, Provider provider) {
if (StringUtils.hasText(providerId)) {
if (unRegisteredUserRepository.existsByProviderIdAndProvider(providerId, provider)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
-- 채팅방 목록/관리 쿼리는 chat_room.last_message_*를 직접 사용하므로
-- 기존 메시지 이력이 있는 방도 최신 메시지 메타데이터를 다시 맞춰 준다.
-- MAX(created_at)으로 최신 메시지를 고르고, 같은 시각이면 id로 타이브레이크한다.
UPDATE chat_room cr
LEFT JOIN (
SELECT
cm1.chat_room_id,
cm1.content,
cm1.created_at
FROM chat_message cm1
JOIN (
SELECT chat_room_id, MAX(created_at) AS max_created_at
FROM chat_message
GROUP BY chat_room_id
) cm2 ON cm2.chat_room_id = cm1.chat_room_id AND cm2.max_created_at = cm1.created_at
WHERE cm1.id = (
SELECT MAX(id)
FROM chat_message
WHERE chat_room_id = cm1.chat_room_id
AND created_at = cm2.max_created_at
)
) latest_msg ON latest_msg.chat_room_id = cr.id
SET cr.last_message_content = latest_msg.content,
cr.last_message_sent_at = latest_msg.created_at;
Original file line number Diff line number Diff line change
Expand Up @@ -718,6 +718,35 @@ void sendMessageSuccess() throws Exception {
.containsExactly("안녕하세요");
}

@Test
@DisplayName("메시지를 전송하면 chat_room last message 메타데이터도 함께 갱신된다")
@Sql(
statements = CHAT_TEST_DATA_CLEANUP_SQL,
config = @SqlConfig(transactionMode = SqlConfig.TransactionMode.ISOLATED),
executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD
)
void sendMessageUpdatesChatRoomLastMessageColumns() throws Exception {
// given
ChatRoom chatRoom = createDirectChatRoom(normalUser, targetUser);
mockLoginUser(normalUser.getId());

// when
performPost("/chats/rooms/" + chatRoom.getId() + "/messages", new ChatMessageSendRequest("메타데이터 확인"))
.andExpect(status().isOk());

// then
TestTransaction.flagForCommit();
TestTransaction.end();

transactionTemplate.execute(status -> {
clearPersistenceContext();
ChatRoom updatedRoom = chatRoomRepository.findById(chatRoom.getId()).orElseThrow();
assertThat(updatedRoom.getLastMessageContent()).isEqualTo("메타데이터 확인");
assertThat(updatedRoom.getLastMessageSentAt()).isNotNull();
return null;
});
}

@Test
@DisplayName("관리자가 문의방에 답변하면 실제 문의 사용자에게 알림을 보낸다")
void adminReplySendsNotificationToInquiryUser() throws Exception {
Expand Down Expand Up @@ -910,6 +939,15 @@ void leaveDirectChatRoomAndShowOnlyNewMessages() throws Exception {
performPost("/chats/rooms/" + chatRoom.getId() + "/messages", new ChatMessageSendRequest("다시 안녕"))
.andExpect(status().isOk());

transactionTemplate.execute(status -> {
clearPersistenceContext();
ChatRoomMember restoredMember = chatRoomMemberRepository
.findByChatRoomIdAndUserId(chatRoom.getId(), normalUser.getId())
.orElseThrow();
assertThat(restoredMember.hasLeft()).isFalse();
return null;
});

mockLoginUser(normalUser.getId());
performGet("/chats/rooms")
.andExpect(status().isOk())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -825,6 +825,9 @@ void sendMessageInDirectRoomSavesMessageAndSendsNotification() {
given(chatRoomMemberRepository.findByChatRoomId(directRoom.getId()))
.willReturn(List.of(senderMember, receiverMember));
given(chatMessageRepository.save(any(ChatMessage.class))).willReturn(savedMessage);
given(chatRoomRepository.updateLastMessageIfLatest(
directRoom.getId(), savedMessage.getId(), savedMessage.getContent(), savedMessage.getCreatedAt()
)).willReturn(1);
given(chatRoomMemberRepository.updateLastReadAtIfOlder(eq(directRoom.getId()), eq(senderId),
any(LocalDateTime.class)))
.willReturn(1);
Expand All @@ -843,6 +846,44 @@ void sendMessageInDirectRoomSavesMessageAndSendsNotification() {
eq("hello"));
}

@Test
@DisplayName("sendMessage는 이미 더 최신 메시지가 있으면 room 마지막 메시지 메타데이터를 덮어쓰지 않는다")
void sendMessageDoesNotOverwriteRoomMetadataWhenNewerMessageAlreadyExists() {
Integer senderId = 10;
Integer receiverId = 20;
User sender = createUser(senderId, "보낸이", UserRole.USER);
User receiver = createUser(receiverId, "받는이", UserRole.USER);
ChatRoom directRoom = createRoom(1, ChatType.DIRECT, LocalDateTime.of(2026, 4, 11, 10, 0));
ChatRoomMember senderMember = createRoomMember(directRoom, sender, false,
LocalDateTime.of(2026, 4, 11, 10, 0));
ChatRoomMember receiverMember = createRoomMember(directRoom, receiver, false,
LocalDateTime.of(2026, 4, 11, 10, 0));
ChatMessage savedMessage = createMessage(100, directRoom, sender, "older",
LocalDateTime.of(2026, 4, 11, 10, 1));

ReflectionTestUtils.setField(directRoom, "lastMessageContent", "newer");
ReflectionTestUtils.setField(directRoom, "lastMessageSentAt", LocalDateTime.of(2026, 4, 11, 10, 2));

given(chatRoomRepository.findById(directRoom.getId())).willReturn(Optional.of(directRoom));
given(userRepository.getById(senderId)).willReturn(sender);
given(chatRoomMemberRepository.findByChatRoomIdAndUserId(directRoom.getId(), senderId))
.willReturn(Optional.of(senderMember));
given(chatRoomMemberRepository.findByChatRoomId(directRoom.getId()))
.willReturn(List.of(senderMember, receiverMember));
given(chatMessageRepository.save(any(ChatMessage.class))).willReturn(savedMessage);
given(chatRoomRepository.updateLastMessageIfLatest(
directRoom.getId(), savedMessage.getId(), savedMessage.getContent(), savedMessage.getCreatedAt()
)).willReturn(0);
given(chatRoomMemberRepository.updateLastReadAtIfOlder(eq(directRoom.getId()), eq(senderId),
any(LocalDateTime.class)))
.willReturn(1);

chatService.sendMessage(senderId, directRoom.getId(), new ChatMessageSendRequest("older"));

assertThat(directRoom.getLastMessageContent()).isEqualTo("newer");
assertThat(directRoom.getLastMessageSentAt()).isEqualTo(LocalDateTime.of(2026, 4, 11, 10, 2));
}

@Test
@DisplayName("sendMessage는 group room에서 메시지를 저장하고 그룹 알림을 보낸다")
void sendMessageInGroupRoomSavesMessageAndSendsGroupNotification() {
Expand All @@ -860,6 +901,9 @@ void sendMessageInGroupRoomSavesMessageAndSendsGroupNotification() {
given(chatRoomMemberRepository.findByChatRoomIdAndUserId(groupRoom.getId(), senderId))
.willReturn(Optional.of(senderMember));
given(chatMessageRepository.save(any(ChatMessage.class))).willReturn(savedMessage);
given(chatRoomRepository.updateLastMessageIfLatest(
groupRoom.getId(), savedMessage.getId(), savedMessage.getContent(), savedMessage.getCreatedAt()
)).willReturn(1);
given(chatRoomMemberRepository.updateLastReadAtIfOlder(eq(groupRoom.getId()), eq(senderId),
any(LocalDateTime.class)))
.willReturn(1);
Expand Down Expand Up @@ -926,6 +970,9 @@ void sendMessageInClubRoomSavesMessageAndSendsGroupNotification() {
given(chatRoomMemberRepository.findByChatRoomIdAndUserId(clubRoom.getId(), senderId))
.willReturn(Optional.of(senderRoomMember));
given(chatMessageRepository.save(any(ChatMessage.class))).willReturn(savedMessage);
given(chatRoomRepository.updateLastMessageIfLatest(
clubRoom.getId(), savedMessage.getId(), savedMessage.getContent(), savedMessage.getCreatedAt()
)).willReturn(1);
given(chatRoomMemberRepository.updateLastReadAtIfOlder(eq(clubRoom.getId()), eq(senderId),
any(LocalDateTime.class)))
.willReturn(1);
Expand Down Expand Up @@ -972,6 +1019,9 @@ void sendMessageAdminBypassesMembershipInSystemAdminRoom() {
given(chatRoomMemberRepository.findByChatRoomId(systemAdminRoom.getId()))
.willReturn(List.of(systemAdminMember, targetMember));
given(chatMessageRepository.save(any(ChatMessage.class))).willReturn(savedMessage);
given(chatRoomRepository.updateLastMessageIfLatest(
systemAdminRoom.getId(), savedMessage.getId(), savedMessage.getContent(), savedMessage.getCreatedAt()
)).willReturn(1);

// when
ChatMessageDetailResponse response = chatService.sendMessage(adminId, systemAdminRoom.getId(),
Expand Down
Loading