feat(home-following): 팔로잉 탭 조회 repository를 추가한다
This commit is contained in:
@@ -0,0 +1,343 @@
|
||||
package kr.co.vividnext.sodalive.v2.home.following.adapter.out.persistence
|
||||
|
||||
import com.querydsl.core.Tuple
|
||||
import com.querydsl.core.types.Expression
|
||||
import com.querydsl.core.types.dsl.BooleanExpression
|
||||
import com.querydsl.jpa.JPAExpressions
|
||||
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.extensions.toUtcIso
|
||||
import kr.co.vividnext.sodalive.live.room.QLiveRoom.liveRoom
|
||||
import kr.co.vividnext.sodalive.member.MemberRole
|
||||
import kr.co.vividnext.sodalive.member.QMember
|
||||
import kr.co.vividnext.sodalive.member.block.QBlockMember
|
||||
import kr.co.vividnext.sodalive.member.following.QCreatorFollowing.creatorFollowing
|
||||
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.HomeFollowingCreator
|
||||
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 org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.stereotype.Repository
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneId
|
||||
import java.time.ZoneOffset
|
||||
|
||||
@Repository
|
||||
class DefaultHomeFollowingQueryRepository(
|
||||
private val queryFactory: JPAQueryFactory,
|
||||
@Value("\${cloud.aws.cloud-front.host}")
|
||||
private val cloudFrontHost: String
|
||||
) : HomeFollowingQueryRepository {
|
||||
override fun findFollowingCreators(memberId: Long, limit: Int): List<HomeFollowingCreator> {
|
||||
val creator = QMember("followingCreator")
|
||||
return queryFactory
|
||||
.select(creator.id, creator.nickname, creator.profileImage)
|
||||
.from(creatorFollowing)
|
||||
.join(creatorFollowing.creator, creator)
|
||||
.where(
|
||||
creatorFollowing.member.id.eq(memberId),
|
||||
creatorFollowing.isActive.isTrue,
|
||||
creator.isActive.isTrue,
|
||||
creator.role.eq(MemberRole.CREATOR),
|
||||
notBlockedCreatorCondition(memberId, creator.id)
|
||||
)
|
||||
.orderBy(creatorFollowing.createdAt.desc(), creatorFollowing.id.desc())
|
||||
.limit(limit.toLong())
|
||||
.fetch()
|
||||
.map { row ->
|
||||
HomeFollowingCreator(
|
||||
creatorId = row.get(creator.id)!!,
|
||||
creatorNickname = row.get(creator.nickname)!!,
|
||||
creatorProfileImageUrl = profileImageUrl(row.get(creator.profileImage))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun findOnAirLives(memberId: Long, canViewAdultContent: Boolean, limit: Int): List<HomeFollowingLive> {
|
||||
val creator = QMember("onAirCreator")
|
||||
return queryFactory
|
||||
.select(liveRoom.id, creator.profileImage, creator.nickname, liveRoom.title, liveRoom.beginDateTime)
|
||||
.from(liveRoom)
|
||||
.join(liveRoom.member, creator)
|
||||
.join(creatorFollowing).on(creatorFollowing.creator.id.eq(creator.id))
|
||||
.where(
|
||||
creatorFollowing.member.id.eq(memberId),
|
||||
creatorFollowing.isActive.isTrue,
|
||||
liveRoom.isActive.isTrue,
|
||||
liveRoom.channelName.isNotNull,
|
||||
liveRoom.channelName.isNotEmpty,
|
||||
creator.isActive.isTrue,
|
||||
creator.role.eq(MemberRole.CREATOR),
|
||||
adultLiveCondition(canViewAdultContent),
|
||||
notBlockedCreatorCondition(memberId, creator.id)
|
||||
)
|
||||
.orderBy(liveRoom.beginDateTime.desc(), liveRoom.id.desc())
|
||||
.limit(limit.toLong())
|
||||
.fetch()
|
||||
.map { row ->
|
||||
HomeFollowingLive(
|
||||
liveId = row.get(liveRoom.id)!!,
|
||||
creatorProfileImageUrl = profileImageUrl(row.get(creator.profileImage)),
|
||||
creatorNickname = row.get(creator.nickname)!!,
|
||||
title = row.get(liveRoom.title)!!,
|
||||
startedAtUtc = row.get(liveRoom.beginDateTime)!!.toUtcIso()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun findMonthlySchedules(
|
||||
memberId: Long,
|
||||
canViewAdultContent: Boolean,
|
||||
now: LocalDateTime,
|
||||
limit: Int
|
||||
): List<HomeFollowingSchedule> {
|
||||
val window = monthlyScheduleWindow(now)
|
||||
val liveSchedules = findLiveSchedules(memberId, canViewAdultContent, window)
|
||||
val audioSchedules = findAudioSchedules(memberId, canViewAdultContent, window)
|
||||
return (liveSchedules + audioSchedules)
|
||||
.sortedWith(
|
||||
compareBy<HomeFollowingSchedule> { it.scheduledAtUtc }
|
||||
.thenBy { it.type.sortOrder }
|
||||
.thenBy { it.targetId }
|
||||
)
|
||||
.take(limit)
|
||||
}
|
||||
|
||||
override fun findRecentNews(
|
||||
memberId: Long,
|
||||
canViewAdultContent: Boolean,
|
||||
nowUtc: LocalDateTime,
|
||||
limit: Int
|
||||
): List<HomeFollowingNews> {
|
||||
val creator = QMember("newsCreator")
|
||||
return queryFactory
|
||||
.select(
|
||||
homeFollowingNewsInbox.id,
|
||||
homeFollowingNewsInbox.newsType,
|
||||
homeFollowingNewsInbox.creatorProfileImagePath,
|
||||
homeFollowingNewsInbox.creatorNickname,
|
||||
homeFollowingNewsInbox.title,
|
||||
homeFollowingNewsInbox.body,
|
||||
homeFollowingNewsInbox.thumbnailImagePath,
|
||||
homeFollowingNewsInbox.targetId,
|
||||
homeFollowingNewsInbox.occurredAtUtc,
|
||||
homeFollowingNewsInbox.visibleFromAtUtc,
|
||||
homeFollowingNewsInbox.rank
|
||||
)
|
||||
.from(homeFollowingNewsInbox)
|
||||
.join(creator).on(creator.id.eq(homeFollowingNewsInbox.creatorId))
|
||||
.where(
|
||||
homeFollowingNewsInbox.memberId.eq(memberId),
|
||||
homeFollowingNewsInbox.isActive.isTrue,
|
||||
homeFollowingNewsInbox.visibleFromAtUtc.loe(nowUtc),
|
||||
creator.isActive.isTrue,
|
||||
creator.role.eq(MemberRole.CREATOR),
|
||||
adultNewsCondition(canViewAdultContent),
|
||||
notBlockedCreatorCondition(memberId, homeFollowingNewsInbox.creatorId),
|
||||
activeNewsTargetCondition()
|
||||
)
|
||||
.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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun findLiveSchedules(
|
||||
memberId: Long,
|
||||
canViewAdultContent: Boolean,
|
||||
window: ScheduleWindow
|
||||
): List<HomeFollowingSchedule> {
|
||||
val creator = QMember("scheduleLiveCreator")
|
||||
return queryFactory
|
||||
.select(
|
||||
liveRoom.id,
|
||||
creator.id,
|
||||
creator.profileImage,
|
||||
creator.nickname,
|
||||
liveRoom.title,
|
||||
liveRoom.beginDateTime,
|
||||
liveRoom.channelName
|
||||
)
|
||||
.from(liveRoom)
|
||||
.join(liveRoom.member, creator)
|
||||
.join(creatorFollowing).on(creatorFollowing.creator.id.eq(creator.id))
|
||||
.where(
|
||||
creatorFollowing.member.id.eq(memberId),
|
||||
creatorFollowing.isActive.isTrue,
|
||||
liveRoom.isActive.isTrue,
|
||||
liveRoom.beginDateTime.goe(window.startUtc),
|
||||
liveRoom.beginDateTime.lt(window.endUtc),
|
||||
creator.isActive.isTrue,
|
||||
creator.role.eq(MemberRole.CREATOR),
|
||||
adultLiveCondition(canViewAdultContent),
|
||||
notBlockedCreatorCondition(memberId, creator.id)
|
||||
)
|
||||
.fetch()
|
||||
.map { row -> row.toLiveSchedule(creator) }
|
||||
}
|
||||
|
||||
private fun findAudioSchedules(
|
||||
memberId: Long,
|
||||
canViewAdultContent: Boolean,
|
||||
window: ScheduleWindow
|
||||
): List<HomeFollowingSchedule> {
|
||||
val creator = QMember("scheduleAudioCreator")
|
||||
return queryFactory
|
||||
.select(
|
||||
audioContent.id,
|
||||
creator.id,
|
||||
creator.profileImage,
|
||||
creator.nickname,
|
||||
audioContent.title,
|
||||
audioContent.releaseDate
|
||||
)
|
||||
.from(audioContent)
|
||||
.join(audioContent.member, creator)
|
||||
.join(creatorFollowing).on(creatorFollowing.creator.id.eq(creator.id))
|
||||
.where(
|
||||
creatorFollowing.member.id.eq(memberId),
|
||||
creatorFollowing.isActive.isTrue,
|
||||
audioContent.duration.isNotNull,
|
||||
audioContent.releaseDate.isNotNull,
|
||||
audioContent.releaseDate.goe(window.startUtc),
|
||||
audioContent.releaseDate.lt(window.endUtc),
|
||||
creator.isActive.isTrue,
|
||||
creator.role.eq(MemberRole.CREATOR),
|
||||
adultAudioCondition(canViewAdultContent),
|
||||
notBlockedCreatorCondition(memberId, creator.id)
|
||||
)
|
||||
.fetch()
|
||||
.map { row -> row.toAudioSchedule(creator) }
|
||||
}
|
||||
|
||||
private fun Tuple.toLiveSchedule(creator: QMember): HomeFollowingSchedule {
|
||||
val liveId = get(liveRoom.id)!!
|
||||
val channelName = get(liveRoom.channelName)
|
||||
return HomeFollowingSchedule(
|
||||
scheduleId = "${CreatorActivityType.LIVE}:$liveId",
|
||||
creatorId = get(creator.id)!!,
|
||||
creatorProfileImageUrl = profileImageUrl(get(creator.profileImage)),
|
||||
creatorNickname = get(creator.nickname)!!,
|
||||
title = get(liveRoom.title)!!,
|
||||
type = CreatorActivityType.LIVE,
|
||||
targetId = liveId,
|
||||
scheduledAtUtc = get(liveRoom.beginDateTime)!!.toUtcIso(),
|
||||
isOnAir = !channelName.isNullOrBlank()
|
||||
)
|
||||
}
|
||||
|
||||
private fun Tuple.toAudioSchedule(creator: QMember): HomeFollowingSchedule {
|
||||
val contentId = get(audioContent.id)!!
|
||||
return HomeFollowingSchedule(
|
||||
scheduleId = "${CreatorActivityType.AUDIO}:$contentId",
|
||||
creatorId = get(creator.id)!!,
|
||||
creatorProfileImageUrl = profileImageUrl(get(creator.profileImage)),
|
||||
creatorNickname = get(creator.nickname)!!,
|
||||
title = get(audioContent.title)!!,
|
||||
type = CreatorActivityType.AUDIO,
|
||||
targetId = contentId,
|
||||
scheduledAtUtc = get(audioContent.releaseDate)!!.toUtcIso(),
|
||||
isOnAir = false
|
||||
)
|
||||
}
|
||||
|
||||
private fun monthlyScheduleWindow(now: LocalDateTime): ScheduleWindow {
|
||||
val kstNow = now.atOffset(ZoneOffset.UTC).atZoneSameInstant(KST_ZONE_ID).toLocalDateTime()
|
||||
val startKst = kstNow.toLocalDate().atStartOfDay()
|
||||
val endKst = startKst.toLocalDate().plusMonths(1).withDayOfMonth(1).atStartOfDay()
|
||||
return ScheduleWindow(startUtc = startKst.toUtcFromKst(), endUtc = endKst.toUtcFromKst())
|
||||
}
|
||||
|
||||
private fun LocalDateTime.toUtcFromKst(): LocalDateTime {
|
||||
return atZone(KST_ZONE_ID).withZoneSameInstant(ZoneOffset.UTC).toLocalDateTime()
|
||||
}
|
||||
|
||||
private fun adultLiveCondition(canViewAdultContent: Boolean): BooleanExpression? {
|
||||
return if (canViewAdultContent) null else liveRoom.isAdult.isFalse
|
||||
}
|
||||
|
||||
private fun adultAudioCondition(canViewAdultContent: Boolean): BooleanExpression? {
|
||||
return if (canViewAdultContent) null else audioContent.isAdult.isFalse
|
||||
}
|
||||
|
||||
private fun adultNewsCondition(canViewAdultContent: Boolean): BooleanExpression? {
|
||||
return if (canViewAdultContent) null else homeFollowingNewsInbox.isAdult.isFalse
|
||||
}
|
||||
|
||||
private fun activeNewsTargetCondition(): BooleanExpression {
|
||||
val newsAudioContent = QAudioContent("newsAudioContent")
|
||||
val newsCommunity = QCreatorCommunity("newsCommunity")
|
||||
val activeAudioExists = JPAExpressions
|
||||
.selectOne()
|
||||
.from(newsAudioContent)
|
||||
.where(
|
||||
newsAudioContent.id.eq(homeFollowingNewsInbox.targetId),
|
||||
newsAudioContent.isActive.isTrue
|
||||
)
|
||||
.exists()
|
||||
val activeCommunityExists = JPAExpressions
|
||||
.selectOne()
|
||||
.from(newsCommunity)
|
||||
.where(
|
||||
newsCommunity.id.eq(homeFollowingNewsInbox.targetId),
|
||||
newsCommunity.isActive.isTrue
|
||||
)
|
||||
.exists()
|
||||
|
||||
return homeFollowingNewsInbox.newsType.eq(FollowingNewsType.CREATOR_RANKING)
|
||||
.or(homeFollowingNewsInbox.newsType.eq(FollowingNewsType.AUDIO_CONTENT).and(activeAudioExists))
|
||||
.or(homeFollowingNewsInbox.newsType.eq(FollowingNewsType.COMMUNITY_POST).and(activeCommunityExists))
|
||||
}
|
||||
|
||||
private fun notBlockedCreatorCondition(memberId: Long, creatorIdPath: Expression<Long>): BooleanExpression {
|
||||
val blockMember = QBlockMember("homeFollowingBlockMember")
|
||||
return JPAExpressions
|
||||
.selectOne()
|
||||
.from(blockMember)
|
||||
.where(
|
||||
blockMember.isActive.isTrue,
|
||||
blockMember.member.id.eq(memberId).and(blockMember.blockedMember.id.eq(creatorIdPath))
|
||||
.or(blockMember.member.id.eq(creatorIdPath).and(blockMember.blockedMember.id.eq(memberId)))
|
||||
)
|
||||
.notExists()
|
||||
}
|
||||
|
||||
private fun profileImageUrl(path: String?): String {
|
||||
return path.toCdnUrl(cloudFrontHost) ?: "$cloudFrontHost/profile/default-profile.png"
|
||||
}
|
||||
|
||||
private val CreatorActivityType.sortOrder: Int
|
||||
get() = when (this) {
|
||||
CreatorActivityType.LIVE -> 0
|
||||
else -> 1
|
||||
}
|
||||
|
||||
private data class ScheduleWindow(
|
||||
val startUtc: LocalDateTime,
|
||||
val endUtc: LocalDateTime
|
||||
)
|
||||
|
||||
companion object {
|
||||
private val KST_ZONE_ID: ZoneId = ZoneId.of("Asia/Seoul")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user