feat(admin-content): 관리자 콘텐츠 개별 정산 요율 수정을 지원한다

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-04-07 13:38:17 +09:00
parent 1853c28f14
commit 9e6326f08a
6 changed files with 185 additions and 1 deletions

View File

@@ -110,6 +110,7 @@ class AdminAudioContentQueryRepositoryImpl(
audioContentTheme.theme,
audioContentTheme.id,
audioContent.price,
audioContent.settlementRatio,
audioContent.limited,
audioContent.remaining,
audioContent.isAdult,

View File

@@ -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<GetContentMainTabItem> {

View File

@@ -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,

View File

@@ -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?

View File

@@ -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,

View File

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