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

This commit is contained in:
2026-06-22 19:17:45 +09:00
parent 13b679d091
commit 046ce700c7
4 changed files with 469 additions and 27 deletions

View File

@@ -4,7 +4,6 @@ import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.v2.api.creator.channel.donation.application.CreatorChannelDonationFacade
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
@@ -13,7 +12,6 @@ import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
@RestController
@ConditionalOnProperty(name = ["creator-channel.donation-tab.enabled"], havingValue = "true")
@RequestMapping("/api/v2/creator-channels")
class CreatorChannelDonationController(
private val creatorChannelDonationFacade: CreatorChannelDonationFacade

View File

@@ -1,13 +1,39 @@
package kr.co.vividnext.sodalive.v2.creator.channel.donation.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.DonationRankingPeriod
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.donation.domain.CreatorChannelDonation
import kr.co.vividnext.sodalive.v2.creator.channel.donation.domain.CreatorChannelDonationQueryPolicy
import kr.co.vividnext.sodalive.v2.creator.channel.donation.domain.CreatorChannelDonationRanking
import kr.co.vividnext.sodalive.v2.creator.channel.donation.domain.CreatorChannelDonationTab
import kr.co.vividnext.sodalive.v2.creator.channel.donation.port.out.CreatorChannelDonationCreatorRecord
import kr.co.vividnext.sodalive.v2.creator.channel.donation.port.out.CreatorChannelDonationQueryPort
import kr.co.vividnext.sodalive.v2.creator.channel.donation.port.out.CreatorChannelDonationRankingPort
import kr.co.vividnext.sodalive.v2.creator.channel.donation.port.out.CreatorChannelDonationRankingRecord
import kr.co.vividnext.sodalive.v2.creator.channel.donation.port.out.CreatorChannelDonationRecord
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
class CreatorChannelDonationQueryService {
@Transactional(readOnly = true)
class CreatorChannelDonationQueryService(
private val queryPortProvider: ObjectProvider<CreatorChannelDonationQueryPort>,
private val rankingPort: CreatorChannelDonationRankingPort,
private val queryPolicy: CreatorChannelDonationQueryPolicy,
private val messageSource: SodaMessageSource,
private val langContext: LangContext,
@Value("\${cloud.aws.cloud-front.host}")
private val cloudFrontHost: String
) {
fun getDonationTab(
creatorId: Long,
viewer: Member,
@@ -15,6 +41,73 @@ class CreatorChannelDonationQueryService {
size: Int?,
now: LocalDateTime
): CreatorChannelDonationTab {
throw SodaException(messageKey = "common.error.invalid_request")
val donationPage = 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 fetchedDonations = queryPort.findChannelDonations(
creatorId = creatorId,
viewerId = viewerId,
now = now,
offset = donationPage.offset,
limit = donationPage.fetchLimit
)
return CreatorChannelDonationTab(
donationCount = queryPort.countChannelDonations(creatorId, viewerId, now),
rankings = findRankings(creator, viewerId),
donations = queryPolicy.limitItems(fetchedDonations, donationPage).map { it.toDomain() },
page = donationPage,
hasNext = queryPolicy.hasNext(fetchedDonations, donationPage)
)
}
private fun validateCreatorRole(creator: CreatorChannelDonationCreatorRecord) {
when (creator.role) {
MemberRole.CREATOR -> return
else -> throw SodaException(messageKey = "member.validation.creator_not_found")
}
}
private fun findRankings(
creator: CreatorChannelDonationCreatorRecord,
viewerId: Long
): List<CreatorChannelDonationRanking> {
val isViewerCreator = viewerId == creator.creatorId
if (!isViewerCreator && !creator.isVisibleDonationRank) return emptyList()
return rankingPort.findTopRankings(
creatorId = creator.creatorId,
period = creator.donationRankingPeriod ?: DonationRankingPeriod.CUMULATIVE,
withDonationCan = isViewerCreator
).map { it.toDomain() }
}
private fun CreatorChannelDonationRecord.toDomain() = CreatorChannelDonation(
nickname = nickname.removeDeletedNicknamePrefix(),
profileImageUrl = profileImagePath.toCdnUrl(cloudFrontHost) ?: defaultProfileImageUrl(),
can = can,
message = message.orEmpty(),
createdAt = createdAt
)
private fun CreatorChannelDonationRankingRecord.toDomain() = CreatorChannelDonationRanking(
userId = userId,
nickname = nickname,
profileImage = profileImage,
donationCan = donationCan
)
private fun defaultProfileImageUrl(): String = "$cloudFrontHost/profile/default-profile.png"
}