feat(recommend): 홈 추천 조회 서비스를 추가한다
This commit is contained in:
@@ -1,8 +1,91 @@
|
||||
package kr.co.vividnext.sodalive.v2.recommend.application
|
||||
|
||||
import kr.co.vividnext.sodalive.v2.recommend.domain.RecommendedActivityType
|
||||
import kr.co.vividnext.sodalive.v2.recommend.domain.RecommendedSectionType
|
||||
import kr.co.vividnext.sodalive.v2.recommend.port.out.HomeAiCharacterRecommendationRecord
|
||||
import kr.co.vividnext.sodalive.v2.recommend.port.out.HomeBannerRecommendationRecord
|
||||
import kr.co.vividnext.sodalive.v2.recommend.port.out.HomeCheerCreatorRecommendationRecord
|
||||
import kr.co.vividnext.sodalive.v2.recommend.port.out.HomeFirstAudioContentRecord
|
||||
import kr.co.vividnext.sodalive.v2.recommend.port.out.HomeLiveRecommendationRecord
|
||||
import kr.co.vividnext.sodalive.v2.recommend.port.out.HomePopularCommunityRecommendationRecord
|
||||
import kr.co.vividnext.sodalive.v2.recommend.port.out.HomeRecommendationQueryPort
|
||||
import kr.co.vividnext.sodalive.v2.recommend.port.out.RecentDebutCreatorRecord
|
||||
import kr.co.vividnext.sodalive.v2.recommend.port.out.RecentlyActiveCreatorRecord
|
||||
import kr.co.vividnext.sodalive.v2.recommend.port.out.RecommendationSnapshotPort
|
||||
import kr.co.vividnext.sodalive.v2.recommend.port.out.RecommendationSnapshotRecord
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import java.time.LocalDateTime
|
||||
|
||||
@Service
|
||||
@Transactional(readOnly = true)
|
||||
class HomeRecommendationQueryService(
|
||||
private val queryPort: HomeRecommendationQueryPort,
|
||||
private val snapshotPort: RecommendationSnapshotPort
|
||||
) {
|
||||
fun findLiveRecommendations(limit: Int = DEFAULT_LIVE_LIMIT): List<HomeLiveRecommendationRecord> {
|
||||
return queryPort.findLiveRecommendations(limit)
|
||||
}
|
||||
|
||||
fun findHomeBanners(limit: Int = DEFAULT_BANNER_LIMIT): List<HomeBannerRecommendationRecord> {
|
||||
return queryPort.findHomeBanners(limit)
|
||||
}
|
||||
|
||||
fun findRecentlyActiveCreators(limit: Int = DEFAULT_ACTIVE_CREATOR_LIMIT): List<RecentlyActiveCreatorRecord> {
|
||||
return queryPort.findRecentlyActiveCreators(limit)
|
||||
}
|
||||
|
||||
fun findRecentDebutCreators(
|
||||
now: LocalDateTime,
|
||||
limit: Int = DEFAULT_RECENT_DEBUT_CREATOR_LIMIT
|
||||
): List<RecentDebutCreatorRecord> {
|
||||
return queryPort.findRecentDebutCreators(now, limit)
|
||||
}
|
||||
|
||||
fun findFirstAudioContents(
|
||||
now: LocalDateTime,
|
||||
limit: Int = DEFAULT_FIRST_AUDIO_CONTENT_LIMIT
|
||||
): List<HomeFirstAudioContentRecord> {
|
||||
return queryPort.findFirstAudioContents(now, limit)
|
||||
}
|
||||
|
||||
fun findAiCharacterRecommendations(
|
||||
limit: Int = DEFAULT_AI_CHARACTER_LIMIT
|
||||
): List<HomeAiCharacterRecommendationRecord> {
|
||||
val snapshots = latestSnapshots(RecommendedSectionType.AI_CHARACTER, limit)
|
||||
val detailsById = queryPort.findAiCharacterRecommendationDetails(snapshots.map { it.targetId })
|
||||
.associateBy { it.characterId }
|
||||
|
||||
return snapshots.mapNotNull { detailsById[it.targetId] }
|
||||
}
|
||||
|
||||
fun findCheerCreatorRecommendations(
|
||||
limit: Int = DEFAULT_CHEER_CREATOR_LIMIT
|
||||
): List<HomeCheerCreatorRecommendationRecord> {
|
||||
val snapshots = latestSnapshots(RecommendedSectionType.CHEER_CREATOR, limit)
|
||||
val detailsById = queryPort.findCheerCreatorRecommendationDetails(snapshots.map { it.targetId })
|
||||
.associateBy { it.creatorId }
|
||||
|
||||
return snapshots.mapNotNull { detailsById[it.targetId] }
|
||||
}
|
||||
|
||||
fun findPopularCommunityRecommendations(
|
||||
limit: Int = DEFAULT_POPULAR_COMMUNITY_LIMIT,
|
||||
includeAdultCommunities: Boolean = false
|
||||
): List<HomePopularCommunityRecommendationRecord> {
|
||||
val snapshots = latestSnapshots(RecommendedSectionType.POPULAR_COMMUNITY, POPULAR_COMMUNITY_CANDIDATE_LIMIT)
|
||||
val detailsById = queryPort.findPopularCommunityRecommendationDetails(
|
||||
snapshots.map { it.targetId },
|
||||
includeAdultCommunities
|
||||
)
|
||||
.associateBy { it.communityId }
|
||||
val selectedCreatorIds = mutableSetOf<Long>()
|
||||
|
||||
return snapshots.mapNotNull { snapshot ->
|
||||
detailsById[snapshot.targetId]?.takeIf { selectedCreatorIds.add(it.creatorId) }
|
||||
}.take(limit)
|
||||
}
|
||||
|
||||
class HomeRecommendationQueryService {
|
||||
fun resolveAudioContentActivityType(theme: String): RecommendedActivityType {
|
||||
return if (theme == LIVE_REPLAY_THEME) {
|
||||
RecommendedActivityType.LIVE_REPLAY
|
||||
@@ -11,7 +94,20 @@ class HomeRecommendationQueryService {
|
||||
}
|
||||
}
|
||||
|
||||
private fun latestSnapshots(sectionType: RecommendedSectionType, limit: Int): List<RecommendationSnapshotRecord> {
|
||||
return snapshotPort.findLatestSnapshots(sectionType).take(limit)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val DEFAULT_LIVE_LIMIT = 20
|
||||
private const val DEFAULT_BANNER_LIMIT = 20
|
||||
private const val DEFAULT_ACTIVE_CREATOR_LIMIT = 10
|
||||
private const val DEFAULT_RECENT_DEBUT_CREATOR_LIMIT = 10
|
||||
private const val DEFAULT_FIRST_AUDIO_CONTENT_LIMIT = 10
|
||||
private const val DEFAULT_AI_CHARACTER_LIMIT = 10
|
||||
private const val DEFAULT_CHEER_CREATOR_LIMIT = 8
|
||||
private const val DEFAULT_POPULAR_COMMUNITY_LIMIT = 10
|
||||
private const val POPULAR_COMMUNITY_CANDIDATE_LIMIT = 20
|
||||
private const val LIVE_REPLAY_THEME = "다시듣기"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,19 @@
|
||||
package kr.co.vividnext.sodalive.v2.recommend.port.out
|
||||
|
||||
import kr.co.vividnext.sodalive.v2.recommend.domain.RecommendedActivityType
|
||||
import java.time.LocalDateTime
|
||||
|
||||
interface HomeRecommendationQueryPort {
|
||||
fun findLiveRecommendations(limit: Int): List<HomeLiveRecommendationRecord>
|
||||
|
||||
fun findHomeBanners(limit: Int): List<HomeBannerRecommendationRecord>
|
||||
|
||||
fun findRecentlyActiveCreators(limit: Int): List<RecentlyActiveCreatorRecord>
|
||||
|
||||
fun findRecentDebutCreators(now: LocalDateTime, limit: Int): List<RecentDebutCreatorRecord>
|
||||
|
||||
fun findFirstAudioContents(now: LocalDateTime, limit: Int): List<HomeFirstAudioContentRecord>
|
||||
|
||||
fun findAiCharacterSnapshots(
|
||||
windowStart: LocalDateTime,
|
||||
snapshotAt: LocalDateTime,
|
||||
@@ -20,4 +31,91 @@ interface HomeRecommendationQueryPort {
|
||||
snapshotAt: LocalDateTime,
|
||||
limit: Int
|
||||
): List<RecommendationSnapshotRecord>
|
||||
|
||||
fun findAiCharacterRecommendationDetails(characterIds: List<Long>): List<HomeAiCharacterRecommendationRecord>
|
||||
|
||||
fun findCheerCreatorRecommendationDetails(creatorIds: List<Long>): List<HomeCheerCreatorRecommendationRecord>
|
||||
|
||||
fun findPopularCommunityRecommendationDetails(
|
||||
communityIds: List<Long>,
|
||||
includeAdultCommunities: Boolean
|
||||
): List<HomePopularCommunityRecommendationRecord>
|
||||
}
|
||||
|
||||
data class HomeLiveRecommendationRecord(
|
||||
val liveRoomId: Long,
|
||||
val creatorId: Long,
|
||||
val creatorNickname: String,
|
||||
val creatorProfileImage: String?,
|
||||
val title: String,
|
||||
val coverImage: String?,
|
||||
val beginDateTime: LocalDateTime,
|
||||
val channelName: String
|
||||
)
|
||||
|
||||
data class HomeBannerRecommendationRecord(
|
||||
val bannerId: Long,
|
||||
val type: String,
|
||||
val thumbnailImage: String,
|
||||
val eventId: Long?,
|
||||
val creatorId: Long?,
|
||||
val seriesId: Long?,
|
||||
val link: String?,
|
||||
val orders: Int,
|
||||
val randomTieBreaker: Double
|
||||
)
|
||||
|
||||
data class RecentlyActiveCreatorRecord(
|
||||
val creatorId: Long,
|
||||
val creatorNickname: String,
|
||||
val creatorProfileImage: String?,
|
||||
val activityType: RecommendedActivityType,
|
||||
val activityAt: LocalDateTime,
|
||||
val targetId: Long?
|
||||
)
|
||||
|
||||
data class RecentDebutCreatorRecord(
|
||||
val creatorId: Long,
|
||||
val creatorNickname: String,
|
||||
val creatorProfileImage: String?,
|
||||
val debutAt: LocalDateTime,
|
||||
val score: Double,
|
||||
val randomTieBreaker: Double
|
||||
)
|
||||
|
||||
data class HomeFirstAudioContentRecord(
|
||||
val contentId: Long,
|
||||
val creatorId: Long,
|
||||
val creatorNickname: String,
|
||||
val creatorProfileImage: String?,
|
||||
val title: String,
|
||||
val coverImage: String?,
|
||||
val releaseDate: LocalDateTime,
|
||||
val recencyScore: Int,
|
||||
val randomTieBreaker: Double
|
||||
)
|
||||
|
||||
data class HomeAiCharacterRecommendationRecord(
|
||||
val characterId: Long,
|
||||
val name: String,
|
||||
val description: String,
|
||||
val totalChatCount: Long,
|
||||
val originalWorkTitle: String?
|
||||
)
|
||||
|
||||
data class HomeCheerCreatorRecommendationRecord(
|
||||
val creatorId: Long,
|
||||
val creatorNickname: String,
|
||||
val creatorProfileImage: String?
|
||||
)
|
||||
|
||||
data class HomePopularCommunityRecommendationRecord(
|
||||
val communityId: Long,
|
||||
val creatorId: Long,
|
||||
val creatorNickname: String,
|
||||
val creatorProfileImage: String?,
|
||||
val content: String,
|
||||
val createdAt: LocalDateTime,
|
||||
val likeCount: Long,
|
||||
val commentCount: Long
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user