From 2ef8e8e489783a92f7e7a8b3f4c927d528c46052 Mon Sep 17 00:00:00 2001 From: Klaus Date: Sun, 31 May 2026 18:18:50 +0900 Subject: [PATCH] =?UTF-8?q?feat(recommend):=20=EC=BD=98=ED=85=90=EC=B8=A0?= =?UTF-8?q?=20=EC=A1=B0=ED=9A=8C=20=EC=9D=B4=EB=A0=A5=20=EC=A0=80=EC=9E=A5?= =?UTF-8?q?=20=EC=96=B4=EB=8C=91=ED=84=B0=EB=A5=BC=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...torContentViewHistoryPersistenceAdapter.kt | 38 ++++++++ ...ontentViewHistoryPersistenceAdapterTest.kt | 97 +++++++++++++++++++ 2 files changed, 135 insertions(+) create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/adapter/out/persistence/CreatorContentViewHistoryPersistenceAdapter.kt create mode 100644 src/test/kotlin/kr/co/vividnext/sodalive/v2/recommend/adapter/out/persistence/CreatorContentViewHistoryPersistenceAdapterTest.kt diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/adapter/out/persistence/CreatorContentViewHistoryPersistenceAdapter.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/adapter/out/persistence/CreatorContentViewHistoryPersistenceAdapter.kt new file mode 100644 index 00000000..1569db7e --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/adapter/out/persistence/CreatorContentViewHistoryPersistenceAdapter.kt @@ -0,0 +1,38 @@ +package kr.co.vividnext.sodalive.v2.recommend.adapter.out.persistence + +import com.querydsl.jpa.impl.JPAQueryFactory +import kr.co.vividnext.sodalive.content.QAudioContent.audioContent +import kr.co.vividnext.sodalive.content.theme.QAudioContentTheme.audioContentTheme +import kr.co.vividnext.sodalive.v2.recommend.port.out.CreatorContentViewHistoryPort +import kr.co.vividnext.sodalive.v2.recommend.port.out.CreatorContentViewHistoryRecord +import org.springframework.stereotype.Repository + +@Repository +class CreatorContentViewHistoryPersistenceAdapter( + private val repository: CreatorContentViewHistoryRepository, + private val queryFactory: JPAQueryFactory +) : CreatorContentViewHistoryPort { + override fun findGenreIdByContentId(contentId: Long): Long? { + return queryFactory + .select(audioContentTheme.id) + .from(audioContent) + .innerJoin(audioContent.theme, audioContentTheme) + .where( + audioContent.id.eq(contentId), + audioContent.isActive.isTrue, + audioContentTheme.isActive.isTrue + ) + .fetchFirst() + } + + override fun save(record: CreatorContentViewHistoryRecord) { + repository.save( + CreatorContentViewHistory( + memberId = record.memberId, + contentId = record.contentId, + genreId = record.genreId, + viewedAt = record.viewedAt + ) + ) + } +} diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/recommend/adapter/out/persistence/CreatorContentViewHistoryPersistenceAdapterTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/recommend/adapter/out/persistence/CreatorContentViewHistoryPersistenceAdapterTest.kt new file mode 100644 index 00000000..f89ca736 --- /dev/null +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/recommend/adapter/out/persistence/CreatorContentViewHistoryPersistenceAdapterTest.kt @@ -0,0 +1,97 @@ +package kr.co.vividnext.sodalive.v2.recommend.adapter.out.persistence + +import com.querydsl.jpa.impl.JPAQueryFactory +import kr.co.vividnext.sodalive.configs.QueryDslConfig +import kr.co.vividnext.sodalive.content.AudioContent +import kr.co.vividnext.sodalive.content.theme.AudioContentTheme +import kr.co.vividnext.sodalive.member.Member +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNull +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.orm.jpa.DataJpaTest +import org.springframework.context.annotation.Import +import java.time.LocalDateTime +import javax.persistence.EntityManager + +@DataJpaTest( + properties = [ + "spring.cache.type=none", + "spring.datasource.url=jdbc:h2:mem:testdb;MODE=MySQL;NON_KEYWORDS=VALUE" + ] +) +@Import(QueryDslConfig::class) +class CreatorContentViewHistoryPersistenceAdapterTest @Autowired constructor( + private val entityManager: EntityManager, + private val repository: CreatorContentViewHistoryRepository, + queryFactory: JPAQueryFactory +) { + private val adapter = CreatorContentViewHistoryPersistenceAdapter(repository, queryFactory) + + @Test + @DisplayName("콘텐츠 조회 이력 저장용 genreId는 content_theme id를 조회한다") + fun shouldFindContentThemeIdByContentId() { + val creator = saveMember("history-theme-creator") + val theme = saveTheme("history-theme") + val content = saveAudioContent(creator, theme, isActive = true) + flushAndClear() + + val themeId = adapter.findGenreIdByContentId(content.id!!) + + assertEquals(theme.id, themeId) + } + + @Test + @DisplayName("비활성 콘텐츠 또는 비활성 테마는 조회 이력 저장 대상 테마를 반환하지 않는다") + fun shouldNotFindThemeIdWhenContentOrThemeIsInactive() { + val creator = saveMember("history-inactive-creator") + val activeTheme = saveTheme("history-active-theme") + val inactiveTheme = saveTheme("history-inactive-theme", isActive = false) + val inactiveContent = saveAudioContent(creator, activeTheme, isActive = false) + val inactiveThemeContent = saveAudioContent(creator, inactiveTheme, isActive = true) + flushAndClear() + + assertNull(adapter.findGenreIdByContentId(inactiveContent.id!!)) + assertNull(adapter.findGenreIdByContentId(inactiveThemeContent.id!!)) + } + + private fun saveMember(nickname: String): Member { + val member = Member( + email = "$nickname@test.com", + password = "password", + nickname = nickname + ) + entityManager.persist(member) + return member + } + + private fun saveTheme(name: String, isActive: Boolean = true): AudioContentTheme { + val theme = AudioContentTheme( + theme = name, + image = "$name.png", + isActive = isActive + ) + entityManager.persist(theme) + return theme + } + + private fun saveAudioContent(creator: Member, theme: AudioContentTheme, isActive: Boolean): AudioContent { + val content = AudioContent( + title = "content-${creator.nickname}-${theme.theme}", + detail = "detail", + languageCode = "ko", + releaseDate = LocalDateTime.of(2026, 5, 30, 10, 0) + ) + content.member = creator + content.theme = theme + content.isActive = isActive + entityManager.persist(content) + return content + } + + private fun flushAndClear() { + entityManager.flush() + entityManager.clear() + } +}