feat(home): 홈 추천 통합 facade를 추가한다

This commit is contained in:
2026-06-01 13:50:02 +09:00
parent 09cba1ffeb
commit f77bd7b8e2

View File

@@ -0,0 +1,226 @@
package kr.co.vividnext.sodalive.v2.api.home.application
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
import kr.co.vividnext.sodalive.member.contentpreference.isAdultVisibleByPolicy
import kr.co.vividnext.sodalive.v2.api.home.dto.HomeActiveCreatorItem
import kr.co.vividnext.sodalive.v2.api.home.dto.HomeAiCharacterItem
import kr.co.vividnext.sodalive.v2.api.home.dto.HomeBannerItem
import kr.co.vividnext.sodalive.v2.api.home.dto.HomeCreatorItem
import kr.co.vividnext.sodalive.v2.api.home.dto.HomeFirstAudioContentItem
import kr.co.vividnext.sodalive.v2.api.home.dto.HomeGenreCreatorGroupItem
import kr.co.vividnext.sodalive.v2.api.home.dto.HomeLiveItem
import kr.co.vividnext.sodalive.v2.api.home.dto.HomePopularCommunityItem
import kr.co.vividnext.sodalive.v2.api.home.dto.HomeRecommendationPageResponse
import kr.co.vividnext.sodalive.v2.api.home.dto.HomeRecommendationResponse
import kr.co.vividnext.sodalive.v2.api.home.dto.imageUrl
import kr.co.vividnext.sodalive.v2.api.home.dto.toUtcIso
import kr.co.vividnext.sodalive.v2.recommend.application.HomeRecommendationQueryService
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.HomeGenreCreatorRecommendationGroup
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.RecentDebutCreatorRecord
import kr.co.vividnext.sodalive.v2.recommend.port.out.RecentlyActiveCreatorRecord
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Component
import java.time.LocalDateTime
@Component
class HomeRecommendationFacade(
private val queryService: HomeRecommendationQueryService,
private val memberContentPreferenceService: MemberContentPreferenceService,
@Value("\${cloud.aws.cloud-front.host}")
private val cloudFrontHost: String
) {
fun getHomeRecommendations(member: Member?): HomeRecommendationResponse {
val now = LocalDateTime.now()
val includeAdult = resolveAdultVisibility(member)
return HomeRecommendationResponse(
lives = queryService.findLiveRecommendations(
limit = HOME_LIVE_LIMIT,
includeAdultLives = includeAdult
).map { it.toItem() },
banners = queryService.findHomeBanners(HOME_BANNER_LIMIT).map { it.toItem() },
recentlyActiveCreators = queryService.findRecentlyActiveCreators(HOME_ACTIVE_CREATOR_LIMIT, includeAdult)
.map { it.toItem() },
recentDebutCreators = queryService.findRecentDebutCreators(
now,
limit = HOME_RECENT_DEBUT_CREATOR_LIMIT,
includeAdultContents = includeAdult
)
.map { it.toItem() },
firstAudioContents = queryService.findFirstAudioContents(
now,
limit = HOME_FIRST_AUDIO_CONTENT_LIMIT,
includeAdultContents = includeAdult
)
.map { it.toItem() },
aiCharacters = queryService.findAiCharacterRecommendations(limit = HOME_AI_CHARACTER_LIMIT).map { it.toItem() },
genreCreators = queryService.findGenreCreatorRecommendations(
memberId = member?.id,
includeAdultGenres = includeAdult,
genreLimit = HOME_GENRE_CREATOR_GENRE_LIMIT,
creatorLimit = HOME_GENRE_CREATOR_CREATOR_LIMIT
).map { it.toItem() },
cheerCreators = queryService.findCheerCreatorRecommendations(HOME_CHEER_CREATOR_LIMIT)
.map { it.toCreatorItem() },
popularCommunities = queryService.findPopularCommunityRecommendations(
limit = HOME_POPULAR_COMMUNITY_LIMIT,
includeAdultCommunities = includeAdult
).map { it.toItem() }
)
}
fun getLives(member: Member, page: Int, size: Int): HomeRecommendationPageResponse<HomeLiveItem> {
val fetched = queryService.findLiveRecommendations(
offset = page.toOffset(size),
limit = size + 1,
includeAdultLives = resolveAdultVisibility(member)
)
return fetched.toPage(page, size) { it.toItem() }
}
fun getRecentDebutCreators(member: Member, page: Int, size: Int): HomeRecommendationPageResponse<HomeCreatorItem> {
val fetched = queryService.findRecentDebutCreators(
now = LocalDateTime.now(),
offset = page.toOffset(size),
limit = size + 1,
includeAdultContents = resolveAdultVisibility(member)
)
return fetched.toPage(page, size) { it.toItem() }
}
fun getFirstAudioContents(member: Member, page: Int, size: Int): HomeRecommendationPageResponse<HomeFirstAudioContentItem> {
val fetched = queryService.findFirstAudioContents(
now = LocalDateTime.now(),
offset = page.toOffset(size),
limit = size + 1,
includeAdultContents = resolveAdultVisibility(member)
)
return fetched.toPage(page, size) { it.toItem() }
}
fun getAiCharacters(member: Member, page: Int, size: Int): HomeRecommendationPageResponse<HomeAiCharacterItem> {
val fetched = queryService.findAiCharacterRecommendations(offset = page.toOffset(size), limit = size + 1)
return fetched.toPage(page, size) { it.toItem() }
}
private fun resolveAdultVisibility(member: Member?): Boolean {
if (member == null) return false
val preference = memberContentPreferenceService.initializeDefaultPreference(member)
return isAdultVisibleByPolicy(member, preference.isAdultContentVisible)
}
private fun Int.toOffset(size: Int): Int = this * size
private fun <S, T> List<S>.toPage(
page: Int,
size: Int,
transform: (S) -> T
): HomeRecommendationPageResponse<T> {
val items = this.take(size).map(transform)
val hasNext = this.size > size
return HomeRecommendationPageResponse(items = items, page = page, size = size, hasNext = hasNext)
}
private fun HomeLiveRecommendationRecord.toItem() = HomeLiveItem(
liveRoomId = liveRoomId,
creatorId = creatorId,
creatorNickname = creatorNickname,
creatorProfileImage = imageUrl(cloudFrontHost, creatorProfileImage),
title = title,
coverImage = imageUrl(cloudFrontHost, coverImage),
beginDateTime = beginDateTime.toUtcIso(),
channelName = channelName
)
private fun HomeBannerRecommendationRecord.toItem() = HomeBannerItem(
bannerId = bannerId,
type = type,
thumbnailImage = imageUrl(cloudFrontHost, thumbnailImage),
eventId = eventId,
creatorId = creatorId,
seriesId = seriesId,
link = link
)
private fun RecentlyActiveCreatorRecord.toItem() = HomeActiveCreatorItem(
creatorId = creatorId,
creatorNickname = creatorNickname,
creatorProfileImage = imageUrl(cloudFrontHost, creatorProfileImage),
activityType = activityType.name,
activityAt = activityAt.toUtcIso(),
targetId = targetId
)
private fun RecentDebutCreatorRecord.toItem() = HomeCreatorItem(
creatorId = creatorId,
creatorNickname = creatorNickname,
creatorProfileImage = imageUrl(cloudFrontHost, creatorProfileImage)
)
private fun HomeFirstAudioContentRecord.toItem() = HomeFirstAudioContentItem(
contentId = contentId,
creatorId = creatorId,
creatorNickname = creatorNickname,
creatorProfileImage = imageUrl(cloudFrontHost, creatorProfileImage),
title = title,
coverImage = imageUrl(cloudFrontHost, coverImage),
releaseDate = releaseDate.toUtcIso()
)
private fun HomeAiCharacterRecommendationRecord.toItem() = HomeAiCharacterItem(
characterId = characterId,
name = name,
description = description,
totalChatCount = totalChatCount,
originalWorkTitle = originalWorkTitle
)
private fun HomeGenreCreatorRecommendationGroup.toItem() = HomeGenreCreatorGroupItem(
genreId = genreId,
genreName = genreName,
creators = creators.map {
HomeCreatorItem(
creatorId = it.creatorId,
creatorNickname = it.creatorNickname,
creatorProfileImage = imageUrl(cloudFrontHost, it.creatorProfileImage)
)
}
)
private fun HomeCheerCreatorRecommendationRecord.toCreatorItem() = HomeCreatorItem(
creatorId = creatorId,
creatorNickname = creatorNickname,
creatorProfileImage = imageUrl(cloudFrontHost, creatorProfileImage)
)
private fun HomePopularCommunityRecommendationRecord.toItem() = HomePopularCommunityItem(
communityId = communityId,
creatorId = creatorId,
creatorNickname = creatorNickname,
creatorProfileImage = imageUrl(cloudFrontHost, creatorProfileImage),
content = content,
createdAt = createdAt.toUtcIso(),
likeCount = likeCount,
commentCount = commentCount
)
companion object {
private const val HOME_LIVE_LIMIT = 20
private const val HOME_BANNER_LIMIT = 20
private const val HOME_ACTIVE_CREATOR_LIMIT = 10
private const val HOME_RECENT_DEBUT_CREATOR_LIMIT = 10
private const val HOME_FIRST_AUDIO_CONTENT_LIMIT = 10
private const val HOME_AI_CHARACTER_LIMIT = 10
private const val HOME_GENRE_CREATOR_GENRE_LIMIT = 5
private const val HOME_GENRE_CREATOR_CREATOR_LIMIT = 8
private const val HOME_CHEER_CREATOR_LIMIT = 8
private const val HOME_POPULAR_COMMUNITY_LIMIT = 10
}
}