feat(recommendation): New & Hot 전체보기 조회를 추가한다
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user