From 43179de810dc21038fdaba63939da27eb0532375 Mon Sep 17 00:00:00 2001 From: Klaus Date: Sun, 31 May 2026 18:19:28 +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=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=EB=A5=BC=20=EC=B6=94=EA=B0=80=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CreatorContentViewHistoryService.kt | 28 ++++++++ .../CreatorContentViewHistoryServiceTest.kt | 64 +++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/application/CreatorContentViewHistoryService.kt create mode 100644 src/test/kotlin/kr/co/vividnext/sodalive/v2/recommend/application/CreatorContentViewHistoryServiceTest.kt diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/application/CreatorContentViewHistoryService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/application/CreatorContentViewHistoryService.kt new file mode 100644 index 00000000..04f3d8cf --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/recommend/application/CreatorContentViewHistoryService.kt @@ -0,0 +1,28 @@ +package kr.co.vividnext.sodalive.v2.recommend.application + +import kr.co.vividnext.sodalive.v2.recommend.port.out.CreatorContentViewHistoryPort +import kr.co.vividnext.sodalive.v2.recommend.port.out.CreatorContentViewHistoryRecord +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Propagation +import org.springframework.transaction.annotation.Transactional +import java.time.LocalDateTime + +@Service +class CreatorContentViewHistoryService( + private val port: CreatorContentViewHistoryPort +) { + @Transactional(propagation = Propagation.REQUIRES_NEW) + fun recordView(memberId: Long?, contentId: Long, viewedAt: LocalDateTime = LocalDateTime.now()) { + if (memberId == null) return + + val genreId = port.findGenreIdByContentId(contentId) ?: return + port.save( + CreatorContentViewHistoryRecord( + memberId = memberId, + contentId = contentId, + genreId = genreId, + viewedAt = viewedAt + ) + ) + } +} diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/recommend/application/CreatorContentViewHistoryServiceTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/recommend/application/CreatorContentViewHistoryServiceTest.kt new file mode 100644 index 00000000..8508a413 --- /dev/null +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/recommend/application/CreatorContentViewHistoryServiceTest.kt @@ -0,0 +1,64 @@ +package kr.co.vividnext.sodalive.v2.recommend.application + +import kr.co.vividnext.sodalive.v2.recommend.port.out.CreatorContentViewHistoryPort +import kr.co.vividnext.sodalive.v2.recommend.port.out.CreatorContentViewHistoryRecord +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import java.time.LocalDateTime + +class CreatorContentViewHistoryServiceTest { + private val port = FakeCreatorContentViewHistoryPort() + private val service = CreatorContentViewHistoryService(port) + + @Test + @DisplayName("인증 회원의 콘텐츠 상세 진입 시 memberId/contentId/genreId/viewedAt을 저장한다") + fun shouldRecordAuthenticatedMemberContentViewWithGenreAndViewedAt() { + val viewedAt = LocalDateTime.of(2026, 5, 31, 10, 0) + port.genreIdByContentId[20L] = 30L + + service.recordView(memberId = 10L, contentId = 20L, viewedAt = viewedAt) + + assertEquals( + listOf( + CreatorContentViewHistoryRecord( + memberId = 10L, + contentId = 20L, + genreId = 30L, + viewedAt = viewedAt + ) + ), + port.savedRecords + ) + } + + @Test + @DisplayName("비회원 콘텐츠 상세 진입은 조회 이력을 저장하지 않는다") + fun shouldNotRecordAnonymousContentView() { + service.recordView(memberId = null, contentId = 20L) + + assertTrue(port.savedRecords.isEmpty()) + } + + @Test + @DisplayName("콘텐츠의 활성 장르를 찾지 못하면 조회 이력을 저장하지 않는다") + fun shouldNotRecordWhenContentGenreDoesNotExist() { + service.recordView(memberId = 10L, contentId = 20L) + + assertTrue(port.savedRecords.isEmpty()) + } + + private class FakeCreatorContentViewHistoryPort : CreatorContentViewHistoryPort { + val genreIdByContentId = mutableMapOf() + val savedRecords = mutableListOf() + + override fun findGenreIdByContentId(contentId: Long): Long? { + return genreIdByContentId[contentId] + } + + override fun save(record: CreatorContentViewHistoryRecord) { + savedRecords.add(record) + } + } +}