From 7ad514dcc0a710502d45abecff39a81ba33a5d05 Mon Sep 17 00:00:00 2001 From: Klaus Date: Mon, 1 Jun 2026 17:55:53 +0900 Subject: [PATCH] =?UTF-8?q?feat(content):=20=EC=BD=98=ED=85=90=EC=B8=A0=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20=EC=9D=B4=EB=A0=A5=20=EC=8B=A4=ED=8C=A8=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EB=A5=BC=20=EB=82=A8=EA=B8=B4=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sodalive/content/AudioContentService.kt | 11 +++ .../content/AudioContentServiceTest.kt | 93 +++++++++++++++++++ 2 files changed, 104 insertions(+) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt index b6b127e1..be2a1af0 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt @@ -40,6 +40,7 @@ import kr.co.vividnext.sodalive.member.block.BlockMemberRepository import kr.co.vividnext.sodalive.member.contentpreference.isAdultVisibleByPolicy import kr.co.vividnext.sodalive.utils.generateFileName import kr.co.vividnext.sodalive.v2.recommend.application.CreatorContentViewHistoryService +import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Value import org.springframework.cache.annotation.Cacheable import org.springframework.context.ApplicationEventPublisher @@ -91,6 +92,8 @@ class AudioContentService( @Value("\${cloud.aws.cloud-front.host}") private val coverImageHost: String ) { + private val log = LoggerFactory.getLogger(javaClass) + @Transactional fun audioContentLike(request: PutAudioContentLikeRequest, member: Member): PutAudioContentLikeResponse { var audioContentLike = audioContentLikeRepository.findByMemberIdAndContentId( @@ -820,6 +823,14 @@ class AudioContentService( memberId = member.id!!, contentId = audioContent.id!! ) + }.onFailure { ex -> + log.warn( + "event=creator_content_view_history_record_failure memberId={} contentId={} error={}", + member.id, + audioContent.id, + ex.message, + ex + ) } return GetAudioContentDetailResponse( diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/content/AudioContentServiceTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/content/AudioContentServiceTest.kt index f7dfc6fc..33781d74 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/content/AudioContentServiceTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/content/AudioContentServiceTest.kt @@ -29,10 +29,15 @@ import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith import org.mockito.Mockito +import org.springframework.boot.test.system.CapturedOutput +import org.springframework.boot.test.system.OutputCaptureExtension import org.springframework.context.ApplicationEventPublisher +import java.time.LocalDateTime import java.util.Optional +@ExtendWith(OutputCaptureExtension::class) class AudioContentServiceTest { private lateinit var repository: AudioContentRepository private lateinit var explorerQueryRepository: ExplorerQueryRepository @@ -240,6 +245,34 @@ class AudioContentServiceTest { assertEquals(audioContent.id!!, recordViewInvocation.arguments[1]) } + @Test + @DisplayName("콘텐츠 조회 이력 기록 실패는 상세 응답을 실패시키지 않고 로그로 남긴다") + fun shouldLogViewHistoryFailureWithoutFailingDetail(output: CapturedOutput) { + val viewer = createMember(id = 1003L, nickname = "history-failure-viewer") + val creator = createMember(id = 2003L, nickname = "history-failure-creator") + val audioContent = createAudioContent(creator) + stubSuccessfulDetailDependencies(viewer, creator, audioContent) + Mockito.doThrow(IllegalStateException("history failed")) + .`when`(creatorContentViewHistoryService) + .recordView( + memberId = Mockito.eq(viewer.id!!), + contentId = Mockito.eq(audioContent.id!!), + viewedAt = anyLocalDateTime() + ) + + val response = service.getDetail( + id = audioContent.id!!, + member = viewer, + isAdultContentVisible = false, + timezone = "Asia/Seoul" + ) + + assertEquals(audioContent.id!!, response.contentId) + assertTrue(output.out.contains("event=creator_content_view_history_record_failure")) + assertTrue(output.out.contains("memberId=${viewer.id}")) + assertTrue(output.out.contains("contentId=${audioContent.id}")) + } + private fun createMember(id: Long, nickname: String): Member { val member = Member( email = "$nickname@test.com", @@ -277,4 +310,64 @@ class AudioContentServiceTest { return audioContent } + + private fun stubSuccessfulDetailDependencies(viewer: Member, creator: Member, audioContent: AudioContent) { + Mockito.`when`(repository.findById(audioContent.id!!)).thenReturn(Optional.of(audioContent)) + Mockito.`when`(explorerQueryRepository.getMember(creator.id!!)).thenReturn(creator) + Mockito.`when`( + orderRepository.isExistOrderedAndOrderType( + memberId = viewer.id!!, + contentId = audioContent.id!! + ) + ).thenReturn(Pair(true, OrderType.KEEP)) + Mockito.`when`(explorerQueryRepository.getCreatorFollowing(creator.id!!, viewer.id!!)).thenReturn(null) + Mockito.`when`( + limitedEditionOrderRepository.getOrderSequence( + contentId = audioContent.id!!, + memberId = viewer.id!! + ) + ).thenReturn(null) + Mockito.`when`( + audioContentCloudFront.generateSignedURL( + resourcePath = audioContent.content!!, + expirationTime = 7_200_000L + ) + ).thenReturn("https://signed.test/audio") + Mockito.`when`( + repository.getCreatorOtherContentList( + cloudfrontHost = "https://cdn.test", + contentId = audioContent.id!!, + creatorId = creator.id!!, + isAdult = false + ) + ).thenReturn(emptyList()) + Mockito.`when`( + repository.getSameThemeOtherContentList( + cloudfrontHost = "https://cdn.test", + contentId = audioContent.id!!, + themeId = audioContent.theme!!.id!!, + isAdult = false + ) + ).thenReturn(emptyList()) + Mockito.`when`(audioContentLikeRepository.totalCountAudioContentLike(audioContent.id!!)).thenReturn(0) + Mockito.`when`(audioContentLikeRepository.findByMemberIdAndContentId(viewer.id!!, audioContent.id!!)).thenReturn(null) + Mockito.`when`( + pinContentRepository.findByContentIdAndMemberId( + contentId = audioContent.id!!, + memberId = viewer.id!!, + active = true + ) + ).thenReturn(null) + Mockito.`when`(pinContentRepository.getPinContentList(memberId = viewer.id!!, active = true)).thenReturn(emptyList()) + Mockito.`when`( + contentThemeTranslationRepository.findByContentThemeIdAndLocale( + contentThemeId = audioContent.theme!!.id!!, + locale = "ko" + ) + ).thenReturn(null) + } + + private fun anyLocalDateTime(): LocalDateTime { + return Mockito.any(LocalDateTime::class.java) ?: LocalDateTime.MIN + } }