feat(creator): 채널 홈 조회 서비스를 추가한다

This commit is contained in:
2026-06-13 18:52:10 +09:00
parent 951bd1b2d1
commit ec68d827a6
3 changed files with 682 additions and 0 deletions

View File

@@ -0,0 +1,265 @@
package kr.co.vividnext.sodalive.v2.creator.channel.application
import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.i18n.LangContext
import kr.co.vividnext.sodalive.i18n.SodaMessageSource
import kr.co.vividnext.sodalive.member.Gender
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.isAdultVisibleByPolicy
import kr.co.vividnext.sodalive.v2.creator.channel.domain.CreatorChannelActivity
import kr.co.vividnext.sodalive.v2.creator.channel.domain.CreatorChannelAudioContent
import kr.co.vividnext.sodalive.v2.creator.channel.domain.CreatorChannelCommunityPost
import kr.co.vividnext.sodalive.v2.creator.channel.domain.CreatorChannelCreator
import kr.co.vividnext.sodalive.v2.creator.channel.domain.CreatorChannelDonation
import kr.co.vividnext.sodalive.v2.creator.channel.domain.CreatorChannelFanTalk
import kr.co.vividnext.sodalive.v2.creator.channel.domain.CreatorChannelFanTalkSummary
import kr.co.vividnext.sodalive.v2.creator.channel.domain.CreatorChannelHome
import kr.co.vividnext.sodalive.v2.creator.channel.domain.CreatorChannelHomeQueryPolicy
import kr.co.vividnext.sodalive.v2.creator.channel.domain.CreatorChannelLive
import kr.co.vividnext.sodalive.v2.creator.channel.domain.CreatorChannelSchedule
import kr.co.vividnext.sodalive.v2.creator.channel.domain.CreatorChannelSeries
import kr.co.vividnext.sodalive.v2.creator.channel.domain.CreatorChannelSns
import kr.co.vividnext.sodalive.v2.creator.channel.port.out.CreatorChannelActivityRecord
import kr.co.vividnext.sodalive.v2.creator.channel.port.out.CreatorChannelAudioContentRecord
import kr.co.vividnext.sodalive.v2.creator.channel.port.out.CreatorChannelCommunityPostRecord
import kr.co.vividnext.sodalive.v2.creator.channel.port.out.CreatorChannelCreatorRecord
import kr.co.vividnext.sodalive.v2.creator.channel.port.out.CreatorChannelDonationRecord
import kr.co.vividnext.sodalive.v2.creator.channel.port.out.CreatorChannelFanTalkRecord
import kr.co.vividnext.sodalive.v2.creator.channel.port.out.CreatorChannelFanTalkSummaryRecord
import kr.co.vividnext.sodalive.v2.creator.channel.port.out.CreatorChannelHomeQueryPort
import kr.co.vividnext.sodalive.v2.creator.channel.port.out.CreatorChannelLiveRecord
import kr.co.vividnext.sodalive.v2.creator.channel.port.out.CreatorChannelScheduleRecord
import kr.co.vividnext.sodalive.v2.creator.channel.port.out.CreatorChannelSeriesRecord
import kr.co.vividnext.sodalive.v2.creator.channel.port.out.CreatorChannelSnsRecord
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.time.LocalDateTime
@Service
@Transactional(readOnly = true)
class CreatorChannelHomeQueryService(
private val queryPort: CreatorChannelHomeQueryPort,
private val queryPolicy: CreatorChannelHomeQueryPolicy,
private val memberContentPreferenceService: MemberContentPreferenceService,
private val messageSource: SodaMessageSource,
private val langContext: LangContext,
@Value("\${cloud.aws.cloud-front.host}")
private val cloudFrontHost: String
) {
fun getHome(
creatorId: Long,
viewer: Member,
now: LocalDateTime = LocalDateTime.now()
): CreatorChannelHome {
val viewerId = viewer.id!!
val creator = queryPort.findCreator(creatorId, viewerId)
?: throw SodaException(messageKey = "member.validation.user_not_found")
if (queryPort.existsBlockedBetween(viewerId, creatorId)) {
val messageTemplate = messageSource
.getMessage("explorer.creator.blocked_access", langContext.lang)
.orEmpty()
throw SodaException(message = String.format(messageTemplate, creator.nickname))
}
validateCreatorRole(creator)
val preference = memberContentPreferenceService.getStoredPreference(viewer)
val canViewAdultContent = isAdultVisibleByPolicy(viewer, preference.isAdultContentVisible)
val isViewerCreator = viewerId == creatorId
val effectiveViewerGender = viewer.effectiveGender()
val latestAudioContent = queryPort
.findLatestAudioContent(creatorId, now, canViewAdultContent)
?.toDomain()
val audioContents = queryPolicy.excludeLatestAudioContent(
queryPort.findAudioContents(
creatorId = creatorId,
now = now,
latestAudioContentId = latestAudioContent?.audioContentId,
canViewAdultContent = canViewAdultContent
).map { it.toDomain() },
latestAudioContent?.audioContentId
)
return CreatorChannelHome(
creator = creator.toDomain(),
currentLive = queryPort.findCurrentLive(
creatorId = creatorId,
now = now,
canViewAdultContent = canViewAdultContent,
viewerId = viewerId,
isViewerCreator = isViewerCreator,
effectiveViewerGender = effectiveViewerGender
)?.toDomain(),
latestAudioContent = latestAudioContent,
channelDonations = queryPort.findChannelDonations(creatorId, viewerId, now).map { it.toDomain() },
notices = queryPort.findCommunityPosts(
creatorId = creatorId,
viewerId = viewerId,
isFixed = true,
canViewAdultContent = canViewAdultContent
).map { it.toDomain() },
schedules = queryPolicy.limitSchedules(
queryPort.findSchedules(
creatorId = creatorId,
now = now,
canViewAdultContent = canViewAdultContent,
viewerId = viewerId,
isViewerCreator = isViewerCreator,
effectiveViewerGender = effectiveViewerGender
).map { it.toDomain() },
now,
canViewAdultContent
),
audioContents = audioContents,
series = queryPort.findSeries(
creatorId = creatorId,
viewerId = viewerId,
now = now,
canViewAdultContent = canViewAdultContent,
contentType = preference.contentType
).map { it.toDomain() },
communities = queryPort.findCommunityPosts(
creatorId = creatorId,
viewerId = viewerId,
isFixed = false,
canViewAdultContent = canViewAdultContent
).map { it.toDomain() },
fanTalk = queryPort.findFanTalkSummary(creatorId, viewerId).toDomain(),
introduce = creator.introduce,
activity = queryPort.findActivity(creatorId, now).toDomain(),
sns = queryPort.findSns(creatorId).toDomain()
)
}
private fun validateCreatorRole(creator: CreatorChannelCreatorRecord) {
when (creator.role) {
MemberRole.CREATOR -> return
else -> throw SodaException(messageKey = "member.validation.creator_not_found")
}
}
private fun Member.effectiveGender(): Gender {
auth?.let { return if (it.gender == 1) Gender.MALE else Gender.FEMALE }
return gender
}
private fun CreatorChannelCreatorRecord.toDomain() = CreatorChannelCreator(
creatorId = creatorId,
characterId = characterId,
nickname = nickname,
profileImageUrl = profileImagePath.toCdnUrl() ?: defaultProfileImageUrl(),
followerCount = followerCount,
isAiChatAvailable = isAiChatAvailable,
isDmAvailable = isDmAvailable,
isFollow = isFollow,
isNotify = isNotify
)
private fun CreatorChannelLiveRecord.toDomain() = CreatorChannelLive(
liveId = liveId,
title = title,
coverImageUrl = coverImagePath.toCdnUrl(),
beginDateTime = beginDateTime,
price = price,
isAdult = isAdult
)
private fun CreatorChannelAudioContentRecord.toDomain() = CreatorChannelAudioContent(
audioContentId = audioContentId,
title = title,
duration = duration,
imageUrl = imagePath.toCdnUrl(),
price = price,
isAdult = isAdult,
isPointAvailable = isPointAvailable,
isFirstContent = isFirstContent,
publishedAt = publishedAt,
seriesName = seriesName,
isOriginalSeries = isOriginalSeries
)
private fun CreatorChannelDonationRecord.toDomain() = CreatorChannelDonation(
nickname = nickname,
profileImageUrl = profileImagePath.toCdnUrl() ?: defaultProfileImageUrl(),
can = can,
message = message,
createdAt = createdAt
)
private fun CreatorChannelScheduleRecord.toDomain() = CreatorChannelSchedule(
scheduledAt = scheduledAt,
title = title,
type = type,
targetId = targetId,
isAdult = isAdult
)
private fun CreatorChannelSeriesRecord.toDomain() = CreatorChannelSeries(
seriesId = seriesId,
title = title,
coverImageUrl = coverImagePath.toCdnUrl().orEmpty(),
numberOfContent = numberOfContent,
isNew = isNew,
isOriginal = isOriginal
)
private fun CreatorChannelCommunityPostRecord.toDomain() = CreatorChannelCommunityPost(
postId = postId,
creatorId = creatorId,
creatorNickname = creatorNickname,
creatorProfileUrl = creatorProfilePath.toCdnUrl() ?: defaultProfileImageUrl(),
imageUrl = imagePath.toCdnUrl(),
audioUrl = audioPath.toCdnUrl(),
content = content,
price = price,
date = date,
existOrdered = existOrdered,
likeCount = likeCount,
commentCount = commentCount
)
private fun CreatorChannelFanTalkSummaryRecord.toDomain() = CreatorChannelFanTalkSummary(
totalCount = totalCount,
latestFanTalk = latestFanTalk?.toDomain()
)
private fun CreatorChannelFanTalkRecord.toDomain() = CreatorChannelFanTalk(
fanTalkId = fanTalkId,
memberId = memberId,
nickname = nickname,
profileImageUrl = profileImagePath.toCdnUrl() ?: defaultProfileImageUrl(),
content = content,
languageCode = languageCode,
createdAt = createdAt
)
private fun CreatorChannelActivityRecord.toDomain() = CreatorChannelActivity(
debutDate = debutDate,
dDay = dDay,
liveCount = liveCount,
liveDurationHours = liveDurationHours,
liveContributorCount = liveContributorCount,
audioContentCount = audioContentCount,
seriesCount = seriesCount
)
private fun CreatorChannelSnsRecord.toDomain() = CreatorChannelSns(
instagramUrl = instagramUrl,
fancimmUrl = fancimmUrl,
xUrl = xUrl,
youtubeUrl = youtubeUrl,
kakaoOpenChatUrl = kakaoOpenChatUrl
)
private fun String?.toCdnUrl(): String? {
if (isNullOrBlank()) return null
if (startsWith("https://") || startsWith("http://")) return this
return "$cloudFrontHost/$this"
}
private fun defaultProfileImageUrl(): String = "$cloudFrontHost/profile/default-profile.png"
}

