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 + } + } +}