From 78f4c5623210fe9b6caac41efddeebd2b7871335 Mon Sep 17 00:00:00 2001 From: Klaus Date: Mon, 29 Dec 2025 12:08:41 +0900 Subject: [PATCH] =?UTF-8?q?=ED=9B=84=EC=9B=90=20=EB=9E=AD=ED=82=B9=20?= =?UTF-8?q?=EC=BA=90=EC=8B=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sodalive/explorer/ExplorerService.kt | 9 ++- .../explorer/MemberDonationRankingResponse.kt | 19 ++++-- .../CreatorDonationRankingQueryRepository.kt | 65 +++++++++++++++++++ .../profile/CreatorDonationRankingService.kt | 53 +++++++++++++++ 4 files changed, 137 insertions(+), 9 deletions(-) create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/CreatorDonationRankingQueryRepository.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/CreatorDonationRankingService.kt 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 9a81fcd9..3cad7f4e 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerService.kt @@ -14,6 +14,7 @@ import kr.co.vividnext.sodalive.explorer.profile.ChannelNotice import kr.co.vividnext.sodalive.explorer.profile.ChannelNoticeRepository import kr.co.vividnext.sodalive.explorer.profile.CreatorCheers import kr.co.vividnext.sodalive.explorer.profile.CreatorCheersRepository +import kr.co.vividnext.sodalive.explorer.profile.CreatorDonationRankingService import kr.co.vividnext.sodalive.explorer.profile.PostWriteCheersRequest import kr.co.vividnext.sodalive.explorer.profile.PutWriteCheersRequest import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.CreatorCommunityService @@ -43,6 +44,8 @@ import kotlin.random.Random class ExplorerService( private val memberService: MemberService, private val audioContentService: AudioContentService, + private val donationRankingService: CreatorDonationRankingService, + private val queryRepository: ExplorerQueryRepository, private val cheersRepository: CreatorCheersRepository, private val noticeRepository: ChannelNoticeRepository, @@ -217,9 +220,9 @@ class ExplorerService( val memberDonationRanking = if ( isCreator && (creatorId == member.id!! || creatorAccount.isVisibleDonationRank) ) { - queryRepository.getMemberDonationRanking( - creatorId, - 10, + donationRankingService.getMemberDonationRanking( + creatorId = creatorId, + limit = 10, withDonationCan = creatorId == member.id!! ) } else { diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/MemberDonationRankingResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/MemberDonationRankingResponse.kt index 0776312a..53e7dc69 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/explorer/MemberDonationRankingResponse.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/MemberDonationRankingResponse.kt @@ -1,5 +1,8 @@ package kr.co.vividnext.sodalive.explorer +import com.fasterxml.jackson.annotation.JsonProperty +import java.io.Serializable + data class GetDonationAllResponse( val accumulatedCansToday: Int, val accumulatedCansLastWeek: Int, @@ -7,11 +10,15 @@ data class GetDonationAllResponse( val isVisibleDonationRank: Boolean, val totalCount: Int, val userDonationRanking: List -) +) : Serializable data class MemberDonationRankingResponse( - val userId: Long, - val nickname: String, - val profileImage: String, - val donationCan: Int -) + @JsonProperty("userId") val userId: Long, + @JsonProperty("nickname") val nickname: String, + @JsonProperty("profileImage") val profileImage: String, + @JsonProperty("donationCan") val donationCan: Int +) : Serializable + +data class MemberDonationRankingListResponse( + @JsonProperty("rankings") val rankings: List +) : Serializable 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 new file mode 100644 index 00000000..8929bce5 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/CreatorDonationRankingQueryRepository.kt @@ -0,0 +1,65 @@ +package kr.co.vividnext.sodalive.explorer.profile + +import com.fasterxml.jackson.annotation.JsonProperty +import com.querydsl.core.annotations.QueryProjection +import com.querydsl.jpa.impl.JPAQueryFactory +import kr.co.vividnext.sodalive.can.use.CanUsage +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.DayOfWeek +import java.time.LocalDateTime +import java.time.LocalTime +import java.time.temporal.TemporalAdjusters + +@Repository +class CreatorDonationRankingQueryRepository(private val queryFactory: JPAQueryFactory) { + fun getMemberDonationRanking( + creatorId: Long, + limit: Long + ): List { + val now = LocalDateTime.now() + val lastMonday = now.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)) + .minusWeeks(1) + .with(LocalTime.MIN) + val lastSunday = lastMonday.plusDays(6).with(LocalTime.MAX) + + val donationCan = useCan.rewardCan.add(useCan.can).sum() + return queryFactory + .select( + QDonationRankingProjection( + member.id, + member.nickname, + member.profileImage, + donationCan + ) + ) + .from(useCanCalculate) + .innerJoin(useCanCalculate.useCan, useCan) + .innerJoin(useCan.member, member) + .where( + useCan.member.isActive.isTrue + .and(useCan.isRefund.isFalse) + .and(useCanCalculate.recipientCreatorId.eq(creatorId)) + .and( + useCan.canUsage.eq(CanUsage.DONATION) + .or(useCan.canUsage.eq(CanUsage.SPIN_ROULETTE)) + .or(useCan.canUsage.eq(CanUsage.LIVE)) + ) + .and(useCan.createdAt.between(lastMonday, lastSunday)) + ) + .offset(0) + .limit(limit) + .groupBy(member.id) + .orderBy(donationCan.desc(), member.id.desc()) + .fetch() + } +} + +data class DonationRankingProjection @QueryProjection constructor( + @JsonProperty("memberId") val memberId: Long, + @JsonProperty("nickname") val nickname: String, + @JsonProperty("profileImage") val profileImage: String, + @JsonProperty("donationCan") val donationCan: Int +) 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 new file mode 100644 index 00000000..cbf51577 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/explorer/profile/CreatorDonationRankingService.kt @@ -0,0 +1,53 @@ +package kr.co.vividnext.sodalive.explorer.profile + +import kr.co.vividnext.sodalive.explorer.MemberDonationRankingListResponse +import kr.co.vividnext.sodalive.explorer.MemberDonationRankingResponse +import org.springframework.data.redis.core.RedisTemplate +import org.springframework.stereotype.Service +import java.time.DayOfWeek +import java.time.Duration +import java.time.LocalDateTime +import java.time.LocalTime +import java.time.temporal.ChronoUnit +import java.time.temporal.TemporalAdjusters + +@Service +class CreatorDonationRankingService( + private val repository: CreatorDonationRankingQueryRepository, + private val redisTemplate: RedisTemplate +) { + fun getMemberDonationRanking( + creatorId: Long, + limit: Long, + withDonationCan: Boolean + ): List { + val cacheKey = "creator_donation_ranking:$creatorId:$limit:$withDonationCan" + val cachedData = redisTemplate.opsForValue().get(cacheKey) as? MemberDonationRankingListResponse + if (cachedData != null) { + return cachedData.rankings + } + + val memberDonationRanking = repository.getMemberDonationRanking(creatorId, limit) + + val result = memberDonationRanking.map { + MemberDonationRankingResponse( + it.memberId, + it.nickname, + it.profileImage, + if (withDonationCan) it.donationCan else 0 + ) + } + + val now = LocalDateTime.now() + val nextMonday = now.with(TemporalAdjusters.next(DayOfWeek.MONDAY)).with(LocalTime.MIN) + val secondsUntilNextMonday = ChronoUnit.SECONDS.between(now, nextMonday) + + redisTemplate.opsForValue().set( + cacheKey, + MemberDonationRankingListResponse(result), + Duration.ofSeconds(secondsUntilNextMonday) + ) + + return result + } +}