test #426
@@ -1,14 +1,36 @@
|
|||||||
package kr.co.vividnext.sodalive.v2.creator.channel.fantalk.application
|
package kr.co.vividnext.sodalive.v2.creator.channel.fantalk.application
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.common.SodaException
|
||||||
|
import kr.co.vividnext.sodalive.extensions.removeDeletedNicknamePrefix
|
||||||
|
import kr.co.vividnext.sodalive.i18n.LangContext
|
||||||
|
import kr.co.vividnext.sodalive.i18n.SodaMessageSource
|
||||||
import kr.co.vividnext.sodalive.member.Member
|
import kr.co.vividnext.sodalive.member.Member
|
||||||
|
import kr.co.vividnext.sodalive.member.MemberRole
|
||||||
|
import kr.co.vividnext.sodalive.v2.common.domain.toCdnUrl
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.domain.CreatorChannelFanTalk
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.domain.CreatorChannelFanTalkQueryPolicy
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.domain.CreatorChannelFanTalkReply
|
||||||
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.domain.CreatorChannelFanTalkTab
|
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.domain.CreatorChannelFanTalkTab
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.port.out.CreatorChannelFanTalkCreatorRecord
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.port.out.CreatorChannelFanTalkQueryPort
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.port.out.CreatorChannelFanTalkRecord
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.port.out.CreatorChannelFanTalkReplyRecord
|
||||||
|
import org.springframework.beans.factory.ObjectProvider
|
||||||
|
import org.springframework.beans.factory.annotation.Value
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
import org.springframework.transaction.annotation.Transactional
|
import org.springframework.transaction.annotation.Transactional
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
class CreatorChannelFanTalkQueryService {
|
class CreatorChannelFanTalkQueryService(
|
||||||
|
private val queryPortProvider: ObjectProvider<CreatorChannelFanTalkQueryPort>,
|
||||||
|
private val queryPolicy: CreatorChannelFanTalkQueryPolicy,
|
||||||
|
private val messageSource: SodaMessageSource,
|
||||||
|
private val langContext: LangContext,
|
||||||
|
@Value("\${cloud.aws.cloud-front.host}")
|
||||||
|
private val cloudFrontHost: String
|
||||||
|
) {
|
||||||
fun getFanTalkTab(
|
fun getFanTalkTab(
|
||||||
creatorId: Long,
|
creatorId: Long,
|
||||||
viewer: Member,
|
viewer: Member,
|
||||||
@@ -16,6 +38,79 @@ class CreatorChannelFanTalkQueryService {
|
|||||||
size: Int?,
|
size: Int?,
|
||||||
now: LocalDateTime = LocalDateTime.now()
|
now: LocalDateTime = LocalDateTime.now()
|
||||||
): CreatorChannelFanTalkTab {
|
): CreatorChannelFanTalkTab {
|
||||||
throw UnsupportedOperationException("CreatorChannelFanTalkQueryService is implemented in Phase 3")
|
val fanTalkPage = queryPolicy.createPage(page, size)
|
||||||
|
val queryPort = queryPortProvider.getObject()
|
||||||
|
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 fetchedFanTalks = queryPort.findFanTalks(
|
||||||
|
creatorId = creatorId,
|
||||||
|
viewerId = viewerId,
|
||||||
|
offset = fanTalkPage.offset,
|
||||||
|
limit = fanTalkPage.fetchLimit
|
||||||
|
)
|
||||||
|
val fanTalkRecords = queryPolicy.limitItems(fetchedFanTalks, fanTalkPage)
|
||||||
|
val repliesByParentId = findRepliesByParentId(queryPort, creatorId, fanTalkRecords)
|
||||||
|
|
||||||
|
return CreatorChannelFanTalkTab(
|
||||||
|
fanTalkCount = queryPort.countFanTalks(creatorId, viewerId),
|
||||||
|
fanTalks = fanTalkRecords.map { it.toDomain(repliesByParentId[it.fanTalkId].orEmpty()) },
|
||||||
|
page = fanTalkPage,
|
||||||
|
hasNext = queryPolicy.hasNext(fetchedFanTalks, fanTalkPage)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun validateCreatorRole(creator: CreatorChannelFanTalkCreatorRecord) {
|
||||||
|
when (creator.role) {
|
||||||
|
MemberRole.CREATOR -> return
|
||||||
|
else -> throw SodaException(messageKey = "member.validation.creator_not_found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun findRepliesByParentId(
|
||||||
|
queryPort: CreatorChannelFanTalkQueryPort,
|
||||||
|
creatorId: Long,
|
||||||
|
fanTalkRecords: List<CreatorChannelFanTalkRecord>
|
||||||
|
): Map<Long, List<CreatorChannelFanTalkReply>> {
|
||||||
|
val parentFanTalkIds = fanTalkRecords.map { it.fanTalkId }
|
||||||
|
if (parentFanTalkIds.isEmpty()) return emptyMap()
|
||||||
|
return queryPort.findCreatorReplies(creatorId, parentFanTalkIds)
|
||||||
|
.groupBy(
|
||||||
|
keySelector = { it.parentFanTalkId },
|
||||||
|
valueTransform = { it.toDomain() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun CreatorChannelFanTalkRecord.toDomain(
|
||||||
|
creatorReplies: List<CreatorChannelFanTalkReply>
|
||||||
|
) = CreatorChannelFanTalk(
|
||||||
|
fanTalkId = fanTalkId,
|
||||||
|
writerId = writerId,
|
||||||
|
writerNickname = writerNickname.removeDeletedNicknamePrefix(),
|
||||||
|
writerProfileImageUrl = writerProfileImagePath.toCdnUrl(cloudFrontHost) ?: defaultProfileImageUrl(),
|
||||||
|
content = content,
|
||||||
|
createdAt = createdAt,
|
||||||
|
creatorReplies = creatorReplies
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun CreatorChannelFanTalkReplyRecord.toDomain() = CreatorChannelFanTalkReply(
|
||||||
|
fanTalkId = fanTalkId,
|
||||||
|
writerId = writerId,
|
||||||
|
writerNickname = writerNickname.removeDeletedNicknamePrefix(),
|
||||||
|
writerProfileImageUrl = writerProfileImagePath.toCdnUrl(cloudFrontHost) ?: defaultProfileImageUrl(),
|
||||||
|
content = content,
|
||||||
|
createdAt = createdAt
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun defaultProfileImageUrl(): String = "$cloudFrontHost/profile/default-profile.png"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,277 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.creator.channel.fantalk.application
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.common.SodaException
|
||||||
|
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.Member
|
||||||
|
import kr.co.vividnext.sodalive.member.MemberProvider
|
||||||
|
import kr.co.vividnext.sodalive.member.MemberRole
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.domain.CreatorChannelFanTalkQueryPolicy
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.port.out.CreatorChannelFanTalkCreatorRecord
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.port.out.CreatorChannelFanTalkQueryPort
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.port.out.CreatorChannelFanTalkRecord
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.port.out.CreatorChannelFanTalkReplyRecord
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
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.springframework.beans.factory.ObjectProvider
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
|
class CreatorChannelFanTalkQueryServiceTest {
|
||||||
|
@Test
|
||||||
|
@DisplayName("creatorId에 해당하는 회원이 없으면 user_not_found 예외를 던진다")
|
||||||
|
fun shouldThrowUserNotFoundWhenCreatorMemberDoesNotExist() {
|
||||||
|
val port = FakeCreatorChannelFanTalkQueryPort().apply { creator = null }
|
||||||
|
val service = createService(port)
|
||||||
|
val viewer = createMember(id = 10L)
|
||||||
|
|
||||||
|
val exception = assertThrows(SodaException::class.java) {
|
||||||
|
service.getFanTalkTab(1L, viewer, null, null, LocalDateTime.of(2026, 6, 21, 10, 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals("member.validation.user_not_found", exception.messageKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("대상 회원 role이 CREATOR가 아니면 creator_not_found 예외를 던진다")
|
||||||
|
fun shouldThrowCreatorNotFoundWhenMemberIsNotCreator() {
|
||||||
|
val port = FakeCreatorChannelFanTalkQueryPort().apply { creator = creator?.copy(role = MemberRole.USER) }
|
||||||
|
val service = createService(port)
|
||||||
|
val viewer = createMember(id = 10L)
|
||||||
|
|
||||||
|
val exception = assertThrows(SodaException::class.java) {
|
||||||
|
service.getFanTalkTab(1L, viewer, null, null, LocalDateTime.of(2026, 6, 21, 10, 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals("member.validation.creator_not_found", exception.messageKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("차단 관계가 있으면 기존 차단 메시지 예외를 던진다")
|
||||||
|
fun shouldThrowBlockedAccessWhenViewerAndTargetAreBlocked() {
|
||||||
|
val port = FakeCreatorChannelFanTalkQueryPort().apply { blocked = true }
|
||||||
|
val service = createService(port)
|
||||||
|
val viewer = createMember(id = 10L)
|
||||||
|
|
||||||
|
val exception = assertThrows(SodaException::class.java) {
|
||||||
|
service.getFanTalkTab(1L, viewer, null, null, LocalDateTime.of(2026, 6, 21, 10, 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
assertNull(exception.messageKey)
|
||||||
|
assertEquals("Channel access is restricted at creator's request.", exception.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("FanTalk 탭 서비스는 요청 fallback과 조회 컨텍스트를 port에 전달하고 탭을 조립한다")
|
||||||
|
fun shouldResolveRequestFallbacksAndAssembleFanTalkTab() {
|
||||||
|
val port = FakeCreatorChannelFanTalkQueryPort().apply {
|
||||||
|
fanTalkCount = 60
|
||||||
|
fanTalks = (1L..21L).map { fanTalkRecord(it) }
|
||||||
|
creatorReplies = listOf(
|
||||||
|
fanTalkReplyRecord(fanTalkId = 101L, parentFanTalkId = 1L),
|
||||||
|
fanTalkReplyRecord(fanTalkId = 102L, parentFanTalkId = 2L),
|
||||||
|
fanTalkReplyRecord(fanTalkId = 103L, parentFanTalkId = 21L)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val service = createService(port)
|
||||||
|
val viewer = createMember(id = 10L)
|
||||||
|
|
||||||
|
val tab = service.getFanTalkTab(
|
||||||
|
creatorId = 1L,
|
||||||
|
viewer = viewer,
|
||||||
|
page = -1,
|
||||||
|
size = 10,
|
||||||
|
now = LocalDateTime.of(2026, 6, 21, 10, 0)
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(60, tab.fanTalkCount)
|
||||||
|
assertEquals(0, tab.page.page)
|
||||||
|
assertEquals(20, tab.page.size)
|
||||||
|
assertEquals(0L, port.listOffset)
|
||||||
|
assertEquals(21, port.listLimit)
|
||||||
|
assertEquals(20, tab.fanTalks.size)
|
||||||
|
assertTrue(tab.hasNext)
|
||||||
|
assertEquals(
|
||||||
|
(1L..20L).toList(),
|
||||||
|
port.replyParentFanTalkIds
|
||||||
|
)
|
||||||
|
assertEquals(101L, tab.fanTalks[0].creatorReplies.single().fanTalkId)
|
||||||
|
assertEquals(102L, tab.fanTalks[1].creatorReplies.single().fanTalkId)
|
||||||
|
assertEquals(emptyList<Long>(), tab.fanTalks[19].creatorReplies.map { it.fanTalkId })
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("FanTalk 목록이 비어 있으면 답글 조회 없이 빈 목록과 hasNext=false를 반환한다")
|
||||||
|
fun shouldReturnEmptyFanTalksWithoutFindingReplies() {
|
||||||
|
val port = FakeCreatorChannelFanTalkQueryPort().apply {
|
||||||
|
fanTalkCount = 5
|
||||||
|
fanTalks = emptyList()
|
||||||
|
}
|
||||||
|
val service = createService(port)
|
||||||
|
val viewer = createMember(id = 10L)
|
||||||
|
|
||||||
|
val tab = service.getFanTalkTab(1L, viewer, 3, 20, LocalDateTime.of(2026, 6, 21, 10, 0))
|
||||||
|
|
||||||
|
assertEquals(5, tab.fanTalkCount)
|
||||||
|
assertEquals(emptyList<Long>(), tab.fanTalks.map { it.fanTalkId })
|
||||||
|
assertEquals(false, tab.hasNext)
|
||||||
|
assertNull(port.replyParentFanTalkIds)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("FanTalk과 creator reply 작성자의 프로필 URL과 탈퇴 닉네임 prefix를 변환한다")
|
||||||
|
fun shouldConvertWriterProfileUrlsAndDeletedNicknamePrefixes() {
|
||||||
|
val port = FakeCreatorChannelFanTalkQueryPort().apply {
|
||||||
|
fanTalks = listOf(
|
||||||
|
fanTalkRecord(
|
||||||
|
fanTalkId = 1L,
|
||||||
|
writerNickname = "deleted_fan",
|
||||||
|
writerProfileImagePath = "profile/fan.png"
|
||||||
|
),
|
||||||
|
fanTalkRecord(
|
||||||
|
fanTalkId = 2L,
|
||||||
|
writerNickname = "normal",
|
||||||
|
writerProfileImagePath = null
|
||||||
|
)
|
||||||
|
)
|
||||||
|
creatorReplies = listOf(
|
||||||
|
fanTalkReplyRecord(
|
||||||
|
fanTalkId = 101L,
|
||||||
|
parentFanTalkId = 1L,
|
||||||
|
writerNickname = "deleted_creator",
|
||||||
|
writerProfileImagePath = "https://images.test/creator.png"
|
||||||
|
),
|
||||||
|
fanTalkReplyRecord(
|
||||||
|
fanTalkId = 102L,
|
||||||
|
parentFanTalkId = 2L,
|
||||||
|
writerNickname = "creator",
|
||||||
|
writerProfileImagePath = " "
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val service = createService(port)
|
||||||
|
val viewer = createMember(id = 10L)
|
||||||
|
|
||||||
|
val fanTalks = service.getFanTalkTab(1L, viewer, null, null, LocalDateTime.of(2026, 6, 21, 10, 0))
|
||||||
|
.fanTalks
|
||||||
|
|
||||||
|
assertEquals("fan", fanTalks[0].writerNickname)
|
||||||
|
assertEquals("https://cdn.test/profile/fan.png", fanTalks[0].writerProfileImageUrl)
|
||||||
|
assertEquals("creator", fanTalks[0].creatorReplies.single().writerNickname)
|
||||||
|
assertEquals("https://images.test/creator.png", fanTalks[0].creatorReplies.single().writerProfileImageUrl)
|
||||||
|
assertEquals("normal", fanTalks[1].writerNickname)
|
||||||
|
assertEquals("https://cdn.test/profile/default-profile.png", fanTalks[1].writerProfileImageUrl)
|
||||||
|
assertEquals("https://cdn.test/profile/default-profile.png", fanTalks[1].creatorReplies.single().writerProfileImageUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createService(port: FakeCreatorChannelFanTalkQueryPort): CreatorChannelFanTalkQueryService {
|
||||||
|
val langContext = LangContext()
|
||||||
|
langContext.setLang(Lang.EN)
|
||||||
|
return CreatorChannelFanTalkQueryService(
|
||||||
|
queryPortProvider = FixedCreatorChannelFanTalkQueryPortProvider(port),
|
||||||
|
queryPolicy = CreatorChannelFanTalkQueryPolicy(),
|
||||||
|
messageSource = SodaMessageSource(),
|
||||||
|
langContext = langContext,
|
||||||
|
cloudFrontHost = "https://cdn.test"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createMember(id: Long): Member {
|
||||||
|
return Member(
|
||||||
|
email = "member$id@test.com",
|
||||||
|
password = "password",
|
||||||
|
nickname = "member$id",
|
||||||
|
provider = MemberProvider.EMAIL
|
||||||
|
).apply { this.id = id }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class FixedCreatorChannelFanTalkQueryPortProvider(
|
||||||
|
private val port: CreatorChannelFanTalkQueryPort
|
||||||
|
) : ObjectProvider<CreatorChannelFanTalkQueryPort> {
|
||||||
|
override fun getObject(vararg args: Any?): CreatorChannelFanTalkQueryPort = port
|
||||||
|
|
||||||
|
override fun getIfAvailable(): CreatorChannelFanTalkQueryPort = port
|
||||||
|
|
||||||
|
override fun getIfUnique(): CreatorChannelFanTalkQueryPort = port
|
||||||
|
|
||||||
|
override fun getObject(): CreatorChannelFanTalkQueryPort = port
|
||||||
|
}
|
||||||
|
|
||||||
|
private class FakeCreatorChannelFanTalkQueryPort : CreatorChannelFanTalkQueryPort {
|
||||||
|
var creator: CreatorChannelFanTalkCreatorRecord? = CreatorChannelFanTalkCreatorRecord(
|
||||||
|
creatorId = 1L,
|
||||||
|
role = MemberRole.CREATOR,
|
||||||
|
nickname = "creator"
|
||||||
|
)
|
||||||
|
var blocked = false
|
||||||
|
var fanTalkCount = 1
|
||||||
|
var fanTalks = listOf(fanTalkRecord(1L))
|
||||||
|
var creatorReplies = emptyList<CreatorChannelFanTalkReplyRecord>()
|
||||||
|
var listOffset: Long? = null
|
||||||
|
var listLimit: Int? = null
|
||||||
|
var replyParentFanTalkIds: List<Long>? = null
|
||||||
|
|
||||||
|
override fun findCreator(creatorId: Long, viewerId: Long?): CreatorChannelFanTalkCreatorRecord? = creator
|
||||||
|
|
||||||
|
override fun existsBlockedBetween(viewerId: Long, creatorId: Long): Boolean = blocked
|
||||||
|
|
||||||
|
override fun countFanTalks(creatorId: Long, viewerId: Long): Int = fanTalkCount
|
||||||
|
|
||||||
|
override fun findFanTalks(
|
||||||
|
creatorId: Long,
|
||||||
|
viewerId: Long,
|
||||||
|
offset: Long,
|
||||||
|
limit: Int
|
||||||
|
): List<CreatorChannelFanTalkRecord> {
|
||||||
|
listOffset = offset
|
||||||
|
listLimit = limit
|
||||||
|
return fanTalks
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun findCreatorReplies(
|
||||||
|
creatorId: Long,
|
||||||
|
parentFanTalkIds: List<Long>
|
||||||
|
): List<CreatorChannelFanTalkReplyRecord> {
|
||||||
|
replyParentFanTalkIds = parentFanTalkIds
|
||||||
|
return creatorReplies
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fanTalkRecord(
|
||||||
|
fanTalkId: Long,
|
||||||
|
writerId: Long = 10L + fanTalkId,
|
||||||
|
writerNickname: String = "fan-$fanTalkId",
|
||||||
|
writerProfileImagePath: String? = "profile/$fanTalkId.png"
|
||||||
|
): CreatorChannelFanTalkRecord {
|
||||||
|
return CreatorChannelFanTalkRecord(
|
||||||
|
fanTalkId = fanTalkId,
|
||||||
|
writerId = writerId,
|
||||||
|
writerNickname = writerNickname,
|
||||||
|
writerProfileImagePath = writerProfileImagePath,
|
||||||
|
content = "content-$fanTalkId",
|
||||||
|
createdAt = LocalDateTime.of(2026, 6, 21, 10, 0).plusMinutes(fanTalkId)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fanTalkReplyRecord(
|
||||||
|
fanTalkId: Long,
|
||||||
|
parentFanTalkId: Long,
|
||||||
|
writerId: Long = 1L,
|
||||||
|
writerNickname: String = "creator",
|
||||||
|
writerProfileImagePath: String? = "profile/creator.png"
|
||||||
|
): CreatorChannelFanTalkReplyRecord {
|
||||||
|
return CreatorChannelFanTalkReplyRecord(
|
||||||
|
fanTalkId = fanTalkId,
|
||||||
|
parentFanTalkId = parentFanTalkId,
|
||||||
|
writerId = writerId,
|
||||||
|
writerNickname = writerNickname,
|
||||||
|
writerProfileImagePath = writerProfileImagePath,
|
||||||
|
content = "reply-$fanTalkId",
|
||||||
|
createdAt = LocalDateTime.of(2026, 6, 21, 11, 0).plusMinutes(fanTalkId)
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user