From f1f80ae386a13b4217f742256f83ad2594ae8b89 Mon Sep 17 00:00:00 2001 From: Klaus Date: Tue, 3 Feb 2026 15:48:42 +0900 Subject: [PATCH] =?UTF-8?q?=ED=9B=84=EC=9B=90=EB=9E=AD=ED=82=B9=20?= =?UTF-8?q?=EA=B8=B0=EA=B0=84=20=EC=84=A0=ED=83=9D=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 프로필 업데이트에 후원랭킹 기간 선택을 추가하고 프로필 후원랭킹 조회가 선택한 기간을 사용한다 --- .../sodalive/explorer/ExplorerService.kt | 16 +++++-- .../CreatorDonationRankingQueryRepository.kt | 35 +++++++++++++- .../profile/CreatorDonationRankingService.kt | 47 ++++++++++++++++--- .../kr/co/vividnext/sodalive/member/Member.kt | 7 +++ .../sodalive/member/MemberService.kt | 4 ++ .../sodalive/member/ProfileUpdateRequest.kt | 1 + 6 files changed, 99 insertions(+), 11 deletions(-) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerService.kt index bd003ee8..6d0a4b62 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerService.kt @@ -24,6 +24,7 @@ import kr.co.vividnext.sodalive.i18n.Lang import kr.co.vividnext.sodalive.i18n.LangContext import kr.co.vividnext.sodalive.i18n.SodaMessageSource 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.MemberRole import kr.co.vividnext.sodalive.member.MemberService @@ -215,6 +216,7 @@ class ExplorerService( val notificationUserIds = queryRepository.getNotificationUserIds(creatorId) val creatorFollowing = queryRepository.getCreatorFollowing(creatorId = creatorId, memberId = member.id!!) val notificationRecipientCount = notificationUserIds.size + val donationRankingPeriod = creatorAccount.donationRankingPeriod ?: DonationRankingPeriod.CUMULATIVE // 후원랭킹 val memberDonationRanking = if ( @@ -223,7 +225,8 @@ class ExplorerService( donationRankingService.getMemberDonationRanking( creatorId = creatorId, limit = 10, - withDonationCan = creatorId == member.id!! + withDonationCan = creatorId == member.id!!, + period = donationRankingPeriod ) } else { listOf() @@ -399,16 +402,23 @@ class ExplorerService( pageable: Pageable, member: Member ): GetDonationAllResponse { + val creatorAccount = queryRepository.getMember(creatorId) + ?: throw SodaException(messageKey = "member.validation.user_not_found") + val donationRankingPeriod = creatorAccount.donationRankingPeriod ?: DonationRankingPeriod.CUMULATIVE val currentDate = LocalDate.now().atTime(0, 0, 0) val firstDayOfLastWeek = currentDate.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)).minusDays(7) val firstDayOfMonth = currentDate.with(TemporalAdjusters.firstDayOfMonth()) - val donationMemberTotal = donationRankingService.getMemberDonationRankingTotal(creatorId) + val donationMemberTotal = donationRankingService.getMemberDonationRankingTotal( + creatorId, + donationRankingPeriod + ) val donationRanking = donationRankingService.getMemberDonationRanking( creatorId = creatorId, offset = pageable.offset, limit = pageable.pageSize.toLong(), - withDonationCan = creatorId == member.id!! + withDonationCan = creatorId == member.id!!, + period = donationRankingPeriod ) return GetDonationAllResponse( diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/CreatorDonationRankingQueryRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/CreatorDonationRankingQueryRepository.kt index 7ab5c2fa..51e02323 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/CreatorDonationRankingQueryRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/CreatorDonationRankingQueryRepository.kt @@ -1,6 +1,7 @@ package kr.co.vividnext.sodalive.explorer.profile import com.fasterxml.jackson.annotation.JsonProperty +import com.querydsl.core.BooleanBuilder import com.querydsl.core.annotations.QueryProjection import com.querydsl.jpa.impl.JPAQueryFactory 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.member.QMember.member import org.springframework.stereotype.Repository +import java.time.LocalDateTime +import java.time.ZoneId @Repository class CreatorDonationRankingQueryRepository(private val queryFactory: JPAQueryFactory) { fun getMemberDonationRanking( creatorId: Long, offset: Long, - limit: Long + limit: Long, + startDate: LocalDateTime? = null, + endDate: LocalDateTime? = null ): List { val donationCan = useCan.rewardCan.add(useCan.can).sum() return queryFactory @@ -38,6 +43,7 @@ class CreatorDonationRankingQueryRepository(private val queryFactory: JPAQueryFa .or(useCan.canUsage.eq(CanUsage.SPIN_ROULETTE)) .or(useCan.canUsage.eq(CanUsage.LIVE)) ) + .and(buildDateRangeCondition(startDate, endDate)) ) .offset(offset) .limit(limit) @@ -46,7 +52,11 @@ class CreatorDonationRankingQueryRepository(private val queryFactory: JPAQueryFa .fetch() } - fun getMemberDonationRankingTotal(creatorId: Long): Int { + fun getMemberDonationRankingTotal( + creatorId: Long, + startDate: LocalDateTime? = null, + endDate: LocalDateTime? = null + ): Int { return queryFactory .select(member.id) .from(useCanCalculate) @@ -61,11 +71,32 @@ class CreatorDonationRankingQueryRepository(private val queryFactory: JPAQueryFa .or(useCan.canUsage.eq(CanUsage.SPIN_ROULETTE)) .or(useCan.canUsage.eq(CanUsage.LIVE)) ) + .and(buildDateRangeCondition(startDate, endDate)) ) .groupBy(member.id) .fetch() .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( diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/CreatorDonationRankingService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/CreatorDonationRankingService.kt index bb8bf903..5505321b 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/CreatorDonationRankingService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/CreatorDonationRankingService.kt @@ -2,11 +2,13 @@ package kr.co.vividnext.sodalive.explorer.profile import kr.co.vividnext.sodalive.explorer.MemberDonationRankingListResponse import kr.co.vividnext.sodalive.explorer.MemberDonationRankingResponse +import kr.co.vividnext.sodalive.member.DonationRankingPeriod import org.springframework.beans.factory.annotation.Value import org.springframework.data.redis.core.RedisTemplate import org.springframework.stereotype.Service import java.time.DayOfWeek import java.time.Duration +import java.time.LocalDate import java.time.LocalDateTime import java.time.LocalTime import java.time.temporal.ChronoUnit @@ -20,14 +22,22 @@ class CreatorDonationRankingService( @Value("\${cloud.aws.cloud-front.host}") private val imageHost: String ) { - fun getMemberDonationRankingTotal(creatorId: Long): Int { - val cacheKey = "creator_donation_ranking_member_total_v2:$creatorId" + fun getMemberDonationRankingTotal( + 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 if (cachedTotal != null) { 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 nextMonday = now.with(TemporalAdjusters.next(DayOfWeek.MONDAY)).with(LocalTime.MIN) @@ -46,15 +56,27 @@ class CreatorDonationRankingService( creatorId: Long, offset: Long = 0, limit: Long = 10, - withDonationCan: Boolean + withDonationCan: Boolean, + period: DonationRankingPeriod = DonationRankingPeriod.CUMULATIVE ): List { - 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 if (cachedData != null) { 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 { MemberDonationRankingResponse( @@ -77,4 +99,17 @@ class CreatorDonationRankingService( return result } + + private fun getWeeklyDateRange(period: DonationRankingPeriod): Pair? { + 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 + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/Member.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/Member.kt index 2eaa42f3..75ff126f 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/Member.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/Member.kt @@ -47,6 +47,9 @@ data class Member( var isVisibleDonationRank: Boolean = true, + @Enumerated(value = EnumType.STRING) + var donationRankingPeriod: DonationRankingPeriod? = DonationRankingPeriod.CUMULATIVE, + var isActive: Boolean = true, var container: String = "web", @@ -178,3 +181,7 @@ enum class MemberRole { enum class MemberProvider { EMAIL, KAKAO, GOOGLE, APPLE, LINE } + +enum class DonationRankingPeriod { + WEEKLY, CUMULATIVE +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt index 02684ef2..e92fdc3a 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt @@ -726,6 +726,10 @@ class MemberService( member.isVisibleDonationRank = profileUpdateRequest.isVisibleDonationRank } + if (profileUpdateRequest.donationRankingPeriod != null) { + member.donationRankingPeriod = profileUpdateRequest.donationRankingPeriod + } + return ProfileResponse(member, cloudFrontHost, profileUpdateRequest.container) } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/ProfileUpdateRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/ProfileUpdateRequest.kt index 806f00d8..4f5d97de 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/ProfileUpdateRequest.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/ProfileUpdateRequest.kt @@ -14,5 +14,6 @@ data class ProfileUpdateRequest( val websiteUrl: String? = null, val blogUrl: String? = null, val isVisibleDonationRank: Boolean? = null, + val donationRankingPeriod: DonationRankingPeriod? = null, val container: String )