feat(channel-donation): 소유자 전용 후원 숨김 API와 조회 필터를 추가한다

This commit is contained in:
2026-11-05 02:20:34 +09:00
parent f073de374d
commit 4fbe4ced28
15 changed files with 812 additions and 10 deletions
@@ -6,6 +6,7 @@ import kr.co.vividnext.sodalive.i18n.LangContext
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.v2.api.creator.channel.donation.application.CreatorChannelDonationFacade
import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.web.bind.annotation.DeleteMapping
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestMapping
@@ -18,6 +19,16 @@ class CreatorChannelDonationController(
private val creatorChannelDonationFacade: CreatorChannelDonationFacade,
private val langContext: LangContext
) {
@DeleteMapping("/{creatorId}/donations/{donationId}")
fun hideDonation(
@PathVariable creatorId: Long,
@PathVariable donationId: Long,
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
): ApiResponse<Unit> {
creatorChannelDonationFacade.hide(creatorId, donationId, requireMember(member))
return ApiResponse.ok<Unit>()
}
@GetMapping("/{creatorId}/donations")
fun getDonationTab(
@PathVariable creatorId: Long,
@@ -4,6 +4,7 @@ import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.v2.api.creator.channel.donation.dto.CreatorChannelDonationTabResponse
import kr.co.vividnext.sodalive.v2.creator.channel.donation.application.CreatorChannelDonationQueryService
import kr.co.vividnext.sodalive.v2.creator.channel.donation.application.HideCreatorChannelDonationService
import kr.co.vividnext.sodalive.v2.creatorname.application.CreatorLocalizedNameService
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
@@ -13,8 +14,14 @@ import java.time.LocalDateTime
@Transactional(readOnly = true)
class CreatorChannelDonationFacade(
private val creatorChannelDonationQueryService: CreatorChannelDonationQueryService,
private val creatorLocalizedNameService: CreatorLocalizedNameService
private val creatorLocalizedNameService: CreatorLocalizedNameService,
private val hideCreatorChannelDonationService: HideCreatorChannelDonationService
) {
@Transactional
fun hide(creatorId: Long, donationId: Long, actor: Member) {
hideCreatorChannelDonationService.hide(creatorId, donationId, actor)
}
fun getDonationTab(
creatorId: Long,
viewer: Member,
@@ -1,5 +1,6 @@
package kr.co.vividnext.sodalive.v2.api.creator.channel.donation.dto
import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.annotation.JsonProperty
import kr.co.vividnext.sodalive.extensions.toUtcIso
import kr.co.vividnext.sodalive.v2.creator.channel.donation.domain.CreatorChannelDonation
@@ -48,6 +49,8 @@ data class MemberDonationRankingResponse(
}
data class CreatorChannelDonationResponse(
@JsonInclude(JsonInclude.Include.NON_NULL)
val donationId: Long?,
val nickname: String,
val profileImageUrl: String,
val can: Int,
@@ -59,6 +62,7 @@ data class CreatorChannelDonationResponse(
companion object {
fun from(donation: CreatorChannelDonation): CreatorChannelDonationResponse {
return CreatorChannelDonationResponse(
donationId = donation.donationId,
nickname = donation.nickname,
profileImageUrl = donation.profileImageUrl,
can = donation.can,
@@ -87,6 +87,7 @@ class DefaultCreatorChannelDonationQueryRepository(
.select(
Projections.constructor(
CreatorChannelDonationRecord::class.java,
channelDonationMessage.id,
channelDonationMessage.member.id,
donorName.displayName,
channelDonationMessage.member.profileImage,
@@ -113,6 +114,7 @@ class DefaultCreatorChannelDonationQueryRepository(
): BooleanExpression {
val monthRange = queryPolicy.currentKstMonthRange(now)
return channelDonationMessage.creator.id.eq(creatorId)
.and(channelDonationMessage.isHidden.isFalse)
.and(channelDonationMessage.createdAt.goe(monthRange.startInclusiveUtc))
.and(channelDonationMessage.createdAt.lt(monthRange.endExclusiveUtc))
}
@@ -68,7 +68,7 @@ class CreatorChannelDonationQueryService(
return CreatorChannelDonationTab(
donationCount = queryPort.countChannelDonations(creatorId, viewerId, now),
rankings = findRankings(creator, viewerId),
donations = queryPolicy.limitItems(fetchedDonations, donationPage).map { it.toDomain(creatorId, viewerId, lang) },
donations = queryPolicy.limitItems(fetchedDonations, donationPage).map { it.toDomain(creatorId, viewer, lang) },
page = donationPage,
hasNext = queryPolicy.hasNext(fetchedDonations, donationPage)
)
@@ -95,9 +95,11 @@ class CreatorChannelDonationQueryService(
).map { it.toDomain() }
}
private fun CreatorChannelDonationRecord.toDomain(creatorId: Long, viewerId: Long, lang: Lang): CreatorChannelDonation {
private fun CreatorChannelDonationRecord.toDomain(creatorId: Long, viewer: Member, lang: Lang): CreatorChannelDonation {
val viewerId = viewer.id!!
val isMasked = isSecret && viewerId != creatorId && viewerId != memberId
return CreatorChannelDonation(
donationId = donationId.takeIf { viewer.role == MemberRole.CREATOR && viewerId == creatorId },
memberId = memberId,
nickname = when {
isMasked -> messageSource.getMessage("explorer.channel_donation.anonymous.nickname", lang).orEmpty()
@@ -0,0 +1,23 @@
package kr.co.vividnext.sodalive.v2.creator.channel.donation.application
import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.MemberRole
import kr.co.vividnext.sodalive.v2.creator.channel.donation.port.out.HideCreatorChannelDonationPort
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
@Service
class HideCreatorChannelDonationService(
private val hideCreatorChannelDonationPort: HideCreatorChannelDonationPort
) {
@Transactional
fun hide(creatorId: Long, donationId: Long, actor: Member) {
if (actor.role != MemberRole.CREATOR || actor.id != creatorId) {
throw SodaException(messageKey = "common.error.access_denied")
}
if (!hideCreatorChannelDonationPort.hideOwnedDonation(creatorId, donationId)) {
throw SodaException(messageKey = "common.error.invalid_request")
}
}
}
@@ -19,6 +19,7 @@ data class CreatorChannelDonationRanking(
)
data class CreatorChannelDonation(
val donationId: Long?,
val memberId: Long,
val nickname: String,
val profileImageUrl: String,
@@ -35,6 +35,7 @@ data class CreatorChannelDonationCreatorRecord(
)
data class CreatorChannelDonationRecord(
val donationId: Long,
val memberId: Long,
val nickname: String,
val profileImagePath: String?,