feat(creator-channel): FanTalk 탭 조회 서비스를 구현한다

This commit is contained in:
2026-06-22 15:51:47 +09:00
parent e2a3aeefc2
commit 2848f07573
2 changed files with 374 additions and 2 deletions

View File

@@ -1,14 +1,36 @@
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.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.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.transaction.annotation.Transactional
import java.time.LocalDateTime
@Service
@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(
creatorId: Long,
viewer: Member,
@@ -16,6 +38,79 @@ class CreatorChannelFanTalkQueryService {
size: Int?,
now: LocalDateTime = LocalDateTime.now()
): 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"
}