feat(home): 최근 소식 원천 데이터를 보강한다
This commit is contained in:
+230
-11
@@ -5,6 +5,8 @@ import kr.co.vividnext.sodalive.configs.QueryDslConfig
|
||||
import kr.co.vividnext.sodalive.content.AudioContent
|
||||
import kr.co.vividnext.sodalive.content.theme.AudioContentTheme
|
||||
import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.CreatorCommunity
|
||||
import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.comment.CreatorCommunityComment
|
||||
import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.like.CreatorCommunityLike
|
||||
import kr.co.vividnext.sodalive.live.room.LiveRoom
|
||||
import kr.co.vividnext.sodalive.member.Member
|
||||
import kr.co.vividnext.sodalive.member.MemberRole
|
||||
@@ -219,8 +221,8 @@ class DefaultHomeFollowingQueryRepositoryTest @Autowired constructor(
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("최근 소식은 활성 노출 가능 inbox를 최신순으로 조회하고 creatorId 없이 nullable rank만 반환한다")
|
||||
fun shouldFindRecentNewsWithoutCreatorIdAndWithNullableRank() {
|
||||
@DisplayName("최근 소식은 노출 가능한 랭킹 inbox 중 rank가 있는 row만 최신순으로 조회한다")
|
||||
fun shouldFindRecentNewsWithRankedCreatorRankingPayloadOnly() {
|
||||
val viewer = saveMember("news-viewer", MemberRole.USER)
|
||||
val creator = saveMember("news-creator", MemberRole.CREATOR)
|
||||
val blockedCreator = saveMember("news-blocked", MemberRole.CREATOR)
|
||||
@@ -228,7 +230,7 @@ class DefaultHomeFollowingQueryRepositoryTest @Autowired constructor(
|
||||
saveFollowing(viewer, creator)
|
||||
saveFollowing(viewer, blockedCreator)
|
||||
saveFollowing(viewer, nonCreator)
|
||||
val oldVisible = saveNews(viewer.id!!, creator.id!!, "old", LocalDateTime.of(2026, 6, 25, 8, 0), rank = null)
|
||||
saveNews(viewer.id!!, creator.id!!, "old-without-rank", LocalDateTime.of(2026, 6, 25, 8, 0), rank = null)
|
||||
val latestVisible = saveNews(viewer.id!!, creator.id!!, "latest", LocalDateTime.of(2026, 6, 25, 9, 0), rank = 3)
|
||||
saveNews(viewer.id!!, creator.id!!, "future", LocalDateTime.of(2026, 6, 25, 10, 0), rank = 1)
|
||||
saveNews(viewer.id!!, creator.id!!, "adult", LocalDateTime.of(2026, 6, 25, 9, 30), isAdult = true)
|
||||
@@ -244,8 +246,8 @@ class DefaultHomeFollowingQueryRepositoryTest @Autowired constructor(
|
||||
limit = 30
|
||||
)
|
||||
|
||||
assertEquals(listOf(latestVisible.id!!.toString(), oldVisible.id!!.toString()), news.map { it.newsId })
|
||||
assertEquals(listOf(3, null), news.map { it.rank })
|
||||
assertEquals(listOf(latestVisible.id!!.toString()), news.map { it.newsId })
|
||||
assertEquals(listOf(3), news.map { it.creatorRanking?.rank })
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -254,8 +256,8 @@ class DefaultHomeFollowingQueryRepositoryTest @Autowired constructor(
|
||||
val viewer = saveMember("news-utc-viewer", MemberRole.USER)
|
||||
val creator = saveMember("news-utc-creator", MemberRole.CREATOR)
|
||||
saveFollowing(viewer, creator)
|
||||
val visibleNow = saveNews(viewer.id!!, creator.id!!, "visible-now", LocalDateTime.of(2026, 6, 25, 14, 30))
|
||||
saveNews(viewer.id!!, creator.id!!, "future-utc", LocalDateTime.of(2026, 6, 25, 14, 31))
|
||||
val visibleNow = saveNews(viewer.id!!, creator.id!!, "visible-now", LocalDateTime.of(2026, 6, 25, 14, 30), rank = 1)
|
||||
saveNews(viewer.id!!, creator.id!!, "future-utc", LocalDateTime.of(2026, 6, 25, 14, 31), rank = 2)
|
||||
flushAndClear()
|
||||
|
||||
val news = repository.findRecentNews(
|
||||
@@ -268,6 +270,198 @@ class DefaultHomeFollowingQueryRepositoryTest @Autowired constructor(
|
||||
assertEquals(listOf(visibleNow.id!!.toString()), news.map { it.newsId })
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("최근 소식은 타입별 원천 데이터를 nested payload로 채운다")
|
||||
fun shouldPopulateRecentNewsNestedPayloadsFromSourceTargets() {
|
||||
val viewer = saveMember("news-payload-viewer", MemberRole.USER)
|
||||
val creator = saveMember("news-payload-creator", MemberRole.CREATOR, profileImage = "payload-profile.png")
|
||||
val otherMember = saveMember("news-payload-other", MemberRole.USER)
|
||||
val theme = saveTheme("news-payload-theme")
|
||||
saveFollowing(viewer, creator)
|
||||
val audio = saveAudioContent(creator, theme, LocalDateTime.of(2026, 6, 25, 7, 0)).apply {
|
||||
title = "source audio title"
|
||||
coverImage = "audio/source-cover.png"
|
||||
}
|
||||
val post = saveCommunityPost(creator, "source community content", isActive = true).apply {
|
||||
imagePath = "community/source-image.png"
|
||||
}
|
||||
saveCommunityLike(viewer, post, isActive = true)
|
||||
saveCommunityLike(otherMember, post, isActive = true)
|
||||
saveCommunityLike(otherMember, post, isActive = false)
|
||||
saveCommunityComment(viewer, post, isActive = true)
|
||||
val inactiveComment = saveCommunityComment(otherMember, post, isActive = false)
|
||||
val childComment = saveCommunityComment(otherMember, post, isActive = true)
|
||||
childComment.parent = inactiveComment
|
||||
post.createdAt = LocalDateTime.of(2026, 6, 25, 6, 30)
|
||||
saveNews(
|
||||
memberId = viewer.id!!,
|
||||
creatorId = creator.id!!,
|
||||
sourceKey = "payload-audio",
|
||||
visibleFromAtUtc = LocalDateTime.of(2026, 6, 25, 9, 0),
|
||||
newsType = FollowingNewsType.AUDIO_CONTENT,
|
||||
targetId = audio.id!!
|
||||
)
|
||||
saveNews(
|
||||
memberId = viewer.id!!,
|
||||
creatorId = creator.id!!,
|
||||
sourceKey = "payload-post",
|
||||
visibleFromAtUtc = LocalDateTime.of(2026, 6, 25, 9, 1),
|
||||
newsType = FollowingNewsType.COMMUNITY_POST,
|
||||
targetId = post.id!!
|
||||
)
|
||||
flushAndClear()
|
||||
|
||||
val news = repository.findRecentNews(
|
||||
memberId = viewer.id!!,
|
||||
canViewAdultContent = true,
|
||||
nowUtc = LocalDateTime.of(2026, 6, 25, 10, 0),
|
||||
limit = 30
|
||||
)
|
||||
|
||||
val communityNews = news.first { it.type == FollowingNewsType.COMMUNITY_POST }.communityPost!!
|
||||
assertEquals(post.id!!, communityNews.postId)
|
||||
assertEquals("https://cdn.test/payload-profile.png", communityNews.creatorProfileImage)
|
||||
assertEquals("news-payload-creator", communityNews.creatorNickname)
|
||||
assertEquals("https://cdn.test/community/source-image.png", communityNews.imageUrl)
|
||||
assertEquals("source community content", communityNews.content)
|
||||
assertEquals("2026-06-25T06:30:00Z", communityNews.createdAt)
|
||||
assertEquals(2, communityNews.likeCount)
|
||||
assertEquals(1, communityNews.commentCount)
|
||||
|
||||
val audioNews = news.first { it.type == FollowingNewsType.AUDIO_CONTENT }.audioContent!!
|
||||
assertEquals(audio.id!!, audioNews.contentId)
|
||||
assertEquals("https://cdn.test/audio/source-cover.png", audioNews.contentImageUrl)
|
||||
assertEquals("source audio title", audioNews.title)
|
||||
assertEquals("https://cdn.test/payload-profile.png", audioNews.creatorProfileImageUrl)
|
||||
assertEquals("news-payload-creator", audioNews.creatorNickname)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("최근 소식은 현재 활성 팔로우가 아닌 크리에이터 inbox를 제외한다")
|
||||
fun shouldExcludeRecentNewsWhenFollowingIsInactive() {
|
||||
val viewer = saveMember("news-inactive-following-viewer", MemberRole.USER)
|
||||
val creator = saveMember("news-inactive-following-creator", MemberRole.CREATOR)
|
||||
saveFollowing(viewer, creator, isActive = false)
|
||||
saveNews(
|
||||
memberId = viewer.id!!,
|
||||
creatorId = creator.id!!,
|
||||
sourceKey = "inactive-following-news",
|
||||
visibleFromAtUtc = LocalDateTime.of(2026, 6, 25, 9, 0),
|
||||
rank = 1
|
||||
)
|
||||
flushAndClear()
|
||||
|
||||
val news = repository.findRecentNews(
|
||||
memberId = viewer.id!!,
|
||||
canViewAdultContent = true,
|
||||
nowUtc = LocalDateTime.of(2026, 6, 25, 10, 0),
|
||||
limit = 30
|
||||
)
|
||||
|
||||
assertTrue(news.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("최근 소식은 유료 커뮤니티 게시글을 제외한다")
|
||||
fun shouldExcludePaidCommunityPostRecentNews() {
|
||||
val viewer = saveMember("news-paid-viewer", MemberRole.USER)
|
||||
val creator = saveMember("news-paid-creator", MemberRole.CREATOR)
|
||||
saveFollowing(viewer, creator)
|
||||
val freePost = saveCommunityPost(creator, "free-post", isActive = true)
|
||||
val negativeFreePost = saveCommunityPost(creator, "negative-free-post", isActive = true).apply { price = -1 }
|
||||
val paidPost = saveCommunityPost(creator, "paid-post", isActive = true).apply { price = 10 }
|
||||
saveNews(
|
||||
memberId = viewer.id!!,
|
||||
creatorId = creator.id!!,
|
||||
sourceKey = "free-post",
|
||||
visibleFromAtUtc = LocalDateTime.of(2026, 6, 25, 9, 0),
|
||||
newsType = FollowingNewsType.COMMUNITY_POST,
|
||||
targetId = freePost.id!!
|
||||
)
|
||||
saveNews(
|
||||
memberId = viewer.id!!,
|
||||
creatorId = creator.id!!,
|
||||
sourceKey = "negative-free-post",
|
||||
visibleFromAtUtc = LocalDateTime.of(2026, 6, 25, 9, 1),
|
||||
newsType = FollowingNewsType.COMMUNITY_POST,
|
||||
targetId = negativeFreePost.id!!
|
||||
)
|
||||
saveNews(
|
||||
memberId = viewer.id!!,
|
||||
creatorId = creator.id!!,
|
||||
sourceKey = "paid-post",
|
||||
visibleFromAtUtc = LocalDateTime.of(2026, 6, 25, 9, 2),
|
||||
newsType = FollowingNewsType.COMMUNITY_POST,
|
||||
targetId = paidPost.id!!
|
||||
)
|
||||
flushAndClear()
|
||||
|
||||
val news = repository.findRecentNews(
|
||||
memberId = viewer.id!!,
|
||||
canViewAdultContent = true,
|
||||
nowUtc = LocalDateTime.of(2026, 6, 25, 10, 0),
|
||||
limit = 30
|
||||
)
|
||||
|
||||
assertEquals(listOf(negativeFreePost.id!!, freePost.id!!), news.map { it.communityPost?.postId })
|
||||
assertTrue(news.all { it.audioContent == null })
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("최근 소식은 비성인 사용자의 오디오와 커뮤니티 원천 target 현재 성인 상태를 반영해 제외한다")
|
||||
fun shouldExcludeAdultSourceTargetsForNonAdultViewer() {
|
||||
val viewer = saveMember("news-adult-source-viewer", MemberRole.USER)
|
||||
val creator = saveMember("news-adult-source-creator", MemberRole.CREATOR)
|
||||
val theme = saveTheme("news-adult-source-theme")
|
||||
saveFollowing(viewer, creator)
|
||||
val adultAudio = saveAudioContent(
|
||||
creator,
|
||||
theme,
|
||||
LocalDateTime.of(2026, 6, 25, 8, 0),
|
||||
isActive = true,
|
||||
isAdult = true
|
||||
)
|
||||
val adultPost = saveCommunityPost(creator, "adult-post", isActive = true, isAdult = true)
|
||||
saveNews(
|
||||
memberId = viewer.id!!,
|
||||
creatorId = creator.id!!,
|
||||
sourceKey = "adult-audio",
|
||||
visibleFromAtUtc = LocalDateTime.of(2026, 6, 25, 9, 0),
|
||||
isAdult = false,
|
||||
newsType = FollowingNewsType.AUDIO_CONTENT,
|
||||
targetId = adultAudio.id!!
|
||||
)
|
||||
saveNews(
|
||||
memberId = viewer.id!!,
|
||||
creatorId = creator.id!!,
|
||||
sourceKey = "adult-post",
|
||||
visibleFromAtUtc = LocalDateTime.of(2026, 6, 25, 9, 1),
|
||||
isAdult = false,
|
||||
newsType = FollowingNewsType.COMMUNITY_POST,
|
||||
targetId = adultPost.id!!
|
||||
)
|
||||
flushAndClear()
|
||||
|
||||
val hiddenNews = repository.findRecentNews(
|
||||
memberId = viewer.id!!,
|
||||
canViewAdultContent = false,
|
||||
nowUtc = LocalDateTime.of(2026, 6, 25, 10, 0),
|
||||
limit = 30
|
||||
)
|
||||
val visibleNews = repository.findRecentNews(
|
||||
memberId = viewer.id!!,
|
||||
canViewAdultContent = true,
|
||||
nowUtc = LocalDateTime.of(2026, 6, 25, 10, 0),
|
||||
limit = 30
|
||||
)
|
||||
|
||||
assertEquals(emptyList<Long>(), hiddenNews.map { it.communityPost?.postId ?: it.audioContent?.contentId })
|
||||
assertEquals(
|
||||
listOf(adultPost.id!!, adultAudio.id!!),
|
||||
visibleNews.map { it.communityPost?.postId ?: it.audioContent?.contentId }
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("최근 소식은 오디오와 커뮤니티 원천 target이 비활성화되면 제외한다")
|
||||
fun shouldExcludeRecentNewsWhenSourceTargetIsInactive() {
|
||||
@@ -322,7 +516,7 @@ class DefaultHomeFollowingQueryRepositoryTest @Autowired constructor(
|
||||
|
||||
assertEquals(
|
||||
listOf(activePost.id!!, activeAudio.id!!),
|
||||
news.map { it.targetId }
|
||||
news.map { it.communityPost?.postId ?: it.audioContent?.contentId }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -387,7 +581,8 @@ class DefaultHomeFollowingQueryRepositoryTest @Autowired constructor(
|
||||
creator: Member,
|
||||
theme: AudioContentTheme,
|
||||
releaseDate: LocalDateTime,
|
||||
isActive: Boolean = true
|
||||
isActive: Boolean = true,
|
||||
isAdult: Boolean = false
|
||||
): AudioContent {
|
||||
val content = AudioContent(
|
||||
title = "audio-$releaseDate",
|
||||
@@ -399,17 +594,23 @@ class DefaultHomeFollowingQueryRepositoryTest @Autowired constructor(
|
||||
this.theme = theme
|
||||
duration = "00:10:00"
|
||||
this.isActive = isActive
|
||||
this.isAdult = isAdult
|
||||
}
|
||||
entityManager.persist(content)
|
||||
return content
|
||||
}
|
||||
|
||||
private fun saveCommunityPost(creator: Member, content: String, isActive: Boolean): CreatorCommunity {
|
||||
private fun saveCommunityPost(
|
||||
creator: Member,
|
||||
content: String,
|
||||
isActive: Boolean,
|
||||
isAdult: Boolean = false
|
||||
): CreatorCommunity {
|
||||
val post = CreatorCommunity(
|
||||
content = content,
|
||||
price = 0,
|
||||
isCommentAvailable = true,
|
||||
isAdult = false,
|
||||
isAdult = isAdult,
|
||||
isActive = isActive
|
||||
).apply {
|
||||
member = creator
|
||||
@@ -418,6 +619,24 @@ class DefaultHomeFollowingQueryRepositoryTest @Autowired constructor(
|
||||
return post
|
||||
}
|
||||
|
||||
private fun saveCommunityLike(member: Member, post: CreatorCommunity, isActive: Boolean): CreatorCommunityLike {
|
||||
val like = CreatorCommunityLike(isActive = isActive).apply {
|
||||
this.member = member
|
||||
creatorCommunity = post
|
||||
}
|
||||
entityManager.persist(like)
|
||||
return like
|
||||
}
|
||||
|
||||
private fun saveCommunityComment(member: Member, post: CreatorCommunity, isActive: Boolean): CreatorCommunityComment {
|
||||
val comment = CreatorCommunityComment(comment = "comment", isActive = isActive).apply {
|
||||
this.member = member
|
||||
creatorCommunity = post
|
||||
}
|
||||
entityManager.persist(comment)
|
||||
return comment
|
||||
}
|
||||
|
||||
private fun saveNews(
|
||||
memberId: Long,
|
||||
creatorId: Long,
|
||||
|
||||
Reference in New Issue
Block a user