diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/audio/recommendation/application/AudioRecommendationFacade.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/audio/recommendation/application/AudioRecommendationFacade.kt new file mode 100644 index 00000000..a1d85469 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/audio/recommendation/application/AudioRecommendationFacade.kt @@ -0,0 +1,15 @@ +package kr.co.vividnext.sodalive.v2.api.audio.recommendation.application + +import kr.co.vividnext.sodalive.member.Member +import kr.co.vividnext.sodalive.v2.api.audio.recommendation.dto.AudioRecommendationsResponse +import kr.co.vividnext.sodalive.v2.audio.recommendation.application.AudioRecommendationQueryService +import org.springframework.stereotype.Component + +@Component +class AudioRecommendationFacade( + private val queryService: AudioRecommendationQueryService +) { + fun getRecommendations(member: Member?): AudioRecommendationsResponse { + return AudioRecommendationsResponse.from(queryService.getRecommendations(member)) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/audio/recommendation/dto/AudioRecommendationsResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/audio/recommendation/dto/AudioRecommendationsResponse.kt new file mode 100644 index 00000000..079bea9b --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/audio/recommendation/dto/AudioRecommendationsResponse.kt @@ -0,0 +1,99 @@ +package kr.co.vividnext.sodalive.v2.api.audio.recommendation.dto + +import com.fasterxml.jackson.annotation.JsonProperty +import kr.co.vividnext.sodalive.v2.api.common.dto.RecommendationBannerResponse +import kr.co.vividnext.sodalive.v2.audio.recommendation.domain.AudioCard +import kr.co.vividnext.sodalive.v2.audio.recommendation.domain.AudioRecommendations +import kr.co.vividnext.sodalive.v2.audio.recommendation.domain.CommentedAudio +import kr.co.vividnext.sodalive.v2.audio.recommendation.domain.OriginalSeries + +data class AudioRecommendationsResponse( + val banners: List, + val originalSeries: List, + val latestAudios: List, + val newAndHotAudios: List, + val freeAudios: List, + val pointAudios: List, + val mostCommentedAudios: List, + val recommendedAudios: List +) { + companion object { + fun from(recommendations: AudioRecommendations): AudioRecommendationsResponse { + return AudioRecommendationsResponse( + banners = recommendations.banners.map(RecommendationBannerResponse::from), + originalSeries = recommendations.originalSeries.map(OriginalSeriesResponse::from), + latestAudios = recommendations.latestAudios.map(AudioCardResponse::from), + newAndHotAudios = recommendations.newAndHotAudios.map(AudioCardResponse::from), + freeAudios = recommendations.freeAudios.map(AudioCardResponse::from), + pointAudios = recommendations.pointAudios.map(AudioCardResponse::from), + mostCommentedAudios = recommendations.mostCommentedAudios.map(CommentedAudioResponse::from), + recommendedAudios = recommendations.recommendedAudios.map(AudioCardResponse::from) + ) + } + } +} + +data class OriginalSeriesResponse( + val seriesId: Long, + val coverImageUrl: String? +) { + companion object { + fun from(series: OriginalSeries): OriginalSeriesResponse { + return OriginalSeriesResponse(series.seriesId, series.coverImageUrl) + } + } +} + +data class AudioCardResponse( + val audioContentId: Long, + val title: String, + val duration: String?, + val imageUrl: String?, + val price: Int, + @JsonProperty("isAdult") + val isAdult: Boolean, + @JsonProperty("isPointAvailable") + val isPointAvailable: Boolean, + @JsonProperty("isFirstContent") + val isFirstContent: Boolean, + @JsonProperty("isOriginalSeries") + val isOriginalSeries: Boolean, + val creatorNickname: String +) { + companion object { + fun from(audio: AudioCard): AudioCardResponse { + return AudioCardResponse( + audioContentId = audio.audioContentId, + title = audio.title, + duration = audio.duration, + imageUrl = audio.imageUrl, + price = audio.price, + isAdult = audio.isAdult, + isPointAvailable = audio.isPointAvailable, + isFirstContent = audio.isFirstContent, + isOriginalSeries = audio.isOriginalSeries, + creatorNickname = audio.creatorNickname + ) + } + } +} + +data class CommentedAudioResponse( + val audioContentId: Long, + val title: String, + val imageUrl: String?, + val latestComment: String, + val latestCommentWriterProfileImageUrl: String +) { + companion object { + fun from(audio: CommentedAudio): CommentedAudioResponse { + return CommentedAudioResponse( + audioContentId = audio.audioContentId, + title = audio.title, + imageUrl = audio.imageUrl, + latestComment = audio.latestComment, + latestCommentWriterProfileImageUrl = audio.latestCommentWriterProfileImageUrl + ) + } + } +} diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/audio/recommendation/application/AudioRecommendationFacadeTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/audio/recommendation/application/AudioRecommendationFacadeTest.kt new file mode 100644 index 00000000..e1082b54 --- /dev/null +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/audio/recommendation/application/AudioRecommendationFacadeTest.kt @@ -0,0 +1,79 @@ +package kr.co.vividnext.sodalive.v2.api.audio.recommendation.application + +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import kr.co.vividnext.sodalive.v2.audio.recommendation.application.AudioRecommendationQueryService +import kr.co.vividnext.sodalive.v2.audio.recommendation.domain.AudioCard +import kr.co.vividnext.sodalive.v2.audio.recommendation.domain.AudioRecommendations +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 org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.mockito.Mockito + +class AudioRecommendationFacadeTest { + private val objectMapper = jacksonObjectMapper() + private val queryService = Mockito.mock(AudioRecommendationQueryService::class.java) + private val facade = AudioRecommendationFacade(queryService) + + @Test + @DisplayName("facade는 도메인 추천 결과를 모든 공개 응답 필드로 변환한다") + fun shouldConvertDomainRecommendationsToResponse() { + Mockito.doReturn(domain()).`when`(queryService).getRecommendations(null) + + val response = facade.getRecommendations(null) + + assertEquals(1, response.banners.size) + assertEquals(1, response.originalSeries.size) + assertEquals(1, response.latestAudios.size) + assertEquals(1, response.newAndHotAudios.size) + assertEquals(1, response.freeAudios.size) + assertEquals(1, response.pointAudios.size) + assertEquals(1, response.mostCommentedAudios.size) + assertEquals(1, response.recommendedAudios.size) + assertEquals(false, response.latestAudios[0].isOriginalSeries) + + val json = objectMapper.readTree(objectMapper.writeValueAsString(response)) + assertEquals(false, json["latestAudios"][0]["isAdult"].asBoolean()) + assertEquals(true, json["latestAudios"][0]["isPointAvailable"].asBoolean()) + assertEquals(true, json["latestAudios"][0]["isFirstContent"].asBoolean()) + assertEquals(false, json["latestAudios"][0]["isOriginalSeries"].asBoolean()) + assertEquals(false, json["latestAudios"][0].has("adult")) + assertEquals(false, json["latestAudios"][0].has("pointAvailable")) + assertEquals("latest comment", json["mostCommentedAudios"][0]["latestComment"].asText()) + } + + private fun domain(): AudioRecommendations { + val card = AudioCard( + audioContentId = 1L, + title = "audio", + duration = "00:01", + imageUrl = "https://cdn.test/audio.png", + price = 0, + isAdult = false, + isPointAvailable = true, + isFirstContent = true, + isOriginalSeries = false, + creatorNickname = "creator" + ) + return AudioRecommendations( + banners = listOf(RecommendationBanner("https://cdn.test/banner.png", null, null, null, "https://link.test")), + originalSeries = listOf(OriginalSeries(2L, "https://cdn.test/series.png")), + latestAudios = listOf(card), + newAndHotAudios = listOf(card.copy(audioContentId = 3L)), + freeAudios = listOf(card.copy(audioContentId = 4L)), + pointAudios = listOf(card.copy(audioContentId = 5L)), + mostCommentedAudios = listOf( + CommentedAudio( + audioContentId = 6L, + title = "commented", + imageUrl = "https://cdn.test/commented.png", + latestComment = "latest comment", + latestCommentWriterProfileImageUrl = "https://cdn.test/profile.png" + ) + ), + recommendedAudios = listOf(card.copy(audioContentId = 7L)) + ) + } +}