fix(content): 무료 콘텐츠 포인트 사용 조건을 보정한다
This commit is contained in:
@@ -967,7 +967,7 @@ class AudioContentService(
|
||||
previousContent = previousContent,
|
||||
nextContent = nextContent,
|
||||
buyerList = buyerList,
|
||||
isAvailableUsePoint = audioContent.isPointAvailable,
|
||||
isAvailableUsePoint = audioContent.isPointAvailable && audioContent.price > 0,
|
||||
translated = translated
|
||||
)
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ data class MainContentAudioResponse(
|
||||
imageUrl = audio.imageUrl,
|
||||
price = audio.price,
|
||||
isAdult = audio.isAdult,
|
||||
isPointAvailable = audio.isPointAvailable,
|
||||
isPointAvailable = audio.isPointAvailable && audio.price > 0,
|
||||
isFirstContent = audio.isFirstContent,
|
||||
isOriginalSeries = audio.isOriginalSeries,
|
||||
creatorNickname = audio.creatorNickname
|
||||
|
||||
@@ -46,7 +46,7 @@ data class ContentOverviewItemResponse(
|
||||
title = audio.title,
|
||||
coverImage = audio.imageUrl,
|
||||
price = audio.price,
|
||||
isPointAvailable = audio.isPointAvailable,
|
||||
isPointAvailable = audio.isPointAvailable && audio.price > 0,
|
||||
creatorNickname = audio.creatorNickname,
|
||||
isAdult = audio.isAdult,
|
||||
isFirstContent = audio.isFirstContent,
|
||||
@@ -65,7 +65,7 @@ data class ContentOverviewItemResponse(
|
||||
title = audio.title,
|
||||
coverImage = coverImage,
|
||||
price = audio.price,
|
||||
isPointAvailable = audio.isPointAvailable,
|
||||
isPointAvailable = audio.isPointAvailable && audio.price > 0,
|
||||
creatorNickname = audio.creatorNickname,
|
||||
isAdult = isAdult,
|
||||
isFirstContent = true,
|
||||
|
||||
@@ -69,7 +69,7 @@ data class AudioCardResponse(
|
||||
imageUrl = audio.imageUrl,
|
||||
price = audio.price,
|
||||
isAdult = audio.isAdult,
|
||||
isPointAvailable = audio.isPointAvailable,
|
||||
isPointAvailable = audio.isPointAvailable && audio.price > 0,
|
||||
isFirstContent = audio.isFirstContent,
|
||||
isOriginalSeries = audio.isOriginalSeries,
|
||||
creatorNickname = audio.creatorNickname
|
||||
|
||||
@@ -32,7 +32,7 @@ data class CreatorChannelAudioContentResponse(
|
||||
imageUrl = audioContent.imageUrl,
|
||||
price = audioContent.price,
|
||||
isAdult = audioContent.isAdult,
|
||||
isPointAvailable = audioContent.isPointAvailable,
|
||||
isPointAvailable = audioContent.isPointAvailable && audioContent.price > 0,
|
||||
isFirstContent = audioContent.isFirstContent,
|
||||
seriesName = audioContent.seriesName,
|
||||
isOriginalSeries = audioContent.isOriginalSeries,
|
||||
|
||||
@@ -267,7 +267,7 @@ class HomeRecommendationFacade(
|
||||
title = title,
|
||||
price = price,
|
||||
coverImage = imageUrl(cloudFrontHost, coverImage),
|
||||
isPointAvailable = isPointAvailable
|
||||
isPointAvailable = isPointAvailable && price > 0
|
||||
)
|
||||
|
||||
private fun HomeAiCharacterRecommendationRecord.toItem() = HomeAiCharacterItem(
|
||||
|
||||
@@ -406,7 +406,7 @@ class DefaultMainContentAllQueryRepository(
|
||||
}
|
||||
|
||||
private fun optionalAudioPointCondition(onlyPointAvailable: Boolean): BooleanExpression? {
|
||||
return if (onlyPointAvailable) audioContent.isPointAvailable.isTrue else null
|
||||
return if (onlyPointAvailable) audioContent.isPointAvailable.isTrue.and(audioContent.price.gt(0)) else null
|
||||
}
|
||||
|
||||
private fun optionalOriginalCondition(onlyOriginal: Boolean): BooleanExpression? {
|
||||
|
||||
@@ -135,7 +135,12 @@ class DefaultAudioRecommendationQueryRepository(
|
||||
now: LocalDateTime
|
||||
): List<AudioCard> {
|
||||
val randomTieBreaker = Expressions.numberTemplate(Double::class.java, "function('rand')")
|
||||
val rows = audioRows(memberId, canViewAdultContent, now, audioContent.isPointAvailable.isTrue) {
|
||||
val rows = audioRows(
|
||||
memberId,
|
||||
canViewAdultContent,
|
||||
now,
|
||||
audioContent.isPointAvailable.isTrue.and(audioContent.price.gt(0))
|
||||
) {
|
||||
orderBy(randomTieBreaker.asc()).limit(limit.toLong())
|
||||
}
|
||||
return rows.toAudioCards(now, canViewAdultContent)
|
||||
|
||||
@@ -26,6 +26,7 @@ import kr.co.vividnext.sodalive.member.block.BlockMemberRepository
|
||||
import kr.co.vividnext.sodalive.v2.home.following.application.HomeFollowingNewsPublishService
|
||||
import kr.co.vividnext.sodalive.v2.recommendation.application.CreatorContentViewHistoryService
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertNull
|
||||
import org.junit.jupiter.api.Assertions.assertThrows
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
@@ -282,6 +283,50 @@ class AudioContentServiceTest {
|
||||
assertTrue(output.out.contains("contentId=${audioContent.id}"))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("콘텐츠 상세는 가격과 저장값을 함께 반영해 포인트 사용 가능 여부를 반환한다")
|
||||
fun shouldReturnEffectivePointAvailabilityForAudioDetail() {
|
||||
val viewer = createMember(id = 1004L, nickname = "free-detail-viewer")
|
||||
val creator = createMember(id = 2004L, nickname = "free-detail-creator")
|
||||
val audioContent = createAudioContent(creator)
|
||||
audioContent.price = 0
|
||||
audioContent.isPointAvailable = true
|
||||
stubSuccessfulDetailDependencies(viewer, creator, audioContent)
|
||||
|
||||
val response = service.getDetail(
|
||||
id = audioContent.id!!,
|
||||
member = viewer,
|
||||
isAdultContentVisible = false,
|
||||
timezone = "Asia/Seoul"
|
||||
)
|
||||
|
||||
assertFalse(response.isAvailableUsePoint)
|
||||
|
||||
audioContent.price = 100
|
||||
audioContent.isPointAvailable = false
|
||||
|
||||
val paidUnavailableResponse = service.getDetail(
|
||||
id = audioContent.id!!,
|
||||
member = viewer,
|
||||
isAdultContentVisible = false,
|
||||
timezone = "Asia/Seoul"
|
||||
)
|
||||
|
||||
assertFalse(paidUnavailableResponse.isAvailableUsePoint)
|
||||
|
||||
audioContent.price = 100
|
||||
audioContent.isPointAvailable = true
|
||||
|
||||
val paidAvailableResponse = service.getDetail(
|
||||
id = audioContent.id!!,
|
||||
member = viewer,
|
||||
isAdultContentVisible = false,
|
||||
timezone = "Asia/Seoul"
|
||||
)
|
||||
|
||||
assertTrue(paidAvailableResponse.isAvailableUsePoint)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("업로드 완료 시 즉시 공개 콘텐츠는 최근 소식을 발행한다")
|
||||
fun shouldPublishNewsWhenUploadCompleteMakesContentPublicImmediately() {
|
||||
|
||||
@@ -339,7 +339,8 @@ class AiCharacterAdminAudioContentControllerTest @Autowired constructor(
|
||||
val content = saveAudioContent(
|
||||
owner = character.creatorMember!!,
|
||||
title = "target night walk",
|
||||
contentPath = "private/target-night-walk.mp3"
|
||||
contentPath = "private/target-night-walk.mp3",
|
||||
price = 0
|
||||
)
|
||||
val otherCharacter = chatCharacterService.createChatCharacterWithDetails(
|
||||
characterUUID = "v2-audio-other-character",
|
||||
@@ -374,6 +375,7 @@ class AiCharacterAdminAudioContentControllerTest @Autowired constructor(
|
||||
.andExpect(jsonPath("$.data.totalCount").value(1))
|
||||
.andExpect(jsonPath("$.data.items[0].audioContentId").value(content.id))
|
||||
.andExpect(jsonPath("$.data.items[0].title").value("target night walk"))
|
||||
.andExpect(jsonPath("$.data.items[0].isPointAvailable").value(true))
|
||||
.andExpect(
|
||||
jsonPath("$.data.items[0].contentUrl").value(
|
||||
"https://signed.example.com/private/target-night-walk.mp3?Expires=1"
|
||||
@@ -495,7 +497,8 @@ class AiCharacterAdminAudioContentControllerTest @Autowired constructor(
|
||||
val content = saveAudioContent(
|
||||
owner = character.creatorMember!!,
|
||||
title = "detail night walk",
|
||||
contentPath = "private/detail-night-walk.mp3"
|
||||
contentPath = "private/detail-night-walk.mp3",
|
||||
price = 0
|
||||
)
|
||||
entityManager.flush()
|
||||
Mockito.`when`(audioContentCloudFront.generateSignedURL(Mockito.anyString(), Mockito.anyLong()))
|
||||
@@ -899,7 +902,8 @@ class AiCharacterAdminAudioContentControllerTest @Autowired constructor(
|
||||
owner: Member,
|
||||
title: String,
|
||||
contentPath: String,
|
||||
releaseDate: LocalDateTime = Instant.now().minusSeconds(60).atOffset(ZoneOffset.UTC).toLocalDateTime()
|
||||
releaseDate: LocalDateTime = Instant.now().minusSeconds(60).atOffset(ZoneOffset.UTC).toLocalDateTime(),
|
||||
price: Int = 100
|
||||
): AudioContent {
|
||||
val theme = AudioContentTheme(theme = "theme-$title", image = "theme.png")
|
||||
entityManager.persist(theme)
|
||||
@@ -907,7 +911,7 @@ class AiCharacterAdminAudioContentControllerTest @Autowired constructor(
|
||||
title = title,
|
||||
detail = "detail",
|
||||
languageCode = "ko",
|
||||
price = 100,
|
||||
price = price,
|
||||
isAdult = false,
|
||||
isPointAvailable = true,
|
||||
isCommentAvailable = true
|
||||
|
||||
@@ -94,6 +94,27 @@ class MainContentAllEndToEndTest @Autowired constructor(
|
||||
.andExpect(jsonPath("$.data.audios.length()").value(1))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("전체 탭 POINT API는 무료 포인트 콘텐츠를 목록과 페이징 후보에서 제외한다")
|
||||
fun shouldExcludeFreePointAudioContentsForPointType() {
|
||||
val fixture = createAudioFixture("main-all-point-paid-e2e", freeControl = true)
|
||||
|
||||
mockMvc.perform(
|
||||
get("/api/v2/audio/contents")
|
||||
.param("type", "POINT")
|
||||
.param("sort", "LATEST")
|
||||
.param("page", "0")
|
||||
.param("size", "1")
|
||||
)
|
||||
.andExpect(status().isOk)
|
||||
.andExpect(jsonPath("$.data.type").value("POINT"))
|
||||
.andExpect(jsonPath("$.data.totalCount").value(1))
|
||||
.andExpect(jsonPath("$.data.audios.length()").value(1))
|
||||
.andExpect(jsonPath("$.data.audios[0].audioContentId").value(fixture.audioContentId))
|
||||
.andExpect(jsonPath("$.data.audios[0].price").value(100))
|
||||
.andExpect(jsonPath("$.data.hasNext").value(false))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("전체 탭 SERIES API는 dayOfWeek 조건으로 시리즈 응답과 빈 audios를 반환한다")
|
||||
fun shouldReturnSeriesContentsFilteredByDayOfWeekThroughControllerServiceAndRepository() {
|
||||
|
||||
@@ -67,6 +67,28 @@ class MainContentAllTabResponseTest {
|
||||
isFirstContent = true,
|
||||
isOriginalSeries = false,
|
||||
creatorNickname = "creator"
|
||||
),
|
||||
MainContentAllAudio(
|
||||
audioContentId = 2L,
|
||||
title = "free audio",
|
||||
imageUrl = "https://cdn/free-audio.jpg",
|
||||
price = 0,
|
||||
isAdult = false,
|
||||
isPointAvailable = true,
|
||||
isFirstContent = false,
|
||||
isOriginalSeries = false,
|
||||
creatorNickname = "creator"
|
||||
),
|
||||
MainContentAllAudio(
|
||||
audioContentId = 3L,
|
||||
title = "point unavailable audio",
|
||||
imageUrl = "https://cdn/point-unavailable-audio.jpg",
|
||||
price = 100,
|
||||
isAdult = false,
|
||||
isPointAvailable = false,
|
||||
isFirstContent = false,
|
||||
isOriginalSeries = false,
|
||||
creatorNickname = "creator"
|
||||
)
|
||||
),
|
||||
series = emptyList(),
|
||||
@@ -83,6 +105,8 @@ class MainContentAllTabResponseTest {
|
||||
assertEquals(true, json["audios"][0]["isPointAvailable"].asBoolean())
|
||||
assertEquals(true, json["audios"][0]["isFirstContent"].asBoolean())
|
||||
assertEquals(false, json["audios"][0]["isOriginalSeries"].asBoolean())
|
||||
assertEquals(false, json["audios"][1]["isPointAvailable"].asBoolean())
|
||||
assertEquals(false, json["audios"][2]["isPointAvailable"].asBoolean())
|
||||
assertEquals(true, json["hasNext"].asBoolean())
|
||||
assertFalse(json["audios"][0].has("duration"))
|
||||
assertFalse(json["series"].any { it.has("publishedDaysOfWeek") })
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
package kr.co.vividnext.sodalive.v2.api.content.overview.dto
|
||||
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import kr.co.vividnext.sodalive.v2.content.recommendation.domain.AudioCard
|
||||
import kr.co.vividnext.sodalive.v2.recommendation.port.out.HomeFirstAudioContentRecord
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
@@ -77,4 +80,71 @@ class ContentOverviewPageResponseTest {
|
||||
assertEquals(true, response.isAdult)
|
||||
assertEquals(true, response.isOriginalSeries)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("NEW_AND_HOT 응답은 무료 콘텐츠의 포인트 가능 여부를 false로 보정한다")
|
||||
fun shouldNormalizePointAvailabilityForFreeNewAndHotAudio() {
|
||||
val freeAudio = audioCard(price = 0, isPointAvailable = true)
|
||||
val paidAudio = audioCard(price = 100, isPointAvailable = true)
|
||||
val paidUnavailableAudio = audioCard(price = 100, isPointAvailable = false)
|
||||
|
||||
assertFalse(ContentOverviewItemResponse.fromNewAndHot(freeAudio).isPointAvailable)
|
||||
assertTrue(ContentOverviewItemResponse.fromNewAndHot(paidAudio).isPointAvailable)
|
||||
assertFalse(ContentOverviewItemResponse.fromNewAndHot(paidUnavailableAudio).isPointAvailable)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("첫 오디오 응답은 무료 콘텐츠의 포인트 가능 여부를 false로 보정한다")
|
||||
fun shouldNormalizePointAvailabilityForFreeFirstAudioContent() {
|
||||
val freeAudio = firstAudio(price = 0, isPointAvailable = true)
|
||||
val paidAudio = firstAudio(price = 100, isPointAvailable = true)
|
||||
val paidUnavailableAudio = firstAudio(price = 100, isPointAvailable = false)
|
||||
|
||||
assertFalse(
|
||||
ContentOverviewItemResponse.fromFirstAudioContent(freeAudio, "https://cdn.test/free.png", false, false)
|
||||
.isPointAvailable
|
||||
)
|
||||
assertTrue(
|
||||
ContentOverviewItemResponse.fromFirstAudioContent(paidAudio, "https://cdn.test/paid.png", false, false)
|
||||
.isPointAvailable
|
||||
)
|
||||
assertFalse(
|
||||
ContentOverviewItemResponse.fromFirstAudioContent(
|
||||
paidUnavailableAudio,
|
||||
"https://cdn.test/paid-unavailable.png",
|
||||
false,
|
||||
false
|
||||
).isPointAvailable
|
||||
)
|
||||
}
|
||||
|
||||
private fun audioCard(price: Int, isPointAvailable: Boolean): AudioCard {
|
||||
return AudioCard(
|
||||
audioContentId = price.toLong() + 1,
|
||||
title = "audio",
|
||||
duration = "00:01",
|
||||
imageUrl = "https://cdn.test/audio.png",
|
||||
price = price,
|
||||
isAdult = false,
|
||||
isPointAvailable = isPointAvailable,
|
||||
isFirstContent = true,
|
||||
isOriginalSeries = false,
|
||||
creatorNickname = "creator"
|
||||
)
|
||||
}
|
||||
|
||||
private fun firstAudio(price: Int, isPointAvailable: Boolean): HomeFirstAudioContentRecord {
|
||||
return HomeFirstAudioContentRecord(
|
||||
contentId = price.toLong() + 1,
|
||||
creatorId = 10L,
|
||||
creatorNickname = "creator",
|
||||
creatorProfileImage = null,
|
||||
title = "first audio",
|
||||
price = price,
|
||||
coverImage = "cover/audio.png",
|
||||
isPointAvailable = isPointAvailable,
|
||||
isAdult = false,
|
||||
isOriginalSeries = false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +54,9 @@ class AudioRecommendationEndToEndTest @Autowired constructor(
|
||||
.andExpect(jsonPath("$.data.latestAudios[0].isOriginalSeries").value(true))
|
||||
.andExpect(jsonPath("$.data.recommendedAudios").isArray)
|
||||
.andExpect(jsonPath("$.data.recommendedAudios[0].audioContentId").value(fixture.audioContentId))
|
||||
.andExpect(jsonPath("$.data.pointAudios.length()").value(1))
|
||||
.andExpect(jsonPath("$.data.pointAudios[0].audioContentId").value(fixture.paidPointAudioContentId))
|
||||
.andExpect(jsonPath("$.data.pointAudios[0].price").value(100))
|
||||
.andExpect(jsonPath("$.data.mostCommentedAudios[0].latestComment").value("latest e2e comment"))
|
||||
.andExpect(
|
||||
jsonPath("$.data.mostCommentedAudios[0].latestCommentWriterProfileImageUrl")
|
||||
@@ -68,6 +71,7 @@ class AudioRecommendationEndToEndTest @Autowired constructor(
|
||||
val writer = saveMember("audio-recommendation-e2e-writer", MemberRole.USER, profileImage = "comment-writer.png")
|
||||
val theme = saveTheme()
|
||||
val audio = saveAudio(creator, theme, now)
|
||||
val paidPointAudio = saveAudio(creator, theme, now.minusMinutes(10), "paid-point", 100)
|
||||
val series = saveSeries(creator)
|
||||
saveSeriesContent(series, audio)
|
||||
saveComment(audio, writer, "latest e2e comment", now.plusMinutes(10))
|
||||
@@ -79,7 +83,8 @@ class AudioRecommendationEndToEndTest @Autowired constructor(
|
||||
|
||||
Fixture(
|
||||
seriesId = series.id!!,
|
||||
audioContentId = audio.id!!
|
||||
audioContentId = audio.id!!,
|
||||
paidPointAudioContentId = paidPointAudio.id!!
|
||||
)
|
||||
}!!
|
||||
}
|
||||
@@ -102,14 +107,20 @@ class AudioRecommendationEndToEndTest @Autowired constructor(
|
||||
return theme
|
||||
}
|
||||
|
||||
private fun saveAudio(creator: Member, theme: AudioContentTheme, releaseDate: LocalDateTime): AudioContent {
|
||||
private fun saveAudio(
|
||||
creator: Member,
|
||||
theme: AudioContentTheme,
|
||||
releaseDate: LocalDateTime,
|
||||
title: String = "audio-recommendation-e2e",
|
||||
price: Int = 0
|
||||
): AudioContent {
|
||||
val audio = AudioContent(
|
||||
title = "audio-recommendation-e2e",
|
||||
title = title,
|
||||
detail = "detail",
|
||||
languageCode = "ko",
|
||||
releaseDate = releaseDate,
|
||||
isAdult = false,
|
||||
price = 0,
|
||||
price = price,
|
||||
isPointAvailable = true
|
||||
)
|
||||
audio.member = creator
|
||||
@@ -174,6 +185,7 @@ class AudioRecommendationEndToEndTest @Autowired constructor(
|
||||
|
||||
private data class Fixture(
|
||||
val seriesId: Long,
|
||||
val audioContentId: Long
|
||||
val audioContentId: Long,
|
||||
val paidPointAudioContentId: Long
|
||||
)
|
||||
}
|
||||
|
||||
@@ -33,10 +33,13 @@ class AudioRecommendationFacadeTest {
|
||||
assertEquals(1, response.mostCommentedAudios.size)
|
||||
assertEquals(1, response.recommendedAudios.size)
|
||||
assertEquals(false, response.latestAudios[0].isOriginalSeries)
|
||||
assertEquals(false, response.latestAudios[0].isPointAvailable)
|
||||
assertEquals(true, response.newAndHotAudios[0].isPointAvailable)
|
||||
assertEquals(false, response.recommendedAudios[0].isPointAvailable)
|
||||
|
||||
val json = objectMapper.readTree(objectMapper.writeValueAsString(response))
|
||||
assertEquals(false, json["latestAudios"][0]["isAdult"].asBoolean())
|
||||
assertEquals(true, json["latestAudios"][0]["isPointAvailable"].asBoolean())
|
||||
assertEquals(false, json["latestAudios"][0]["isPointAvailable"].asBoolean())
|
||||
assertEquals(true, json["latestAudios"][0]["isFirstContent"].asBoolean())
|
||||
assertEquals(false, json["latestAudios"][0]["isOriginalSeries"].asBoolean())
|
||||
assertEquals(false, json["latestAudios"][0].has("adult"))
|
||||
@@ -61,7 +64,7 @@ class AudioRecommendationFacadeTest {
|
||||
banners = listOf(RecommendationBanner("https://cdn.test/banner.png", null, null, null, "https://link.test")),
|
||||
originalSeries = listOf(OriginalSeries(2L, "https://cdn.test/series.png")),
|
||||
latestAudios = listOf(card),
|
||||
newAndHotAudios = listOf(card.copy(audioContentId = 3L)),
|
||||
newAndHotAudios = listOf(card.copy(audioContentId = 3L, price = 100)),
|
||||
freeAudios = listOf(card.copy(audioContentId = 4L)),
|
||||
pointAudios = listOf(card.copy(audioContentId = 5L)),
|
||||
mostCommentedAudios = listOf(
|
||||
@@ -73,7 +76,7 @@ class AudioRecommendationFacadeTest {
|
||||
latestCommentWriterProfileImageUrl = "https://cdn.test/profile.png"
|
||||
)
|
||||
),
|
||||
recommendedAudios = listOf(card.copy(audioContentId = 7L))
|
||||
recommendedAudios = listOf(card.copy(audioContentId = 7L, price = 100, isPointAvailable = false))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package kr.co.vividnext.sodalive.v2.api.creator.channel.common.dto
|
||||
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.common.domain.CreatorChannelAudioContent
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class CreatorChannelAudioContentResponseTest {
|
||||
@Test
|
||||
@DisplayName("크리에이터 채널 오디오 응답은 무료 콘텐츠의 포인트 가능 여부를 false로 보정한다")
|
||||
fun shouldNormalizePointAvailabilityForFreeAudioContent() {
|
||||
val freeAudio = audioContent(price = 0, isPointAvailable = true)
|
||||
val paidAudio = audioContent(price = 100, isPointAvailable = true)
|
||||
val paidUnavailableAudio = audioContent(price = 100, isPointAvailable = false)
|
||||
|
||||
assertFalse(CreatorChannelAudioContentResponse.from(freeAudio).isPointAvailable)
|
||||
assertTrue(CreatorChannelAudioContentResponse.from(paidAudio).isPointAvailable)
|
||||
assertFalse(CreatorChannelAudioContentResponse.from(paidUnavailableAudio).isPointAvailable)
|
||||
}
|
||||
|
||||
private fun audioContent(price: Int, isPointAvailable: Boolean): CreatorChannelAudioContent {
|
||||
return CreatorChannelAudioContent(
|
||||
audioContentId = price.toLong() + 1,
|
||||
title = "audio",
|
||||
duration = "00:10:00",
|
||||
imageUrl = "audio.png",
|
||||
price = price,
|
||||
isAdult = false,
|
||||
isPointAvailable = isPointAvailable,
|
||||
isFirstContent = true,
|
||||
seriesName = "series",
|
||||
isOriginalSeries = true,
|
||||
isOwned = false,
|
||||
isRented = false
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
package kr.co.vividnext.sodalive.v2.api.home.application
|
||||
|
||||
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
|
||||
import kr.co.vividnext.sodalive.v2.recommendation.application.HomeRecommendationQueryService
|
||||
import kr.co.vividnext.sodalive.v2.recommendation.domain.RecommendedSectionType
|
||||
import kr.co.vividnext.sodalive.v2.recommendation.port.out.HomeAiCharacterRecommendationRecord
|
||||
import kr.co.vividnext.sodalive.v2.recommendation.port.out.HomeBannerRecommendationRecord
|
||||
import kr.co.vividnext.sodalive.v2.recommendation.port.out.HomeCheerCreatorRecommendationRecord
|
||||
import kr.co.vividnext.sodalive.v2.recommendation.port.out.HomeFirstAudioContentRecord
|
||||
import kr.co.vividnext.sodalive.v2.recommendation.port.out.HomeGenreCreatorRecommendationGroup
|
||||
import kr.co.vividnext.sodalive.v2.recommendation.port.out.HomeLiveRecommendationRecord
|
||||
import kr.co.vividnext.sodalive.v2.recommendation.port.out.HomePopularCommunityRecommendationRecord
|
||||
import kr.co.vividnext.sodalive.v2.recommendation.port.out.HomeRecommendationQueryPort
|
||||
import kr.co.vividnext.sodalive.v2.recommendation.port.out.RecentDebutCreatorRecord
|
||||
import kr.co.vividnext.sodalive.v2.recommendation.port.out.RecentlyActiveCreatorRecord
|
||||
import kr.co.vividnext.sodalive.v2.recommendation.port.out.RecommendationSnapshotPort
|
||||
import kr.co.vividnext.sodalive.v2.recommendation.port.out.RecommendationSnapshotRecord
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.mockito.Mockito
|
||||
import java.time.LocalDateTime
|
||||
|
||||
class HomeRecommendationFacadeTest {
|
||||
private val queryPort = FakeHomeRecommendationQueryPort()
|
||||
private val queryService = HomeRecommendationQueryService(queryPort, EmptyRecommendationSnapshotPort())
|
||||
private val preferenceService = Mockito.mock(MemberContentPreferenceService::class.java)
|
||||
private val facade = HomeRecommendationFacade(queryService, preferenceService, "https://cdn.test")
|
||||
|
||||
@Test
|
||||
@DisplayName("홈 첫 오디오 응답은 무료 콘텐츠의 포인트 가능 여부를 false로 보정한다")
|
||||
fun shouldNormalizePointAvailabilityForFreeFirstAudioContent() {
|
||||
queryPort.firstAudioContents = listOf(
|
||||
firstAudio(price = 0, isPointAvailable = true),
|
||||
firstAudio(price = 100, isPointAvailable = true),
|
||||
firstAudio(price = 100, isPointAvailable = false)
|
||||
)
|
||||
|
||||
val response = facade.getHomeRecommendations(null)
|
||||
|
||||
assertFalse(response.firstAudioContents[0].isPointAvailable)
|
||||
assertTrue(response.firstAudioContents[1].isPointAvailable)
|
||||
assertFalse(response.firstAudioContents[2].isPointAvailable)
|
||||
}
|
||||
|
||||
private fun firstAudio(price: Int, isPointAvailable: Boolean): HomeFirstAudioContentRecord {
|
||||
return HomeFirstAudioContentRecord(
|
||||
contentId = price.toLong() + 1,
|
||||
creatorId = 10L,
|
||||
creatorNickname = "creator",
|
||||
creatorProfileImage = null,
|
||||
title = "first audio",
|
||||
price = price,
|
||||
coverImage = "cover/audio.png",
|
||||
isPointAvailable = isPointAvailable,
|
||||
isAdult = false,
|
||||
isOriginalSeries = false
|
||||
)
|
||||
}
|
||||
|
||||
private class FakeHomeRecommendationQueryPort : HomeRecommendationQueryPort {
|
||||
var firstAudioContents: List<HomeFirstAudioContentRecord> = emptyList()
|
||||
|
||||
override fun findLiveRecommendations(
|
||||
offset: Long,
|
||||
limit: Int,
|
||||
memberId: Long?,
|
||||
includeAdultLives: Boolean
|
||||
): List<HomeLiveRecommendationRecord> = emptyList()
|
||||
|
||||
override fun findHomeBanners(limit: Int, memberId: Long?): List<HomeBannerRecommendationRecord> = emptyList()
|
||||
|
||||
override fun findRecentlyActiveCreators(
|
||||
limit: Int,
|
||||
memberId: Long?,
|
||||
includeAdultActivities: Boolean
|
||||
): List<RecentlyActiveCreatorRecord> = emptyList()
|
||||
|
||||
override fun findRecentDebutCreators(
|
||||
now: LocalDateTime,
|
||||
offset: Long,
|
||||
limit: Int,
|
||||
memberId: Long?,
|
||||
includeAdultContents: Boolean
|
||||
): List<RecentDebutCreatorRecord> = emptyList()
|
||||
|
||||
override fun findFirstAudioContents(
|
||||
now: LocalDateTime,
|
||||
offset: Long,
|
||||
limit: Int,
|
||||
memberId: Long?,
|
||||
includeAdultContents: Boolean
|
||||
): List<HomeFirstAudioContentRecord> = firstAudioContents
|
||||
|
||||
override fun findAiCharacterSnapshots(
|
||||
windowStart: LocalDateTime,
|
||||
windowEndExclusive: LocalDateTime,
|
||||
limit: Int
|
||||
): List<RecommendationSnapshotRecord> = emptyList()
|
||||
|
||||
override fun findCheerCreatorSnapshots(
|
||||
windowStart: LocalDateTime,
|
||||
windowEndExclusive: LocalDateTime,
|
||||
limit: Int
|
||||
): List<RecommendationSnapshotRecord> = emptyList()
|
||||
|
||||
override fun findPopularCommunitySnapshots(
|
||||
windowStart: LocalDateTime,
|
||||
windowEndExclusive: LocalDateTime,
|
||||
limit: Int
|
||||
): List<RecommendationSnapshotRecord> = emptyList()
|
||||
|
||||
override fun findAiCharacterRecommendationDetails(
|
||||
characterIds: List<Long>
|
||||
): List<HomeAiCharacterRecommendationRecord> = emptyList()
|
||||
|
||||
override fun findRandomAiCharacterRecommendationIds(excludeCharacterIds: List<Long>, limit: Int): List<Long> = emptyList()
|
||||
|
||||
override fun findCheerCreatorRecommendationDetails(
|
||||
creatorIds: List<Long>,
|
||||
memberId: Long?
|
||||
): List<HomeCheerCreatorRecommendationRecord> = emptyList()
|
||||
|
||||
override fun findPopularCommunityRecommendationDetails(
|
||||
communityIds: List<Long>,
|
||||
memberId: Long?,
|
||||
includeAdultCommunities: Boolean
|
||||
): List<HomePopularCommunityRecommendationRecord> = emptyList()
|
||||
|
||||
override fun findGenreCreatorRecommendations(
|
||||
memberId: Long?,
|
||||
includeAdultGenres: Boolean,
|
||||
genreLimit: Int,
|
||||
creatorLimit: Int
|
||||
): List<HomeGenreCreatorRecommendationGroup> = emptyList()
|
||||
}
|
||||
|
||||
private class EmptyRecommendationSnapshotPort : RecommendationSnapshotPort {
|
||||
override fun findLatestSnapshots(
|
||||
sectionType: RecommendedSectionType,
|
||||
offset: Long,
|
||||
limit: Int
|
||||
): List<RecommendationSnapshotRecord> = emptyList()
|
||||
|
||||
override fun findSnapshots(
|
||||
sectionType: RecommendedSectionType,
|
||||
snapshotAt: LocalDateTime,
|
||||
offset: Long,
|
||||
limit: Int
|
||||
): List<RecommendationSnapshotRecord> = emptyList()
|
||||
|
||||
override fun existsLatestSnapshot(sectionType: RecommendedSectionType): Boolean = true
|
||||
|
||||
override fun existsSnapshot(sectionType: RecommendedSectionType, snapshotAt: LocalDateTime): Boolean = true
|
||||
|
||||
override fun replaceSnapshots(
|
||||
sectionType: RecommendedSectionType,
|
||||
snapshotAt: LocalDateTime,
|
||||
newSnapshots: List<RecommendationSnapshotRecord>
|
||||
) = Unit
|
||||
}
|
||||
}
|
||||
@@ -52,6 +52,7 @@ class DefaultMainContentAllQueryRepositoryTest @Autowired constructor(
|
||||
val inactiveTheme = saveTheme("inactive-audio-theme", isActive = false)
|
||||
val free = saveAudioContent(creator, theme, now.minusDays(3), isAdult = false, price = 0)
|
||||
val point = saveAudioContent(creator, theme, now.minusDays(2), isAdult = false, price = 100, isPointAvailable = true)
|
||||
val freePoint = saveAudioContent(creator, theme, now.minusHours(12), isAdult = false, price = 0, isPointAvailable = true)
|
||||
saveAudioContent(creator, theme, now.minusDays(1), isAdult = true, price = 200)
|
||||
saveAudioContent(blockedCreator, theme, now.minusDays(1), isAdult = false, price = 100)
|
||||
saveAudioContent(inactiveCreator, theme, now.minusDays(1), isAdult = false, price = 100)
|
||||
@@ -66,11 +67,12 @@ class DefaultMainContentAllQueryRepositoryTest @Autowired constructor(
|
||||
val freeAudios = repository.findAudios(null, false, now, ContentSort.LATEST, 0, 20, onlyFree = true)
|
||||
val pointAudios = repository.findAudios(null, false, now, ContentSort.LATEST, 0, 20, onlyPointAvailable = true)
|
||||
|
||||
assertEquals(2, repository.countAudios(viewer.id, canViewAdultContent = false, now))
|
||||
assertEquals(3, repository.countAudios(viewer.id, canViewAdultContent = false, now))
|
||||
assertEquals(1, repository.countAudios(viewer.id, false, now, onlyPaid = true))
|
||||
assertEquals(listOf(point.id, free.id), visible.map { it.audioContentId })
|
||||
assertEquals(1, repository.countAudios(null, false, now, onlyPointAvailable = true))
|
||||
assertEquals(listOf(freePoint.id, point.id, free.id), visible.map { it.audioContentId })
|
||||
assertEquals(listOf(point.id), paidAudios.map { it.audioContentId })
|
||||
assertEquals(listOf(free.id), freeAudios.map { it.audioContentId })
|
||||
assertEquals(listOf(freePoint.id, free.id), freeAudios.map { it.audioContentId })
|
||||
assertEquals(listOf(point.id), pointAudios.map { it.audioContentId })
|
||||
assertEquals("https://cdn.test/audio.png", visible.first().imageUrl)
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ class DefaultAudioRecommendationQueryRepositoryTest @Autowired constructor(
|
||||
val now = LocalDateTime.of(2026, 6, 23, 12, 0)
|
||||
val creator = saveMember("audio-creator", MemberRole.CREATOR)
|
||||
val theme = saveTheme()
|
||||
val first = saveAudio(
|
||||
saveAudio(
|
||||
creator = creator,
|
||||
theme = theme,
|
||||
title = "first",
|
||||
@@ -108,6 +108,15 @@ class DefaultAudioRecommendationQueryRepositoryTest @Autowired constructor(
|
||||
isPointAvailable = false,
|
||||
coverImage = "latest.png"
|
||||
)
|
||||
val paidPoint = saveAudio(
|
||||
creator = creator,
|
||||
theme = theme,
|
||||
title = "paid-point",
|
||||
releaseDate = now.minusDays(2),
|
||||
price = 20,
|
||||
isPointAvailable = true,
|
||||
coverImage = "paid-point.png"
|
||||
)
|
||||
saveAudio(creator, theme, "adult", now.minusHours(1), isAdult = true)
|
||||
saveAudio(creator, theme, "future", now.plusDays(1))
|
||||
saveAudio(creator, theme, "inactive", now.minusHours(2)).isActive = false
|
||||
@@ -139,11 +148,11 @@ class DefaultAudioRecommendationQueryRepositoryTest @Autowired constructor(
|
||||
val pointAudios = repository.findPointAudios(10, viewer.id, canViewAdultContent = false, now = now)
|
||||
|
||||
assertEquals(12, latestAudios.size)
|
||||
assertEquals(listOf(latest.id, first.id), latestAudios.take(2).map { it.audioContentId })
|
||||
assertEquals(listOf(latest.id, paidPoint.id), latestAudios.take(2).map { it.audioContentId })
|
||||
assertEquals(10, freeAudios.size)
|
||||
assertEquals(true, freeAudios.all { it.price == 0 })
|
||||
assertEquals(10, pointAudios.size)
|
||||
assertEquals(true, pointAudios.all { it.isPointAvailable })
|
||||
assertEquals(listOf(paidPoint.id), pointAudios.map { it.audioContentId })
|
||||
assertEquals(true, pointAudios.all { it.isPointAvailable && it.price > 0 })
|
||||
val latestCard = latestAudios.first()
|
||||
assertEquals("latest", latestCard.title)
|
||||
assertEquals("00:01", latestCard.duration)
|
||||
@@ -154,8 +163,8 @@ class DefaultAudioRecommendationQueryRepositoryTest @Autowired constructor(
|
||||
assertEquals(false, latestCard.isFirstContent)
|
||||
assertEquals(true, latestCard.isOriginalSeries)
|
||||
assertEquals(creator.nickname, latestCard.creatorNickname)
|
||||
assertEquals(true, latestAudios[1].isFirstContent)
|
||||
assertEquals(false, latestAudios[1].isOriginalSeries)
|
||||
assertEquals(true, latestAudios[2].isFirstContent)
|
||||
assertEquals(false, latestAudios[2].isOriginalSeries)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user