Merge pull request 'test' (#384) from test into main

Reviewed-on: #384
This commit is contained in:
2026-02-04 12:52:24 +00:00
24 changed files with 493 additions and 64 deletions

View File

@@ -68,6 +68,19 @@ class AdminMemberStatisticsRepository(private val queryFactory: JPAQueryFactory)
.size .size
} }
fun getTotalSignUpLineCount(startDate: LocalDateTime, endDate: LocalDateTime): Int {
return queryFactory
.select(member.id)
.from(member)
.where(
member.createdAt.goe(startDate),
member.createdAt.loe(endDate),
member.provider.eq(MemberProvider.LINE)
)
.fetch()
.size
}
fun getTotalAuthCount(startDate: LocalDateTime, endDate: LocalDateTime): Int { fun getTotalAuthCount(startDate: LocalDateTime, endDate: LocalDateTime): Int {
return queryFactory return queryFactory
.select(auth.id) .select(auth.id)
@@ -189,6 +202,25 @@ class AdminMemberStatisticsRepository(private val queryFactory: JPAQueryFactory)
.fetch() .fetch()
} }
fun getSignUpLineCountInRange(startDate: LocalDateTime, endDate: LocalDateTime): List<DateAndMemberCount> {
return queryFactory
.select(
QDateAndMemberCount(
getFormattedDate(member.createdAt),
member.id.countDistinct().castToNum(Int::class.java)
)
)
.from(member)
.where(
member.createdAt.goe(startDate),
member.createdAt.loe(endDate),
member.provider.eq(MemberProvider.LINE)
)
.groupBy(getFormattedDate(member.createdAt))
.orderBy(getFormattedDate(member.createdAt).desc())
.fetch()
}
fun getAuthCountInRange(startDate: LocalDateTime, endDate: LocalDateTime): List<DateAndMemberCount> { fun getAuthCountInRange(startDate: LocalDateTime, endDate: LocalDateTime): List<DateAndMemberCount> {
return queryFactory return queryFactory
.select( .select(

View File

@@ -58,6 +58,10 @@ class AdminMemberStatisticsService(private val repository: AdminMemberStatistics
startDate = startDateTime, startDate = startDateTime,
endDate = endDateTime endDate = endDateTime
) )
val totalSignUpLineCount = repository.getTotalSignUpLineCount(
startDate = startDateTime,
endDate = endDateTime
)
val totalAuthCount = repository.getTotalAuthCount(startDate = startDateTime, endDate = endDateTime) val totalAuthCount = repository.getTotalAuthCount(startDate = startDateTime, endDate = endDateTime)
val totalSignOutCount = repository.getTotalSignOutCount(startDate = startDateTime, endDate = endDateTime) val totalSignOutCount = repository.getTotalSignOutCount(startDate = startDateTime, endDate = endDateTime)
val totalPaymentMemberCount = repository.getPaymentMemberCount(startDate = startDateTime, endDate = endDateTime) val totalPaymentMemberCount = repository.getPaymentMemberCount(startDate = startDateTime, endDate = endDateTime)
@@ -92,6 +96,11 @@ class AdminMemberStatisticsService(private val repository: AdminMemberStatistics
endDate = endDateTime endDate = endDateTime
).associateBy({ it.date }, { it.memberCount }) ).associateBy({ it.date }, { it.memberCount })
val signUpLineCountInRange = repository.getSignUpLineCountInRange(
startDate = startDateTime,
endDate = endDateTime
).associateBy({ it.date }, { it.memberCount })
val authCountInRange = repository.getAuthCountInRange( val authCountInRange = repository.getAuthCountInRange(
startDate = startDateTime, startDate = startDateTime,
endDate = endDateTime endDate = endDateTime
@@ -121,6 +130,7 @@ class AdminMemberStatisticsService(private val repository: AdminMemberStatistics
signUpEmailCount = signUpEmailCountInRange[date] ?: 0, signUpEmailCount = signUpEmailCountInRange[date] ?: 0,
signUpKakaoCount = signUpKakaoCountInRange[date] ?: 0, signUpKakaoCount = signUpKakaoCountInRange[date] ?: 0,
signUpGoogleCount = signUpGoogleCountInRange[date] ?: 0, signUpGoogleCount = signUpGoogleCountInRange[date] ?: 0,
signUpLineCount = signUpLineCountInRange[date] ?: 0,
signOutCount = signOutCountInRange[date] ?: 0, signOutCount = signOutCountInRange[date] ?: 0,
paymentMemberCount = paymentMemberCountInRangeMap[date] ?: 0 paymentMemberCount = paymentMemberCountInRangeMap[date] ?: 0
) )
@@ -134,6 +144,7 @@ class AdminMemberStatisticsService(private val repository: AdminMemberStatistics
totalSignUpEmailCount = totalSignUpEmailCount, totalSignUpEmailCount = totalSignUpEmailCount,
totalSignUpKakaoCount = totalSignUpKakaoCount, totalSignUpKakaoCount = totalSignUpKakaoCount,
totalSignUpGoogleCount = totalSignUpGoogleCount, totalSignUpGoogleCount = totalSignUpGoogleCount,
totalSignUpLineCount = totalSignUpLineCount,
totalSignOutCount = totalSignOutCount, totalSignOutCount = totalSignOutCount,
totalPaymentMemberCount = totalPaymentMemberCount, totalPaymentMemberCount = totalPaymentMemberCount,
items = items items = items

View File

@@ -7,6 +7,7 @@ data class GetMemberStatisticsResponse(
val totalSignUpEmailCount: Int, val totalSignUpEmailCount: Int,
val totalSignUpKakaoCount: Int, val totalSignUpKakaoCount: Int,
val totalSignUpGoogleCount: Int, val totalSignUpGoogleCount: Int,
val totalSignUpLineCount: Int,
val totalSignOutCount: Int, val totalSignOutCount: Int,
val totalPaymentMemberCount: Int, val totalPaymentMemberCount: Int,
val items: List<GetMemberStatisticsItem> val items: List<GetMemberStatisticsItem>
@@ -19,6 +20,7 @@ data class GetMemberStatisticsItem(
val signUpEmailCount: Int, val signUpEmailCount: Int,
val signUpKakaoCount: Int, val signUpKakaoCount: Int,
val signUpGoogleCount: Int, val signUpGoogleCount: Int,
val signUpLineCount: Int,
val signOutCount: Int, val signOutCount: Int,
val paymentMemberCount: Int val paymentMemberCount: Int
) )

View File

@@ -317,8 +317,9 @@ class HomeService(
val themeList = if (theme.isBlank()) { val themeList = if (theme.isBlank()) {
contentThemeService.getActiveThemeOfContent( contentThemeService.getActiveThemeOfContent(
isAdult = isAdult, isAdult = isAdult,
isFree = true, isFree = false,
contentType = contentType contentType = contentType,
excludeThemes = listOf("다시듣기")
) )
} else { } else {
listOf(theme) listOf(theme)

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

@@ -22,11 +22,13 @@ import kr.co.vividnext.sodalive.explorer.profile.TimeDifferenceResult
import kr.co.vividnext.sodalive.i18n.Lang 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.GenderRestriction
import kr.co.vividnext.sodalive.live.room.LiveRoom import kr.co.vividnext.sodalive.live.room.LiveRoom
import kr.co.vividnext.sodalive.live.room.LiveRoomType import kr.co.vividnext.sodalive.live.room.LiveRoomType
import kr.co.vividnext.sodalive.live.room.QLiveRoom.liveRoom import kr.co.vividnext.sodalive.live.room.QLiveRoom.liveRoom
import kr.co.vividnext.sodalive.live.room.cancel.QLiveRoomCancel.liveRoomCancel import kr.co.vividnext.sodalive.live.room.cancel.QLiveRoomCancel.liveRoomCancel
import kr.co.vividnext.sodalive.live.room.visit.QLiveRoomVisit.liveRoomVisit import kr.co.vividnext.sodalive.live.room.visit.QLiveRoomVisit.liveRoomVisit
import kr.co.vividnext.sodalive.member.Gender
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.QMember import kr.co.vividnext.sodalive.member.QMember
@@ -342,6 +344,21 @@ class ExplorerQueryRepository(
.and(liveRoom.cancel.id.isNull) .and(liveRoom.cancel.id.isNull)
.and(liveRoom.isActive.isTrue) .and(liveRoom.isActive.isTrue)
val effectiveGender = if (userMember.auth != null) {
if (userMember.auth!!.gender == 1) Gender.MALE else Gender.FEMALE
} else {
userMember.gender
}
if (effectiveGender != Gender.NONE) {
val genderCondition = when (effectiveGender) {
Gender.MALE -> liveRoom.genderRestriction.`in`(GenderRestriction.ALL, GenderRestriction.MALE_ONLY)
Gender.FEMALE -> liveRoom.genderRestriction.`in`(GenderRestriction.ALL, GenderRestriction.FEMALE_ONLY)
Gender.NONE -> liveRoom.genderRestriction.isNotNull
}
where = where.and(genderCondition.or(liveRoom.member.id.eq(userMember.id)))
}
if (userMember.auth == null) { if (userMember.auth == null) {
where = where.and(liveRoom.isAdult.isFalse) where = where.and(liveRoom.isAdult.isFalse)
} }

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,23 +399,37 @@ 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(
accumulatedCansToday = if (creatorId == member.id!!) { accumulatedCansToday = if (isCreatorSelf) {
queryRepository.getDonationCoinsDateRange( queryRepository.getDonationCoinsDateRange(
creatorId, creatorId,
currentDate, currentDate,
@@ -421,7 +438,7 @@ class ExplorerService(
} else { } else {
0 0
}, },
accumulatedCansLastWeek = if (creatorId == member.id!!) { accumulatedCansLastWeek = if (isCreatorSelf) {
queryRepository.getDonationCoinsDateRange( queryRepository.getDonationCoinsDateRange(
creatorId, creatorId,
firstDayOfLastWeek, firstDayOfLastWeek,
@@ -430,7 +447,7 @@ class ExplorerService(
} else { } else {
0 0
}, },
accumulatedCansThisMonth = if (creatorId == member.id!!) { accumulatedCansThisMonth = if (isCreatorSelf) {
queryRepository.getDonationCoinsDateRange( queryRepository.getDonationCoinsDateRange(
creatorId, creatorId,
firstDayOfMonth, firstDayOfMonth,
@@ -439,11 +456,16 @@ class ExplorerService(
} else { } else {
0 0
}, },
isVisibleDonationRank = if (creatorId == member.id!!) { isVisibleDonationRank = if (isCreatorSelf) {
queryRepository.getVisibleDonationRank(creatorId) queryRepository.getVisibleDonationRank(creatorId)
} else { } else {
false false
}, },
donationRankingPeriod = if (isCreatorSelf) {
donationRankingPeriod
} else {
null
},
totalCount = donationMemberTotal, totalCount = donationMemberTotal,
userDonationRanking = donationRanking userDonationRanking = donationRanking
) )

View File

@@ -1,6 +1,7 @@
package kr.co.vividnext.sodalive.explorer package kr.co.vividnext.sodalive.explorer
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty
import kr.co.vividnext.sodalive.member.DonationRankingPeriod
import java.io.Serializable import java.io.Serializable
data class GetDonationAllResponse( data class GetDonationAllResponse(
@@ -8,6 +9,7 @@ data class GetDonationAllResponse(
val accumulatedCansLastWeek: Int, val accumulatedCansLastWeek: Int,
val accumulatedCansThisMonth: Int, val accumulatedCansThisMonth: Int,
val isVisibleDonationRank: Boolean, val isVisibleDonationRank: Boolean,
val donationRankingPeriod: DonationRankingPeriod?,
val totalCount: Int, val totalCount: Int,
val userDonationRanking: List<MemberDonationRankingResponse> val userDonationRanking: List<MemberDonationRankingResponse>
) : Serializable ) : Serializable

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

@@ -3,6 +3,7 @@ package kr.co.vividnext.sodalive.fcm
import kr.co.vividnext.sodalive.content.comment.AudioContentCommentRepository import kr.co.vividnext.sodalive.content.comment.AudioContentCommentRepository
import kr.co.vividnext.sodalive.i18n.Lang import kr.co.vividnext.sodalive.i18n.Lang
import kr.co.vividnext.sodalive.i18n.SodaMessageSource import kr.co.vividnext.sodalive.i18n.SodaMessageSource
import kr.co.vividnext.sodalive.live.room.GenderRestriction
import kr.co.vividnext.sodalive.member.MemberRepository import kr.co.vividnext.sodalive.member.MemberRepository
import org.springframework.scheduling.annotation.Async import org.springframework.scheduling.annotation.Async
import org.springframework.stereotype.Component import org.springframework.stereotype.Component
@@ -33,7 +34,8 @@ class FcmEvent(
val auditionId: Long? = null, val auditionId: Long? = null,
val commentParentId: Long? = null, val commentParentId: Long? = null,
val myMemberId: Long? = null, val myMemberId: Long? = null,
val isAvailableJoinCreator: Boolean? = null val isAvailableJoinCreator: Boolean? = null,
val genderRestriction: GenderRestriction? = null
) )
@Component @Component
@@ -69,7 +71,8 @@ class FcmSendListener(
val pushTokens = memberRepository.getCreateLiveRoomNotificationRecipientPushTokens( val pushTokens = memberRepository.getCreateLiveRoomNotificationRecipientPushTokens(
creatorId = fcmEvent.creatorId!!, creatorId = fcmEvent.creatorId!!,
isAuth = fcmEvent.isAuth ?: false, isAuth = fcmEvent.isAuth ?: false,
isAvailableJoinCreator = fcmEvent.isAvailableJoinCreator ?: false isAvailableJoinCreator = fcmEvent.isAvailableJoinCreator ?: false,
genderRestriction = fcmEvent.genderRestriction
) )
sendPush(pushTokens, fcmEvent, roomId = fcmEvent.roomId) sendPush(pushTokens, fcmEvent, roomId = fcmEvent.roomId)
} }
@@ -79,7 +82,8 @@ class FcmSendListener(
creatorId = fcmEvent.creatorId!!, creatorId = fcmEvent.creatorId!!,
roomId = fcmEvent.roomId!!, roomId = fcmEvent.roomId!!,
isAuth = fcmEvent.isAuth ?: false, isAuth = fcmEvent.isAuth ?: false,
isAvailableJoinCreator = fcmEvent.isAvailableJoinCreator ?: false isAvailableJoinCreator = fcmEvent.isAvailableJoinCreator ?: false,
genderRestriction = fcmEvent.genderRestriction
) )
sendPush(pushTokens, fcmEvent, roomId = fcmEvent.roomId) sendPush(pushTokens, fcmEvent, roomId = fcmEvent.roomId)
} }

View File

@@ -9,6 +9,7 @@ interface PushTokenRepository : JpaRepository<PushToken, Long>, PushTokenQueryRe
interface PushTokenQueryRepository { interface PushTokenQueryRepository {
fun findByToken(token: String): PushToken? fun findByToken(token: String): PushToken?
fun findByMemberId(memberId: Long): List<PushToken> fun findByMemberId(memberId: Long): List<PushToken>
fun findByMemberIds(memberIds: List<Long>): List<PushToken>
} }
class PushTokenQueryRepositoryImpl( class PushTokenQueryRepositoryImpl(
@@ -27,4 +28,12 @@ class PushTokenQueryRepositoryImpl(
.where(pushToken.member.id.eq(memberId)) .where(pushToken.member.id.eq(memberId))
.fetch() .fetch()
} }
override fun findByMemberIds(memberIds: List<Long>): List<PushToken> {
if (memberIds.isEmpty()) return emptyList()
return queryFactory
.selectFrom(pushToken)
.where(pushToken.member.id.`in`(memberIds))
.fetch()
}
} }

View File

@@ -1448,6 +1448,11 @@ class SodaMessageSource {
) )
private val liveRoomMessages = mapOf( private val liveRoomMessages = mapOf(
"live.room.gender_restricted" to mapOf(
Lang.KO to "입장 가능한 성별이 아닙니다.",
Lang.EN to "Your gender is not allowed to enter this room.",
Lang.JA to "入場可能な性別ではありません。"
),
"live.room.max_reservations" to mapOf( "live.room.max_reservations" to mapOf(
Lang.KO to "예약 라이브는 최대 3개까지 가능합니다.", Lang.KO to "예약 라이브는 최대 3개까지 가능합니다.",
Lang.EN to "You can reserve up to 3 live sessions.", Lang.EN to "You can reserve up to 3 live sessions.",

View File

@@ -15,5 +15,6 @@ data class CreateLiveRoomRequest(
val menuPanId: Long = 0, val menuPanId: Long = 0,
val menuPan: String = "", val menuPan: String = "",
val isActiveMenuPan: Boolean = false, val isActiveMenuPan: Boolean = false,
val isAvailableJoinCreator: Boolean = true val isAvailableJoinCreator: Boolean = true,
val genderRestriction: GenderRestriction = GenderRestriction.ALL
) )

View File

@@ -9,5 +9,6 @@ data class EditLiveRoomInfoRequest(
val menuPanId: Long = 0, val menuPanId: Long = 0,
val menuPan: String = "", val menuPan: String = "",
val isActiveMenuPan: Boolean? = null, val isActiveMenuPan: Boolean? = null,
val isAdult: Boolean? = null val isAdult: Boolean? = null,
val genderRestriction: GenderRestriction? = null
) )

View File

@@ -5,5 +5,6 @@ data class GetRecentRoomInfoResponse(
val notice: String, val notice: String,
var coverImageUrl: String, var coverImageUrl: String,
val coverImagePath: String, val coverImagePath: String,
val numberOfPeople: Int val numberOfPeople: Int,
val genderRestriction: GenderRestriction
) )

View File

@@ -32,7 +32,9 @@ data class LiveRoom(
@Enumerated(value = EnumType.STRING) @Enumerated(value = EnumType.STRING)
val type: LiveRoomType = LiveRoomType.OPEN, val type: LiveRoomType = LiveRoomType.OPEN,
@Column(nullable = true) @Column(nullable = true)
var password: String? = null var password: String? = null,
@Enumerated(value = EnumType.STRING)
var genderRestriction: GenderRestriction = GenderRestriction.ALL
) : BaseEntity() { ) : BaseEntity() {
@OneToOne(fetch = FetchType.LAZY) @OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id", nullable = false) @JoinColumn(name = "member_id", nullable = false)
@@ -67,3 +69,7 @@ enum class LiveRoomType {
enum class LiveRoomStatus { enum class LiveRoomStatus {
NOW, RESERVATION NOW, RESERVATION
} }
enum class GenderRestriction {
ALL, MALE_ONLY, FEMALE_ONLY
}

View File

@@ -14,8 +14,10 @@ import kr.co.vividnext.sodalive.live.room.donation.GetLiveRoomDonationItem
import kr.co.vividnext.sodalive.live.room.donation.QGetLiveRoomDonationItem import kr.co.vividnext.sodalive.live.room.donation.QGetLiveRoomDonationItem
import kr.co.vividnext.sodalive.live.room.like.GetLiveRoomHeartListItem import kr.co.vividnext.sodalive.live.room.like.GetLiveRoomHeartListItem
import kr.co.vividnext.sodalive.live.room.like.QGetLiveRoomHeartListItem import kr.co.vividnext.sodalive.live.room.like.QGetLiveRoomHeartListItem
import kr.co.vividnext.sodalive.member.Gender
import kr.co.vividnext.sodalive.member.MemberRole import kr.co.vividnext.sodalive.member.MemberRole
import kr.co.vividnext.sodalive.member.QMember.member import kr.co.vividnext.sodalive.member.QMember.member
import kr.co.vividnext.sodalive.member.block.QBlockMember.blockMember
import org.springframework.beans.factory.annotation.Value import org.springframework.beans.factory.annotation.Value
import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository import org.springframework.stereotype.Repository
@@ -32,7 +34,8 @@ interface LiveRoomQueryRepository {
timezone: String, timezone: String,
memberId: Long?, memberId: Long?,
isCreator: Boolean, isCreator: Boolean,
isAdult: Boolean isAdult: Boolean,
effectiveGender: Gender?
): List<LiveRoom> ): List<LiveRoom>
fun getLiveRoomListReservationWithDate( fun getLiveRoomListReservationWithDate(
@@ -41,14 +44,16 @@ interface LiveRoomQueryRepository {
limit: Long, limit: Long,
memberId: Long?, memberId: Long?,
isCreator: Boolean, isCreator: Boolean,
isAdult: Boolean isAdult: Boolean,
effectiveGender: Gender?
): List<LiveRoom> ): List<LiveRoom>
fun getLiveRoomListReservationWithoutDate( fun getLiveRoomListReservationWithoutDate(
timezone: String, timezone: String,
memberId: Long?, memberId: Long?,
isCreator: Boolean, isCreator: Boolean,
isAdult: Boolean isAdult: Boolean,
effectiveGender: Gender?
): List<LiveRoom> ): List<LiveRoom>
fun getLiveRoom(id: Long): LiveRoom? fun getLiveRoom(id: Long): LiveRoom?
@@ -76,7 +81,8 @@ class LiveRoomQueryRepositoryImpl(
timezone: String, timezone: String,
memberId: Long?, memberId: Long?,
isCreator: Boolean, isCreator: Boolean,
isAdult: Boolean isAdult: Boolean,
effectiveGender: Gender?
): List<LiveRoom> { ): List<LiveRoom> {
var where = liveRoom.channelName.isNotNull var where = liveRoom.channelName.isNotNull
.and(liveRoom.channelName.isNotEmpty) .and(liveRoom.channelName.isNotEmpty)
@@ -94,10 +100,34 @@ class LiveRoomQueryRepositoryImpl(
) )
} }
return queryFactory if (effectiveGender != null && effectiveGender != Gender.NONE) {
val genderCondition = when (effectiveGender) {
Gender.MALE -> liveRoom.genderRestriction.`in`(GenderRestriction.ALL, GenderRestriction.MALE_ONLY)
Gender.FEMALE -> liveRoom.genderRestriction.`in`(GenderRestriction.ALL, GenderRestriction.FEMALE_ONLY)
Gender.NONE -> liveRoom.genderRestriction.isNotNull
}
where = if (memberId != null) {
where.and(genderCondition.or(liveRoom.member.id.eq(memberId)))
} else {
where.and(genderCondition)
}
}
var select = queryFactory
.selectFrom(liveRoom) .selectFrom(liveRoom)
.innerJoin(liveRoom.member, member) .innerJoin(liveRoom.member, member)
.leftJoin(quarterLiveRankings).on(liveRoom.id.eq(quarterLiveRankings.roomId)) .leftJoin(quarterLiveRankings).on(liveRoom.id.eq(quarterLiveRankings.roomId))
if (memberId != null) {
val blockMemberCondition = blockMember.member.id.eq(member.id)
.and(blockMember.blockedMember.id.eq(memberId))
.and(blockMember.isActive.isTrue)
select = select.leftJoin(blockMember).on(blockMemberCondition)
where = where.and(blockMember.id.isNull)
}
return select
.where(where) .where(where)
.offset(offset) .offset(offset)
.limit(limit) .limit(limit)
@@ -116,7 +146,8 @@ class LiveRoomQueryRepositoryImpl(
limit: Long, limit: Long,
memberId: Long?, memberId: Long?,
isCreator: Boolean, isCreator: Boolean,
isAdult: Boolean isAdult: Boolean,
effectiveGender: Gender?
): List<LiveRoom> { ): List<LiveRoom> {
var where = liveRoom.beginDateTime.goe(date) var where = liveRoom.beginDateTime.goe(date)
.and(liveRoom.beginDateTime.lt(date.plusDays(1))) .and(liveRoom.beginDateTime.lt(date.plusDays(1)))
@@ -138,9 +169,33 @@ class LiveRoomQueryRepositoryImpl(
) )
} }
return queryFactory if (effectiveGender != null && effectiveGender != Gender.NONE) {
val genderCondition = when (effectiveGender) {
Gender.MALE -> liveRoom.genderRestriction.`in`(GenderRestriction.ALL, GenderRestriction.MALE_ONLY)
Gender.FEMALE -> liveRoom.genderRestriction.`in`(GenderRestriction.ALL, GenderRestriction.FEMALE_ONLY)
Gender.NONE -> liveRoom.genderRestriction.isNotNull
}
where = if (memberId != null) {
where.and(genderCondition.or(liveRoom.member.id.eq(memberId)))
} else {
where.and(genderCondition)
}
}
var select = queryFactory
.selectFrom(liveRoom) .selectFrom(liveRoom)
.innerJoin(liveRoom.member, member) .innerJoin(liveRoom.member, member)
if (memberId != null) {
val blockMemberCondition = blockMember.member.id.eq(member.id)
.and(blockMember.blockedMember.id.eq(memberId))
.and(blockMember.isActive.isTrue)
select = select.leftJoin(blockMember).on(blockMemberCondition)
where = where.and(blockMember.id.isNull)
}
return select
.where(where) .where(where)
.offset(offset) .offset(offset)
.limit(limit) .limit(limit)
@@ -152,7 +207,8 @@ class LiveRoomQueryRepositoryImpl(
timezone: String, timezone: String,
memberId: Long?, memberId: Long?,
isCreator: Boolean, isCreator: Boolean,
isAdult: Boolean isAdult: Boolean,
effectiveGender: Gender?
): List<LiveRoom> { ): List<LiveRoom> {
var where = liveRoom.beginDateTime.gt( var where = liveRoom.beginDateTime.gt(
LocalDateTime.now() LocalDateTime.now()
@@ -178,6 +234,19 @@ class LiveRoomQueryRepositoryImpl(
) )
} }
if (effectiveGender != null && effectiveGender != Gender.NONE) {
val genderCondition = when (effectiveGender) {
Gender.MALE -> liveRoom.genderRestriction.`in`(GenderRestriction.ALL, GenderRestriction.MALE_ONLY)
Gender.FEMALE -> liveRoom.genderRestriction.`in`(GenderRestriction.ALL, GenderRestriction.FEMALE_ONLY)
Gender.NONE -> liveRoom.genderRestriction.isNotNull
}
where = if (memberId != null) {
where.and(genderCondition.or(liveRoom.member.id.eq(memberId)))
} else {
where.and(genderCondition)
}
}
val orderBy = if (memberId != null) { val orderBy = if (memberId != null) {
listOf( listOf(
CaseBuilder() CaseBuilder()
@@ -190,10 +259,21 @@ class LiveRoomQueryRepositoryImpl(
listOf(liveRoom.beginDateTime.asc()) listOf(liveRoom.beginDateTime.asc())
} }
return queryFactory var select = queryFactory
.selectFrom(liveRoom) .selectFrom(liveRoom)
.innerJoin(liveRoom.member, member) .innerJoin(liveRoom.member, member)
.limit(10) .limit(10)
if (memberId != null) {
val blockMemberCondition = blockMember.member.id.eq(member.id)
.and(blockMember.blockedMember.id.eq(memberId))
.and(blockMember.isActive.isTrue)
select = select.leftJoin(blockMember).on(blockMemberCondition)
where = where.and(blockMember.id.isNull)
}
return select
.where(where) .where(where)
.orderBy(*orderBy.toTypedArray()) .orderBy(*orderBy.toTypedArray())
.fetch() .fetch()
@@ -231,7 +311,8 @@ class LiveRoomQueryRepositoryImpl(
liveRoom.notice, liveRoom.notice,
liveRoom.coverImage.prepend("/").prepend(cloudFrontHost), liveRoom.coverImage.prepend("/").prepend(cloudFrontHost),
liveRoom.coverImage, liveRoom.coverImage,
liveRoom.numberOfPeople liveRoom.numberOfPeople,
liveRoom.genderRestriction
) )
) )
.from(liveRoom) .from(liveRoom)

View File

@@ -21,6 +21,7 @@ import kr.co.vividnext.sodalive.explorer.ExplorerQueryRepository
import kr.co.vividnext.sodalive.extensions.convertLocalDateTime import kr.co.vividnext.sodalive.extensions.convertLocalDateTime
import kr.co.vividnext.sodalive.fcm.FcmEvent import kr.co.vividnext.sodalive.fcm.FcmEvent
import kr.co.vividnext.sodalive.fcm.FcmEventType import kr.co.vividnext.sodalive.fcm.FcmEventType
import kr.co.vividnext.sodalive.fcm.PushTokenRepository
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.reservation.LiveReservationRepository import kr.co.vividnext.sodalive.live.reservation.LiveReservationRepository
@@ -95,6 +96,7 @@ class LiveRoomService(
private val roomVisitService: LiveRoomVisitService, private val roomVisitService: LiveRoomVisitService,
private val canPaymentService: CanPaymentService, private val canPaymentService: CanPaymentService,
private val chargeRepository: ChargeRepository, private val chargeRepository: ChargeRepository,
private val pushTokenRepository: PushTokenRepository,
private val memberRepository: MemberRepository, private val memberRepository: MemberRepository,
private val tagRepository: LiveTagRepository, private val tagRepository: LiveTagRepository,
private val canRepository: CanRepository, private val canRepository: CanRepository,
@@ -126,6 +128,62 @@ class LiveRoomService(
} }
} }
private fun applyLanguageTagToRoomTags(
memberId: Long?,
tags: List<String>,
languageTagByMemberId: Map<Long, String?>? = null
): List<String> {
val randomizedTags = tags.shuffled()
val languageTag = getCreatorLanguageTag(memberId, languageTagByMemberId) ?: return randomizedTags
val filteredTags = randomizedTags.filterNot { it == languageTag }
return listOf(languageTag) + filteredTags
}
private fun getCreatorLanguageTag(
memberId: Long?,
languageTagByMemberId: Map<Long, String?>? = null
): String? {
if (memberId == null) return null
if (languageTagByMemberId != null && languageTagByMemberId.containsKey(memberId)) {
return languageTagByMemberId[memberId]
}
val tokens = pushTokenRepository.findByMemberId(memberId)
val languageCode = tokens
.filterNot { it.languageCode.isNullOrBlank() }
.maxByOrNull { it.updatedAt ?: LocalDateTime.MIN }
?.languageCode
val languageTag = when (languageCode?.lowercase()?.take(2)) {
"ko" -> "한국어"
"ja" -> "일본어"
"en" -> "영어"
else -> null
}
return languageTag
}
private fun buildLanguageTagMap(memberIds: List<Long>): Map<Long, String?> {
val tokens = pushTokenRepository.findByMemberIds(memberIds)
if (tokens.isEmpty()) return emptyMap()
val latestTokenByMemberId = tokens
.filter { it.member?.id != null }
.groupBy { it.member!!.id!! }
.mapValues { (_, memberTokens) ->
memberTokens.maxByOrNull { it.updatedAt ?: LocalDateTime.MIN }
}
return latestTokenByMemberId.mapValues { (_, token) ->
when (token?.languageCode?.lowercase()?.take(2)) {
"ko" -> "한국어"
"ja" -> "일본어"
"en" -> "영어"
else -> null
}
}
}
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun getRoomList( fun getRoomList(
dateString: String?, dateString: String?,
@@ -135,13 +193,21 @@ class LiveRoomService(
member: Member?, member: Member?,
timezone: String timezone: String
): List<GetRoomListResponse> { ): List<GetRoomListResponse> {
val effectiveGender = member?.let {
if (it.auth != null) {
if (it.auth!!.gender == 1) Gender.MALE else Gender.FEMALE
} else {
it.gender
}
}
val roomList = if (status == LiveRoomStatus.NOW) { val roomList = if (status == LiveRoomStatus.NOW) {
getLiveRoomListNow( getLiveRoomListNow(
pageable, pageable,
timezone, timezone,
memberId = member?.id, memberId = member?.id,
isCreator = member?.role == MemberRole.CREATOR, isCreator = member?.role == MemberRole.CREATOR,
isAdult = member?.auth != null && isAdultContentVisible isAdult = member?.auth != null && isAdultContentVisible,
effectiveGender = effectiveGender
) )
} else if (dateString != null) { } else if (dateString != null) {
getLiveRoomListReservationWithDate( getLiveRoomListReservationWithDate(
@@ -150,25 +216,23 @@ class LiveRoomService(
timezone, timezone,
memberId = member?.id, memberId = member?.id,
isCreator = member?.role == MemberRole.CREATOR, isCreator = member?.role == MemberRole.CREATOR,
isAdult = member?.auth != null && isAdultContentVisible isAdult = member?.auth != null && isAdultContentVisible,
effectiveGender = effectiveGender
) )
} else { } else {
getLiveRoomListReservationWithoutDate( getLiveRoomListReservationWithoutDate(
timezone, timezone,
isCreator = member?.role == MemberRole.CREATOR, isCreator = member?.role == MemberRole.CREATOR,
memberId = member?.id, memberId = member?.id,
isAdult = member?.auth != null && isAdultContentVisible isAdult = member?.auth != null && isAdultContentVisible,
effectiveGender = effectiveGender
) )
} }
val creatorIds = roomList.mapNotNull { it.member?.id }.distinct()
val languageTagByMemberId = buildLanguageTagMap(creatorIds)
return roomList return roomList
.filter {
if (member?.id != null) {
!blockMemberRepository.isBlocked(blockedMemberId = member.id!!, memberId = it.member!!.id!!)
} else {
true
}
}
.map { .map {
val roomInfo = roomInfoRepository.findByIdOrNull(it.id!!) val roomInfo = roomInfoRepository.findByIdOrNull(it.id!!)
@@ -193,6 +257,11 @@ class LiveRoomService(
val beginDateTimeUtc = it.beginDateTime val beginDateTimeUtc = it.beginDateTime
.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) .format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)
val tags = it.tags
.filter { tag -> tag.tag.isActive }
.map { tag -> tag.tag.tag }
.let { list -> applyLanguageTagToRoomTags(it.member?.id, list, languageTagByMemberId) }
GetRoomListResponse( GetRoomListResponse(
roomId = it.id!!, roomId = it.id!!,
title = it.title, title = it.title,
@@ -213,11 +282,7 @@ class LiveRoomService(
}, },
creatorNickname = it.member!!.nickname, creatorNickname = it.member!!.nickname,
creatorId = it.member!!.id!!, creatorId = it.member!!.id!!,
tags = it.tags tags = tags,
.asSequence()
.filter { tag -> tag.tag.isActive }
.map { tag -> tag.tag.tag }
.toList(),
coverImageUrl = if (it.coverImage!!.startsWith("https://")) { coverImageUrl = if (it.coverImage!!.startsWith("https://")) {
it.coverImage!! it.coverImage!!
} else { } else {
@@ -234,7 +299,8 @@ class LiveRoomService(
timezone: String, timezone: String,
memberId: Long?, memberId: Long?,
isCreator: Boolean, isCreator: Boolean,
isAdult: Boolean isAdult: Boolean,
effectiveGender: Gender?
): List<LiveRoom> { ): List<LiveRoom> {
return repository.getLiveRoomListNow( return repository.getLiveRoomListNow(
offset = pageable.offset, offset = pageable.offset,
@@ -242,7 +308,8 @@ class LiveRoomService(
timezone = timezone, timezone = timezone,
memberId = memberId, memberId = memberId,
isCreator = isCreator, isCreator = isCreator,
isAdult = isAdult isAdult = isAdult,
effectiveGender = effectiveGender
) )
} }
@@ -252,7 +319,8 @@ class LiveRoomService(
timezone: String, timezone: String,
memberId: Long?, memberId: Long?,
isCreator: Boolean, isCreator: Boolean,
isAdult: Boolean isAdult: Boolean,
effectiveGender: Gender?
): List<LiveRoom> { ): List<LiveRoom> {
val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd") val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd")
val date = LocalDate.parse(dateString, dateTimeFormatter).atStartOfDay() val date = LocalDate.parse(dateString, dateTimeFormatter).atStartOfDay()
@@ -266,7 +334,8 @@ class LiveRoomService(
limit = pageable.pageSize.toLong(), limit = pageable.pageSize.toLong(),
memberId = memberId, memberId = memberId,
isCreator = isCreator, isCreator = isCreator,
isAdult = isAdult isAdult = isAdult,
effectiveGender = effectiveGender
) )
} }
@@ -274,9 +343,10 @@ class LiveRoomService(
timezone: String, timezone: String,
memberId: Long?, memberId: Long?,
isCreator: Boolean, isCreator: Boolean,
isAdult: Boolean isAdult: Boolean,
effectiveGender: Gender?
): List<LiveRoom> { ): List<LiveRoom> {
return repository.getLiveRoomListReservationWithoutDate(timezone, memberId, isCreator, isAdult) return repository.getLiveRoomListReservationWithoutDate(timezone, memberId, isCreator, isAdult, effectiveGender)
} }
@Transactional @Transactional
@@ -338,7 +408,8 @@ class LiveRoomService(
}, },
type = request.type, type = request.type,
password = request.password, password = request.password,
isAvailableJoinCreator = request.isAvailableJoinCreator isAvailableJoinCreator = request.isAvailableJoinCreator,
genderRestriction = request.genderRestriction
) )
room.member = member room.member = member
@@ -417,7 +488,8 @@ class LiveRoomService(
isAuth = createdRoom.isAdult, isAuth = createdRoom.isAdult,
isAvailableJoinCreator = createdRoom.isAvailableJoinCreator, isAvailableJoinCreator = createdRoom.isAvailableJoinCreator,
roomId = createdRoom.id, roomId = createdRoom.id,
creatorId = createdRoom.member!!.id creatorId = createdRoom.member!!.id,
genderRestriction = createdRoom.genderRestriction
) )
) )
@@ -432,6 +504,10 @@ class LiveRoomService(
throw SodaException(messageKey = "live.room.adult_verification_required") throw SodaException(messageKey = "live.room.adult_verification_required")
} }
if (!member.canEnter(room.genderRestriction) && room.member!!.id!! != member.id!!) {
throw SodaException(messageKey = "live.room.gender_restricted")
}
val beginDateTime = room.beginDateTime val beginDateTime = room.beginDateTime
.atZone(ZoneId.of("UTC")) .atZone(ZoneId.of("UTC"))
.withZoneSameInstant(ZoneId.of(timezone)) .withZoneSameInstant(ZoneId.of(timezone))
@@ -440,12 +516,17 @@ class LiveRoomService(
val beginDateTimeUtc = room.beginDateTime val beginDateTimeUtc = room.beginDateTime
.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) .format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)
val languageTagByMemberId = buildLanguageTagMap(listOfNotNull(room.member?.id))
val response = GetRoomDetailResponse( val response = GetRoomDetailResponse(
roomId = roomId, roomId = roomId,
title = room.title, title = room.title,
notice = room.notice, notice = room.notice,
price = room.price, price = room.price,
tags = room.tags.asSequence().filter { it.tag.isActive }.map { it.tag.tag }.toList(), tags = room.tags.asSequence()
.filter { it.tag.isActive }
.map { it.tag.tag }
.toList()
.let { tags -> applyLanguageTagToRoomTags(room.member?.id, tags, languageTagByMemberId) },
numberOfParticipantsTotal = room.numberOfPeople, numberOfParticipantsTotal = room.numberOfPeople,
numberOfParticipants = 0, numberOfParticipants = 0,
channelName = room.channelName, channelName = room.channelName,
@@ -454,6 +535,7 @@ class LiveRoomService(
isPaid = false, isPaid = false,
isAdult = room.isAdult, isAdult = room.isAdult,
isPrivateRoom = room.type == LiveRoomType.PRIVATE, isPrivateRoom = room.type == LiveRoomType.PRIVATE,
genderRestriction = room.genderRestriction,
password = room.password password = room.password
) )
response.manager = GetRoomDetailManager(room.member!!, cloudFrontHost = cloudFrontHost) response.manager = GetRoomDetailManager(room.member!!, cloudFrontHost = cloudFrontHost)
@@ -573,7 +655,8 @@ class LiveRoomService(
isAuth = room.isAdult, isAuth = room.isAdult,
isAvailableJoinCreator = room.isAvailableJoinCreator, isAvailableJoinCreator = room.isAvailableJoinCreator,
roomId = room.id, roomId = room.id,
creatorId = room.member!!.id creatorId = room.member!!.id,
genderRestriction = room.genderRestriction
) )
) )
} }
@@ -672,6 +755,10 @@ class LiveRoomService(
) )
} }
if (room.member!!.id!! != member.id!! && !member.canEnter(room.genderRestriction)) {
throw SodaException(messageKey = "live.room.gender_restricted")
}
val lock = getOrCreateLock(memberId = member.id!!) val lock = getOrCreateLock(memberId = member.id!!)
lock.write { lock.write {
var roomInfo = roomInfoRepository.findByIdOrNull(request.roomId) var roomInfo = roomInfoRepository.findByIdOrNull(request.roomId)
@@ -786,6 +873,10 @@ class LiveRoomService(
room.isAdult = request.isAdult room.isAdult = request.isAdult
} }
if (request.genderRestriction != null) {
room.genderRestriction = request.genderRestriction
}
if (request.isActiveMenuPan != null) { if (request.isActiveMenuPan != null) {
if (request.isActiveMenuPan) { if (request.isActiveMenuPan) {
if (request.menuPanId > 0) { if (request.menuPanId > 0) {

View File

@@ -1,5 +1,6 @@
package kr.co.vividnext.sodalive.live.room.detail package kr.co.vividnext.sodalive.live.room.detail
import kr.co.vividnext.sodalive.live.room.GenderRestriction
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
@@ -11,6 +12,7 @@ data class GetRoomDetailResponse(
var isPaid: Boolean, var isPaid: Boolean,
val isAdult: Boolean, val isAdult: Boolean,
val isPrivateRoom: Boolean, val isPrivateRoom: Boolean,
val genderRestriction: GenderRestriction,
val password: String?, val password: String?,
val tags: List<String>, val tags: List<String>,
val channelName: String?, val channelName: String?,

View File

@@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.member
import kr.co.vividnext.sodalive.common.BaseEntity import kr.co.vividnext.sodalive.common.BaseEntity
import kr.co.vividnext.sodalive.explorer.GetExplorerSectionCreatorResponse import kr.co.vividnext.sodalive.explorer.GetExplorerSectionCreatorResponse
import kr.co.vividnext.sodalive.live.room.GenderRestriction
import kr.co.vividnext.sodalive.member.auth.Auth import kr.co.vividnext.sodalive.member.auth.Auth
import kr.co.vividnext.sodalive.member.following.CreatorFollowing import kr.co.vividnext.sodalive.member.following.CreatorFollowing
import kr.co.vividnext.sodalive.member.notification.MemberNotification import kr.co.vividnext.sodalive.member.notification.MemberNotification
@@ -46,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",
@@ -148,6 +152,22 @@ data class Member(
follow = follow follow = follow
) )
} }
fun canEnter(restriction: GenderRestriction): Boolean {
val effectiveGender = if (auth != null) {
if (auth!!.gender == 1) Gender.MALE else Gender.FEMALE
} else {
gender
}
if (effectiveGender == Gender.NONE) return true
return when (restriction) {
GenderRestriction.ALL -> true
GenderRestriction.MALE_ONLY -> effectiveGender == Gender.MALE
GenderRestriction.FEMALE_ONLY -> effectiveGender == Gender.FEMALE
}
}
} }
enum class Gender { enum class Gender {
@@ -161,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

@@ -5,6 +5,7 @@ import kr.co.vividnext.sodalive.fcm.PushTokenInfo
import kr.co.vividnext.sodalive.fcm.QPushToken.pushToken import kr.co.vividnext.sodalive.fcm.QPushToken.pushToken
import kr.co.vividnext.sodalive.fcm.QPushTokenInfo import kr.co.vividnext.sodalive.fcm.QPushTokenInfo
import kr.co.vividnext.sodalive.live.reservation.QLiveReservation.liveReservation import kr.co.vividnext.sodalive.live.reservation.QLiveReservation.liveReservation
import kr.co.vividnext.sodalive.live.room.GenderRestriction
import kr.co.vividnext.sodalive.live.room.QLiveRoom.liveRoom import kr.co.vividnext.sodalive.live.room.QLiveRoom.liveRoom
import kr.co.vividnext.sodalive.member.QMember.member import kr.co.vividnext.sodalive.member.QMember.member
import kr.co.vividnext.sodalive.member.auth.QAuth.auth import kr.co.vividnext.sodalive.member.auth.QAuth.auth
@@ -35,14 +36,16 @@ interface MemberQueryRepository {
fun getCreateLiveRoomNotificationRecipientPushTokens( fun getCreateLiveRoomNotificationRecipientPushTokens(
creatorId: Long, creatorId: Long,
isAuth: Boolean, isAuth: Boolean,
isAvailableJoinCreator: Boolean isAvailableJoinCreator: Boolean,
genderRestriction: GenderRestriction? = null
): List<PushTokenInfo> ): List<PushTokenInfo>
fun getStartLiveRoomNotificationRecipientPushTokens( fun getStartLiveRoomNotificationRecipientPushTokens(
creatorId: Long, creatorId: Long,
roomId: Long, roomId: Long,
isAuth: Boolean, isAuth: Boolean,
isAvailableJoinCreator: Boolean isAvailableJoinCreator: Boolean,
genderRestriction: GenderRestriction? = null
): List<PushTokenInfo> ): List<PushTokenInfo>
fun getUploadContentNotificationRecipientPushTokens( fun getUploadContentNotificationRecipientPushTokens(
@@ -132,7 +135,8 @@ class MemberQueryRepositoryImpl(
override fun getCreateLiveRoomNotificationRecipientPushTokens( override fun getCreateLiveRoomNotificationRecipientPushTokens(
creatorId: Long, creatorId: Long,
isAuth: Boolean, isAuth: Boolean,
isAvailableJoinCreator: Boolean isAvailableJoinCreator: Boolean,
genderRestriction: GenderRestriction?
): List<PushTokenInfo> { ): List<PushTokenInfo> {
val member = QMember.member val member = QMember.member
val creator = QMember.member val creator = QMember.member
@@ -158,6 +162,10 @@ class MemberQueryRepositoryImpl(
where = where.and(creatorFollowing.member.role.ne(MemberRole.CREATOR)) where = where.and(creatorFollowing.member.role.ne(MemberRole.CREATOR))
} }
if (genderRestriction != null && genderRestriction != GenderRestriction.ALL) {
where = where.and(getGenderCondition(genderRestriction))
}
return queryFactory return queryFactory
.select( .select(
QPushTokenInfo( QPushTokenInfo(
@@ -180,7 +188,8 @@ class MemberQueryRepositoryImpl(
creatorId: Long, creatorId: Long,
roomId: Long, roomId: Long,
isAuth: Boolean, isAuth: Boolean,
isAvailableJoinCreator: Boolean isAvailableJoinCreator: Boolean,
genderRestriction: GenderRestriction?
): List<PushTokenInfo> { ): List<PushTokenInfo> {
val member = QMember.member val member = QMember.member
val creator = QMember.member val creator = QMember.member
@@ -206,6 +215,10 @@ class MemberQueryRepositoryImpl(
where = where.and(creatorFollowing.member.role.ne(MemberRole.CREATOR)) where = where.and(creatorFollowing.member.role.ne(MemberRole.CREATOR))
} }
if (genderRestriction != null && genderRestriction != GenderRestriction.ALL) {
where = where.and(getGenderCondition(genderRestriction))
}
val followingMemberPushToken = queryFactory val followingMemberPushToken = queryFactory
.select( .select(
QPushTokenInfo( QPushTokenInfo(
@@ -237,6 +250,10 @@ class MemberQueryRepositoryImpl(
where = where.and(auth.isNotNull) where = where.and(auth.isNotNull)
} }
if (genderRestriction != null && genderRestriction != GenderRestriction.ALL) {
where = where.and(getGenderCondition(genderRestriction, liveReservation.member))
}
val reservationMemberPushToken = queryFactory val reservationMemberPushToken = queryFactory
.select( .select(
QPushTokenInfo( QPushTokenInfo(
@@ -256,6 +273,33 @@ class MemberQueryRepositoryImpl(
return (followingMemberPushToken + reservationMemberPushToken).distinctBy { it.token } return (followingMemberPushToken + reservationMemberPushToken).distinctBy { it.token }
} }
private fun getGenderCondition(
genderRestriction: GenderRestriction,
qMember: QMember = member
) = when (genderRestriction) {
GenderRestriction.MALE_ONLY -> {
auth.isNotNull.and(auth.gender.eq(1))
.or(
auth.isNull.and(
qMember.gender.eq(Gender.MALE)
.or(qMember.gender.eq(Gender.NONE))
)
)
}
GenderRestriction.FEMALE_ONLY -> {
auth.isNotNull.and(auth.gender.eq(0))
.or(
auth.isNull.and(
qMember.gender.eq(Gender.FEMALE)
.or(qMember.gender.eq(Gender.NONE))
)
)
}
else -> null
}
override fun getUploadContentNotificationRecipientPushTokens( override fun getUploadContentNotificationRecipientPushTokens(
creatorId: Long, creatorId: Long,
isAuth: Boolean isAuth: Boolean

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