test #426

Merged
klaus merged 415 commits from test into main 2026-06-27 00:35:30 +00:00
2 changed files with 105 additions and 15 deletions
Showing only changes of commit 581c5fd441 - Show all commits

View File

@@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.v2.content.recommendation.application
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
import kr.co.vividnext.sodalive.v2.content.recommendation.domain.AudioCard
import kr.co.vividnext.sodalive.v2.content.recommendation.domain.AudioRecommendationVisibility
import kr.co.vividnext.sodalive.v2.content.recommendation.domain.AudioRecommendations
import kr.co.vividnext.sodalive.v2.content.recommendation.port.out.AudioRecommendationQueryPort
@@ -29,7 +30,7 @@ class AudioRecommendationQueryService(
val visibility = if (canViewAdultContent) AudioRecommendationVisibility.ALL else AudioRecommendationVisibility.SAFE
val memberId = member?.id
val newAndHotSectionType = newAndHotSectionType(visibility)
val newAndHotSnapshots = snapshotPort.findLatestSnapshots(newAndHotSectionType, limit = NEW_AND_HOT_AUDIO_LIMIT)
val newAndHotSnapshots = snapshotPort.findLatestSnapshots(newAndHotSectionType, limit = NEW_AND_HOT_HOME_LIMIT)
val mostCommentedSnapshots = snapshotPort.findLatestSnapshots(
mostCommentedSectionType(visibility),
limit = MOST_COMMENTED_AUDIO_LIMIT
@@ -38,7 +39,12 @@ class AudioRecommendationQueryService(
recommendedAudioSectionType(visibility),
limit = RECOMMENDED_AUDIO_LIMIT
)
val refreshedNewAndHotSnapshots = refreshMissingNewAndHotSnapshots(newAndHotSectionType, newAndHotSnapshots)
val refreshedNewAndHotSnapshots = refreshMissingNewAndHotSnapshots(
newAndHotSectionType,
newAndHotSnapshots,
offset = 0,
limit = NEW_AND_HOT_HOME_LIMIT
)
return AudioRecommendations(
banners = queryPort.findBanners(BANNER_LIMIT, memberId, canViewAdultContent),
@@ -66,6 +72,22 @@ class AudioRecommendationQueryService(
)
}
fun findNewAndHotAudios(member: Member, offset: Long, limit: Int): List<AudioCard> {
val now = LocalDateTime.now()
val canViewAdultContent = canViewAdultContent(member)
val visibility = if (canViewAdultContent) AudioRecommendationVisibility.ALL else AudioRecommendationVisibility.SAFE
val sectionType = newAndHotSectionType(visibility)
val snapshots = snapshotPort.findLatestSnapshots(sectionType, offset, limit)
val refreshedSnapshots = refreshMissingNewAndHotSnapshots(sectionType, snapshots, offset, limit)
return queryPort.findAudioCardsByIds(
refreshedSnapshots.map { it.targetId },
member.id,
canViewAdultContent,
now
)
}
fun resolveVisibility(member: Member?): AudioRecommendationVisibility {
return if (canViewAdultContent(member)) AudioRecommendationVisibility.ALL else AudioRecommendationVisibility.SAFE
}
@@ -93,7 +115,9 @@ class AudioRecommendationQueryService(
private fun refreshMissingNewAndHotSnapshots(
sectionType: RecommendedSectionType,
snapshots: List<RecommendationSnapshotRecord>
snapshots: List<RecommendationSnapshotRecord>,
offset: Long,
limit: Int
): List<RecommendationSnapshotRecord> {
if (snapshots.isNotEmpty()) return snapshots
val today = LocalDate.now(KST_ZONE)
@@ -107,7 +131,7 @@ class AudioRecommendationQueryService(
marker.delete()
throw ex
}
return snapshotPort.findLatestSnapshots(sectionType, limit = NEW_AND_HOT_AUDIO_LIMIT)
return snapshotPort.findLatestSnapshots(sectionType, offset, limit)
}
private fun newAndHotLazyRefreshMarkerKey(date: LocalDate): String {
@@ -125,7 +149,7 @@ class AudioRecommendationQueryService(
const val LATEST_AUDIO_LIMIT = 12
const val FREE_AUDIO_LIMIT = 10
const val POINT_AUDIO_LIMIT = 10
const val NEW_AND_HOT_AUDIO_LIMIT = 12
const val NEW_AND_HOT_HOME_LIMIT = 12
const val MOST_COMMENTED_AUDIO_LIMIT = 5
const val RECOMMENDED_AUDIO_LIMIT = 10
private const val LAZY_REFRESH_MARKER_KEY_PREFIX = "audio-recommendation:new-and-hot:lazy-refresh-attempted"

View File

@@ -1,6 +1,9 @@
package kr.co.vividnext.sodalive.v2.content.recommendation.application
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.MemberRole
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
import kr.co.vividnext.sodalive.v2.content.recommendation.domain.AudioCard
import kr.co.vividnext.sodalive.v2.content.recommendation.domain.AudioRecommendationVisibility
import kr.co.vividnext.sodalive.v2.content.recommendation.port.out.AudioRecommendationQueryPort
import kr.co.vividnext.sodalive.v2.recommendation.domain.RecommendedSectionType
@@ -51,7 +54,7 @@ class AudioRecommendationQueryServiceTest {
.findLatestSnapshots(
RecommendedSectionType.NEW_AND_HOT_AUDIO_SAFE,
0,
AudioRecommendationQueryService.NEW_AND_HOT_AUDIO_LIMIT
AudioRecommendationQueryService.NEW_AND_HOT_HOME_LIMIT
)
Mockito.doReturn(emptyList<RecommendationSnapshotRecord>())
.`when`(snapshotPort)
@@ -100,7 +103,7 @@ class AudioRecommendationQueryServiceTest {
.findLatestSnapshots(
RecommendedSectionType.NEW_AND_HOT_AUDIO_SAFE,
0,
AudioRecommendationQueryService.NEW_AND_HOT_AUDIO_LIMIT
AudioRecommendationQueryService.NEW_AND_HOT_HOME_LIMIT
)
Mockito.doReturn(emptyList<RecommendationSnapshotRecord>())
.`when`(snapshotPort)
@@ -127,19 +130,14 @@ class AudioRecommendationQueryServiceTest {
@Test
@DisplayName("인증 회원 성인 정책은 조회용 저장 preference를 사용한다")
fun shouldUseStoredPreferenceForMemberAdultVisibility() {
val member = kr.co.vividnext.sodalive.member.Member(
email = "adult@test.com",
password = "password",
nickname = "adult",
role = kr.co.vividnext.sodalive.member.MemberRole.USER
)
val member = member(id = 10L)
Mockito.doReturn(true).`when`(preferenceService).canViewAdultContent(member)
Mockito.doReturn(listOf(snapshot(RecommendedSectionType.NEW_AND_HOT_AUDIO_ALL, 10L)))
.`when`(snapshotPort)
.findLatestSnapshots(
RecommendedSectionType.NEW_AND_HOT_AUDIO_ALL,
0,
AudioRecommendationQueryService.NEW_AND_HOT_AUDIO_LIMIT
AudioRecommendationQueryService.NEW_AND_HOT_HOME_LIMIT
)
service.getRecommendations(member)
@@ -149,10 +147,52 @@ class AudioRecommendationQueryServiceTest {
Mockito.verify(snapshotPort).findLatestSnapshots(
RecommendedSectionType.NEW_AND_HOT_AUDIO_ALL,
0,
AudioRecommendationQueryService.NEW_AND_HOT_AUDIO_LIMIT
AudioRecommendationQueryService.NEW_AND_HOT_HOME_LIMIT
)
}
@Test
@DisplayName("추천 탭 첫 화면은 New & Hot 스냅샷을 12개만 조회한다")
fun shouldKeepNewAndHotHomeLimitAtTwelve() {
val member = member(id = 10L)
Mockito.doReturn(true).`when`(preferenceService).canViewAdultContent(member)
Mockito.doReturn(listOf(snapshot(RecommendedSectionType.NEW_AND_HOT_AUDIO_ALL, 10L))).`when`(snapshotPort)
.findLatestSnapshots(
RecommendedSectionType.NEW_AND_HOT_AUDIO_ALL,
0,
AudioRecommendationQueryService.NEW_AND_HOT_HOME_LIMIT
)
service.getRecommendations(member)
Mockito.verify(snapshotPort).findLatestSnapshots(
RecommendedSectionType.NEW_AND_HOT_AUDIO_ALL,
0,
AudioRecommendationQueryService.NEW_AND_HOT_HOME_LIMIT
)
}
@Test
@DisplayName("New & Hot 전체보기는 스냅샷 offset과 limit으로 오디오 카드를 조회한다")
fun shouldFindNewAndHotAudiosWithOffsetAndLimit() {
val member = member(id = 10L)
val snapshots = listOf(
snapshot(RecommendedSectionType.NEW_AND_HOT_AUDIO_ALL, 3L),
snapshot(RecommendedSectionType.NEW_AND_HOT_AUDIO_ALL, 4L),
snapshot(RecommendedSectionType.NEW_AND_HOT_AUDIO_ALL, 5L)
)
Mockito.doReturn(true).`when`(preferenceService).canViewAdultContent(member)
Mockito.doReturn(snapshots).`when`(snapshotPort)
.findLatestSnapshots(RecommendedSectionType.NEW_AND_HOT_AUDIO_ALL, 20L, 21)
Mockito.doReturn(listOf(audioCard(3L), audioCard(4L), audioCard(5L))).`when`(queryPort)
.findAudioCardsByIds(eqValue(listOf(3L, 4L, 5L)), eqValue(member.id), eqValue(true), anyLocalDateTime())
val result = service.findNewAndHotAudios(member, offset = 20L, limit = 21)
assertEquals(listOf(3L, 4L, 5L), result.map { it.audioContentId })
Mockito.verify(snapshotPort).findLatestSnapshots(RecommendedSectionType.NEW_AND_HOT_AUDIO_ALL, 20L, 21)
}
@Test
@DisplayName("visibility와 섹션 조합은 신규 RecommendedSectionType으로 매핑된다")
fun shouldMapVisibilityToAudioSectionTypes() {
@@ -186,6 +226,32 @@ class AudioRecommendationQueryServiceTest {
return Mockito.any(LocalDateTime::class.java) ?: LocalDateTime.now()
}
private fun member(id: Long): Member {
return Member(
email = "viewer$id@test.com",
password = "password",
nickname = "viewer$id",
role = MemberRole.USER
).apply {
this.id = id
}
}
private fun audioCard(id: Long): AudioCard {
return AudioCard(
audioContentId = id,
title = "audio$id",
duration = "00:01",
imageUrl = "https://cdn.test/audio$id.png",
price = id.toInt(),
isAdult = false,
isPointAvailable = true,
isFirstContent = true,
isOriginalSeries = false,
creatorNickname = "creator$id"
)
}
private fun snapshot(sectionType: RecommendedSectionType, targetId: Long): RecommendationSnapshotRecord {
return RecommendationSnapshotRecord(
sectionType = sectionType,