Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Api: ✏️ Append LastMessage Field in ChatRoomDetail Response #196

Merged
merged 6 commits into from
Nov 14, 2024
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 @@ -5,6 +5,7 @@
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import io.swagger.v3.oas.annotations.media.Schema;
import kr.co.pennyway.domain.domains.chatroom.domain.ChatRoom;
import kr.co.pennyway.domain.domains.chatroom.dto.ChatRoomDetail;
import lombok.Builder;

import java.time.LocalDateTime;
Expand All @@ -13,6 +14,23 @@
import java.util.Set;

public final class ChatRoomRes {
/**
* 채팅방 정보를 담기 위한 DTO
*
* @param chatRoom {@link ChatRoomDetail} : 채팅방 정보
* @param unreadMessageCount long : 읽지 않은 메시지 수
* @param lastMessage {@link ChatRes.ChatDetail} : 가장 최근 메시지. 없을 경우 null
*/
public record Info(
ChatRoomDetail chatRoom,
long unreadMessageCount,
ChatRes.ChatDetail lastMessage
) {
public static Info of(ChatRoomDetail chatRoom, long unreadMessageCount, ChatRes.ChatDetail recentMessage) {
return new Info(chatRoom, unreadMessageCount, recentMessage);
}
}

@Schema(description = "채팅방 상세 정보")
public record Detail(
@Schema(description = "채팅방 ID", type = "long")
Expand All @@ -33,10 +51,12 @@ public record Detail(
@JsonSerialize(using = LocalDateTimeSerializer.class)
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
LocalDateTime createdAt,
@Schema(description = "마지막 메시지 정보. 없을 경우 null이 반환된다.")
ChatRes.ChatDetail lastMessage,
@Schema(description = "읽지 않은 메시지 수. 100 이상의 값을 가지면, 100으로 표시된다.")
long unreadMessageCount
) {
public Detail(Long id, String title, String description, String backgroundImageUrl, boolean isPrivate, boolean isAdmin, int participantCount, LocalDateTime createdAt, long unreadMessageCount) {
public Detail(Long id, String title, String description, String backgroundImageUrl, boolean isPrivate, boolean isAdmin, int participantCount, LocalDateTime createdAt, ChatRes.ChatDetail lastMessage, long unreadMessageCount) {
this.id = id;
this.title = title;
this.description = Objects.toString(description, "");
Expand All @@ -45,10 +65,11 @@ public Detail(Long id, String title, String description, String backgroundImageU
this.isAdmin = isAdmin;
this.participantCount = participantCount;
this.createdAt = createdAt;
this.lastMessage = lastMessage;
this.unreadMessageCount = (unreadMessageCount > 100) ? 100 : unreadMessageCount;
}

public static Detail of(ChatRoom chatRoom, boolean isAdmin, int participantCount, long unreadMessageCount) {
public static Detail of(ChatRoom chatRoom, ChatRes.ChatDetail lastMessage, boolean isAdmin, int participantCount, long unreadMessageCount) {
return new Detail(
chatRoom.getId(),
chatRoom.getTitle(),
Expand All @@ -58,9 +79,25 @@ public static Detail of(ChatRoom chatRoom, boolean isAdmin, int participantCount
isAdmin,
participantCount,
chatRoom.getCreatedAt(),
lastMessage,
unreadMessageCount
);
}

public static Detail from(ChatRoomRes.Info info) {
return new Detail(
info.chatRoom().id(),
info.chatRoom().title(),
info.chatRoom().description(),
info.chatRoom().backgroundImageUrl(),
info.chatRoom().password() != null,
info.chatRoom().isAdmin(),
info.chatRoom().participantCount(),
info.chatRoom().createdAt(),
info.lastMessage(),
info.unreadMessageCount()
);
}
}

@Schema(description = "채팅방 요약 정보")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,17 @@

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

@Mapper
public final class ChatRoomMapper {
/**
* 채팅방 상세 정보를 SliceResponseTemplate 형태로 변환한다.
* 해당 메서드는 언제나 채팅방 검색 응답으로 사용되며, 마지막 메시지 정보는 null로 설정된다.
*
* @param details
* @param pageable
* @return
*/
public static SliceResponseTemplate<ChatRoomRes.Detail> toChatRoomResDetails(Slice<ChatRoomDetail> details, Pageable pageable) {
List<ChatRoomRes.Detail> contents = new ArrayList<>();
for (ChatRoomDetail detail : details.getContent()) {
Expand All @@ -31,6 +38,7 @@ public static SliceResponseTemplate<ChatRoomRes.Detail> toChatRoomResDetails(Sli
detail.isAdmin(),
detail.participantCount(),
detail.createdAt(),
null,
0
)
);
Expand All @@ -39,31 +47,31 @@ public static SliceResponseTemplate<ChatRoomRes.Detail> toChatRoomResDetails(Sli
return SliceResponseTemplate.of(contents, pageable, contents.size(), details.hasNext());
}

public static List<ChatRoomRes.Detail> toChatRoomResDetails(Map<ChatRoomDetail, Long> details) {
public static List<ChatRoomRes.Detail> toChatRoomResDetails(List<ChatRoomRes.Info> details) {
List<ChatRoomRes.Detail> responses = new ArrayList<>();

for (Map.Entry<ChatRoomDetail, Long> entry : details.entrySet()) {
ChatRoomDetail detail = entry.getKey();
for (ChatRoomRes.Info info : details) {
responses.add(
new ChatRoomRes.Detail(
detail.id(),
detail.title(),
detail.description(),
detail.backgroundImageUrl(),
detail.password() != null,
detail.isAdmin(),
detail.participantCount(),
detail.createdAt(),
entry.getValue()
info.chatRoom().id(),
info.chatRoom().title(),
info.chatRoom().description(),
info.chatRoom().backgroundImageUrl(),
info.chatRoom().password() != null,
info.chatRoom().isAdmin(),
info.chatRoom().participantCount(),
info.chatRoom().createdAt(),
info.lastMessage(),
info.unreadMessageCount()
)
);
}

return responses;
}

public static ChatRoomRes.Detail toChatRoomResDetail(ChatRoom chatRoom, boolean isAdmin, int participantCount, long unreadMessageCount) {
return ChatRoomRes.Detail.of(chatRoom, isAdmin, participantCount, unreadMessageCount);
public static ChatRoomRes.Detail toChatRoomResDetail(ChatRoom chatRoom, ChatRes.ChatDetail lastMessage, boolean isAdmin, int participantCount, long unreadMessageCount) {
return ChatRoomRes.Detail.of(chatRoom, lastMessage, isAdmin, participantCount, unreadMessageCount);
}

public static ChatRoomRes.RoomWithParticipants toChatRoomResRoomWithParticipants(ChatMemberResult.Detail myInfo, List<ChatMemberResult.Detail> recentParticipants, List<ChatMemberResult.Summary> otherParticipants, List<ChatMessage> chatMessages) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package kr.co.pennyway.api.apis.chat.service;

import kr.co.pennyway.api.apis.chat.dto.ChatRes;
import kr.co.pennyway.api.apis.chat.dto.ChatRoomRes;
import kr.co.pennyway.domain.common.redis.message.domain.ChatMessage;
import kr.co.pennyway.domain.common.redis.message.service.ChatMessageService;
import kr.co.pennyway.domain.domains.chatroom.dto.ChatRoomDetail;
import kr.co.pennyway.domain.domains.chatroom.service.ChatRoomService;
Expand All @@ -11,9 +14,8 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.HashMap;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

@Slf4j
@Service
Expand All @@ -26,17 +28,19 @@ public class ChatRoomSearchService {
/**
* 사용자 ID가 속한 채팅방 목록을 조회한다.
*
* @return 채팅방 목록 (채팅방 정보, 읽지 않은 메시지 수)
* @return 채팅방 목록. {@link ChatRoomRes.Info} 리스트 형태로 반환
*/
@Transactional(readOnly = true)
public Map<ChatRoomDetail, Long> readChatRooms(Long userId) {
public List<ChatRoomRes.Info> readChatRooms(Long userId) {
List<ChatRoomDetail> chatRooms = chatRoomService.readChatRoomsByUserId(userId);
Map<ChatRoomDetail, Long> result = new HashMap<>();
List<ChatRoomRes.Info> result = new ArrayList<>();

for (ChatRoomDetail chatRoom : chatRooms) {
Long lastReadMessageId = chatMessageStatusService.readLastReadMessageId(userId, chatRoom.id());
ChatMessage lastMessage = chatMessageService.readRecentMessages(chatRoom.id(), 1).stream().findFirst().orElse(null);
Long unreadCount = chatMessageService.countUnreadMessages(chatRoom.id(), lastReadMessageId);
result.put(chatRoom, unreadCount);

result.add(ChatRoomRes.Info.of(chatRoom, unreadCount, lastMessage == null ? null : ChatRes.ChatDetail.from(lastMessage)));
}

return result;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public class ChatMemberUseCase {
public ChatRoomRes.Detail joinChatRoom(Long userId, Long chatRoomId, Integer password) {
Triple<ChatRoom, Integer, Long> chatRoom = chatMemberJoinService.execute(userId, chatRoomId, password);

return ChatRoomMapper.toChatRoomResDetail(chatRoom.getLeft(), false, chatRoom.getMiddle(), chatRoom.getRight());
return ChatRoomMapper.toChatRoomResDetail(chatRoom.getLeft(), null, false, chatRoom.getMiddle(), chatRoom.getRight());
}

public List<ChatMemberRes.MemberDetail> readChatMembers(Long chatRoomId, Set<Long> chatMemberIds) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
import org.springframework.data.domain.Slice;

import java.util.List;
import java.util.Map;
import java.util.Set;

@UseCase
Expand All @@ -31,11 +30,11 @@ public class ChatRoomUseCase {
public ChatRoomRes.Detail createChatRoom(ChatRoomReq.Create request, Long userId) {
ChatRoom chatRoom = chatRoomSaveService.createChatRoom(request, userId);

return ChatRoomMapper.toChatRoomResDetail(chatRoom, true, 1, 0);
return ChatRoomMapper.toChatRoomResDetail(chatRoom, null, true, 1, 0);
}

public List<ChatRoomRes.Detail> getChatRooms(Long userId) {
Map<ChatRoomDetail, Long> chatRooms = chatRoomSearchService.readChatRooms(userId);
List<ChatRoomRes.Info> chatRooms = chatRoomSearchService.readChatRooms(userId);

return ChatRoomMapper.toChatRoomResDetails(chatRooms);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ void createChatRoomSuccess() throws Exception {
// given
ChatRoom fixture = ChatRoomFixture.PRIVATE_CHAT_ROOM.toEntity(1L);
ChatRoomReq.Create request = ChatRoomFixture.PRIVATE_CHAT_ROOM.toCreateRequest();
given(chatRoomUseCase.createChatRoom(request, 1L)).willReturn(ChatRoomRes.Detail.of(fixture, true, 1, 10));
given(chatRoomUseCase.createChatRoom(request, 1L)).willReturn(ChatRoomRes.Detail.of(fixture, null, true, 1, 10));

// when
ResultActions result = performPostChatRoom(request);
Expand All @@ -70,7 +70,7 @@ void createChatRoomSuccessWithNullBackgroundImageUrl() throws Exception {
ChatRoom fixture = ChatRoomFixture.PUBLIC_CHAT_ROOM.toEntity(1L);
ChatRoomReq.Create request = ChatRoomFixture.PUBLIC_CHAT_ROOM.toCreateRequest();

given(chatRoomUseCase.createChatRoom(request, 1L)).willReturn(ChatRoomRes.Detail.of(fixture, true, 1, 10));
given(chatRoomUseCase.createChatRoom(request, 1L)).willReturn(ChatRoomRes.Detail.of(fixture, null, true, 1, 10));

// when
ResultActions result = performPostChatRoom(request);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
package kr.co.pennyway.api.apis.chat.service;

import kr.co.pennyway.api.apis.chat.dto.ChatRoomRes;
import kr.co.pennyway.domain.common.redis.message.domain.ChatMessage;
import kr.co.pennyway.domain.common.redis.message.domain.ChatMessageBuilder;
import kr.co.pennyway.domain.common.redis.message.service.ChatMessageService;
import kr.co.pennyway.domain.common.redis.message.type.MessageCategoryType;
import kr.co.pennyway.domain.common.redis.message.type.MessageContentType;
import kr.co.pennyway.domain.domains.chatroom.dto.ChatRoomDetail;
import kr.co.pennyway.domain.domains.chatroom.service.ChatRoomService;
import kr.co.pennyway.domain.domains.chatstatus.service.ChatMessageStatusService;
Expand All @@ -15,11 +20,12 @@
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
class ChatRoomSearchServiceTest {
Expand All @@ -34,7 +40,7 @@ class ChatRoomSearchServiceTest {
private ChatMessageService chatMessageService;

@Test
@DisplayName("사용자의 채팅방 목록과 각 방의 읽지 않은 메시지 수를 정상적으로 조회한다")
@DisplayName("사용자의 채팅방 목록과 각 방의 읽지 않은 메시지 수, 마지막 메시지를 정상적으로 조회한다")
void successReadChatRooms() {
// given
Long userId = 1L;
Expand All @@ -46,21 +52,29 @@ void successReadChatRooms() {
given(chatRoomService.readChatRoomsByUserId(userId)).willReturn(chatRooms);

// room1: 마지막으로 읽은 메시지 ID 10, 읽지 않은 메시지 5개
ChatMessage firstRoomLastMessage = ChatMessageBuilder.builder().chatRoomId(2L).chatId(1L).content("Hello").contentType(MessageContentType.TEXT).categoryType(MessageCategoryType.NORMAL).sender(userId).build();

given(chatMessageStatusService.readLastReadMessageId(userId, 1L)).willReturn(10L);
given(chatMessageService.countUnreadMessages(1L, 10L)).willReturn(5L);
given(chatMessageService.readRecentMessages(1L, 1)).willReturn(List.of(firstRoomLastMessage));

// room2: 마지막으로 읽은 메시지 ID 20, 읽지 않은 메시지 3개
ChatMessage secondRoomLastMessage = ChatMessageBuilder.builder().chatRoomId(2L).chatId(100L).content("jayang님이 입장하셨습니다.").contentType(MessageContentType.TEXT).categoryType(MessageCategoryType.SYSTEM).sender(userId).build();

given(chatMessageStatusService.readLastReadMessageId(userId, 2L)).willReturn(20L);
given(chatMessageService.countUnreadMessages(2L, 20L)).willReturn(3L);
given(chatMessageService.readRecentMessages(2L, 1)).willReturn(List.of(secondRoomLastMessage));

// when
Map<ChatRoomDetail, Long> result = chatRoomSearchService.readChatRooms(userId);
List<ChatRoomRes.Info> result = chatRoomSearchService.readChatRooms(userId);

// then
assertAll(
() -> assertEquals(2, result.size()),
() -> assertEquals(5L, result.get(chatRooms.get(0))),
() -> assertEquals(3L, result.get(chatRooms.get(1)))
() -> assertEquals(2, result.size(), "조회된 채팅방 목록은 2개여야 한다."),
() -> assertEquals(5L, result.get(0).unreadMessageCount(), "Room1의 읽지 않은 메시지 수는 5개여야 한다."),
() -> assertEquals(firstRoomLastMessage.getChatId(), result.get(0).lastMessage().chatId(), "Room1의 마지막 메시지는 ID가 일치해야 한다."),
() -> assertEquals(3L, result.get(1).unreadMessageCount(), "Room2의 읽지 않은 메시지 수는 3개여야 한다."),
() -> assertEquals(secondRoomLastMessage.getChatId(), result.get(1).lastMessage().chatId(), "Room2의 마지막 메시지는 ID가 일치해야 한다.")
);
}

Expand All @@ -72,10 +86,13 @@ void returnEmptyMapWhenNoRooms() {
given(chatRoomService.readChatRoomsByUserId(userId)).willReturn(Collections.emptyList());

// when
Map<ChatRoomDetail, Long> result = chatRoomSearchService.readChatRooms(userId);
List<ChatRoomRes.Info> result = chatRoomSearchService.readChatRooms(userId);

// then
assertTrue(result.isEmpty());
verify(chatMessageStatusService, never()).readLastReadMessageId(eq(userId), anyLong());
verify(chatMessageService, never()).countUnreadMessages(anyLong(), anyLong());
verify(chatMessageService, never()).countUnreadMessages(anyLong(), anyLong());
}

@Test
Expand Down Expand Up @@ -110,10 +127,11 @@ void verifyServiceCallOrder() {
new ChatRoomDetail(1L, "Room1", "", "", 123456, LocalDateTime.now(), true, 2)
);

InOrder inOrder = inOrder(chatRoomService, chatMessageStatusService, chatMessageService);
InOrder inOrder = inOrder(chatRoomService, chatMessageStatusService, chatMessageService, chatMessageService);

given(chatRoomService.readChatRoomsByUserId(userId)).willReturn(chatRooms);
given(chatMessageStatusService.readLastReadMessageId(userId, 1L)).willReturn(10L);
given(chatMessageService.readRecentMessages(1L, 1)).willReturn(Collections.emptyList());
given(chatMessageService.countUnreadMessages(userId, 10L)).willReturn(5L);

// when
Expand Down
Loading