feat(recommend): 홈 추천 조회 서비스를 추가한다

This commit is contained in:
2026-05-31 16:32:43 +09:00
parent 3cd4e689dc
commit 14822f351b
3 changed files with 604 additions and 2 deletions

View File

@@ -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 = "다시듣기"
}
}

View File

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