From 9e6326f08ae0fd923147082abc774cd6daef4ef3 Mon Sep 17 00:00:00 2001 From: Klaus Date: Tue, 7 Apr 2026 13:38:17 +0900 Subject: [PATCH] =?UTF-8?q?feat(admin-content):=20=EA=B4=80=EB=A6=AC?= =?UTF-8?q?=EC=9E=90=20=EC=BD=98=ED=85=90=EC=B8=A0=20=EA=B0=9C=EB=B3=84=20?= =?UTF-8?q?=EC=A0=95=EC=82=B0=20=EC=9A=94=EC=9C=A8=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=EC=9D=84=20=EC=A7=80=EC=9B=90=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- .../admin/content/AdminContentRepository.kt | 1 + .../admin/content/AdminContentService.kt | 15 +- .../content/GetAdminContentListResponse.kt | 1 + .../content/UpdateAdminContentRequest.kt | 2 + .../sodalive/content/AudioContent.kt | 1 + .../admin/content/AdminContentServiceTest.kt | 166 ++++++++++++++++++ 6 files changed, 185 insertions(+), 1 deletion(-) create mode 100644 src/test/kotlin/kr/co/vividnext/sodalive/admin/content/AdminContentServiceTest.kt diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/AdminContentRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/AdminContentRepository.kt index af36cb06..283d1745 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/AdminContentRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/AdminContentRepository.kt @@ -110,6 +110,7 @@ class AdminAudioContentQueryRepositoryImpl( audioContentTheme.theme, audioContentTheme.id, audioContent.price, + audioContent.settlementRatio, audioContent.limited, audioContent.remaining, audioContent.isAdult, diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/AdminContentService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/AdminContentService.kt index dbdbd277..b903c420 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/AdminContentService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/AdminContentService.kt @@ -93,7 +93,8 @@ class AdminContentService( @Transactional fun updateAudioContent(coverImage: MultipartFile?, requestString: String) { - val request = objectMapper.readValue(requestString, UpdateAdminContentRequest::class.java) + val requestNode = objectMapper.readTree(requestString) + val request = objectMapper.treeToValue(requestNode, UpdateAdminContentRequest::class.java) val audioContent = repository.findByIdOrNull(id = request.id) ?: throw SodaException(messageKey = "admin.content.not_found") @@ -145,6 +146,18 @@ class AdminContentService( val theme = themeRepository.findByIdAndActive(id = request.themeId) audioContent.theme = theme } + + if (request.isSettlementRatioDeleted == true) { + if (requestNode.has("settlementRatio")) { + throw SodaException(messageKey = "common.error.invalid_request") + } + audioContent.settlementRatio = null + } else if (request.settlementRatio != null) { + if (request.settlementRatio !in 0..100) { + throw SodaException(messageKey = "common.error.invalid_request") + } + audioContent.settlementRatio = request.settlementRatio + } } fun getContentMainTabList(): List { diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/GetAdminContentListResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/GetAdminContentListResponse.kt index 1fe1ba7e..8052b84d 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/GetAdminContentListResponse.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/GetAdminContentListResponse.kt @@ -18,6 +18,7 @@ data class GetAdminContentListItem @QueryProjection constructor( val theme: String, val themeId: Long, val price: Int, + val settlementRatio: Int?, val totalContentCount: Int?, val remainingContentCount: Int?, val isAdult: Boolean, diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/UpdateAdminContentRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/UpdateAdminContentRequest.kt index 9b0a6e7c..1a51947c 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/UpdateAdminContentRequest.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/UpdateAdminContentRequest.kt @@ -7,6 +7,8 @@ data class UpdateAdminContentRequest( val detail: String?, val curationId: Long?, val themeId: Long?, + val settlementRatio: Int?, + val isSettlementRatioDeleted: Boolean?, val isAdult: Boolean?, val isActive: Boolean?, val isCommentAvailable: Boolean? diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContent.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContent.kt index a7cf23a1..eaa37f68 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContent.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContent.kt @@ -35,6 +35,7 @@ data class AudioContent( var languageCode: String?, var playCount: Long = 0, var price: Int = 0, + var settlementRatio: Int? = null, var releaseDate: LocalDateTime? = null, val limited: Int? = null, var remaining: Int? = null, diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/admin/content/AdminContentServiceTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/admin/content/AdminContentServiceTest.kt new file mode 100644 index 00000000..027bd62b --- /dev/null +++ b/src/test/kotlin/kr/co/vividnext/sodalive/admin/content/AdminContentServiceTest.kt @@ -0,0 +1,166 @@ +package kr.co.vividnext.sodalive.admin.content + +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import kr.co.vividnext.sodalive.admin.content.curation.AdminContentCurationRepository +import kr.co.vividnext.sodalive.admin.content.tab.AdminContentMainTabRepository +import kr.co.vividnext.sodalive.admin.content.theme.AdminContentThemeRepository +import kr.co.vividnext.sodalive.aws.cloudfront.AudioContentCloudFront +import kr.co.vividnext.sodalive.aws.s3.S3Uploader +import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.content.AudioContent +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.mockito.Mockito +import java.util.Optional + +class AdminContentServiceTest { + private lateinit var repository: AdminContentRepository + private lateinit var themeRepository: AdminContentThemeRepository + private lateinit var audioContentCloudFront: AudioContentCloudFront + private lateinit var curationRepository: AdminContentCurationRepository + private lateinit var contentMainTabRepository: AdminContentMainTabRepository + private lateinit var s3Uploader: S3Uploader + private lateinit var service: AdminContentService + + @BeforeEach + fun setup() { + repository = Mockito.mock(AdminContentRepository::class.java) + themeRepository = Mockito.mock(AdminContentThemeRepository::class.java) + audioContentCloudFront = Mockito.mock(AudioContentCloudFront::class.java) + curationRepository = Mockito.mock(AdminContentCurationRepository::class.java) + contentMainTabRepository = Mockito.mock(AdminContentMainTabRepository::class.java) + s3Uploader = Mockito.mock(S3Uploader::class.java) + + service = AdminContentService( + repository = repository, + themeRepository = themeRepository, + audioContentCloudFront = audioContentCloudFront, + curationRepository = curationRepository, + contentMainTabRepository = contentMainTabRepository, + objectMapper = jacksonObjectMapper(), + s3Uploader = s3Uploader, + bucket = "test-bucket" + ) + } + + @Test + @DisplayName("관리자 콘텐츠 수정은 유효한 개별 정산 요율을 저장한다") + fun shouldUpdateSettlementRatioWhenValidValueIsProvided() { + val audioContent = createAudioContent(settlementRatio = 70) + Mockito.`when`(repository.findById(1L)).thenReturn(Optional.of(audioContent)) + + service.updateAudioContent( + coverImage = null, + requestString = """ + {"id":1,"isDefaultCoverImage":false,"settlementRatio":80} + """.trimIndent() + ) + + assertEquals(80, audioContent.settlementRatio) + } + + @Test + @DisplayName("관리자 콘텐츠 수정은 삭제 플래그로 개별 정산 요율을 삭제한다") + fun shouldDeleteSettlementRatioWhenDeleteFlagIsTrue() { + val audioContent = createAudioContent(settlementRatio = 70) + Mockito.`when`(repository.findById(1L)).thenReturn(Optional.of(audioContent)) + + service.updateAudioContent( + coverImage = null, + requestString = """ + {"id":1,"isDefaultCoverImage":false,"isSettlementRatioDeleted":true} + """.trimIndent() + ) + + assertNull(audioContent.settlementRatio) + } + + @Test + @DisplayName("관리자 콘텐츠 수정은 삭제 플래그와 정산 요율을 함께 보내면 예외를 던진다") + fun shouldThrowWhenDeleteFlagAndSettlementRatioAreProvidedTogether() { + val audioContent = createAudioContent(settlementRatio = 70) + Mockito.`when`(repository.findById(1L)).thenReturn(Optional.of(audioContent)) + + val exception = assertThrows(SodaException::class.java) { + service.updateAudioContent( + coverImage = null, + requestString = """ + {"id":1,"isDefaultCoverImage":false,"settlementRatio":80,"isSettlementRatioDeleted":true} + """.trimIndent() + ) + } + + assertEquals("common.error.invalid_request", exception.messageKey) + assertEquals(70, audioContent.settlementRatio) + } + + @Test + @DisplayName("관리자 콘텐츠 수정은 삭제 플래그와 null 정산 요율 키를 함께 보내도 예외를 던진다") + fun shouldThrowWhenDeleteFlagAndNullSettlementRatioKeyAreProvidedTogether() { + val audioContent = createAudioContent(settlementRatio = 70) + Mockito.`when`(repository.findById(1L)).thenReturn(Optional.of(audioContent)) + + val exception = assertThrows(SodaException::class.java) { + service.updateAudioContent( + coverImage = null, + requestString = """ + {"id":1,"isDefaultCoverImage":false,"settlementRatio":null,"isSettlementRatioDeleted":true} + """.trimIndent() + ) + } + + assertEquals("common.error.invalid_request", exception.messageKey) + assertEquals(70, audioContent.settlementRatio) + } + + @Test + @DisplayName("관리자 콘텐츠 수정은 범위를 벗어난 정산 요율이면 예외를 던진다") + fun shouldThrowWhenSettlementRatioIsOutOfRange() { + val audioContent = createAudioContent(settlementRatio = 70) + Mockito.`when`(repository.findById(1L)).thenReturn(Optional.of(audioContent)) + + val exception = assertThrows(SodaException::class.java) { + service.updateAudioContent( + coverImage = null, + requestString = """ + {"id":1,"isDefaultCoverImage":false,"settlementRatio":101} + """.trimIndent() + ) + } + + assertEquals("common.error.invalid_request", exception.messageKey) + assertEquals(70, audioContent.settlementRatio) + } + + @Test + @DisplayName("관리자 콘텐츠 수정은 삭제 플래그와 정산 요율이 없으면 기존 값을 유지한다") + fun shouldKeepSettlementRatioWhenNoSettlementRatioFieldsAreProvided() { + val audioContent = createAudioContent(settlementRatio = 70) + Mockito.`when`(repository.findById(1L)).thenReturn(Optional.of(audioContent)) + + service.updateAudioContent( + coverImage = null, + requestString = """ + {"id":1,"isDefaultCoverImage":false} + """.trimIndent() + ) + + assertEquals(70, audioContent.settlementRatio) + } + + private fun createAudioContent(settlementRatio: Int?): AudioContent { + return AudioContent( + title = "title", + detail = "detail", + languageCode = "ko", + price = 100, + settlementRatio = settlementRatio + ).apply { + id = 1L + } + } +}