feat(home): 최근 소식 원천 데이터를 보강한다

This commit is contained in:
2026-06-30 21:35:13 +09:00
parent 17b1305a95
commit 5d5547361c
2 changed files with 386 additions and 36 deletions

View File

@@ -8,6 +8,8 @@ import com.querydsl.jpa.impl.JPAQueryFactory
import kr.co.vividnext.sodalive.content.QAudioContent
import kr.co.vividnext.sodalive.content.QAudioContent.audioContent
import kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.QCreatorCommunity
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.QLiveRoom.liveRoom
import kr.co.vividnext.sodalive.member.MemberRole
@@ -18,7 +20,10 @@ import kr.co.vividnext.sodalive.v2.common.domain.CreatorActivityType
import kr.co.vividnext.sodalive.v2.common.domain.toCdnUrl
import kr.co.vividnext.sodalive.v2.home.following.adapter.out.persistence.QHomeFollowingNewsInbox.homeFollowingNewsInbox
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.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
@@ -116,50 +121,109 @@ class DefaultHomeFollowingQueryRepository(
limit: Int
): List<HomeFollowingNews> {
val creator = QMember("newsCreator")
return queryFactory
val newsAudioContent = QAudioContent("recentNewsAudioContent")
val newsCommunity = QCreatorCommunity("recentNewsCommunity")
val rows = queryFactory
.select(
homeFollowingNewsInbox.id,
homeFollowingNewsInbox.newsType,
homeFollowingNewsInbox.creatorProfileImagePath,
homeFollowingNewsInbox.creatorNickname,
homeFollowingNewsInbox.title,
homeFollowingNewsInbox.body,
homeFollowingNewsInbox.thumbnailImagePath,
creator.profileImage,
creator.nickname,
homeFollowingNewsInbox.targetId,
homeFollowingNewsInbox.occurredAtUtc,
homeFollowingNewsInbox.visibleFromAtUtc,
homeFollowingNewsInbox.rank
homeFollowingNewsInbox.rank,
newsAudioContent.title,
newsAudioContent.coverImage,
newsCommunity.content,
newsCommunity.imagePath,
newsCommunity.createdAt
)
.from(homeFollowingNewsInbox)
.join(creator).on(creator.id.eq(homeFollowingNewsInbox.creatorId))
.leftJoin(newsAudioContent).on(
homeFollowingNewsInbox.newsType.eq(FollowingNewsType.AUDIO_CONTENT),
newsAudioContent.id.eq(homeFollowingNewsInbox.targetId)
)
.leftJoin(newsCommunity).on(
homeFollowingNewsInbox.newsType.eq(FollowingNewsType.COMMUNITY_POST),
newsCommunity.id.eq(homeFollowingNewsInbox.targetId)
)
.where(
homeFollowingNewsInbox.memberId.eq(memberId),
homeFollowingNewsInbox.isActive.isTrue,
homeFollowingNewsInbox.visibleFromAtUtc.loe(nowUtc),
creator.isActive.isTrue,
creator.role.eq(MemberRole.CREATOR),
activeFollowingCondition(memberId, homeFollowingNewsInbox.creatorId),
adultNewsCondition(canViewAdultContent),
notBlockedCreatorCondition(memberId, homeFollowingNewsInbox.creatorId),
activeNewsTargetCondition()
activeNewsTargetCondition(canViewAdultContent)
)
.orderBy(homeFollowingNewsInbox.visibleFromAtUtc.desc(), homeFollowingNewsInbox.id.desc())
.limit(limit.toLong())
.fetch()
.map { row ->
HomeFollowingNews(
newsId = row.get(homeFollowingNewsInbox.id)!!.toString(),
type = row.get(homeFollowingNewsInbox.newsType)!!,
creatorProfileImageUrl = profileImageUrl(row.get(homeFollowingNewsInbox.creatorProfileImagePath)),
creatorNickname = row.get(homeFollowingNewsInbox.creatorNickname)!!,
title = row.get(homeFollowingNewsInbox.title)!!,
body = row.get(homeFollowingNewsInbox.body)!!,
thumbnailImageUrl = row.get(homeFollowingNewsInbox.thumbnailImagePath).toCdnUrl(cloudFrontHost),
targetId = row.get(homeFollowingNewsInbox.targetId)!!,
occurredAtUtc = row.get(homeFollowingNewsInbox.occurredAtUtc)!!.toUtcIso(),
visibleFromAtUtc = row.get(homeFollowingNewsInbox.visibleFromAtUtc)!!.toUtcIso(),
rank = row.get(homeFollowingNewsInbox.rank)
val communityPostIds = rows
.filter { it.get(homeFollowingNewsInbox.newsType) == FollowingNewsType.COMMUNITY_POST }
.map { it.get(homeFollowingNewsInbox.targetId)!! }
val likeCounts = communityLikeCounts(communityPostIds)
val commentCounts = communityCommentCounts(communityPostIds)
return rows.map { row -> row.toHomeFollowingNews(creator, newsAudioContent, newsCommunity, likeCounts, commentCounts) }
}
private fun Tuple.toHomeFollowingNews(
newsCreator: QMember,
newsAudioContent: QAudioContent,
newsCommunity: QCreatorCommunity,
likeCounts: Map<Long, Int>,
commentCounts: Map<Long, Int>
): HomeFollowingNews {
val type = get(homeFollowingNewsInbox.newsType)!!
val targetId = get(homeFollowingNewsInbox.targetId)!!
val creatorProfileImageUrl = profileImageUrl(get(newsCreator.profileImage))
val creatorNickname = get(newsCreator.nickname)!!
val visibleFromAtUtc = get(homeFollowingNewsInbox.visibleFromAtUtc)!!.toUtcIso()
val rank = get(homeFollowingNewsInbox.rank)
return HomeFollowingNews(
newsId = get(homeFollowingNewsInbox.id)!!.toString(),
type = type,
visibleFromAtUtc = visibleFromAtUtc,
creatorRanking = if (type == FollowingNewsType.CREATOR_RANKING && rank != null) {
HomeFollowingCreatorRankingNews(
rank = rank,
creatorId = targetId,
nickname = creatorNickname,
profileImageUrl = creatorProfileImageUrl
)
} else {
null
},
audioContent = if (type == FollowingNewsType.AUDIO_CONTENT) {
HomeFollowingContentNews(
contentId = targetId,
contentImageUrl = get(newsAudioContent.coverImage).toCdnUrl(cloudFrontHost),
title = get(newsAudioContent.title)!!,
creatorProfileImageUrl = creatorProfileImageUrl,
creatorNickname = creatorNickname
)
} else {
null
},
communityPost = if (type == FollowingNewsType.COMMUNITY_POST) {
HomeFollowingCommunityPostNews(
postId = targetId,
creatorProfileImage = creatorProfileImageUrl,
creatorNickname = creatorNickname,
imageUrl = get(newsCommunity.imagePath).toCdnUrl(cloudFrontHost),
content = get(newsCommunity.content)!!,
createdAt = get(newsCommunity.createdAt)!!.toUtcIso(),
likeCount = likeCounts[targetId] ?: 0,
commentCount = commentCounts[targetId] ?: 0
)
} else {
null
}
)
}
private fun findLiveSchedules(
@@ -284,7 +348,44 @@ class DefaultHomeFollowingQueryRepository(
return if (canViewAdultContent) null else homeFollowingNewsInbox.isAdult.isFalse
}
private fun activeNewsTargetCondition(): BooleanExpression {
private fun communityLikeCounts(postIds: List<Long>): Map<Long, Int> {
if (postIds.isEmpty()) return emptyMap()
return queryFactory
.select(creatorCommunityLike.creatorCommunity.id, creatorCommunityLike.id.count())
.from(creatorCommunityLike)
.where(
creatorCommunityLike.creatorCommunity.id.`in`(postIds),
creatorCommunityLike.isActive.isTrue
)
.groupBy(creatorCommunityLike.creatorCommunity.id)
.fetch()
.associate {
it.get(creatorCommunityLike.creatorCommunity.id)!! to
(it.get(creatorCommunityLike.id.count())?.toInt() ?: 0)
}
}
private fun communityCommentCounts(postIds: List<Long>): Map<Long, Int> {
if (postIds.isEmpty()) return emptyMap()
return queryFactory
.select(creatorCommunityComment.creatorCommunity.id, creatorCommunityComment.id.count())
.from(creatorCommunityComment)
.where(
creatorCommunityComment.creatorCommunity.id.`in`(postIds),
creatorCommunityComment.isActive.isTrue,
creatorCommunityComment.parent.isNull
)
.groupBy(creatorCommunityComment.creatorCommunity.id)
.fetch()
.associate {
it.get(creatorCommunityComment.creatorCommunity.id)!! to
(it.get(creatorCommunityComment.id.count())?.toInt() ?: 0)
}
}
private fun activeNewsTargetCondition(canViewAdultContent: Boolean): BooleanExpression {
val newsAudioContent = QAudioContent("newsAudioContent")
val newsCommunity = QCreatorCommunity("newsCommunity")
val activeAudioExists = JPAExpressions
@@ -292,7 +393,8 @@ class DefaultHomeFollowingQueryRepository(
.from(newsAudioContent)
.where(
newsAudioContent.id.eq(homeFollowingNewsInbox.targetId),
newsAudioContent.isActive.isTrue
newsAudioContent.isActive.isTrue,
adultAudioNewsTargetCondition(canViewAdultContent, newsAudioContent)
)
.exists()
val activeCommunityExists = JPAExpressions
@@ -300,15 +402,44 @@ class DefaultHomeFollowingQueryRepository(
.from(newsCommunity)
.where(
newsCommunity.id.eq(homeFollowingNewsInbox.targetId),
newsCommunity.isActive.isTrue
newsCommunity.isActive.isTrue,
newsCommunity.price.loe(0),
adultCommunityNewsTargetCondition(canViewAdultContent, newsCommunity)
)
.exists()
return homeFollowingNewsInbox.newsType.eq(FollowingNewsType.CREATOR_RANKING)
.and(homeFollowingNewsInbox.rank.isNotNull)
.or(homeFollowingNewsInbox.newsType.eq(FollowingNewsType.AUDIO_CONTENT).and(activeAudioExists))
.or(homeFollowingNewsInbox.newsType.eq(FollowingNewsType.COMMUNITY_POST).and(activeCommunityExists))
}
private fun adultAudioNewsTargetCondition(
canViewAdultContent: Boolean,
newsAudioContent: QAudioContent
): BooleanExpression? {
return if (canViewAdultContent) null else newsAudioContent.isAdult.isFalse
}
private fun adultCommunityNewsTargetCondition(
canViewAdultContent: Boolean,
newsCommunity: QCreatorCommunity
): BooleanExpression? {
return if (canViewAdultContent) null else newsCommunity.isAdult.isFalse
}
private fun activeFollowingCondition(memberId: Long, creatorIdPath: Expression<Long>): BooleanExpression {
return JPAExpressions
.selectOne()
.from(creatorFollowing)
.where(
creatorFollowing.member.id.eq(memberId),
creatorFollowing.creator.id.eq(creatorIdPath),
creatorFollowing.isActive.isTrue
)
.exists()
}
private fun notBlockedCreatorCondition(memberId: Long, creatorIdPath: Expression<Long>): BooleanExpression {
val blockMember = QBlockMember("homeFollowingBlockMember")
return JPAExpressions