feat(creator): 채널 홈 조회 정책을 추가한다

This commit is contained in:
2026-06-12 17:06:49 +09:00
parent f2c2473a47
commit 530e38c1ad
2 changed files with 154 additions and 0 deletions

View File

@@ -0,0 +1,37 @@
package kr.co.vividnext.sodalive.v2.creator.channel.domain
import kr.co.vividnext.sodalive.v2.common.domain.CreatorActivityType
import java.time.LocalDateTime
class CreatorChannelHomeQueryPolicy {
fun limitSchedules(
schedules: List<CreatorChannelSchedule>,
now: LocalDateTime
): List<CreatorChannelSchedule> {
return schedules
.filter { it.scheduledAt > now }
.sortedWith(compareBy<CreatorChannelSchedule> { it.scheduledAt }.thenBy { it.type.scheduleOrder() })
.take(3)
}
fun excludeLatestAudioContent(
audioContents: List<CreatorChannelAudioContent>,
latestAudioContentId: Long?
): List<CreatorChannelAudioContent> {
return audioContents.filter { it.audioContentId != latestAudioContentId }
}
fun markFirstAudioContent(audioContents: List<CreatorChannelAudioContent>): List<CreatorChannelAudioContent> {
val firstAudioContentId = audioContents
.minWithOrNull(compareBy<CreatorChannelAudioContent> { it.publishedAt }.thenBy { it.audioContentId })
?.audioContentId
return audioContents.map { audioContent ->
audioContent.copy(isFirstContent = audioContent.audioContentId == firstAudioContentId)
}
}
private fun CreatorActivityType.scheduleOrder(): Int {
return if (this == CreatorActivityType.LIVE) 0 else 1
}
}

View File

@@ -0,0 +1,117 @@
package kr.co.vividnext.sodalive.v2.creator.channel.domain
import kr.co.vividnext.sodalive.v2.common.domain.CreatorActivityType
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
import java.time.LocalDateTime
class CreatorChannelHomeQueryPolicyTest {
private val policy = CreatorChannelHomeQueryPolicy()
@Test
@DisplayName("스케줄은 예약 시각 오름차순 최대 3개만 남긴다")
fun shouldLimitSchedulesToEarliestThree() {
val now = LocalDateTime.of(2026, 6, 12, 9, 0)
val schedules = listOf(
schedule(targetId = 4L, scheduledAt = LocalDateTime.of(2026, 6, 12, 13, 0)),
schedule(targetId = 2L, scheduledAt = LocalDateTime.of(2026, 6, 12, 11, 0)),
schedule(targetId = 1L, scheduledAt = LocalDateTime.of(2026, 6, 12, 10, 0)),
schedule(targetId = 3L, scheduledAt = LocalDateTime.of(2026, 6, 12, 12, 0))
)
val limited = policy.limitSchedules(schedules, now)
assertEquals(listOf(1L, 2L, 3L), limited.map { it.targetId })
}
@Test
@DisplayName("스케줄은 현재 시각 이후 예약만 남긴다")
fun shouldOnlyKeepSchedulesAfterNow() {
val now = LocalDateTime.of(2026, 6, 12, 10, 0)
val schedules = listOf(
schedule(targetId = 1L, scheduledAt = now.minusMinutes(1)),
schedule(targetId = 2L, scheduledAt = now),
schedule(targetId = 3L, scheduledAt = now.plusMinutes(1))
)
val limited = policy.limitSchedules(schedules, now)
assertEquals(listOf(3L), limited.map { it.targetId })
}
@Test
@DisplayName("같은 예약 시각이면 라이브가 오디오보다 먼저 온다")
fun shouldSortLiveBeforeAudioWhenScheduledAtIsSame() {
val scheduledAt = LocalDateTime.of(2026, 6, 12, 10, 0)
val schedules = listOf(
schedule(targetId = 2L, scheduledAt = scheduledAt, type = CreatorActivityType.AUDIO),
schedule(targetId = 1L, scheduledAt = scheduledAt, type = CreatorActivityType.LIVE)
)
val limited = policy.limitSchedules(schedules, scheduledAt.minusMinutes(1))
assertEquals(listOf(CreatorActivityType.LIVE, CreatorActivityType.AUDIO), limited.map { it.type })
}
@Test
@DisplayName("오디오 목록에서는 latestAudioContentId와 같은 콘텐츠를 제외한다")
fun shouldExcludeLatestAudioContent() {
val audioContents = listOf(audioContent(1L), audioContent(2L), audioContent(3L))
val filtered = policy.excludeLatestAudioContent(audioContents, latestAudioContentId = 2L)
assertEquals(listOf(1L, 3L), filtered.map { it.audioContentId })
}
@Test
@DisplayName("오디오 콘텐츠의 첫 공개 콘텐츠 여부는 공개 시각 오름차순, 동일 시각이면 id 오름차순으로 판정한다")
fun shouldMarkFirstAudioContentByPublishedAtAndId() {
val publishedAt = LocalDateTime.of(2026, 6, 12, 10, 0)
val audioContents = listOf(
audioContent(3L, publishedAt = publishedAt.plusDays(1)),
audioContent(2L, publishedAt = publishedAt),
audioContent(1L, publishedAt = publishedAt)
)
val marked = policy.markFirstAudioContent(audioContents)
assertTrue(marked.first { it.audioContentId == 1L }.isFirstContent)
assertFalse(marked.first { it.audioContentId == 2L }.isFirstContent)
assertFalse(marked.first { it.audioContentId == 3L }.isFirstContent)
}
private fun schedule(
targetId: Long,
scheduledAt: LocalDateTime,
type: CreatorActivityType = CreatorActivityType.LIVE
): CreatorChannelSchedule {
return CreatorChannelSchedule(
scheduledAt = scheduledAt,
title = "schedule-$targetId",
type = type,
targetId = targetId
)
}
private fun audioContent(
audioContentId: Long,
publishedAt: LocalDateTime = LocalDateTime.of(2026, 6, 12, 10, 0)
): CreatorChannelAudioContent {
return CreatorChannelAudioContent(
audioContentId = audioContentId,
title = "audio-$audioContentId",
duration = null,
imageUrl = null,
price = 0,
isAdult = false,
isPointAvailable = false,
isFirstContent = false,
publishedAt = publishedAt,
seriesName = null,
isOriginalSeries = null
)
}
}