feat(audio-recommendation): 추천 조회 snapshot fallback을 적용한다

This commit is contained in:
2026-06-23 21:06:25 +09:00
parent 6a6deb33a3
commit ab67e36d96
5 changed files with 437 additions and 16 deletions
@@ -0,0 +1,179 @@
package kr.co.vividnext.sodalive.v2.api.audio.recommendation.adapter.`in`.web
import kr.co.vividnext.sodalive.admin.content.series.genre.SeriesGenre
import kr.co.vividnext.sodalive.content.AudioContent
import kr.co.vividnext.sodalive.content.comment.AudioContentComment
import kr.co.vividnext.sodalive.content.theme.AudioContentTheme
import kr.co.vividnext.sodalive.creator.admin.content.series.Series
import kr.co.vividnext.sodalive.creator.admin.content.series.SeriesContent
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.MemberRole
import kr.co.vividnext.sodalive.support.EmbeddedRedisInitializer
import kr.co.vividnext.sodalive.v2.recommendation.adapter.out.persistence.RecommendationSnapshot
import kr.co.vividnext.sodalive.v2.recommendation.domain.RecommendedSectionType
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.test.context.ContextConfiguration
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
import org.springframework.transaction.support.TransactionTemplate
import java.time.LocalDateTime
import javax.persistence.EntityManager
@SpringBootTest(
properties = [
"cloud.aws.cloud-front.host=https://cdn.test",
"spring.cache.type=none",
"spring.datasource.url=jdbc:h2:mem:audio-recommendation-e2e;MODE=MySQL;NON_KEYWORDS=VALUE;DB_CLOSE_ON_EXIT=FALSE"
]
)
@AutoConfigureMockMvc
@ContextConfiguration(initializers = [EmbeddedRedisInitializer::class])
class AudioRecommendationEndToEndTest @Autowired constructor(
private val mockMvc: MockMvc,
private val entityManager: EntityManager,
private val transactionTemplate: TransactionTemplate
) {
@Test
@DisplayName("오디오 추천 API는 controller-service-repository를 거쳐 추천 섹션 응답을 반환한다")
fun shouldReturnRecommendationsThroughControllerServiceAndRepository() {
val fixture = createFixture()
mockMvc.perform(get("/api/v2/audio/recommendations"))
.andExpect(status().isOk)
.andExpect(jsonPath("$.success").value(true))
.andExpect(jsonPath("$.data.originalSeries").isArray)
.andExpect(jsonPath("$.data.originalSeries[0].seriesId").value(fixture.seriesId))
.andExpect(jsonPath("$.data.latestAudios").isArray)
.andExpect(jsonPath("$.data.latestAudios[0].audioContentId").value(fixture.audioContentId))
.andExpect(jsonPath("$.data.latestAudios[0].isOriginalSeries").value(true))
.andExpect(jsonPath("$.data.recommendedAudios").isArray)
.andExpect(jsonPath("$.data.recommendedAudios[0].audioContentId").value(fixture.audioContentId))
.andExpect(jsonPath("$.data.mostCommentedAudios[0].latestComment").value("latest e2e comment"))
.andExpect(
jsonPath("$.data.mostCommentedAudios[0].latestCommentWriterProfileImageUrl")
.value("https://cdn.test/comment-writer.png")
)
}
private fun createFixture(): Fixture {
return transactionTemplate.execute {
val now = LocalDateTime.now().minusHours(1)
val creator = saveMember("audio-recommendation-e2e-creator", MemberRole.CREATOR)
val writer = saveMember("audio-recommendation-e2e-writer", MemberRole.USER, profileImage = "comment-writer.png")
val theme = saveTheme()
val audio = saveAudio(creator, theme, now)
val series = saveSeries(creator)
saveSeriesContent(series, audio)
saveComment(audio, writer, "latest e2e comment", now.plusMinutes(10))
saveSnapshot(RecommendedSectionType.NEW_AND_HOT_AUDIO_SAFE, audio.id!!, now)
saveSnapshot(RecommendedSectionType.MOST_COMMENTED_AUDIO_SAFE, audio.id!!, now)
saveSnapshot(RecommendedSectionType.RECOMMENDED_AUDIO_SAFE, audio.id!!, now)
entityManager.flush()
entityManager.clear()
Fixture(
seriesId = series.id!!,
audioContentId = audio.id!!
)
}!!
}
private fun saveMember(nickname: String, role: MemberRole, profileImage: String? = "$nickname.png"): Member {
val member = Member(
email = "$nickname@test.com",
password = "password",
nickname = nickname,
profileImage = profileImage,
role = role
)
entityManager.persist(member)
return member
}
private fun saveTheme(): AudioContentTheme {
val theme = AudioContentTheme(theme = "recommendation-e2e-theme", image = "theme.png", isActive = true)
entityManager.persist(theme)
return theme
}
private fun saveAudio(creator: Member, theme: AudioContentTheme, releaseDate: LocalDateTime): AudioContent {
val audio = AudioContent(
title = "audio-recommendation-e2e",
detail = "detail",
languageCode = "ko",
releaseDate = releaseDate,
isAdult = false,
price = 0,
isPointAvailable = true
)
audio.member = creator
audio.theme = theme
audio.isActive = true
audio.coverImage = "audio-recommendation-e2e.png"
audio.duration = "00:10"
entityManager.persist(audio)
return audio
}
private fun saveSeries(creator: Member): Series {
val genre = SeriesGenre("recommendation-e2e-genre")
entityManager.persist(genre)
val series = Series(
title = "recommendation-e2e-series",
introduction = "intro",
isOriginal = true,
isAdult = false,
isActive = true
)
series.member = creator
series.genre = genre
series.coverImage = "series.png"
entityManager.persist(series)
return series
}
private fun saveSeriesContent(series: Series, audio: AudioContent) {
val seriesContent = SeriesContent()
seriesContent.series = series
seriesContent.content = audio
entityManager.persist(seriesContent)
}
private fun saveComment(
audio: AudioContent,
writer: Member,
commentBody: String,
createdAt: LocalDateTime
): AudioContentComment {
val comment = AudioContentComment(comment = commentBody, languageCode = "ko", isActive = true)
comment.audioContent = audio
comment.member = writer
comment.createdAt = createdAt
comment.updatedAt = createdAt
entityManager.persist(comment)
return comment
}
private fun saveSnapshot(sectionType: RecommendedSectionType, targetId: Long, snapshotAt: LocalDateTime) {
entityManager.persist(
RecommendationSnapshot(
sectionType = sectionType,
targetId = targetId,
score = 1.0,
snapshotAt = snapshotAt,
randomTieBreaker = 0.0
)
)
}
private data class Fixture(
val seriesId: Long,
val audioContentId: Long
)
}