Compare commits

...

2 Commits

Author SHA1 Message Date
3cabc9de95 후원랭킹 기간 선택 반영
크리에이터 본인 조회 시 후원랭킹 기간을 선택하도록
period 파라미터를 제공한다.
2026-02-03 16:05:26 +09:00
f1f80ae386 후원랭킹 기간 선택 반영
프로필 업데이트에 후원랭킹 기간 선택을 추가하고
프로필 후원랭킹 조회가 선택한 기간을 사용한다
2026-02-03 15:48:42 +09:00
7 changed files with 109 additions and 12 deletions

View File

@@ -7,6 +7,7 @@ import kr.co.vividnext.sodalive.explorer.profile.PostWriteCheersRequest
import kr.co.vividnext.sodalive.explorer.profile.PutWriteCheersRequest import kr.co.vividnext.sodalive.explorer.profile.PutWriteCheersRequest
import kr.co.vividnext.sodalive.i18n.LangContext import kr.co.vividnext.sodalive.i18n.LangContext
import kr.co.vividnext.sodalive.i18n.SodaMessageSource 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.Member
import org.springframework.data.domain.Pageable import org.springframework.data.domain.Pageable
import org.springframework.security.access.prepost.PreAuthorize import org.springframework.security.access.prepost.PreAuthorize
@@ -75,11 +76,12 @@ class ExplorerController(
@GetMapping("/profile/{id}/donation-rank") @GetMapping("/profile/{id}/donation-rank")
fun getCreatorProfileDonationRanking( fun getCreatorProfileDonationRanking(
@PathVariable("id") creatorId: Long, @PathVariable("id") creatorId: Long,
@RequestParam("period", required = false) period: DonationRankingPeriod? = null,
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?, @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?,
pageable: Pageable pageable: Pageable
) = run { ) = run {
if (member == null) throw SodaException(messageKey = "common.error.bad_credentials") if (member == null) throw SodaException(messageKey = "common.error.bad_credentials")
ApiResponse.ok(service.getCreatorProfileDonationRanking(creatorId, pageable, member)) ApiResponse.ok(service.getCreatorProfileDonationRanking(creatorId, period, pageable, member))
} }
@PostMapping("/profile/cheers") @PostMapping("/profile/cheers")

View File

@@ -24,6 +24,7 @@ import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.i18n.LangContext import kr.co.vividnext.sodalive.i18n.LangContext
import kr.co.vividnext.sodalive.i18n.SodaMessageSource import kr.co.vividnext.sodalive.i18n.SodaMessageSource
import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailUser import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailUser
import kr.co.vividnext.sodalive.member.DonationRankingPeriod
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.member.MemberRole
import kr.co.vividnext.sodalive.member.MemberService import kr.co.vividnext.sodalive.member.MemberService
@@ -215,6 +216,7 @@ class ExplorerService(
val notificationUserIds = queryRepository.getNotificationUserIds(creatorId) val notificationUserIds = queryRepository.getNotificationUserIds(creatorId)
val creatorFollowing = queryRepository.getCreatorFollowing(creatorId = creatorId, memberId = member.id!!) val creatorFollowing = queryRepository.getCreatorFollowing(creatorId = creatorId, memberId = member.id!!)
val notificationRecipientCount = notificationUserIds.size val notificationRecipientCount = notificationUserIds.size
val donationRankingPeriod = creatorAccount.donationRankingPeriod ?: DonationRankingPeriod.CUMULATIVE
// 후원랭킹 // 후원랭킹
val memberDonationRanking = if ( val memberDonationRanking = if (
@@ -223,7 +225,8 @@ class ExplorerService(
donationRankingService.getMemberDonationRanking( donationRankingService.getMemberDonationRanking(
creatorId = creatorId, creatorId = creatorId,
limit = 10, limit = 10,
withDonationCan = creatorId == member.id!! withDonationCan = creatorId == member.id!!,
period = donationRankingPeriod
) )
} else { } else {
listOf() listOf()
@@ -396,19 +399,33 @@ class ExplorerService(
fun getCreatorProfileDonationRanking( fun getCreatorProfileDonationRanking(
creatorId: Long, creatorId: Long,
period: DonationRankingPeriod?,
pageable: Pageable, pageable: Pageable,
member: Member member: Member
): GetDonationAllResponse { ): GetDonationAllResponse {
val creatorAccount = queryRepository.getMember(creatorId)
?: throw SodaException(messageKey = "member.validation.user_not_found")
val donationRankingPeriod = creatorAccount.donationRankingPeriod ?: DonationRankingPeriod.CUMULATIVE
val isCreatorSelf = creatorId == member.id!!
val effectivePeriod = if (isCreatorSelf && period != null) {
period
} else {
donationRankingPeriod
}
val currentDate = LocalDate.now().atTime(0, 0, 0) val currentDate = LocalDate.now().atTime(0, 0, 0)
val firstDayOfLastWeek = currentDate.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)).minusDays(7) val firstDayOfLastWeek = currentDate.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)).minusDays(7)
val firstDayOfMonth = currentDate.with(TemporalAdjusters.firstDayOfMonth()) val firstDayOfMonth = currentDate.with(TemporalAdjusters.firstDayOfMonth())
val donationMemberTotal = donationRankingService.getMemberDonationRankingTotal(creatorId) val donationMemberTotal = donationRankingService.getMemberDonationRankingTotal(
creatorId,
effectivePeriod
)
val donationRanking = donationRankingService.getMemberDonationRanking( val donationRanking = donationRankingService.getMemberDonationRanking(
creatorId = creatorId, creatorId = creatorId,
offset = pageable.offset, offset = pageable.offset,
limit = pageable.pageSize.toLong(), limit = pageable.pageSize.toLong(),
withDonationCan = creatorId == member.id!! withDonationCan = isCreatorSelf,
period = effectivePeriod
) )
return GetDonationAllResponse( return GetDonationAllResponse(

View File

@@ -1,6 +1,7 @@
package kr.co.vividnext.sodalive.explorer.profile package kr.co.vividnext.sodalive.explorer.profile
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty
import com.querydsl.core.BooleanBuilder
import com.querydsl.core.annotations.QueryProjection import com.querydsl.core.annotations.QueryProjection
import com.querydsl.jpa.impl.JPAQueryFactory import com.querydsl.jpa.impl.JPAQueryFactory
import kr.co.vividnext.sodalive.can.use.CanUsage import kr.co.vividnext.sodalive.can.use.CanUsage
@@ -8,13 +9,17 @@ import kr.co.vividnext.sodalive.can.use.QUseCan.useCan
import kr.co.vividnext.sodalive.can.use.QUseCanCalculate.useCanCalculate import kr.co.vividnext.sodalive.can.use.QUseCanCalculate.useCanCalculate
import kr.co.vividnext.sodalive.member.QMember.member import kr.co.vividnext.sodalive.member.QMember.member
import org.springframework.stereotype.Repository import org.springframework.stereotype.Repository
import java.time.LocalDateTime
import java.time.ZoneId
@Repository @Repository
class CreatorDonationRankingQueryRepository(private val queryFactory: JPAQueryFactory) { class CreatorDonationRankingQueryRepository(private val queryFactory: JPAQueryFactory) {
fun getMemberDonationRanking( fun getMemberDonationRanking(
creatorId: Long, creatorId: Long,
offset: Long, offset: Long,
limit: Long limit: Long,
startDate: LocalDateTime? = null,
endDate: LocalDateTime? = null
): List<DonationRankingProjection> { ): List<DonationRankingProjection> {
val donationCan = useCan.rewardCan.add(useCan.can).sum() val donationCan = useCan.rewardCan.add(useCan.can).sum()
return queryFactory return queryFactory
@@ -38,6 +43,7 @@ class CreatorDonationRankingQueryRepository(private val queryFactory: JPAQueryFa
.or(useCan.canUsage.eq(CanUsage.SPIN_ROULETTE)) .or(useCan.canUsage.eq(CanUsage.SPIN_ROULETTE))
.or(useCan.canUsage.eq(CanUsage.LIVE)) .or(useCan.canUsage.eq(CanUsage.LIVE))
) )
.and(buildDateRangeCondition(startDate, endDate))
) )
.offset(offset) .offset(offset)
.limit(limit) .limit(limit)
@@ -46,7 +52,11 @@ class CreatorDonationRankingQueryRepository(private val queryFactory: JPAQueryFa
.fetch() .fetch()
} }
fun getMemberDonationRankingTotal(creatorId: Long): Int { fun getMemberDonationRankingTotal(
creatorId: Long,
startDate: LocalDateTime? = null,
endDate: LocalDateTime? = null
): Int {
return queryFactory return queryFactory
.select(member.id) .select(member.id)
.from(useCanCalculate) .from(useCanCalculate)
@@ -61,11 +71,32 @@ class CreatorDonationRankingQueryRepository(private val queryFactory: JPAQueryFa
.or(useCan.canUsage.eq(CanUsage.SPIN_ROULETTE)) .or(useCan.canUsage.eq(CanUsage.SPIN_ROULETTE))
.or(useCan.canUsage.eq(CanUsage.LIVE)) .or(useCan.canUsage.eq(CanUsage.LIVE))
) )
.and(buildDateRangeCondition(startDate, endDate))
) )
.groupBy(member.id) .groupBy(member.id)
.fetch() .fetch()
.size .size
} }
private fun buildDateRangeCondition(
startDate: LocalDateTime?,
endDate: LocalDateTime?
): BooleanBuilder {
val condition = BooleanBuilder()
if (startDate != null && endDate != null) {
val startUtc = startDate
.atZone(ZoneId.of("Asia/Seoul"))
.withZoneSameInstant(ZoneId.of("UTC"))
.toLocalDateTime()
val endUtc = endDate
.atZone(ZoneId.of("Asia/Seoul"))
.withZoneSameInstant(ZoneId.of("UTC"))
.toLocalDateTime()
condition.and(useCanCalculate.createdAt.goe(startUtc))
condition.and(useCanCalculate.createdAt.lt(endUtc))
}
return condition
}
} }
data class DonationRankingProjection @QueryProjection constructor( data class DonationRankingProjection @QueryProjection constructor(

View File

@@ -2,11 +2,13 @@ package kr.co.vividnext.sodalive.explorer.profile
import kr.co.vividnext.sodalive.explorer.MemberDonationRankingListResponse import kr.co.vividnext.sodalive.explorer.MemberDonationRankingListResponse
import kr.co.vividnext.sodalive.explorer.MemberDonationRankingResponse import kr.co.vividnext.sodalive.explorer.MemberDonationRankingResponse
import kr.co.vividnext.sodalive.member.DonationRankingPeriod
import org.springframework.beans.factory.annotation.Value import org.springframework.beans.factory.annotation.Value
import org.springframework.data.redis.core.RedisTemplate import org.springframework.data.redis.core.RedisTemplate
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import java.time.DayOfWeek import java.time.DayOfWeek
import java.time.Duration import java.time.Duration
import java.time.LocalDate
import java.time.LocalDateTime import java.time.LocalDateTime
import java.time.LocalTime import java.time.LocalTime
import java.time.temporal.ChronoUnit import java.time.temporal.ChronoUnit
@@ -20,14 +22,22 @@ class CreatorDonationRankingService(
@Value("\${cloud.aws.cloud-front.host}") @Value("\${cloud.aws.cloud-front.host}")
private val imageHost: String private val imageHost: String
) { ) {
fun getMemberDonationRankingTotal(creatorId: Long): Int { fun getMemberDonationRankingTotal(
val cacheKey = "creator_donation_ranking_member_total_v2:$creatorId" creatorId: Long,
period: DonationRankingPeriod = DonationRankingPeriod.CUMULATIVE
): Int {
val cacheKey = "creator_donation_ranking_member_total_v2:$creatorId:$period"
val cachedTotal = redisTemplate.opsForValue().get(cacheKey) as? Int val cachedTotal = redisTemplate.opsForValue().get(cacheKey) as? Int
if (cachedTotal != null) { if (cachedTotal != null) {
return cachedTotal return cachedTotal
} }
val total = repository.getMemberDonationRankingTotal(creatorId) val weeklyDateRange = getWeeklyDateRange(period)
val total = if (weeklyDateRange == null) {
repository.getMemberDonationRankingTotal(creatorId)
} else {
repository.getMemberDonationRankingTotal(creatorId, weeklyDateRange.first, weeklyDateRange.second)
}
val now = LocalDateTime.now() val now = LocalDateTime.now()
val nextMonday = now.with(TemporalAdjusters.next(DayOfWeek.MONDAY)).with(LocalTime.MIN) val nextMonday = now.with(TemporalAdjusters.next(DayOfWeek.MONDAY)).with(LocalTime.MIN)
@@ -46,15 +56,27 @@ class CreatorDonationRankingService(
creatorId: Long, creatorId: Long,
offset: Long = 0, offset: Long = 0,
limit: Long = 10, limit: Long = 10,
withDonationCan: Boolean withDonationCan: Boolean,
period: DonationRankingPeriod = DonationRankingPeriod.CUMULATIVE
): List<MemberDonationRankingResponse> { ): List<MemberDonationRankingResponse> {
val cacheKey = "creator_donation_ranking_v2:$creatorId:$offset:$limit:$withDonationCan" val cacheKey = "creator_donation_ranking_v2:$creatorId:$period:$offset:$limit:$withDonationCan"
val cachedData = redisTemplate.opsForValue().get(cacheKey) as? MemberDonationRankingListResponse val cachedData = redisTemplate.opsForValue().get(cacheKey) as? MemberDonationRankingListResponse
if (cachedData != null) { if (cachedData != null) {
return cachedData.rankings return cachedData.rankings
} }
val memberDonationRanking = repository.getMemberDonationRanking(creatorId, offset, limit) val weeklyDateRange = getWeeklyDateRange(period)
val memberDonationRanking = if (weeklyDateRange == null) {
repository.getMemberDonationRanking(creatorId, offset, limit)
} else {
repository.getMemberDonationRanking(
creatorId,
offset,
limit,
weeklyDateRange.first,
weeklyDateRange.second
)
}
val result = memberDonationRanking.map { val result = memberDonationRanking.map {
MemberDonationRankingResponse( MemberDonationRankingResponse(
@@ -77,4 +99,17 @@ class CreatorDonationRankingService(
return result return result
} }
private fun getWeeklyDateRange(period: DonationRankingPeriod): Pair<LocalDateTime, LocalDateTime>? {
if (period != DonationRankingPeriod.WEEKLY) {
return null
}
val currentDate = LocalDate.now()
val lastWeekMonday = currentDate.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)).minusWeeks(1)
val startDate = lastWeekMonday.atStartOfDay()
val endDate = startDate.plusDays(7)
return startDate to endDate
}
} }

View File

@@ -47,6 +47,9 @@ data class Member(
var isVisibleDonationRank: Boolean = true, var isVisibleDonationRank: Boolean = true,
@Enumerated(value = EnumType.STRING)
var donationRankingPeriod: DonationRankingPeriod? = DonationRankingPeriod.CUMULATIVE,
var isActive: Boolean = true, var isActive: Boolean = true,
var container: String = "web", var container: String = "web",
@@ -178,3 +181,7 @@ enum class MemberRole {
enum class MemberProvider { enum class MemberProvider {
EMAIL, KAKAO, GOOGLE, APPLE, LINE EMAIL, KAKAO, GOOGLE, APPLE, LINE
} }
enum class DonationRankingPeriod {
WEEKLY, CUMULATIVE
}

View File

@@ -726,6 +726,10 @@ class MemberService(
member.isVisibleDonationRank = profileUpdateRequest.isVisibleDonationRank member.isVisibleDonationRank = profileUpdateRequest.isVisibleDonationRank
} }
if (profileUpdateRequest.donationRankingPeriod != null) {
member.donationRankingPeriod = profileUpdateRequest.donationRankingPeriod
}
return ProfileResponse(member, cloudFrontHost, profileUpdateRequest.container) return ProfileResponse(member, cloudFrontHost, profileUpdateRequest.container)
} }

View File

@@ -14,5 +14,6 @@ data class ProfileUpdateRequest(
val websiteUrl: String? = null, val websiteUrl: String? = null,
val blogUrl: String? = null, val blogUrl: String? = null,
val isVisibleDonationRank: Boolean? = null, val isVisibleDonationRank: Boolean? = null,
val donationRankingPeriod: DonationRankingPeriod? = null,
val container: String val container: String
) )