diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/application/HomeRecommendationFacade.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/application/HomeRecommendationFacade.kt new file mode 100644 index 00000000..25b054cd --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/application/HomeRecommendationFacade.kt @@ -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 { + 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 { + 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 { + 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 { + 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 List.toPage( + page: Int, + size: Int, + transform: (S) -> T + ): HomeRecommendationPageResponse { + 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 + } +}