feat(creator-channel): 커뮤니티 탭 조회 서비스를 추가한다

This commit is contained in:
2026-06-21 22:15:37 +09:00
parent 00695d5b33
commit 0620e54cbd
2 changed files with 454 additions and 0 deletions

View File

@@ -0,0 +1,140 @@
package kr.co.vividnext.sodalive.v2.creator.channel.community.application
import kr.co.vividnext.sodalive.aws.cloudfront.AudioContentCloudFront
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.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.common.domain.toCdnUrl
import kr.co.vividnext.sodalive.v2.creator.channel.community.domain.CreatorChannelCommunityPost
import kr.co.vividnext.sodalive.v2.creator.channel.community.domain.CreatorChannelCommunityQueryPolicy
import kr.co.vividnext.sodalive.v2.creator.channel.community.domain.CreatorChannelCommunityTab
import kr.co.vividnext.sodalive.v2.creator.channel.community.port.out.CreatorChannelCommunityCreatorRecord
import kr.co.vividnext.sodalive.v2.creator.channel.community.port.out.CreatorChannelCommunityPostRecord
import kr.co.vividnext.sodalive.v2.creator.channel.community.port.out.CreatorChannelCommunityQueryPort
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 CreatorChannelCommunityQueryService(
private val queryPortProvider: ObjectProvider<CreatorChannelCommunityQueryPort>,
private val queryPolicy: CreatorChannelCommunityQueryPolicy,
private val memberContentPreferenceService: MemberContentPreferenceService,
private val audioContentCloudFront: AudioContentCloudFront,
private val messageSource: SodaMessageSource,
private val langContext: LangContext,
@Value("\${cloud.aws.cloud-front.host}")
private val cloudFrontHost: String
) {
fun getCommunityTab(
creatorId: Long,
viewer: Member,
page: Int?,
size: Int?,
now: LocalDateTime = LocalDateTime.now()
): CreatorChannelCommunityTab {
val communityPage = 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 preference = memberContentPreferenceService.getStoredPreference(viewer)
val canViewAdultContent = isAdultVisibleByPolicy(viewer, preference.isAdultContentVisible)
val fetchedPosts = queryPort.findCommunityPosts(
creatorId = creatorId,
viewerId = viewerId,
canViewAdultContent = canViewAdultContent,
offset = communityPage.offset,
limit = communityPage.fetchLimit
)
return CreatorChannelCommunityTab(
communityPostCount = queryPort.countCommunityPosts(
creatorId = creatorId,
viewerId = viewerId,
canViewAdultContent = canViewAdultContent
),
communityPosts = queryPolicy.limitItems(fetchedPosts, communityPage).map { it.toDomain(viewerId) },
page = communityPage,
hasNext = queryPolicy.hasNext(fetchedPosts, communityPage)
)
}
fun findHomeCommunityPosts(
creatorId: Long,
viewerId: Long,
isPinned: Boolean,
canViewAdultContent: Boolean,
limit: Int
): List<CreatorChannelCommunityPost> {
return queryPortProvider.getObject()
.findHomeCommunityPosts(
creatorId = creatorId,
viewerId = viewerId,
isPinned = isPinned,
canViewAdultContent = canViewAdultContent,
limit = limit
)
.map { it.toDomain(viewerId) }
}
private fun validateCreatorRole(creator: CreatorChannelCommunityCreatorRecord) {
when (creator.role) {
MemberRole.CREATOR -> return
else -> throw SodaException(messageKey = "member.validation.creator_not_found")
}
}
private fun CreatorChannelCommunityPostRecord.toDomain(viewerId: Long): CreatorChannelCommunityPost {
val canAccessPaidContent = price <= 0 || viewerId == creatorId || existOrdered
return CreatorChannelCommunityPost(
postId = postId,
creatorId = creatorId,
creatorNickname = creatorNickname,
creatorProfileUrl = creatorProfilePath.toCdnUrl(cloudFrontHost) ?: defaultProfileImageUrl(),
imageUrl = if (canAccessPaidContent) imagePath.toCdnUrl(cloudFrontHost) else null,
audioUrl = if (canAccessPaidContent) audioPath.toSignedAudioUrl() else null,
content = queryPolicy.maskPaidContent(
content = content,
price = price,
isCreatorSelf = viewerId == creatorId,
existOrdered = existOrdered
),
price = price,
createdAt = createdAt,
existOrdered = existOrdered || viewerId == creatorId,
isCommentAvailable = isCommentAvailable,
likeCount = likeCount,
commentCount = commentCount,
isPinned = isPinned
)
}
private fun String?.toSignedAudioUrl(): String? {
if (isNullOrBlank()) return null
return audioContentCloudFront.generateSignedURL(this, AUDIO_SIGNED_URL_EXPIRATION_MILLIS)
}
private fun defaultProfileImageUrl(): String = "$cloudFrontHost/profile/default-profile.png"
companion object {
private const val AUDIO_SIGNED_URL_EXPIRATION_MILLIS = 1000L * 60 * 30
}
}