View File

@@ -1,8 +1,10 @@
package kr.co.vividnext.sodalive.v2.creator.channel.domain
import kr.co.vividnext.sodalive.v2.common.domain.CreatorActivityType
import org.springframework.stereotype.Component
import java.time.LocalDateTime
@Component
class CreatorChannelHomeQueryPolicy {
fun limitSchedules(
schedules: List<CreatorChannelSchedule>,

View File

@@ -1,6 +1,18 @@
package kr.co.vividnext.sodalive.v2.creator.channel.application
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.content.ContentType
import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.i18n.LangContext
import kr.co.vividnext.sodalive.i18n.SodaMessageSource
import kr.co.vividnext.sodalive.member.Gender
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.MemberProvider
import kr.co.vividnext.sodalive.member.MemberRole
import kr.co.vividnext.sodalive.member.auth.Auth
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
import kr.co.vividnext.sodalive.member.contentpreference.ViewerContentPreference
import kr.co.vividnext.sodalive.v2.common.domain.CreatorActivityType
import kr.co.vividnext.sodalive.v2.creator.channel.domain.CreatorChannelActivity
import kr.co.vividnext.sodalive.v2.creator.channel.domain.CreatorChannelAudioContent
@@ -10,22 +22,152 @@ import kr.co.vividnext.sodalive.v2.creator.channel.domain.CreatorChannelDonation
import kr.co.vividnext.sodalive.v2.creator.channel.domain.CreatorChannelFanTalk
import kr.co.vividnext.sodalive.v2.creator.channel.domain.CreatorChannelFanTalkSummary
import kr.co.vividnext.sodalive.v2.creator.channel.domain.CreatorChannelHome
import kr.co.vividnext.sodalive.v2.creator.channel.domain.CreatorChannelHomeQueryPolicy
import kr.co.vividnext.sodalive.v2.creator.channel.domain.CreatorChannelLive
import kr.co.vividnext.sodalive.v2.creator.channel.domain.CreatorChannelSchedule
import kr.co.vividnext.sodalive.v2.creator.channel.domain.CreatorChannelSeries
import kr.co.vividnext.sodalive.v2.creator.channel.domain.CreatorChannelSns
import kr.co.vividnext.sodalive.v2.creator.channel.dto.CreatorChannelHomeResponse
import kr.co.vividnext.sodalive.v2.creator.channel.port.out.CreatorChannelActivityRecord
import kr.co.vividnext.sodalive.v2.creator.channel.port.out.CreatorChannelAudioContentRecord
import kr.co.vividnext.sodalive.v2.creator.channel.port.out.CreatorChannelCommunityPostRecord
import kr.co.vividnext.sodalive.v2.creator.channel.port.out.CreatorChannelCreatorRecord
import kr.co.vividnext.sodalive.v2.creator.channel.port.out.CreatorChannelDonationRecord
import kr.co.vividnext.sodalive.v2.creator.channel.port.out.CreatorChannelFanTalkRecord
import kr.co.vividnext.sodalive.v2.creator.channel.port.out.CreatorChannelFanTalkSummaryRecord
import kr.co.vividnext.sodalive.v2.creator.channel.port.out.CreatorChannelHomeQueryPort
import kr.co.vividnext.sodalive.v2.creator.channel.port.out.CreatorChannelLiveRecord
import kr.co.vividnext.sodalive.v2.creator.channel.port.out.CreatorChannelScheduleRecord
import kr.co.vividnext.sodalive.v2.creator.channel.port.out.CreatorChannelSeriesRecord
import kr.co.vividnext.sodalive.v2.creator.channel.port.out.CreatorChannelSnsRecord
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Assertions.assertThrows
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 CreatorChannelHomeQueryServiceTest {
private val objectMapper = jacksonObjectMapper()
@Test
@DisplayName("크리에이터 채널 홈 서비스는 모든 섹션을 조립하고 최종 정책을 적용한다")
fun shouldAssembleCreatorChannelHomeWithFinalPolicies() {
val port = FakeCreatorChannelHomeQueryPort()
val service = createService(port, canViewAdultContent = false)
val viewer = createMember(id = 10L, gender = Gender.FEMALE, authGender = 1)
val now = LocalDateTime.of(2026, 6, 13, 10, 0)
val home = service.getHome(creatorId = 1L, viewer = viewer, now = now)
assertEquals(1L, home.creator.creatorId)
assertEquals("https://cdn.test/profile/creator.png", home.creator.profileImageUrl)
assertEquals("https://cdn.test/live.png", home.currentLive?.coverImageUrl)
assertEquals("https://cdn.test/audio/latest.png", home.latestAudioContent?.imageUrl)
assertEquals(listOf(203L, 202L), home.audioContents.map { it.audioContentId })
assertEquals(listOf(402L, 401L, 404L), home.schedules.map { it.targetId })
assertFalse(home.schedules.any { it.isAdult })
assertEquals("https://cdn.test/profile/fan.png", home.channelDonations.first().profileImageUrl)
assertEquals("https://cdn.test/community.png", home.notices.first().imageUrl)
assertEquals("https://cdn.test/series.png", home.series.first().coverImageUrl)
assertEquals("introduce", home.introduce)
assertEquals(Gender.MALE, port.currentLiveEffectiveViewerGender)
assertEquals(Gender.MALE, port.schedulesEffectiveViewerGender)
assertEquals(10L, port.currentLiveViewerId)
assertEquals(10L, port.schedulesViewerId)
assertFalse(port.currentLiveIsViewerCreator == true)
assertFalse(port.schedulesIsViewerCreator == true)
assertEquals(false, port.currentLiveCanViewAdultContent)
assertEquals(false, port.schedulesCanViewAdultContent)
assertEquals(201L, port.audioContentsLatestAudioContentId)
}
@Test
@DisplayName("조회자가 크리에이터 본인이면 라이브 조회 정책 컨텍스트에 본인 여부를 전달한다")
fun shouldPassViewerCreatorFlagToLivePolicyQueries() {
val port = FakeCreatorChannelHomeQueryPort()
val service = createService(port)
val viewer = createMember(id = 1L, gender = Gender.FEMALE, authGender = null)
service.getHome(creatorId = 1L, viewer = viewer, now = LocalDateTime.of(2026, 6, 13, 10, 0))
assertTrue(port.currentLiveIsViewerCreator == true)
assertTrue(port.schedulesIsViewerCreator == true)
assertEquals(Gender.FEMALE, port.currentLiveEffectiveViewerGender)
assertEquals(Gender.FEMALE, port.schedulesEffectiveViewerGender)
}
@Test
@DisplayName("creatorId에 해당하는 회원이 없으면 user_not_found 예외를 던진다")
fun shouldThrowUserNotFoundWhenCreatorMemberDoesNotExist() {
val port = FakeCreatorChannelHomeQueryPort().apply {
creator = null
}
val service = createService(port)
val viewer = createMember(id = 10L)
val exception = assertThrows(SodaException::class.java) {
service.getHome(creatorId = 999L, viewer = viewer, now = LocalDateTime.of(2026, 6, 13, 10, 0))
}
assertEquals("member.validation.user_not_found", exception.messageKey)
}
@Test
@DisplayName("대상 회원 role이 CREATOR가 아니면 creator_not_found 예외를 던진다")
fun shouldThrowCreatorNotFoundWhenMemberIsNotCreator() {
val port = FakeCreatorChannelHomeQueryPort().apply {
creator = creator?.copy(role = MemberRole.USER)
}
val service = createService(port)
val viewer = createMember(id = 10L)
val exception = assertThrows(SodaException::class.java) {
service.getHome(creatorId = 1L, viewer = viewer, now = LocalDateTime.of(2026, 6, 13, 10, 0))
}
assertEquals("member.validation.creator_not_found", exception.messageKey)
}
@Test
@DisplayName("차단 관계가 있으면 대상 회원이 크리에이터가 아니어도 접근 차단 예외를 먼저 던진다")
fun shouldThrowBlockedAccessBeforeCreatorNotFoundWhenViewerAndTargetAreBlocked() {
val port = FakeCreatorChannelHomeQueryPort().apply {
creator = creator?.copy(role = MemberRole.USER)
blocked = true
}
val service = createService(port)
val viewer = createMember(id = 10L)
val exception = assertThrows(SodaException::class.java) {
service.getHome(creatorId = 1L, viewer = viewer, now = LocalDateTime.of(2026, 6, 13, 10, 0))
}
assertNull(exception.messageKey)
assertEquals("creator님의 요청으로 채널 접근이 제한됩니다.", exception.message)
}
@Test
@DisplayName("조회자와 크리에이터 사이에 차단 관계가 있으면 기존 채널 접근 차단 메시지를 던진다")
fun shouldThrowBlockedAccessWhenViewerAndCreatorAreBlocked() {
val port = FakeCreatorChannelHomeQueryPort().apply {
blocked = true
}
val service = createService(port)
val viewer = createMember(id = 10L)
val exception = assertThrows(SodaException::class.java) {
service.getHome(creatorId = 1L, viewer = viewer, now = LocalDateTime.of(2026, 6, 13, 10, 0))
}
assertNull(exception.messageKey)
assertEquals("creator님의 요청으로 채널 접근이 제한됩니다.", exception.message)
}
@Test
@DisplayName("크리에이터 채널 홈 모델은 홈 탭 전체 섹션을 담고 응답 DTO로 변환된다")
fun shouldConvertCreatorChannelHomeToResponse() {
@@ -233,4 +375,277 @@ class CreatorChannelHomeQueryServiceTest {
)
)
}
private fun createService(
port: FakeCreatorChannelHomeQueryPort,
canViewAdultContent: Boolean = true
): CreatorChannelHomeQueryService {
val preferenceService = Mockito.mock(MemberContentPreferenceService::class.java)
Mockito.`when`(
preferenceService.getStoredPreference(Mockito.any(Member::class.java) ?: createMember(id = 0L))
).thenReturn(
ViewerContentPreference(
countryCode = "US",
isAdultContentVisible = canViewAdultContent,
contentType = ContentType.ALL,
isAdult = canViewAdultContent
)
)
val messageSource = SodaMessageSource()
val langContext = LangContext()
langContext.setLang(Lang.KO)
return CreatorChannelHomeQueryService(
queryPort = port,
queryPolicy = CreatorChannelHomeQueryPolicy(),
memberContentPreferenceService = preferenceService,
messageSource = messageSource,
langContext = langContext,
cloudFrontHost = "https://cdn.test"
)
}
private fun createMember(
id: Long,
gender: Gender = Gender.NONE,
authGender: Int? = null
): Member {
val member = Member(
email = "member$id@test.com",
password = "password",
nickname = "member$id",
provider = MemberProvider.EMAIL,
gender = gender
)
member.id = id
authGender?.let {
Auth(
name = "name",
birth = "19900101",
uniqueCi = "ci$id",
di = "di$id",
gender = it
).member = member
}
return member
}
}
private class FakeCreatorChannelHomeQueryPort : CreatorChannelHomeQueryPort {
var creator: CreatorChannelCreatorRecord? = CreatorChannelCreatorRecord(
creatorId = 1L,
role = MemberRole.CREATOR,
characterId = 11L,
nickname = "creator",
profileImagePath = "profile/creator.png",
introduce = "introduce",
followerCount = 100,
isAiChatAvailable = true,
isDmAvailable = true,
isFollow = true,
isNotify = false
)
var blocked = false
var currentLiveViewerId: Long? = null
var currentLiveIsViewerCreator: Boolean? = null
var currentLiveEffectiveViewerGender: Gender? = null
var currentLiveCanViewAdultContent: Boolean? = null
var schedulesViewerId: Long? = null
var schedulesIsViewerCreator: Boolean? = null
var schedulesEffectiveViewerGender: Gender? = null
var schedulesCanViewAdultContent: Boolean? = null
var audioContentsLatestAudioContentId: Long? = null
override fun findCreator(creatorId: Long, viewerId: Long?): CreatorChannelCreatorRecord? = creator
override fun existsBlockedBetween(viewerId: Long, creatorId: Long): Boolean = blocked
override fun findCurrentLive(
creatorId: Long,
now: LocalDateTime,
canViewAdultContent: Boolean,
viewerId: Long?,
isViewerCreator: Boolean,
effectiveViewerGender: Gender?
): CreatorChannelLiveRecord? {
currentLiveViewerId = viewerId
currentLiveIsViewerCreator = isViewerCreator
currentLiveEffectiveViewerGender = effectiveViewerGender
currentLiveCanViewAdultContent = canViewAdultContent
return CreatorChannelLiveRecord(
liveId = 101L,
title = "live",
coverImagePath = "live.png",
beginDateTime = LocalDateTime.of(2026, 6, 13, 9, 0),
price = 10,
isAdult = false
)
}
override fun findLatestAudioContent(
creatorId: Long,
now: LocalDateTime,
canViewAdultContent: Boolean
): CreatorChannelAudioContentRecord? = audioContentRecord(201L, "audio/latest.png")
override fun findChannelDonations(
creatorId: Long,
viewerId: Long?,
now: LocalDateTime,
limit: Int
): List<CreatorChannelDonationRecord> = listOf(
CreatorChannelDonationRecord(
nickname = "fan",
profileImagePath = "profile/fan.png",
can = 30,
message = "thanks",
createdAt = LocalDateTime.of(2026, 6, 13, 8, 0)
)
)
override fun findCommunityPosts(
creatorId: Long,
viewerId: Long?,
isFixed: Boolean,
canViewAdultContent: Boolean,
limit: Int
): List<CreatorChannelCommunityPostRecord> = listOf(
CreatorChannelCommunityPostRecord(
postId = if (isFixed) 301L else 302L,
creatorId = creatorId,
creatorNickname = "creator",
creatorProfilePath = "profile/creator.png",
imagePath = "community.png",
audioPath = "community.mp3",
content = if (isFixed) "notice" else "community",
price = 0,
date = LocalDateTime.of(2026, 6, 13, 7, 0),
existOrdered = false,
likeCount = 3,
commentCount = 4
)
)
override fun findSchedules(
creatorId: Long,
now: LocalDateTime,
canViewAdultContent: Boolean,
viewerId: Long?,
isViewerCreator: Boolean,
effectiveViewerGender: Gender?,
limit: Int
): List<CreatorChannelScheduleRecord> {
schedulesViewerId = viewerId
schedulesIsViewerCreator = isViewerCreator
schedulesEffectiveViewerGender = effectiveViewerGender
schedulesCanViewAdultContent = canViewAdultContent
return listOf(
scheduleRecord(401L, LocalDateTime.of(2026, 6, 13, 12, 0), CreatorActivityType.AUDIO, false),
scheduleRecord(402L, LocalDateTime.of(2026, 6, 13, 12, 0), CreatorActivityType.LIVE, false),
scheduleRecord(403L, LocalDateTime.of(2026, 6, 13, 13, 0), CreatorActivityType.LIVE, true),
scheduleRecord(404L, LocalDateTime.of(2026, 6, 13, 14, 0), CreatorActivityType.AUDIO, false),
scheduleRecord(405L, LocalDateTime.of(2026, 6, 13, 15, 0), CreatorActivityType.LIVE, false),
scheduleRecord(406L, LocalDateTime.of(2026, 6, 13, 9, 0), CreatorActivityType.LIVE, false)
)
}
override fun findAudioContents(
creatorId: Long,
now: LocalDateTime,
latestAudioContentId: Long?,
canViewAdultContent: Boolean,
limit: Int
): List<CreatorChannelAudioContentRecord> {
audioContentsLatestAudioContentId = latestAudioContentId
return listOf(
audioContentRecord(201L, "audio/latest.png"),
audioContentRecord(203L, "audio/203.png"),
audioContentRecord(202L, "audio/202.png")
)
}
override fun findSeries(
creatorId: Long,
viewerId: Long?,
now: LocalDateTime,
canViewAdultContent: Boolean,
contentType: ContentType,
limit: Int
): List<CreatorChannelSeriesRecord> = listOf(
CreatorChannelSeriesRecord(
seriesId = 501L,
title = "series",
coverImagePath = "series.png",
numberOfContent = 2,
isNew = true,
isOriginal = false
)
)
override fun findFanTalkSummary(creatorId: Long, viewerId: Long?): CreatorChannelFanTalkSummaryRecord {
return CreatorChannelFanTalkSummaryRecord(
totalCount = 1,
latestFanTalk = CreatorChannelFanTalkRecord(
fanTalkId = 601L,
memberId = 10L,
nickname = "fan",
profileImagePath = "profile/fan.png",
content = "hello",
languageCode = "ko",
createdAt = LocalDateTime.of(2026, 6, 13, 6, 0)
)
)
}
override fun findActivity(creatorId: Long, now: LocalDateTime): CreatorChannelActivityRecord {
return CreatorChannelActivityRecord(
debutDate = LocalDateTime.of(2026, 6, 1, 0, 0),
dDay = "D+12",
liveCount = 1,
liveDurationHours = 2,
liveContributorCount = 3,
audioContentCount = 4,
seriesCount = 5
)
}
override fun findSns(creatorId: Long): CreatorChannelSnsRecord {
return CreatorChannelSnsRecord(
instagramUrl = "instagram",
fancimmUrl = "fancimm",
xUrl = "x",
youtubeUrl = "youtube",
kakaoOpenChatUrl = "kakao"
)
}
private fun audioContentRecord(audioContentId: Long, imagePath: String): CreatorChannelAudioContentRecord {
return CreatorChannelAudioContentRecord(
audioContentId = audioContentId,
title = "audio-$audioContentId",
duration = "00:10:00",
imagePath = imagePath,
price = 10,
isAdult = false,
isPointAvailable = true,
isFirstContent = false,
publishedAt = LocalDateTime.of(2026, 6, 10, 0, audioContentId.toInt() % 60),
seriesName = null,
isOriginalSeries = null
)
}
private fun scheduleRecord(
targetId: Long,
scheduledAt: LocalDateTime,
type: CreatorActivityType,
isAdult: Boolean
): CreatorChannelScheduleRecord {
return CreatorChannelScheduleRecord(
scheduledAt = scheduledAt,
title = "schedule-$targetId",
type = type,
targetId = targetId,
isAdult = isAdult
)
}
}