후원랭킹 기간 선택 반영

프로필 업데이트에 후원랭킹 기간 선택을 추가하고
프로필 후원랭킹 조회가 선택한 기간을 사용한다
This commit is contained in:
2026-02-03 15:48:42 +09:00
parent 5eca3f770c
commit f1f80ae386
6 changed files with 99 additions and 11 deletions

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.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(

View File

@@ -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<DonationRankingProjection> {
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(

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.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<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
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<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,
@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
}

View File

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

View File

@@ -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
)