diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/audio/recommendation/application/AudioRecommendationQueryService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/audio/recommendation/application/AudioRecommendationQueryService.kt new file mode 100644 index 00000000..b3c234eb --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/audio/recommendation/application/AudioRecommendationQueryService.kt @@ -0,0 +1,73 @@ +package kr.co.vividnext.sodalive.v2.audio.recommendation.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.audio.recommendation.domain.AudioRecommendationVisibility +import kr.co.vividnext.sodalive.v2.audio.recommendation.domain.AudioRecommendations +import kr.co.vividnext.sodalive.v2.audio.recommendation.port.out.AudioRecommendationQueryPort +import kr.co.vividnext.sodalive.v2.recommendation.domain.RecommendedSectionType +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.time.LocalDateTime + +@Service +class AudioRecommendationQueryService( + private val queryPort: AudioRecommendationQueryPort, + private val memberContentPreferenceService: MemberContentPreferenceService +) { + @Transactional(readOnly = true) + fun getRecommendations(member: Member?): AudioRecommendations { + val now = LocalDateTime.now() + val canViewAdultContent = canViewAdultContent(member) + return AudioRecommendations( + banners = queryPort.findBanners(BANNER_LIMIT, member?.id, canViewAdultContent), + originalSeries = queryPort.findOriginalSeries(ORIGINAL_SERIES_LIMIT, member?.id, canViewAdultContent, now), + latestAudios = queryPort.findLatestAudios(LATEST_AUDIO_LIMIT, member?.id, canViewAdultContent, now), + newAndHotAudios = emptyList(), + freeAudios = queryPort.findFreeAudios(FREE_AUDIO_LIMIT, member?.id, canViewAdultContent, now), + pointAudios = queryPort.findPointAudios(POINT_AUDIO_LIMIT, member?.id, canViewAdultContent, now), + mostCommentedAudios = emptyList(), + recommendedAudios = emptyList() + ) + } + + fun resolveVisibility(member: Member?): AudioRecommendationVisibility { + return if (canViewAdultContent(member)) AudioRecommendationVisibility.ALL else AudioRecommendationVisibility.SAFE + } + + fun newAndHotSectionType(visibility: AudioRecommendationVisibility): RecommendedSectionType { + return when (visibility) { + AudioRecommendationVisibility.SAFE -> RecommendedSectionType.NEW_AND_HOT_AUDIO_SAFE + AudioRecommendationVisibility.ALL -> RecommendedSectionType.NEW_AND_HOT_AUDIO_ALL + } + } + + fun mostCommentedSectionType(visibility: AudioRecommendationVisibility): RecommendedSectionType { + return when (visibility) { + AudioRecommendationVisibility.SAFE -> RecommendedSectionType.MOST_COMMENTED_AUDIO_SAFE + AudioRecommendationVisibility.ALL -> RecommendedSectionType.MOST_COMMENTED_AUDIO_ALL + } + } + + fun recommendedAudioSectionType(visibility: AudioRecommendationVisibility): RecommendedSectionType { + return when (visibility) { + AudioRecommendationVisibility.SAFE -> RecommendedSectionType.RECOMMENDED_AUDIO_SAFE + AudioRecommendationVisibility.ALL -> RecommendedSectionType.RECOMMENDED_AUDIO_ALL + } + } + + private fun canViewAdultContent(member: Member?): Boolean { + if (member == null) return false + val preference = memberContentPreferenceService.initializeDefaultPreference(member) + return isAdultVisibleByPolicy(member, preference.isAdultContentVisible) + } + + companion object { + const val BANNER_LIMIT = 20 + const val ORIGINAL_SERIES_LIMIT = 12 + const val LATEST_AUDIO_LIMIT = 12 + const val FREE_AUDIO_LIMIT = 10 + const val POINT_AUDIO_LIMIT = 10 + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/audio/recommendation/port/out/AudioRecommendationQueryPort.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/audio/recommendation/port/out/AudioRecommendationQueryPort.kt new file mode 100644 index 00000000..7e5a9f2c --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/audio/recommendation/port/out/AudioRecommendationQueryPort.kt @@ -0,0 +1,22 @@ +package kr.co.vividnext.sodalive.v2.audio.recommendation.port.out + +import kr.co.vividnext.sodalive.v2.audio.recommendation.domain.AudioCard +import kr.co.vividnext.sodalive.v2.audio.recommendation.domain.CommentedAudio +import kr.co.vividnext.sodalive.v2.audio.recommendation.domain.OriginalSeries +import kr.co.vividnext.sodalive.v2.common.domain.RecommendationBanner +import java.time.LocalDateTime + +interface AudioRecommendationQueryPort { + fun findBanners(limit: Int, memberId: Long?, canViewAdultContent: Boolean): List + fun findOriginalSeries(limit: Int, memberId: Long?, canViewAdultContent: Boolean, now: LocalDateTime): List + fun findLatestAudios(limit: Int, memberId: Long?, canViewAdultContent: Boolean, now: LocalDateTime): List + fun findFreeAudios(limit: Int, memberId: Long?, canViewAdultContent: Boolean, now: LocalDateTime): List + fun findPointAudios(limit: Int, memberId: Long?, canViewAdultContent: Boolean, now: LocalDateTime): List + fun findAudioCardsByIds( + contentIds: List, + memberId: Long?, + canViewAdultContent: Boolean, + now: LocalDateTime + ): List + fun findCommentedAudiosByIds(contentIds: List, memberId: Long?, canViewAdultContent: Boolean): List +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/domain/RecommendedSectionType.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/domain/RecommendedSectionType.kt index 40c8a662..1845449f 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/domain/RecommendedSectionType.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/recommendation/domain/RecommendedSectionType.kt @@ -9,5 +9,11 @@ enum class RecommendedSectionType(val code: String) { AI_CHARACTER("AI_CHARACTER"), GENRE_CREATOR("GENRE_CREATOR"), CHEER_CREATOR("CHEER_CREATOR"), - POPULAR_COMMUNITY("POPULAR_COMMUNITY") + POPULAR_COMMUNITY("POPULAR_COMMUNITY"), + NEW_AND_HOT_AUDIO_SAFE("NEW_AND_HOT_AUDIO_SAFE"), + NEW_AND_HOT_AUDIO_ALL("NEW_AND_HOT_AUDIO_ALL"), + MOST_COMMENTED_AUDIO_SAFE("MOST_COMMENTED_AUDIO_SAFE"), + MOST_COMMENTED_AUDIO_ALL("MOST_COMMENTED_AUDIO_ALL"), + RECOMMENDED_AUDIO_SAFE("RECOMMENDED_AUDIO_SAFE"), + RECOMMENDED_AUDIO_ALL("RECOMMENDED_AUDIO_ALL") } diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/audio/recommendation/application/AudioRecommendationQueryServiceTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/audio/recommendation/application/AudioRecommendationQueryServiceTest.kt new file mode 100644 index 00000000..31697da8 --- /dev/null +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/audio/recommendation/application/AudioRecommendationQueryServiceTest.kt @@ -0,0 +1,51 @@ +package kr.co.vividnext.sodalive.v2.audio.recommendation.application + +import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService +import kr.co.vividnext.sodalive.v2.audio.recommendation.domain.AudioRecommendationVisibility +import kr.co.vividnext.sodalive.v2.audio.recommendation.port.out.AudioRecommendationQueryPort +import kr.co.vividnext.sodalive.v2.recommendation.domain.RecommendedSectionType +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.mockito.Mockito + +class AudioRecommendationQueryServiceTest { + private val queryPort = Mockito.mock(AudioRecommendationQueryPort::class.java) + private val preferenceService = Mockito.mock(MemberContentPreferenceService::class.java) + private val service = AudioRecommendationQueryService(queryPort, preferenceService) + + @Test + @DisplayName("비회원은 SAFE visibility를 사용한다") + fun shouldResolveSafeVisibilityForAnonymous() { + assertEquals(AudioRecommendationVisibility.SAFE, service.resolveVisibility(null)) + } + + @Test + @DisplayName("visibility와 섹션 조합은 신규 RecommendedSectionType으로 매핑된다") + fun shouldMapVisibilityToAudioSectionTypes() { + assertEquals( + RecommendedSectionType.NEW_AND_HOT_AUDIO_SAFE, + service.newAndHotSectionType(AudioRecommendationVisibility.SAFE) + ) + assertEquals( + RecommendedSectionType.NEW_AND_HOT_AUDIO_ALL, + service.newAndHotSectionType(AudioRecommendationVisibility.ALL) + ) + assertEquals( + RecommendedSectionType.MOST_COMMENTED_AUDIO_SAFE, + service.mostCommentedSectionType(AudioRecommendationVisibility.SAFE) + ) + assertEquals( + RecommendedSectionType.MOST_COMMENTED_AUDIO_ALL, + service.mostCommentedSectionType(AudioRecommendationVisibility.ALL) + ) + assertEquals( + RecommendedSectionType.RECOMMENDED_AUDIO_SAFE, + service.recommendedAudioSectionType(AudioRecommendationVisibility.SAFE) + ) + assertEquals( + RecommendedSectionType.RECOMMENDED_AUDIO_ALL, + service.recommendedAudioSectionType(AudioRecommendationVisibility.ALL) + ) + } +}