feat(chat): 채팅방 리스트 조회 API를 추가한다

This commit is contained in:
2026-05-14 16:12:14 +09:00
parent 3a2c21c896
commit acd0393a0e
8 changed files with 650 additions and 2 deletions

View File

@@ -0,0 +1,179 @@
package kr.co.vividnext.sodalive.v2.chat
import kr.co.vividnext.sodalive.chat.room.ChatMessageType
import kr.co.vividnext.sodalive.chat.room.repository.ChatRoomRepository
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.v2.chat.dto.ChatRoomListQueryDto
import kr.co.vividnext.sodalive.v2.chat.service.ChatRoomListService
import kr.co.vividnext.sodalive.v2.usercreatorchat.UserCreatorChatMessageType
import kr.co.vividnext.sodalive.v2.usercreatorchat.repository.UserCreatorChatRoomRepository
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.mockito.Mockito
import org.springframework.data.domain.PageRequest
import java.time.LocalDateTime
class ChatRoomListServiceTest {
private lateinit var aiRoomRepository: ChatRoomRepository
private lateinit var dmRoomRepository: UserCreatorChatRoomRepository
private lateinit var service: ChatRoomListService
@BeforeEach
fun setUp() {
aiRoomRepository = Mockito.mock(ChatRoomRepository::class.java)
dmRoomRepository = Mockito.mock(UserCreatorChatRoomRepository::class.java)
service = ChatRoomListService(
aiRoomRepository = aiRoomRepository,
dmRoomRepository = dmRoomRepository,
cloudFrontHost = "https://cdn.test"
)
}
@Test
@DisplayName("전체 채팅 리스트는 AI와 DM을 최신순으로 병합하고 30개 페이징한다")
fun shouldMergeAiAndDmRoomsByLastMessageAt() {
val member = member(1L)
val aiLastAt = LocalDateTime.of(2026, 5, 14, 11, 0)
val dmLastAt = LocalDateTime.of(2026, 5, 14, 12, 0)
Mockito.`when`(aiRoomRepository.findAiChatListRooms(1L, null, null, null, PageRequest.of(0, 31))).thenReturn(
listOf(
ChatRoomListQueryDto(
roomId = 10L,
chatType = "AI",
targetName = "AI 캐릭터",
targetImagePath = "character/a.png",
lastMessage = "AI hello",
messageType = ChatMessageType.TEXT.name,
lastMessageAt = aiLastAt
)
)
)
Mockito.`when`(dmRoomRepository.findDmChatListRooms(1L, null, null, null, PageRequest.of(0, 31))).thenReturn(
listOf(
ChatRoomListQueryDto(
roomId = 20L,
chatType = "DM",
targetName = "creator",
targetImagePath = null,
lastMessage = "안녕하세요. 문의드립니다. 길게 보냅니다.",
messageType = UserCreatorChatMessageType.TEXT.name,
lastMessageAt = dmLastAt
)
)
)
val response = service.getRooms(member, filter = "ALL", cursor = null, limit = 30)
assertFalse(response.hasMore)
assertEquals(listOf("DM", "AI"), response.rooms.map { it.chatType })
assertEquals("creator", response.rooms[0].targetName)
assertEquals("https://cdn.test/profile/default-profile.png", response.rooms[0].targetImageUrl)
assertEquals("안녕하세요. 문의드립니다. ...", response.rooms[0].lastMessage)
assertEquals("2026-05-14T12:00:00Z", response.rooms[0].lastMessageAt)
}
@Test
@DisplayName("DM 필터는 DM 방만 조회하고 음성 메시지 요약 문구를 사용한다")
fun shouldReturnOnlyDmRoomsWithVoicePreview() {
val member = member(1L)
Mockito.`when`(dmRoomRepository.findDmChatListRooms(1L, null, null, null, PageRequest.of(0, 31))).thenReturn(
listOf(
ChatRoomListQueryDto(
roomId = 20L,
chatType = "DM",
targetName = "creator",
targetImagePath = "profile/creator.png",
lastMessage = null,
messageType = UserCreatorChatMessageType.VOICE.name,
lastMessageAt = LocalDateTime.of(2026, 5, 14, 12, 0)
)
)
)
val response = service.getRooms(member, filter = "DM", cursor = null, limit = 30)
assertEquals(1, response.rooms.size)
assertEquals("DM", response.rooms[0].chatType)
assertEquals("[음성 메시지]", response.rooms[0].lastMessage)
assertEquals("https://cdn.test/profile/creator.png", response.rooms[0].targetImageUrl)
Mockito.verifyNoInteractions(aiRoomRepository)
}
@Test
@DisplayName("31번째 항목이 있으면 hasMore와 nextCursor를 반환한다")
fun shouldReturnNextCursorWhenMoreThanLimit() {
val member = member(1L)
val rows = (1L..31L).map { index ->
ChatRoomListQueryDto(
roomId = index,
chatType = "AI",
targetName = "AI $index",
targetImagePath = null,
lastMessage = "message $index",
messageType = ChatMessageType.TEXT.name,
lastMessageAt = LocalDateTime.of(2026, 5, 14, 12, 0).minusMinutes(index)
)
}
Mockito.`when`(aiRoomRepository.findAiChatListRooms(1L, null, null, null, PageRequest.of(0, 31))).thenReturn(rows)
val response = service.getRooms(member, filter = "AI", cursor = null, limit = 30)
assertEquals(30, response.rooms.size)
assertTrue(response.hasMore)
assertEquals("AI", response.rooms.last().chatType)
assertEquals("30", response.nextCursor?.substringAfterLast(":"))
}
@Test
@DisplayName("커서는 동일 시간의 다음 정렬 항목을 누락하지 않는다")
fun shouldKeepRoomsWithSameTimestampAfterCursor() {
val member = member(1L)
val cursorAt = LocalDateTime.of(2026, 5, 14, 12, 0)
Mockito.`when`(aiRoomRepository.findAiChatListRooms(1L, cursorAt, "DM", 20L, PageRequest.of(0, 31))).thenReturn(
listOf(
ChatRoomListQueryDto(
roomId = 30L,
chatType = "AI",
targetName = "AI same time",
targetImagePath = "",
lastMessage = "same time",
messageType = ChatMessageType.TEXT.name,
lastMessageAt = cursorAt
)
)
)
Mockito.`when`(dmRoomRepository.findDmChatListRooms(1L, cursorAt, "DM", 20L, PageRequest.of(0, 31))).thenReturn(
listOf(
ChatRoomListQueryDto(
roomId = 20L,
chatType = "DM",
targetName = "cursor row",
targetImagePath = "profile/cursor.png",
lastMessage = "cursor",
messageType = UserCreatorChatMessageType.TEXT.name,
lastMessageAt = cursorAt
),
ChatRoomListQueryDto(
roomId = 10L,
chatType = "DM",
targetName = "older",
targetImagePath = "profile/older.png",
lastMessage = "older",
messageType = UserCreatorChatMessageType.TEXT.name,
lastMessageAt = cursorAt.minusMinutes(1)
)
)
)
val response = service.getRooms(member, filter = "ALL", cursor = "2026-05-14T12:00:00:DM:20", limit = 30)
assertEquals(listOf(30L, 10L), response.rooms.map { it.roomId })
assertEquals("https://cdn.test/profile/default-profile.png", response.rooms[0].targetImageUrl)
}
private fun member(id: Long) = Member(password = "pw", nickname = "user").apply { this.id = id }
}