test #426
@@ -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.Member
|
||||||
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
|
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.AudioRecommendationVisibility
|
||||||
import kr.co.vividnext.sodalive.v2.content.recommendation.domain.AudioRecommendations
|
import kr.co.vividnext.sodalive.v2.content.recommendation.domain.AudioRecommendations
|
||||||
import kr.co.vividnext.sodalive.v2.content.recommendation.port.out.AudioRecommendationQueryPort
|
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 visibility = if (canViewAdultContent) AudioRecommendationVisibility.ALL else AudioRecommendationVisibility.SAFE
|
||||||
val memberId = member?.id
|
val memberId = member?.id
|
||||||
val newAndHotSectionType = newAndHotSectionType(visibility)
|
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(
|
val mostCommentedSnapshots = snapshotPort.findLatestSnapshots(
|
||||||
mostCommentedSectionType(visibility),
|
mostCommentedSectionType(visibility),
|
||||||
limit = MOST_COMMENTED_AUDIO_LIMIT
|
limit = MOST_COMMENTED_AUDIO_LIMIT
|
||||||
@@ -38,7 +39,12 @@ class AudioRecommendationQueryService(
|
|||||||
recommendedAudioSectionType(visibility),
|
recommendedAudioSectionType(visibility),
|
||||||
limit = RECOMMENDED_AUDIO_LIMIT
|
limit = RECOMMENDED_AUDIO_LIMIT
|
||||||
)
|
)
|
||||||
val refreshedNewAndHotSnapshots = refreshMissingNewAndHotSnapshots(newAndHotSectionType, newAndHotSnapshots)
|
val refreshedNewAndHotSnapshots = refreshMissingNewAndHotSnapshots(
|
||||||
|
newAndHotSectionType,
|
||||||
|
newAndHotSnapshots,
|
||||||
|
offset = 0,
|
||||||
|
limit = NEW_AND_HOT_HOME_LIMIT
|
||||||
|
)
|
||||||
|
|
||||||
return AudioRecommendations(
|
return AudioRecommendations(
|
||||||
banners = queryPort.findBanners(BANNER_LIMIT, memberId, canViewAdultContent),
|
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 {
|
fun resolveVisibility(member: Member?): AudioRecommendationVisibility {
|
||||||
return if (canViewAdultContent(member)) AudioRecommendationVisibility.ALL else AudioRecommendationVisibility.SAFE
|
return if (canViewAdultContent(member)) AudioRecommendationVisibility.ALL else AudioRecommendationVisibility.SAFE
|
||||||
}
|
}
|
||||||
@@ -93,7 +115,9 @@ class AudioRecommendationQueryService(
|
|||||||
|
|
||||||
private fun refreshMissingNewAndHotSnapshots(
|
private fun refreshMissingNewAndHotSnapshots(
|
||||||
sectionType: RecommendedSectionType,
|
sectionType: RecommendedSectionType,
|
||||||
snapshots: List<RecommendationSnapshotRecord>
|
snapshots: List<RecommendationSnapshotRecord>,
|
||||||
|
offset: Long,
|
||||||
|
limit: Int
|
||||||
): List<RecommendationSnapshotRecord> {
|
): List<RecommendationSnapshotRecord> {
|
||||||
if (snapshots.isNotEmpty()) return snapshots
|
if (snapshots.isNotEmpty()) return snapshots
|
||||||
val today = LocalDate.now(KST_ZONE)
|
val today = LocalDate.now(KST_ZONE)
|
||||||
@@ -107,7 +131,7 @@ class AudioRecommendationQueryService(
|
|||||||
marker.delete()
|
marker.delete()
|
||||||
throw ex
|
throw ex
|
||||||
}
|
}
|
||||||
return snapshotPort.findLatestSnapshots(sectionType, limit = NEW_AND_HOT_AUDIO_LIMIT)
|
return snapshotPort.findLatestSnapshots(sectionType, offset, limit)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun newAndHotLazyRefreshMarkerKey(date: LocalDate): String {
|
private fun newAndHotLazyRefreshMarkerKey(date: LocalDate): String {
|
||||||
@@ -125,7 +149,7 @@ class AudioRecommendationQueryService(
|
|||||||
const val LATEST_AUDIO_LIMIT = 12
|
const val LATEST_AUDIO_LIMIT = 12
|
||||||
const val FREE_AUDIO_LIMIT = 10
|
const val FREE_AUDIO_LIMIT = 10
|
||||||
const val POINT_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 MOST_COMMENTED_AUDIO_LIMIT = 5
|
||||||
const val RECOMMENDED_AUDIO_LIMIT = 10
|
const val RECOMMENDED_AUDIO_LIMIT = 10
|
||||||
private const val LAZY_REFRESH_MARKER_KEY_PREFIX = "audio-recommendation:new-and-hot:lazy-refresh-attempted"
|
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
|
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.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.AudioRecommendationVisibility
|
||||||
import kr.co.vividnext.sodalive.v2.content.recommendation.port.out.AudioRecommendationQueryPort
|
import kr.co.vividnext.sodalive.v2.content.recommendation.port.out.AudioRecommendationQueryPort
|
||||||
import kr.co.vividnext.sodalive.v2.recommendation.domain.RecommendedSectionType
|
import kr.co.vividnext.sodalive.v2.recommendation.domain.RecommendedSectionType
|
||||||
@@ -51,7 +54,7 @@ class AudioRecommendationQueryServiceTest {
|
|||||||
.findLatestSnapshots(
|
.findLatestSnapshots(
|
||||||
RecommendedSectionType.NEW_AND_HOT_AUDIO_SAFE,
|
RecommendedSectionType.NEW_AND_HOT_AUDIO_SAFE,
|
||||||
0,
|
0,
|
||||||
AudioRecommendationQueryService.NEW_AND_HOT_AUDIO_LIMIT
|
AudioRecommendationQueryService.NEW_AND_HOT_HOME_LIMIT
|
||||||
)
|
)
|
||||||
Mockito.doReturn(emptyList<RecommendationSnapshotRecord>())
|
Mockito.doReturn(emptyList<RecommendationSnapshotRecord>())
|
||||||
.`when`(snapshotPort)
|
.`when`(snapshotPort)
|
||||||
@@ -100,7 +103,7 @@ class AudioRecommendationQueryServiceTest {
|
|||||||
.findLatestSnapshots(
|
.findLatestSnapshots(
|
||||||
RecommendedSectionType.NEW_AND_HOT_AUDIO_SAFE,
|
RecommendedSectionType.NEW_AND_HOT_AUDIO_SAFE,
|
||||||
0,
|
0,
|
||||||
AudioRecommendationQueryService.NEW_AND_HOT_AUDIO_LIMIT
|
AudioRecommendationQueryService.NEW_AND_HOT_HOME_LIMIT
|
||||||
)
|
)
|
||||||
Mockito.doReturn(emptyList<RecommendationSnapshotRecord>())
|
Mockito.doReturn(emptyList<RecommendationSnapshotRecord>())
|
||||||
.`when`(snapshotPort)
|
.`when`(snapshotPort)
|
||||||
@@ -127,19 +130,14 @@ class AudioRecommendationQueryServiceTest {
|
|||||||
@Test
|
@Test
|
||||||
@DisplayName("인증 회원 성인 정책은 조회용 저장 preference를 사용한다")
|
@DisplayName("인증 회원 성인 정책은 조회용 저장 preference를 사용한다")
|
||||||
fun shouldUseStoredPreferenceForMemberAdultVisibility() {
|
fun shouldUseStoredPreferenceForMemberAdultVisibility() {
|
||||||
val member = kr.co.vividnext.sodalive.member.Member(
|
val member = member(id = 10L)
|
||||||
email = "adult@test.com",
|
|
||||||
password = "password",
|
|
||||||
nickname = "adult",
|
|
||||||
role = kr.co.vividnext.sodalive.member.MemberRole.USER
|
|
||||||
)
|
|
||||||
Mockito.doReturn(true).`when`(preferenceService).canViewAdultContent(member)
|
Mockito.doReturn(true).`when`(preferenceService).canViewAdultContent(member)
|
||||||
Mockito.doReturn(listOf(snapshot(RecommendedSectionType.NEW_AND_HOT_AUDIO_ALL, 10L)))
|
Mockito.doReturn(listOf(snapshot(RecommendedSectionType.NEW_AND_HOT_AUDIO_ALL, 10L)))
|
||||||
.`when`(snapshotPort)
|
.`when`(snapshotPort)
|
||||||
.findLatestSnapshots(
|
.findLatestSnapshots(
|
||||||
RecommendedSectionType.NEW_AND_HOT_AUDIO_ALL,
|
RecommendedSectionType.NEW_AND_HOT_AUDIO_ALL,
|
||||||
0,
|
0,
|
||||||
AudioRecommendationQueryService.NEW_AND_HOT_AUDIO_LIMIT
|
AudioRecommendationQueryService.NEW_AND_HOT_HOME_LIMIT
|
||||||
)
|
)
|
||||||
|
|
||||||
service.getRecommendations(member)
|
service.getRecommendations(member)
|
||||||
@@ -149,10 +147,52 @@ class AudioRecommendationQueryServiceTest {
|
|||||||
Mockito.verify(snapshotPort).findLatestSnapshots(
|
Mockito.verify(snapshotPort).findLatestSnapshots(
|
||||||
RecommendedSectionType.NEW_AND_HOT_AUDIO_ALL,
|
RecommendedSectionType.NEW_AND_HOT_AUDIO_ALL,
|
||||||
0,
|
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
|
@Test
|
||||||
@DisplayName("visibility와 섹션 조합은 신규 RecommendedSectionType으로 매핑된다")
|
@DisplayName("visibility와 섹션 조합은 신규 RecommendedSectionType으로 매핑된다")
|
||||||
fun shouldMapVisibilityToAudioSectionTypes() {
|
fun shouldMapVisibilityToAudioSectionTypes() {
|
||||||
@@ -186,6 +226,32 @@ class AudioRecommendationQueryServiceTest {
|
|||||||
return Mockito.any(LocalDateTime::class.java) ?: LocalDateTime.now()
|
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 {
|
private fun snapshot(sectionType: RecommendedSectionType, targetId: Long): RecommendationSnapshotRecord {
|
||||||
return RecommendationSnapshotRecord(
|
return RecommendationSnapshotRecord(
|
||||||
sectionType = sectionType,
|
sectionType = sectionType,
|
||||||
|
|||||||
Reference in New Issue
Block a user