feat(chat): 채팅방 리스트 조회 API를 추가한다
This commit is contained in:
@@ -9,6 +9,8 @@ import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.data.jpa.repository.Query
|
||||
import org.springframework.data.repository.query.Param
|
||||
import org.springframework.stereotype.Repository
|
||||
import java.time.LocalDateTime
|
||||
import kr.co.vividnext.sodalive.v2.chat.dto.ChatRoomListQueryDto as V2ChatRoomListQueryDto
|
||||
|
||||
@Repository
|
||||
interface ChatRoomRepository : JpaRepository<ChatRoom, Long> {
|
||||
@@ -64,5 +66,52 @@ interface ChatRoomRepository : JpaRepository<ChatRoom, Long> {
|
||||
pageable: Pageable
|
||||
): List<ChatRoomListQueryDto>
|
||||
|
||||
@Query(
|
||||
"""
|
||||
SELECT new kr.co.vividnext.sodalive.v2.chat.dto.ChatRoomListQueryDto(
|
||||
r.id,
|
||||
'AI',
|
||||
pc.character.name,
|
||||
pc.character.imagePath,
|
||||
m.message,
|
||||
str(m.messageType),
|
||||
m.createdAt
|
||||
)
|
||||
FROM ChatRoom r
|
||||
JOIN r.participants p
|
||||
JOIN r.participants pc
|
||||
JOIN r.messages m
|
||||
WHERE p.member.id = :memberId
|
||||
AND p.isActive = true
|
||||
AND pc.participantType = kr.co.vividnext.sodalive.chat.room.ParticipantType.CHARACTER
|
||||
AND pc.isActive = true
|
||||
AND r.isActive = true
|
||||
AND m.isActive = true
|
||||
AND (
|
||||
:cursorAt IS NULL
|
||||
OR m.createdAt < :cursorAt
|
||||
OR (m.createdAt = :cursorAt AND 'AI' < :cursorChatType)
|
||||
OR (m.createdAt = :cursorAt AND 'AI' = :cursorChatType AND r.id < :cursorRoomId)
|
||||
)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM ChatMessage newer
|
||||
WHERE newer.chatRoom = r
|
||||
AND newer.isActive = true
|
||||
AND (
|
||||
newer.createdAt > m.createdAt
|
||||
OR (newer.createdAt = m.createdAt AND newer.id > m.id)
|
||||
)
|
||||
)
|
||||
ORDER BY m.createdAt DESC, r.id DESC
|
||||
"""
|
||||
)
|
||||
fun findAiChatListRooms(
|
||||
@Param("memberId") memberId: Long,
|
||||
@Param("cursorAt") cursorAt: LocalDateTime?,
|
||||
@Param("cursorChatType") cursorChatType: String?,
|
||||
@Param("cursorRoomId") cursorRoomId: Long?,
|
||||
pageable: Pageable
|
||||
): List<V2ChatRoomListQueryDto>
|
||||
|
||||
fun findByIdAndIsActiveTrue(id: Long): ChatRoom?
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package kr.co.vividnext.sodalive.v2.chat.controller
|
||||
|
||||
import kr.co.vividnext.sodalive.common.ApiResponse
|
||||
import kr.co.vividnext.sodalive.common.SodaException
|
||||
import kr.co.vividnext.sodalive.member.Member
|
||||
import kr.co.vividnext.sodalive.v2.chat.service.ChatRoomListService
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RequestParam
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v2/chat/rooms")
|
||||
class ChatRoomListController(
|
||||
private val service: ChatRoomListService
|
||||
) {
|
||||
@GetMapping
|
||||
fun getRooms(
|
||||
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?,
|
||||
@RequestParam(defaultValue = "ALL") filter: String,
|
||||
@RequestParam(required = false) cursor: String?,
|
||||
@RequestParam(defaultValue = "30") limit: Int
|
||||
) = run {
|
||||
if (member == null) throw SodaException(messageKey = "common.error.bad_credentials")
|
||||
ApiResponse.ok(service.getRooms(member, filter, cursor, limit))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package kr.co.vividnext.sodalive.v2.chat.dto
|
||||
|
||||
import java.time.LocalDateTime
|
||||
|
||||
data class ChatRoomListPageResponse(
|
||||
val rooms: List<ChatRoomListItemResponse>,
|
||||
val hasMore: Boolean,
|
||||
val nextCursor: String?
|
||||
)
|
||||
|
||||
data class ChatRoomListItemResponse(
|
||||
val roomId: Long,
|
||||
val chatType: String,
|
||||
val targetName: String,
|
||||
val targetImageUrl: String,
|
||||
val lastMessage: String,
|
||||
val lastMessageAt: String
|
||||
)
|
||||
|
||||
data class ChatRoomListQueryDto(
|
||||
val roomId: Long,
|
||||
val chatType: String,
|
||||
val targetName: String,
|
||||
val targetImagePath: String?,
|
||||
val lastMessage: String?,
|
||||
val messageType: String,
|
||||
val lastMessageAt: LocalDateTime
|
||||
)
|
||||
@@ -0,0 +1,156 @@
|
||||
package kr.co.vividnext.sodalive.v2.chat.service
|
||||
|
||||
import kr.co.vividnext.sodalive.chat.room.ChatMessageType
|
||||
import kr.co.vividnext.sodalive.chat.room.repository.ChatRoomRepository
|
||||
import kr.co.vividnext.sodalive.common.SodaException
|
||||
import kr.co.vividnext.sodalive.member.Member
|
||||
import kr.co.vividnext.sodalive.v2.chat.dto.ChatRoomListItemResponse
|
||||
import kr.co.vividnext.sodalive.v2.chat.dto.ChatRoomListPageResponse
|
||||
import kr.co.vividnext.sodalive.v2.chat.dto.ChatRoomListQueryDto
|
||||
import kr.co.vividnext.sodalive.v2.usercreatorchat.UserCreatorChatMessageType
|
||||
import kr.co.vividnext.sodalive.v2.usercreatorchat.repository.UserCreatorChatRoomRepository
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.data.domain.PageRequest
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneOffset
|
||||
|
||||
@Service
|
||||
@Transactional(readOnly = true)
|
||||
class ChatRoomListService(
|
||||
private val aiRoomRepository: ChatRoomRepository,
|
||||
private val dmRoomRepository: UserCreatorChatRoomRepository,
|
||||
@Value("\${cloud.aws.cloud-front.host}")
|
||||
private val cloudFrontHost: String
|
||||
) {
|
||||
fun getRooms(
|
||||
member: Member,
|
||||
filter: String = ChatRoomListFilter.ALL.name,
|
||||
cursor: String? = null,
|
||||
limit: Int = DEFAULT_LIMIT
|
||||
): ChatRoomListPageResponse {
|
||||
val type = ChatRoomListFilter.from(filter)
|
||||
val safeLimit = limit.coerceIn(1, DEFAULT_LIMIT)
|
||||
val pageable = PageRequest.of(0, safeLimit + 1)
|
||||
val parsedCursor = parseCursor(cursor)
|
||||
val rows = when (type) {
|
||||
ChatRoomListFilter.ALL -> {
|
||||
aiRoomRepository.findAiChatListRooms(
|
||||
member.id!!,
|
||||
parsedCursor?.lastMessageAt,
|
||||
parsedCursor?.chatType,
|
||||
parsedCursor?.roomId,
|
||||
pageable
|
||||
) + dmRoomRepository.findDmChatListRooms(
|
||||
member.id!!,
|
||||
parsedCursor?.lastMessageAt,
|
||||
parsedCursor?.chatType,
|
||||
parsedCursor?.roomId,
|
||||
pageable
|
||||
)
|
||||
}
|
||||
ChatRoomListFilter.AI -> {
|
||||
aiRoomRepository.findAiChatListRooms(
|
||||
member.id!!,
|
||||
parsedCursor?.lastMessageAt,
|
||||
parsedCursor?.chatType,
|
||||
parsedCursor?.roomId,
|
||||
pageable
|
||||
)
|
||||
}
|
||||
ChatRoomListFilter.DM -> {
|
||||
dmRoomRepository.findDmChatListRooms(
|
||||
member.id!!,
|
||||
parsedCursor?.lastMessageAt,
|
||||
parsedCursor?.chatType,
|
||||
parsedCursor?.roomId,
|
||||
pageable
|
||||
)
|
||||
}
|
||||
}.sortedWith(
|
||||
compareByDescending<ChatRoomListQueryDto> { it.lastMessageAt }
|
||||
.thenByDescending { it.chatType }
|
||||
.thenByDescending { it.roomId }
|
||||
)
|
||||
.filter { parsedCursor == null || it.isAfter(parsedCursor) }
|
||||
|
||||
val pageRows = rows.take(safeLimit)
|
||||
return ChatRoomListPageResponse(
|
||||
rooms = pageRows.map { it.toResponse() },
|
||||
hasMore = rows.size > safeLimit,
|
||||
nextCursor = if (rows.size > safeLimit) pageRows.lastOrNull()?.toCursor() else null
|
||||
)
|
||||
}
|
||||
|
||||
private fun ChatRoomListQueryDto.toResponse(): ChatRoomListItemResponse {
|
||||
return ChatRoomListItemResponse(
|
||||
roomId = roomId,
|
||||
chatType = chatType,
|
||||
targetName = targetName,
|
||||
targetImageUrl = imageUrl(targetImagePath),
|
||||
lastMessage = previewMessage(),
|
||||
lastMessageAt = lastMessageAt.toUtcIsoString()
|
||||
)
|
||||
}
|
||||
|
||||
private fun ChatRoomListQueryDto.previewMessage(): String {
|
||||
if (messageType == UserCreatorChatMessageType.VOICE.name) return VOICE_PREVIEW
|
||||
if (messageType == ChatMessageType.IMAGE.name) return lastMessage.orEmpty().ifBlank { "이미지 메시지" }
|
||||
val message = lastMessage.orEmpty()
|
||||
return if (message.length > PREVIEW_LENGTH) message.take(PREVIEW_LENGTH) + "..." else message
|
||||
}
|
||||
|
||||
private fun imageUrl(path: String?): String {
|
||||
return "$cloudFrontHost/${path?.takeIf { it.isNotBlank() } ?: DEFAULT_PROFILE_IMAGE_PATH}"
|
||||
}
|
||||
|
||||
private fun ChatRoomListQueryDto.toCursor(): String {
|
||||
return "$lastMessageAt:$chatType:$roomId"
|
||||
}
|
||||
|
||||
private fun parseCursor(cursor: String?): ChatRoomListCursor? {
|
||||
if (cursor == null) return null
|
||||
val roomId = cursor.substringAfterLast(":").toLong()
|
||||
val cursorWithoutRoomId = cursor.substringBeforeLast(":")
|
||||
val chatType = cursorWithoutRoomId.substringAfterLast(":")
|
||||
val lastMessageAt = LocalDateTime.parse(cursorWithoutRoomId.substringBeforeLast(":"))
|
||||
return ChatRoomListCursor(lastMessageAt, chatType, roomId)
|
||||
}
|
||||
|
||||
private fun LocalDateTime.toUtcIsoString(): String {
|
||||
return atOffset(ZoneOffset.UTC).toInstant().toString()
|
||||
}
|
||||
|
||||
private fun ChatRoomListQueryDto.isAfter(cursor: ChatRoomListCursor): Boolean {
|
||||
if (lastMessageAt.isBefore(cursor.lastMessageAt)) return true
|
||||
if (lastMessageAt.isAfter(cursor.lastMessageAt)) return false
|
||||
if (chatType < cursor.chatType) return true
|
||||
if (chatType > cursor.chatType) return false
|
||||
return roomId < cursor.roomId
|
||||
}
|
||||
|
||||
private data class ChatRoomListCursor(
|
||||
val lastMessageAt: LocalDateTime,
|
||||
val chatType: String,
|
||||
val roomId: Long
|
||||
)
|
||||
|
||||
private enum class ChatRoomListFilter {
|
||||
ALL, AI, DM;
|
||||
|
||||
companion object {
|
||||
fun from(value: String): ChatRoomListFilter {
|
||||
return values().firstOrNull { it.name == value.uppercase() }
|
||||
?: throw SodaException(messageKey = "common.error.invalid_request")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val DEFAULT_LIMIT = 30
|
||||
private const val PREVIEW_LENGTH = 15
|
||||
private const val DEFAULT_PROFILE_IMAGE_PATH = "profile/default-profile.png"
|
||||
private const val VOICE_PREVIEW = "[음성 메시지]"
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
package kr.co.vividnext.sodalive.v2.usercreatorchat.repository
|
||||
|
||||
import kr.co.vividnext.sodalive.v2.chat.dto.ChatRoomListQueryDto
|
||||
import kr.co.vividnext.sodalive.v2.usercreatorchat.UserCreatorChatRoom
|
||||
import org.springframework.data.domain.Pageable
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.data.jpa.repository.Query
|
||||
import org.springframework.data.repository.query.Param
|
||||
import org.springframework.stereotype.Repository
|
||||
import java.time.LocalDateTime
|
||||
|
||||
@Repository
|
||||
interface UserCreatorChatRoomRepository : JpaRepository<UserCreatorChatRoom, Long> {
|
||||
@@ -32,4 +35,51 @@ interface UserCreatorChatRoomRepository : JpaRepository<UserCreatorChatRoom, Lon
|
||||
@Param("firstMemberId") firstMemberId: Long,
|
||||
@Param("secondMemberId") secondMemberId: Long
|
||||
): UserCreatorChatRoom?
|
||||
|
||||
@Query(
|
||||
"""
|
||||
select new kr.co.vividnext.sodalive.v2.chat.dto.ChatRoomListQueryDto(
|
||||
r.id,
|
||||
'DM',
|
||||
opponent.member.nickname,
|
||||
opponent.member.profileImage,
|
||||
m.textMessage,
|
||||
str(m.messageType),
|
||||
m.createdAt
|
||||
)
|
||||
from UserCreatorChatRoom r
|
||||
join r.participants mine
|
||||
join r.participants opponent
|
||||
join r.messages m
|
||||
where r.isActive = true
|
||||
and mine.isActive = true
|
||||
and mine.member.id = :memberId
|
||||
and opponent.isActive = true
|
||||
and opponent.member.id <> :memberId
|
||||
and m.isActive = true
|
||||
and (
|
||||
:cursorAt is null
|
||||
or m.createdAt < :cursorAt
|
||||
or (m.createdAt = :cursorAt and 'DM' < :cursorChatType)
|
||||
or (m.createdAt = :cursorAt and 'DM' = :cursorChatType and r.id < :cursorRoomId)
|
||||
)
|
||||
and not exists (
|
||||
select 1 from UserCreatorChatMessage newer
|
||||
where newer.chatRoom = r
|
||||
and newer.isActive = true
|
||||
and (
|
||||
newer.createdAt > m.createdAt
|
||||
or (newer.createdAt = m.createdAt and newer.id > m.id)
|
||||
)
|
||||
)
|
||||
order by m.createdAt desc, r.id desc
|
||||
"""
|
||||
)
|
||||
fun findDmChatListRooms(
|
||||
@Param("memberId") memberId: Long,
|
||||
@Param("cursorAt") cursorAt: LocalDateTime?,
|
||||
@Param("cursorChatType") cursorChatType: String?,
|
||||
@Param("cursorRoomId") cursorRoomId: Long?,
|
||||
pageable: Pageable
|
||||
): List<ChatRoomListQueryDto>
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
Reference in New Issue
Block a user