feat(home): 팔로잉 탭 최근 소식 정책을 보강한다
This commit is contained in:
@@ -497,12 +497,14 @@ class MemberService(
|
||||
fun creatorFollow(creatorId: Long, isNotify: Boolean, isActive: Boolean, memberId: Long) {
|
||||
if (creatorId == memberId) return
|
||||
|
||||
val creatorFollowing = creatorFollowingRepository.findByCreatorIdAndMemberId(
|
||||
val creatorFollowing = creatorFollowingRepository.findByCreatorIdAndMemberIdForUpdate(
|
||||
creatorId = creatorId,
|
||||
memberId = memberId
|
||||
)
|
||||
|
||||
if (creatorFollowing == null) {
|
||||
if (!isActive) return
|
||||
|
||||
val creator = repository.findByIdOrNull(creatorId)
|
||||
?: throw SodaException(messageKey = "member.validation.creator_not_found")
|
||||
val member = repository.findByIdOrNull(memberId)
|
||||
@@ -515,12 +517,15 @@ class MemberService(
|
||||
} else {
|
||||
creatorFollowing.isNotify = isNotify && isActive
|
||||
creatorFollowing.isActive = isActive
|
||||
if (!isActive) {
|
||||
homeFollowingNewsInboxPort.deactivateByMemberIdAndCreatorId(memberId = memberId, creatorId = creatorId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
fun creatorUnFollow(creatorId: Long, memberId: Long) {
|
||||
val creatorFollowing = creatorFollowingRepository.findByCreatorIdAndMemberId(
|
||||
val creatorFollowing = creatorFollowingRepository.findByCreatorIdAndMemberIdForUpdate(
|
||||
creatorId = creatorId,
|
||||
memberId = memberId
|
||||
)
|
||||
|
||||
@@ -4,12 +4,14 @@ import com.querydsl.jpa.impl.JPAQueryFactory
|
||||
import kr.co.vividnext.sodalive.member.following.QCreatorFollowing.creatorFollowing
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.stereotype.Repository
|
||||
import javax.persistence.LockModeType
|
||||
|
||||
@Repository
|
||||
interface CreatorFollowingRepository : JpaRepository<CreatorFollowing, Long>, CreatorFollowingQueryRepository
|
||||
|
||||
interface CreatorFollowingQueryRepository {
|
||||
fun findByCreatorIdAndMemberId(creatorId: Long, memberId: Long): CreatorFollowing?
|
||||
fun findByCreatorIdAndMemberIdForUpdate(creatorId: Long, memberId: Long): CreatorFollowing?
|
||||
}
|
||||
|
||||
@Repository
|
||||
@@ -25,4 +27,15 @@ class CreatorFollowingQueryRepositoryImpl(
|
||||
)
|
||||
.fetchFirst()
|
||||
}
|
||||
|
||||
override fun findByCreatorIdAndMemberIdForUpdate(creatorId: Long, memberId: Long): CreatorFollowing? {
|
||||
return queryFactory
|
||||
.selectFrom(creatorFollowing)
|
||||
.where(
|
||||
creatorFollowing.creator.id.eq(creatorId)
|
||||
.and(creatorFollowing.member.id.eq(memberId))
|
||||
)
|
||||
.setLockMode(LockModeType.PESSIMISTIC_WRITE)
|
||||
.fetchFirst()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,9 @@ import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.QCreatorCommun
|
||||
import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.comment.QCreatorCommunityComment.creatorCommunityComment
|
||||
import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.like.QCreatorCommunityLike.creatorCommunityLike
|
||||
import kr.co.vividnext.sodalive.extensions.toUtcIso
|
||||
import kr.co.vividnext.sodalive.live.room.GenderRestriction
|
||||
import kr.co.vividnext.sodalive.live.room.QLiveRoom.liveRoom
|
||||
import kr.co.vividnext.sodalive.member.Gender
|
||||
import kr.co.vividnext.sodalive.member.MemberRole
|
||||
import kr.co.vividnext.sodalive.member.QMember
|
||||
import kr.co.vividnext.sodalive.member.block.QBlockMember
|
||||
@@ -22,11 +24,16 @@ import kr.co.vividnext.sodalive.v2.home.following.adapter.out.persistence.QHomeF
|
||||
import kr.co.vividnext.sodalive.v2.home.following.domain.FollowingNewsType
|
||||
import kr.co.vividnext.sodalive.v2.home.following.domain.HomeFollowingCommunityPostNews
|
||||
import kr.co.vividnext.sodalive.v2.home.following.domain.HomeFollowingContentNews
|
||||
import kr.co.vividnext.sodalive.v2.home.following.domain.HomeFollowingContentRankingNews
|
||||
import kr.co.vividnext.sodalive.v2.home.following.domain.HomeFollowingCreator
|
||||
import kr.co.vividnext.sodalive.v2.home.following.domain.HomeFollowingCreatorRankingNews
|
||||
import kr.co.vividnext.sodalive.v2.home.following.domain.HomeFollowingLive
|
||||
import kr.co.vividnext.sodalive.v2.home.following.domain.HomeFollowingNews
|
||||
import kr.co.vividnext.sodalive.v2.home.following.domain.HomeFollowingSchedule
|
||||
import kr.co.vividnext.sodalive.v2.ranking.adapter.out.persistence.QCreatorRankingSnapshot
|
||||
import kr.co.vividnext.sodalive.v2.ranking.adapter.out.persistence.QCreatorRankingSnapshotJob
|
||||
import kr.co.vividnext.sodalive.v2.ranking.domain.CreatorRankingType
|
||||
import kr.co.vividnext.sodalive.v2.ranking.port.out.CreatorRankingSnapshotJobStatus
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.stereotype.Repository
|
||||
import java.time.LocalDateTime
|
||||
@@ -64,7 +71,13 @@ class DefaultHomeFollowingQueryRepository(
|
||||
}
|
||||
}
|
||||
|
||||
override fun findOnAirLives(memberId: Long, canViewAdultContent: Boolean, limit: Int): List<HomeFollowingLive> {
|
||||
override fun findOnAirLives(
|
||||
memberId: Long,
|
||||
canViewAdultContent: Boolean,
|
||||
effectiveViewerGender: Gender,
|
||||
isViewerCreator: Boolean,
|
||||
limit: Int
|
||||
): List<HomeFollowingLive> {
|
||||
val creator = QMember("onAirCreator")
|
||||
return queryFactory
|
||||
.select(liveRoom.id, creator.profileImage, creator.nickname, liveRoom.title, liveRoom.beginDateTime)
|
||||
@@ -80,6 +93,8 @@ class DefaultHomeFollowingQueryRepository(
|
||||
creator.isActive.isTrue,
|
||||
creator.role.eq(MemberRole.CREATOR),
|
||||
adultLiveCondition(canViewAdultContent),
|
||||
genderLiveCondition(memberId, effectiveViewerGender),
|
||||
creatorJoinLiveCondition(memberId, isViewerCreator),
|
||||
notBlockedCreatorCondition(memberId, creator.id)
|
||||
)
|
||||
.orderBy(liveRoom.beginDateTime.desc(), liveRoom.id.desc())
|
||||
@@ -99,11 +114,19 @@ class DefaultHomeFollowingQueryRepository(
|
||||
override fun findMonthlySchedules(
|
||||
memberId: Long,
|
||||
canViewAdultContent: Boolean,
|
||||
effectiveViewerGender: Gender,
|
||||
isViewerCreator: Boolean,
|
||||
now: LocalDateTime,
|
||||
limit: Int
|
||||
): List<HomeFollowingSchedule> {
|
||||
val window = monthlyScheduleWindow(now)
|
||||
val liveSchedules = findLiveSchedules(memberId, canViewAdultContent, window)
|
||||
val liveSchedules = findLiveSchedules(
|
||||
memberId,
|
||||
canViewAdultContent,
|
||||
effectiveViewerGender,
|
||||
isViewerCreator,
|
||||
window
|
||||
)
|
||||
val audioSchedules = findAudioSchedules(memberId, canViewAdultContent, window)
|
||||
return (liveSchedules + audioSchedules)
|
||||
.sortedWith(
|
||||
@@ -141,7 +164,7 @@ class DefaultHomeFollowingQueryRepository(
|
||||
.from(homeFollowingNewsInbox)
|
||||
.join(creator).on(creator.id.eq(homeFollowingNewsInbox.creatorId))
|
||||
.leftJoin(newsAudioContent).on(
|
||||
homeFollowingNewsInbox.newsType.eq(FollowingNewsType.AUDIO_CONTENT),
|
||||
homeFollowingNewsInbox.newsType.`in`(FollowingNewsType.AUDIO_CONTENT, FollowingNewsType.CONTENT_RANKING),
|
||||
newsAudioContent.id.eq(homeFollowingNewsInbox.targetId)
|
||||
)
|
||||
.leftJoin(newsCommunity).on(
|
||||
@@ -157,7 +180,7 @@ class DefaultHomeFollowingQueryRepository(
|
||||
activeFollowingCondition(memberId, homeFollowingNewsInbox.creatorId),
|
||||
adultNewsCondition(canViewAdultContent),
|
||||
notBlockedCreatorCondition(memberId, homeFollowingNewsInbox.creatorId),
|
||||
activeNewsTargetCondition(canViewAdultContent)
|
||||
activeNewsTargetCondition(memberId, canViewAdultContent, nowUtc)
|
||||
)
|
||||
.orderBy(homeFollowingNewsInbox.visibleFromAtUtc.desc(), homeFollowingNewsInbox.id.desc())
|
||||
.limit(limit.toLong())
|
||||
@@ -209,6 +232,16 @@ class DefaultHomeFollowingQueryRepository(
|
||||
} else {
|
||||
null
|
||||
},
|
||||
contentRanking = if (type == FollowingNewsType.CONTENT_RANKING && rank != null) {
|
||||
HomeFollowingContentRankingNews(
|
||||
rank = rank,
|
||||
contentId = targetId,
|
||||
contentImageUrl = get(newsAudioContent.coverImage).toCdnUrl(cloudFrontHost),
|
||||
title = get(newsAudioContent.title)!!
|
||||
)
|
||||
} else {
|
||||
null
|
||||
},
|
||||
communityPost = if (type == FollowingNewsType.COMMUNITY_POST) {
|
||||
HomeFollowingCommunityPostNews(
|
||||
postId = targetId,
|
||||
@@ -229,6 +262,8 @@ class DefaultHomeFollowingQueryRepository(
|
||||
private fun findLiveSchedules(
|
||||
memberId: Long,
|
||||
canViewAdultContent: Boolean,
|
||||
effectiveViewerGender: Gender,
|
||||
isViewerCreator: Boolean,
|
||||
window: ScheduleWindow
|
||||
): List<HomeFollowingSchedule> {
|
||||
val creator = QMember("scheduleLiveCreator")
|
||||
@@ -254,6 +289,8 @@ class DefaultHomeFollowingQueryRepository(
|
||||
creator.isActive.isTrue,
|
||||
creator.role.eq(MemberRole.CREATOR),
|
||||
adultLiveCondition(canViewAdultContent),
|
||||
genderLiveCondition(memberId, effectiveViewerGender),
|
||||
creatorJoinLiveCondition(memberId, isViewerCreator),
|
||||
notBlockedCreatorCondition(memberId, creator.id)
|
||||
)
|
||||
.fetch()
|
||||
@@ -340,6 +377,20 @@ class DefaultHomeFollowingQueryRepository(
|
||||
return if (canViewAdultContent) null else liveRoom.isAdult.isFalse
|
||||
}
|
||||
|
||||
private fun genderLiveCondition(viewerId: Long, effectiveViewerGender: Gender): BooleanExpression? {
|
||||
val genderCondition = when (effectiveViewerGender) {
|
||||
Gender.MALE -> liveRoom.genderRestriction.`in`(GenderRestriction.ALL, GenderRestriction.MALE_ONLY)
|
||||
Gender.FEMALE -> liveRoom.genderRestriction.`in`(GenderRestriction.ALL, GenderRestriction.FEMALE_ONLY)
|
||||
Gender.NONE -> return null
|
||||
}
|
||||
return genderCondition.or(liveRoom.member.id.eq(viewerId))
|
||||
}
|
||||
|
||||
private fun creatorJoinLiveCondition(viewerId: Long, isViewerCreator: Boolean): BooleanExpression? {
|
||||
if (!isViewerCreator) return null
|
||||
return liveRoom.isAvailableJoinCreator.isTrue.or(liveRoom.member.id.eq(viewerId))
|
||||
}
|
||||
|
||||
private fun adultAudioCondition(canViewAdultContent: Boolean): BooleanExpression? {
|
||||
return if (canViewAdultContent) null else audioContent.isAdult.isFalse
|
||||
}
|
||||
@@ -385,7 +436,11 @@ class DefaultHomeFollowingQueryRepository(
|
||||
}
|
||||
}
|
||||
|
||||
private fun activeNewsTargetCondition(canViewAdultContent: Boolean): BooleanExpression {
|
||||
private fun activeNewsTargetCondition(
|
||||
memberId: Long,
|
||||
canViewAdultContent: Boolean,
|
||||
nowUtc: LocalDateTime
|
||||
): BooleanExpression {
|
||||
val newsAudioContent = QAudioContent("newsAudioContent")
|
||||
val newsCommunity = QCreatorCommunity("newsCommunity")
|
||||
val activeAudioExists = JPAExpressions
|
||||
@@ -410,10 +465,84 @@ class DefaultHomeFollowingQueryRepository(
|
||||
|
||||
return homeFollowingNewsInbox.newsType.eq(FollowingNewsType.CREATOR_RANKING)
|
||||
.and(homeFollowingNewsInbox.rank.isNotNull)
|
||||
.and(latestVisibleCreatorRankingBatchCondition(nowUtc))
|
||||
.or(homeFollowingNewsInbox.newsType.eq(FollowingNewsType.AUDIO_CONTENT).and(activeAudioExists))
|
||||
.or(
|
||||
homeFollowingNewsInbox.newsType.eq(FollowingNewsType.CONTENT_RANKING)
|
||||
.and(homeFollowingNewsInbox.rank.isNotNull)
|
||||
.and(activeAudioExists)
|
||||
.and(latestContentRankingNewsCondition(memberId, canViewAdultContent, nowUtc))
|
||||
)
|
||||
.or(homeFollowingNewsInbox.newsType.eq(FollowingNewsType.COMMUNITY_POST).and(activeCommunityExists))
|
||||
}
|
||||
|
||||
private fun latestVisibleCreatorRankingBatchCondition(nowUtc: LocalDateTime): BooleanExpression {
|
||||
val latestCreatorRankingSnapshot = QCreatorRankingSnapshot("latestCreatorRankingSnapshot")
|
||||
val latestCreatorRankingJob = QCreatorRankingSnapshotJob("latestCreatorRankingJob")
|
||||
val anyCreatorRankingJob = QCreatorRankingSnapshotJob("anyCreatorRankingJob")
|
||||
val latestJobVisibleFromAt = JPAExpressions
|
||||
.select(latestCreatorRankingJob.visibleFromAtUtc.max())
|
||||
.from(latestCreatorRankingJob)
|
||||
.where(
|
||||
latestCreatorRankingJob.rankingType.eq(CreatorRankingType.WEEKLY),
|
||||
latestCreatorRankingJob.status.eq(CreatorRankingSnapshotJobStatus.DONE),
|
||||
latestCreatorRankingJob.visibleFromAtUtc.loe(nowUtc)
|
||||
)
|
||||
val hasRankingJob = JPAExpressions
|
||||
.selectOne()
|
||||
.from(anyCreatorRankingJob)
|
||||
.where(
|
||||
anyCreatorRankingJob.rankingType.eq(CreatorRankingType.WEEKLY),
|
||||
anyCreatorRankingJob.status.eq(CreatorRankingSnapshotJobStatus.DONE),
|
||||
anyCreatorRankingJob.visibleFromAtUtc.loe(nowUtc)
|
||||
)
|
||||
.exists()
|
||||
val latestSnapshotVisibleFromAt = JPAExpressions
|
||||
.select(latestCreatorRankingSnapshot.visibleFromAtUtc.max())
|
||||
.from(latestCreatorRankingSnapshot)
|
||||
.where(
|
||||
latestCreatorRankingSnapshot.rankingType.eq(CreatorRankingType.WEEKLY),
|
||||
latestCreatorRankingSnapshot.visibleFromAtUtc.loe(nowUtc)
|
||||
)
|
||||
|
||||
return homeFollowingNewsInbox.visibleFromAtUtc.eq(latestJobVisibleFromAt).or(
|
||||
hasRankingJob.not().and(homeFollowingNewsInbox.visibleFromAtUtc.eq(latestSnapshotVisibleFromAt))
|
||||
)
|
||||
}
|
||||
|
||||
private fun latestContentRankingNewsCondition(
|
||||
memberId: Long,
|
||||
canViewAdultContent: Boolean,
|
||||
nowUtc: LocalDateTime
|
||||
): BooleanExpression {
|
||||
val newerContentRanking = QHomeFollowingNewsInbox("newerContentRanking")
|
||||
return JPAExpressions
|
||||
.selectOne()
|
||||
.from(newerContentRanking)
|
||||
.where(
|
||||
newerContentRanking.memberId.eq(memberId),
|
||||
newerContentRanking.newsType.eq(FollowingNewsType.CONTENT_RANKING),
|
||||
newerContentRanking.targetId.eq(homeFollowingNewsInbox.targetId),
|
||||
newerContentRanking.isActive.isTrue,
|
||||
newerContentRanking.rank.isNotNull,
|
||||
adultNewerContentRankingCondition(canViewAdultContent, newerContentRanking),
|
||||
newerContentRanking.visibleFromAtUtc.loe(nowUtc),
|
||||
newerContentRanking.visibleFromAtUtc.gt(homeFollowingNewsInbox.visibleFromAtUtc)
|
||||
.or(
|
||||
newerContentRanking.visibleFromAtUtc.eq(homeFollowingNewsInbox.visibleFromAtUtc)
|
||||
.and(newerContentRanking.id.gt(homeFollowingNewsInbox.id))
|
||||
)
|
||||
)
|
||||
.notExists()
|
||||
}
|
||||
|
||||
private fun adultNewerContentRankingCondition(
|
||||
canViewAdultContent: Boolean,
|
||||
newerContentRanking: QHomeFollowingNewsInbox
|
||||
): BooleanExpression? {
|
||||
return if (canViewAdultContent) null else newerContentRanking.isAdult.isFalse
|
||||
}
|
||||
|
||||
private fun adultAudioNewsTargetCondition(
|
||||
canViewAdultContent: Boolean,
|
||||
newsAudioContent: QAudioContent
|
||||
|
||||
@@ -52,6 +52,7 @@ interface HomeFollowingNewsInboxJpaRepository : JpaRepository<HomeFollowingNewsI
|
||||
where cf.creator_id = :creatorId
|
||||
and cf.is_active = true
|
||||
order by cf.member_id asc
|
||||
for update
|
||||
""",
|
||||
nativeQuery = true
|
||||
)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package kr.co.vividnext.sodalive.v2.home.following.application
|
||||
|
||||
import kr.co.vividnext.sodalive.member.Gender
|
||||
import kr.co.vividnext.sodalive.member.Member
|
||||
import kr.co.vividnext.sodalive.member.MemberRole
|
||||
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
|
||||
import kr.co.vividnext.sodalive.v2.home.following.domain.HomeFollowing
|
||||
import kr.co.vividnext.sodalive.v2.home.following.port.out.HomeFollowingQueryPort
|
||||
@@ -19,17 +21,36 @@ class HomeFollowingQueryService(
|
||||
fun findHomeFollowing(member: Member): HomeFollowing {
|
||||
val memberId = requireNotNull(member.id)
|
||||
val canViewAdultContent = memberContentPreferenceService.canViewAdultContent(member)
|
||||
val effectiveViewerGender = member.effectiveGender()
|
||||
val isViewerCreator = member.role == MemberRole.CREATOR
|
||||
val now = nowProvider()
|
||||
|
||||
return HomeFollowing(
|
||||
followingCreators = queryPort.findFollowingCreators(memberId, FOLLOWING_CREATORS_LIMIT),
|
||||
onAirLives = queryPort.findOnAirLives(memberId, canViewAdultContent, ON_AIR_LIVES_LIMIT),
|
||||
onAirLives = queryPort.findOnAirLives(
|
||||
memberId,
|
||||
canViewAdultContent,
|
||||
effectiveViewerGender,
|
||||
isViewerCreator,
|
||||
ON_AIR_LIVES_LIMIT
|
||||
),
|
||||
recentChats = emptyList(),
|
||||
monthlySchedules = queryPort.findMonthlySchedules(memberId, canViewAdultContent, now, MONTHLY_SCHEDULES_LIMIT),
|
||||
monthlySchedules = queryPort.findMonthlySchedules(
|
||||
memberId,
|
||||
canViewAdultContent,
|
||||
effectiveViewerGender,
|
||||
isViewerCreator,
|
||||
now,
|
||||
MONTHLY_SCHEDULES_LIMIT
|
||||
),
|
||||
recentNews = queryPort.findRecentNews(memberId, canViewAdultContent, now, RECENT_NEWS_LIMIT)
|
||||
)
|
||||
}
|
||||
|
||||
private fun Member.effectiveGender(): Gender {
|
||||
return auth?.let { if (it.gender == 1) Gender.MALE else Gender.FEMALE } ?: gender
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val FOLLOWING_CREATORS_LIMIT = 20
|
||||
private const val ON_AIR_LIVES_LIMIT = 10
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package kr.co.vividnext.sodalive.v2.home.following.port.out
|
||||
|
||||
import kr.co.vividnext.sodalive.member.Gender
|
||||
import kr.co.vividnext.sodalive.v2.home.following.domain.HomeFollowingCreator
|
||||
import kr.co.vividnext.sodalive.v2.home.following.domain.HomeFollowingLive
|
||||
import kr.co.vividnext.sodalive.v2.home.following.domain.HomeFollowingNews
|
||||
@@ -9,11 +10,19 @@ import java.time.LocalDateTime
|
||||
interface HomeFollowingQueryPort {
|
||||
fun findFollowingCreators(memberId: Long, limit: Int): List<HomeFollowingCreator>
|
||||
|
||||
fun findOnAirLives(memberId: Long, canViewAdultContent: Boolean, limit: Int): List<HomeFollowingLive>
|
||||
fun findOnAirLives(
|
||||
memberId: Long,
|
||||
canViewAdultContent: Boolean,
|
||||
effectiveViewerGender: Gender,
|
||||
isViewerCreator: Boolean,
|
||||
limit: Int
|
||||
): List<HomeFollowingLive>
|
||||
|
||||
fun findMonthlySchedules(
|
||||
memberId: Long,
|
||||
canViewAdultContent: Boolean,
|
||||
effectiveViewerGender: Gender,
|
||||
isViewerCreator: Boolean,
|
||||
now: LocalDateTime,
|
||||
limit: Int
|
||||
): List<HomeFollowingSchedule>
|
||||
|
||||
Reference in New Issue
Block a user