fix(home-following): 최근 대화를 팔로우 크리에이터로 제한한다
This commit is contained in:
@@ -87,6 +87,15 @@ interface ChatRoomRepository : JpaRepository<ChatRoom, Long> {
|
||||
AND pc.isActive = true
|
||||
AND r.isActive = true
|
||||
AND m.isActive = true
|
||||
AND (
|
||||
:followedCreatorsOnly = false
|
||||
OR EXISTS (
|
||||
SELECT cf.id FROM CreatorFollowing cf
|
||||
WHERE cf.member.id = :memberId
|
||||
AND cf.creator.id = pc.character.creatorMember.id
|
||||
AND cf.isActive = true
|
||||
)
|
||||
)
|
||||
AND (
|
||||
:cursorAt IS NULL
|
||||
OR m.createdAt < :cursorAt
|
||||
@@ -110,6 +119,7 @@ interface ChatRoomRepository : JpaRepository<ChatRoom, Long> {
|
||||
@Param("cursorAt") cursorAt: LocalDateTime?,
|
||||
@Param("cursorChatType") cursorChatType: String?,
|
||||
@Param("cursorRoomId") cursorRoomId: Long?,
|
||||
@Param("followedCreatorsOnly") followedCreatorsOnly: Boolean,
|
||||
pageable: Pageable
|
||||
): List<V2ChatRoomListQueryDto>
|
||||
|
||||
|
||||
+7
-1
@@ -17,7 +17,13 @@ class HomeFollowingFacade(
|
||||
}
|
||||
|
||||
val home = homeFollowingQueryService.findHomeFollowing(member)
|
||||
val recentChats = chatRoomListService.getRooms(member, filter = "ALL", cursor = null, limit = 10).rooms
|
||||
val recentChats = chatRoomListService.getRooms(
|
||||
member,
|
||||
filter = "ALL",
|
||||
cursor = null,
|
||||
limit = 10,
|
||||
followedCreatorsOnly = true
|
||||
).rooms
|
||||
|
||||
return HomeFollowingTabResponse.from(home.copy(recentChats = recentChats))
|
||||
}
|
||||
|
||||
@@ -28,7 +28,8 @@ class ChatRoomListService(
|
||||
member: Member,
|
||||
filter: String = ChatRoomListFilter.ALL.name,
|
||||
cursor: String? = null,
|
||||
limit: Int = DEFAULT_LIMIT
|
||||
limit: Int = DEFAULT_LIMIT,
|
||||
followedCreatorsOnly: Boolean = false
|
||||
): ChatRoomListPageResponse {
|
||||
val type = ChatRoomListFilter.from(filter)
|
||||
val safeLimit = limit.coerceIn(1, DEFAULT_LIMIT)
|
||||
@@ -41,12 +42,14 @@ class ChatRoomListService(
|
||||
parsedCursor?.lastMessageAt,
|
||||
parsedCursor?.chatType,
|
||||
parsedCursor?.roomId,
|
||||
followedCreatorsOnly,
|
||||
pageable
|
||||
) + dmRoomRepository.findDmChatListRooms(
|
||||
member.id!!,
|
||||
parsedCursor?.lastMessageAt,
|
||||
parsedCursor?.chatType,
|
||||
parsedCursor?.roomId,
|
||||
followedCreatorsOnly,
|
||||
pageable
|
||||
)
|
||||
}
|
||||
@@ -56,6 +59,7 @@ class ChatRoomListService(
|
||||
parsedCursor?.lastMessageAt,
|
||||
parsedCursor?.chatType,
|
||||
parsedCursor?.roomId,
|
||||
followedCreatorsOnly,
|
||||
pageable
|
||||
)
|
||||
}
|
||||
@@ -65,6 +69,7 @@ class ChatRoomListService(
|
||||
parsedCursor?.lastMessageAt,
|
||||
parsedCursor?.chatType,
|
||||
parsedCursor?.roomId,
|
||||
followedCreatorsOnly,
|
||||
pageable
|
||||
)
|
||||
}
|
||||
|
||||
+10
@@ -57,6 +57,15 @@ interface UserCreatorChatRoomRepository : JpaRepository<UserCreatorChatRoom, Lon
|
||||
and opponent.isActive = true
|
||||
and opponent.member.id <> :memberId
|
||||
and m.isActive = true
|
||||
and (
|
||||
:followedCreatorsOnly = false
|
||||
or exists (
|
||||
select cf.id from CreatorFollowing cf
|
||||
where cf.member.id = :memberId
|
||||
and cf.creator.id = opponent.member.id
|
||||
and cf.isActive = true
|
||||
)
|
||||
)
|
||||
and (
|
||||
:cursorAt is null
|
||||
or m.createdAt < :cursorAt
|
||||
@@ -80,6 +89,7 @@ interface UserCreatorChatRoomRepository : JpaRepository<UserCreatorChatRoom, Lon
|
||||
@Param("cursorAt") cursorAt: LocalDateTime?,
|
||||
@Param("cursorChatType") cursorChatType: String?,
|
||||
@Param("cursorRoomId") cursorRoomId: Long?,
|
||||
@Param("followedCreatorsOnly") followedCreatorsOnly: Boolean,
|
||||
pageable: Pageable
|
||||
): List<ChatRoomListQueryDto>
|
||||
}
|
||||
|
||||
+78
-10
@@ -1,5 +1,10 @@
|
||||
package kr.co.vividnext.sodalive.v2.api.home.following.adapter.`in`.web
|
||||
|
||||
import kr.co.vividnext.sodalive.chat.character.ChatCharacter
|
||||
import kr.co.vividnext.sodalive.chat.room.ChatMessage
|
||||
import kr.co.vividnext.sodalive.chat.room.ChatParticipant
|
||||
import kr.co.vividnext.sodalive.chat.room.ChatRoom
|
||||
import kr.co.vividnext.sodalive.chat.room.ParticipantType
|
||||
import kr.co.vividnext.sodalive.common.CountryContext
|
||||
import kr.co.vividnext.sodalive.content.AudioContent
|
||||
import kr.co.vividnext.sodalive.content.theme.AudioContentTheme
|
||||
@@ -9,6 +14,7 @@ import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.like.CreatorCo
|
||||
import kr.co.vividnext.sodalive.live.room.LiveRoom
|
||||
import kr.co.vividnext.sodalive.member.Member
|
||||
import kr.co.vividnext.sodalive.member.MemberAdapter
|
||||
import kr.co.vividnext.sodalive.member.MemberKind
|
||||
import kr.co.vividnext.sodalive.member.MemberRole
|
||||
import kr.co.vividnext.sodalive.member.following.CreatorFollowing
|
||||
import kr.co.vividnext.sodalive.support.EmbeddedRedisInitializer
|
||||
@@ -85,10 +91,19 @@ class HomeFollowingEndToEndTest @Autowired constructor(
|
||||
.andExpect(jsonPath("$.data.followingCreators[0].creatorNickname").value("home-following-creator"))
|
||||
.andExpect(jsonPath("$.data.onAirLives[0].liveId").value(fixture.liveId))
|
||||
.andExpect(jsonPath("$.data.onAirLives[0].title").value("home-following-live"))
|
||||
.andExpect(jsonPath("$.data.recentChats[0].roomId").value(fixture.chatRoomId))
|
||||
.andExpect(jsonPath("$.data.recentChats[0].chatType").value("DM"))
|
||||
.andExpect(jsonPath("$.data.recentChats[0].targetName").value("home-following-creator"))
|
||||
.andExpect(jsonPath("$.data.recentChats[0].lastMessage").value("recent dm"))
|
||||
.andExpect(jsonPath("$.data.recentChats.length()").value(2))
|
||||
.andExpect(jsonPath("$.data.recentChats[0].roomId").value(fixture.aiChatRoomId))
|
||||
.andExpect(jsonPath("$.data.recentChats[0].chatType").value("AI"))
|
||||
.andExpect(jsonPath("$.data.recentChats[1].roomId").value(fixture.dmChatRoomId))
|
||||
.andExpect(jsonPath("$.data.recentChats[1].chatType").value("DM"))
|
||||
.andExpect(jsonPath("$.data.recentChats[1].targetName").value("home-following-creator"))
|
||||
.andExpect(jsonPath("$.data.recentChats[1].lastMessage").value("recent dm"))
|
||||
.andExpect(
|
||||
jsonPath("$.data.recentChats[?(@.roomId == ${fixture.unfollowedDmRoomIds.first()})]").isEmpty
|
||||
)
|
||||
.andExpect(
|
||||
jsonPath("$.data.recentChats[?(@.roomId == ${fixture.inactiveFollowingDmRoomId})]").isEmpty
|
||||
)
|
||||
.andExpect(jsonPath("$.data.monthlySchedules[0].scheduleId").value("LIVE:${fixture.liveId}"))
|
||||
.andExpect(jsonPath("$.data.monthlySchedules[1].scheduleId").value("AUDIO:${fixture.audioId}"))
|
||||
.andExpect(jsonPath("$.data.recentNews[0].newsId").value(fixture.rankedNewsId.toString()))
|
||||
@@ -172,7 +187,21 @@ class HomeFollowingEndToEndTest @Autowired constructor(
|
||||
val rankedVisibleFromAtUtc = now.minusHours(1)
|
||||
saveCreatorRankingSnapshot(creator, rankNo = 7, visibleFromAtUtc = rankedVisibleFromAtUtc)
|
||||
val rankedNews = saveNews(viewer.id!!, creator.id!!, "ranked-news", rankedVisibleFromAtUtc, rank = 7)
|
||||
val chatRoom = saveDmChatRoom(viewer, creator, now.minusMinutes(10))
|
||||
val dmChatRoom = saveDmChatRoom(viewer, creator, now.minusMinutes(10))
|
||||
val aiCreator = saveMember(
|
||||
"home-following-ai-creator",
|
||||
MemberRole.CREATOR,
|
||||
memberKind = MemberKind.AI_CHARACTER
|
||||
)
|
||||
saveFollowing(viewer, aiCreator)
|
||||
val aiChatRoom = saveAiChatRoom(viewer, aiCreator, now.minusMinutes(5))
|
||||
val unfollowedDmRoomIds = (1..11).map { index ->
|
||||
val unfollowedCreator = saveMember("home-following-unfollowed-$index", MemberRole.CREATOR)
|
||||
saveDmChatRoom(viewer, unfollowedCreator, now.minusSeconds(index.toLong())).id!!
|
||||
}
|
||||
val inactiveFollowingCreator = saveMember("home-following-inactive", MemberRole.CREATOR)
|
||||
saveFollowing(viewer, inactiveFollowingCreator, isActive = false)
|
||||
val inactiveFollowingDmRoom = saveDmChatRoom(viewer, inactiveFollowingCreator, now.minusMinutes(1))
|
||||
entityManager.flush()
|
||||
entityManager.clear()
|
||||
|
||||
@@ -181,7 +210,10 @@ class HomeFollowingEndToEndTest @Autowired constructor(
|
||||
creatorId = creator.id!!,
|
||||
liveId = live.id!!,
|
||||
audioId = audio.id!!,
|
||||
chatRoomId = chatRoom.id!!,
|
||||
dmChatRoomId = dmChatRoom.id!!,
|
||||
aiChatRoomId = aiChatRoom.id!!,
|
||||
unfollowedDmRoomIds = unfollowedDmRoomIds,
|
||||
inactiveFollowingDmRoomId = inactiveFollowingDmRoom.id!!,
|
||||
rankedNewsId = rankedNews.id!!,
|
||||
audioNewsId = audioNews.id!!,
|
||||
communityNewsId = communityNews.id!!,
|
||||
@@ -191,21 +223,27 @@ class HomeFollowingEndToEndTest @Autowired constructor(
|
||||
}!!
|
||||
}
|
||||
|
||||
private fun saveMember(seed: String, role: MemberRole, profileImage: String? = null): Member {
|
||||
private fun saveMember(
|
||||
seed: String,
|
||||
role: MemberRole,
|
||||
profileImage: String? = null,
|
||||
memberKind: MemberKind = MemberKind.HUMAN
|
||||
): Member {
|
||||
val member = Member(
|
||||
email = "$seed@test.com",
|
||||
password = "password",
|
||||
nickname = seed,
|
||||
profileImage = profileImage,
|
||||
role = role,
|
||||
memberKind = memberKind,
|
||||
countryCode = "US"
|
||||
)
|
||||
entityManager.persist(member)
|
||||
return member
|
||||
}
|
||||
|
||||
private fun saveFollowing(member: Member, creator: Member): CreatorFollowing {
|
||||
val following = CreatorFollowing(isActive = true).apply {
|
||||
private fun saveFollowing(member: Member, creator: Member, isActive: Boolean = true): CreatorFollowing {
|
||||
val following = CreatorFollowing(isActive = isActive).apply {
|
||||
this.member = member
|
||||
this.creator = creator
|
||||
}
|
||||
@@ -354,12 +392,42 @@ class HomeFollowingEndToEndTest @Autowired constructor(
|
||||
return room
|
||||
}
|
||||
|
||||
private fun saveAiChatRoom(viewer: Member, creator: Member, messageCreatedAt: LocalDateTime): ChatRoom {
|
||||
val character = ChatCharacter(
|
||||
characterUUID = "home-following-ai-character-uuid",
|
||||
name = "home-following-ai-character",
|
||||
description = "description",
|
||||
systemPrompt = "system"
|
||||
).apply { creatorMember = creator }
|
||||
entityManager.persist(character)
|
||||
val room = ChatRoom(sessionId = "home-following-ai-room", title = "home-following-ai-room")
|
||||
entityManager.persist(room)
|
||||
val viewerParticipant = ChatParticipant(room, ParticipantType.USER, member = viewer)
|
||||
val characterParticipant = ChatParticipant(room, ParticipantType.CHARACTER, character = character)
|
||||
entityManager.persist(viewerParticipant)
|
||||
entityManager.persist(characterParticipant)
|
||||
val message = ChatMessage(
|
||||
message = "recent ai",
|
||||
chatRoom = room,
|
||||
participant = characterParticipant,
|
||||
isActive = true
|
||||
)
|
||||
entityManager.persist(message)
|
||||
entityManager.flush()
|
||||
message.createdAt = messageCreatedAt
|
||||
message.updatedAt = messageCreatedAt
|
||||
return room
|
||||
}
|
||||
|
||||
private data class Fixture(
|
||||
val viewer: Member,
|
||||
val creatorId: Long,
|
||||
val liveId: Long,
|
||||
val audioId: Long,
|
||||
val chatRoomId: Long,
|
||||
val dmChatRoomId: Long,
|
||||
val aiChatRoomId: Long,
|
||||
val unfollowedDmRoomIds: List<Long>,
|
||||
val inactiveFollowingDmRoomId: Long,
|
||||
val rankedNewsId: Long,
|
||||
val audioNewsId: Long,
|
||||
val communityNewsId: Long,
|
||||
|
||||
+4
-2
@@ -60,7 +60,8 @@ class HomeFollowingFacadeTest {
|
||||
)
|
||||
Mockito.doReturn(home).`when`(queryService).findHomeFollowing(member)
|
||||
Mockito.doReturn(ChatRoomListPageResponse(rooms = listOf(recentChat), hasMore = false, nextCursor = null))
|
||||
.`when`(chatRoomListService).getRooms(member, filter = "ALL", cursor = null, limit = 10)
|
||||
.`when`(chatRoomListService)
|
||||
.getRooms(member, filter = "ALL", cursor = null, limit = 10, followedCreatorsOnly = true)
|
||||
|
||||
val response = facade.getFollowingTab(member)
|
||||
|
||||
@@ -71,7 +72,8 @@ class HomeFollowingFacadeTest {
|
||||
assertEquals("LIVE:4", response.monthlySchedules.single().scheduleId)
|
||||
assertEquals("news-5", response.recentNews.single().newsId)
|
||||
Mockito.verify(queryService).findHomeFollowing(member)
|
||||
Mockito.verify(chatRoomListService).getRooms(member, filter = "ALL", cursor = null, limit = 10)
|
||||
Mockito.verify(chatRoomListService)
|
||||
.getRooms(member, filter = "ALL", cursor = null, limit = 10, followedCreatorsOnly = true)
|
||||
}
|
||||
|
||||
private fun homeFollowing(): HomeFollowing {
|
||||
|
||||
@@ -39,7 +39,9 @@ class ChatRoomListServiceTest {
|
||||
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(
|
||||
Mockito.`when`(
|
||||
aiRoomRepository.findAiChatListRooms(1L, null, null, null, false, PageRequest.of(0, 31))
|
||||
).thenReturn(
|
||||
listOf(
|
||||
ChatRoomListQueryDto(
|
||||
roomId = 10L,
|
||||
@@ -52,7 +54,9 @@ class ChatRoomListServiceTest {
|
||||
)
|
||||
)
|
||||
)
|
||||
Mockito.`when`(dmRoomRepository.findDmChatListRooms(1L, null, null, null, PageRequest.of(0, 31))).thenReturn(
|
||||
Mockito.`when`(
|
||||
dmRoomRepository.findDmChatListRooms(1L, null, null, null, false, PageRequest.of(0, 31))
|
||||
).thenReturn(
|
||||
listOf(
|
||||
ChatRoomListQueryDto(
|
||||
roomId = 20L,
|
||||
@@ -80,7 +84,9 @@ class ChatRoomListServiceTest {
|
||||
@DisplayName("DM 필터는 DM 방만 조회하고 음성 메시지 요약 문구를 사용한다")
|
||||
fun shouldReturnOnlyDmRoomsWithVoicePreview() {
|
||||
val member = member(1L)
|
||||
Mockito.`when`(dmRoomRepository.findDmChatListRooms(1L, null, null, null, PageRequest.of(0, 31))).thenReturn(
|
||||
Mockito.`when`(
|
||||
dmRoomRepository.findDmChatListRooms(1L, null, null, null, false, PageRequest.of(0, 31))
|
||||
).thenReturn(
|
||||
listOf(
|
||||
ChatRoomListQueryDto(
|
||||
roomId = 20L,
|
||||
@@ -118,7 +124,9 @@ class ChatRoomListServiceTest {
|
||||
lastMessageAt = LocalDateTime.of(2026, 5, 14, 12, 0).minusMinutes(index)
|
||||
)
|
||||
}
|
||||
Mockito.`when`(aiRoomRepository.findAiChatListRooms(1L, null, null, null, PageRequest.of(0, 31))).thenReturn(rows)
|
||||
Mockito.`when`(
|
||||
aiRoomRepository.findAiChatListRooms(1L, null, null, null, false, PageRequest.of(0, 31))
|
||||
).thenReturn(rows)
|
||||
|
||||
val response = service.getRooms(member, filter = "AI", cursor = null, limit = 30)
|
||||
|
||||
@@ -133,7 +141,9 @@ class ChatRoomListServiceTest {
|
||||
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(
|
||||
Mockito.`when`(
|
||||
aiRoomRepository.findAiChatListRooms(1L, cursorAt, "DM", 20L, false, PageRequest.of(0, 31))
|
||||
).thenReturn(
|
||||
listOf(
|
||||
ChatRoomListQueryDto(
|
||||
roomId = 30L,
|
||||
@@ -146,7 +156,9 @@ class ChatRoomListServiceTest {
|
||||
)
|
||||
)
|
||||
)
|
||||
Mockito.`when`(dmRoomRepository.findDmChatListRooms(1L, cursorAt, "DM", 20L, PageRequest.of(0, 31))).thenReturn(
|
||||
Mockito.`when`(
|
||||
dmRoomRepository.findDmChatListRooms(1L, cursorAt, "DM", 20L, false, PageRequest.of(0, 31))
|
||||
).thenReturn(
|
||||
listOf(
|
||||
ChatRoomListQueryDto(
|
||||
roomId = 20L,
|
||||
@@ -175,5 +187,21 @@ class ChatRoomListServiceTest {
|
||||
assertEquals("https://cdn.test/profile/default-profile.png", response.rooms[0].targetImageUrl)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("팔로우 크리에이터 필터는 AI와 DM 저장소에 전달한다")
|
||||
fun shouldPassFollowedCreatorsOnlyToBothRepositories() {
|
||||
val member = member(1L)
|
||||
val pageable = PageRequest.of(0, 11)
|
||||
Mockito.`when`(aiRoomRepository.findAiChatListRooms(1L, null, null, null, true, pageable))
|
||||
.thenReturn(emptyList())
|
||||
Mockito.`when`(dmRoomRepository.findDmChatListRooms(1L, null, null, null, true, pageable))
|
||||
.thenReturn(emptyList())
|
||||
|
||||
service.getRooms(member, filter = "ALL", cursor = null, limit = 10, followedCreatorsOnly = true)
|
||||
|
||||
Mockito.verify(aiRoomRepository).findAiChatListRooms(1L, null, null, null, true, pageable)
|
||||
Mockito.verify(dmRoomRepository).findDmChatListRooms(1L, null, null, null, true, pageable)
|
||||
}
|
||||
|
||||
private fun member(id: Long) = Member(password = "pw", nickname = "user").apply { this.id = id }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user