feat(contentpreference): 본인인증 후 요청한 콘텐츠 선호값을 저장한다

This commit is contained in:
2026-10-12 13:27:38 +09:00
parent 31167cba1b
commit 6115792dcd
3 changed files with 165 additions and 7 deletions
@@ -129,14 +129,28 @@ class MemberContentPreferenceService(
} }
@Transactional @Transactional
fun markAdultVisibleAfterAuthVerify(memberId: Long) { fun markAdultVisibleAfterAuthVerify(
memberId: Long,
isAdultContentVisible: Boolean = true,
contentType: ContentType = ContentType.ALL
) {
val member = memberRepository.findByIdOrNull(memberId) val member = memberRepository.findByIdOrNull(memberId)
?: throw SodaException(messageKey = "common.error.bad_credentials") ?: throw SodaException(messageKey = "common.error.bad_credentials")
val preference = initializeDefaultPreference(member) val preference = initializeDefaultPreference(member)
if (!preference.isAdultContentVisible) { val now = LocalDateTime.now()
preference.isAdultContentVisible = true var hasChanged = false
preference.adultContentVisibilityChangedAt = LocalDateTime.now() if (preference.isAdultContentVisible != isAdultContentVisible) {
preference.isAdultContentVisible = isAdultContentVisible
preference.adultContentVisibilityChangedAt = now
hasChanged = true
}
if (preference.contentType != contentType) {
preference.contentType = contentType
preference.contentTypeChangedAt = now
hasChanged = true
}
if (hasChanged) {
evictRecommendLiveCacheAfterCommit(memberId) evictRecommendLiveCacheAfterCommit(memberId)
} }
} }
@@ -14,6 +14,8 @@ import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.CsvSource
import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
import org.springframework.cache.concurrent.ConcurrentMapCacheManager import org.springframework.cache.concurrent.ConcurrentMapCacheManager
@@ -166,16 +168,50 @@ class MemberContentPreferenceIntegrationTest @Autowired constructor(
} }
@Test @Test
@DisplayName("authVerify 성공 후 markAdultVisibleAfterAuthVerify를 호출하면 저장값이 true로 반영된다") @DisplayName("인증 후 입력이 없으면 기존 false/FEMALE 행이 true/ALL로 저장된다")
fun shouldMarkAdultVisibleAfterAuthVerify() { fun shouldMarkAdultVisibleAfterAuthVerify() {
val member = saveNonForcedMember("auth-verified-user") val member = saveNonForcedMember("auth-verified-user")
countryContext.setCountryCode("US") countryContext.setCountryCode("US")
service.updatePreference(member, isAdultContentVisible = false, contentType = ContentType.ALL) service.updatePreference(member, isAdultContentVisible = false, contentType = ContentType.FEMALE)
service.markAdultVisibleAfterAuthVerify(member.id!!) service.markAdultVisibleAfterAuthVerify(member.id!!)
entityManager.flush()
entityManager.clear()
val stored = service.getStoredPreference(member) val stored = preferenceRepository.findByMemberId(member.id!!)!!
assertTrue(stored.isAdultContentVisible) assertTrue(stored.isAdultContentVisible)
assertEquals(ContentType.ALL, stored.contentType)
}
@ParameterizedTest
@CsvSource(
"ALL, KR, true", "MALE, KR, true", "FEMALE, KR, true",
"ALL, US, true", "MALE, US, true", "FEMALE, US, true",
"ALL, KR, false", "MALE, KR, false", "FEMALE, KR, false",
"ALL, US, false", "MALE, US, false", "FEMALE, US, false"
)
@DisplayName("인증 후 false와 모든 콘텐츠 유형은 KR과 해외의 기존 행 및 새 행에 영속화된다")
fun shouldPersistExplicitAuthPreferenceAfterReload(
contentType: ContentType,
countryCode: String,
existingRow: Boolean
) {
val member = saveNonForcedMember("auth-input")
countryContext.setCountryCode(countryCode)
saveAuth(member)
val reloadedMember = memberRepository.findById(member.id!!).orElseThrow()
if (existingRow) {
service.initializeDefaultPreference(reloadedMember)
}
assertEquals(existingRow, preferenceRepository.findByMemberId(member.id!!) != null)
service.markAdultVisibleAfterAuthVerify(member.id!!, false, contentType)
entityManager.flush()
entityManager.clear()
val stored = preferenceRepository.findByMemberId(member.id!!)!!
assertFalse(stored.isAdultContentVisible)
assertEquals(contentType, stored.contentType)
} }
@Test @Test
@@ -13,10 +13,16 @@ import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.CsvSource
import org.junit.jupiter.params.provider.ValueSource
import org.mockito.Mockito import org.mockito.Mockito
import org.springframework.cache.Cache import org.springframework.cache.Cache
import org.springframework.cache.CacheManager import org.springframework.cache.CacheManager
import org.springframework.cache.concurrent.ConcurrentMapCacheManager
import org.springframework.dao.DataIntegrityViolationException import org.springframework.dao.DataIntegrityViolationException
import org.springframework.transaction.support.TransactionSynchronization
import org.springframework.transaction.support.TransactionSynchronizationManager
import java.time.LocalDateTime import java.time.LocalDateTime
import java.util.Optional import java.util.Optional
@@ -405,6 +411,108 @@ class MemberContentPreferenceServiceTest {
verifyRecommendLiveCacheEvicted(32L) verifyRecommendLiveCacheEvicted(32L)
} }
@Test
@DisplayName("인증 후 입력이 없으면 기존 false/FEMALE 설정을 true/ALL로 변경한다")
fun shouldResetExistingPreferenceToTrueAndAllAfterAuthVerifyWithoutInputs() {
val member = createMember(id = 33L)
val preference = createPreference(member).apply { contentType = ContentType.FEMALE }
val baselineTime = preference.contentTypeChangedAt
Mockito.`when`(memberRepository.findById(33L)).thenReturn(Optional.of(member))
Mockito.`when`(repository.findByMemberId(33L)).thenReturn(preference)
service.markAdultVisibleAfterAuthVerify(33L)
assertTrue(preference.isAdultContentVisible)
assertEquals(ContentType.ALL, preference.contentType)
assertTrue(preference.adultContentVisibilityChangedAt.isAfter(baselineTime))
assertTrue(preference.contentTypeChangedAt.isAfter(baselineTime))
verifyRecommendLiveCacheEvicted(33L)
}
@ParameterizedTest
@CsvSource(
"false, FEMALE, false, false",
"true, FEMALE, true, false",
"false, MALE, false, true",
"false, ALL, false, true",
"true, MALE, true, true",
"true, ALL, true, true"
)
@DisplayName("본인인증 후 설정 저장 메서드는 국가·인증 상태를 재검사하지 않고 요청값을 반영한다")
fun shouldApplyAuthInputsAndUpdateOnlyChangedFields(
isAdultContentVisible: Boolean,
contentType: ContentType,
adultVisibilityChanged: Boolean,
contentTypeChanged: Boolean
) {
val member = createMember(id = 34L)
countryContext.setCountryCode("KR")
val baselineTime = LocalDateTime.of(2025, 1, 1, 0, 0)
val preference = MemberContentPreference(false, ContentType.FEMALE, baselineTime, baselineTime)
preference.member = member
Mockito.`when`(memberRepository.findById(34L)).thenReturn(Optional.of(member))
Mockito.`when`(repository.findByMemberId(34L)).thenReturn(preference)
service.markAdultVisibleAfterAuthVerify(34L, isAdultContentVisible, contentType)
assertEquals(isAdultContentVisible, preference.isAdultContentVisible)
assertEquals(contentType, preference.contentType)
if (adultVisibilityChanged) {
assertTrue(preference.adultContentVisibilityChangedAt.isAfter(baselineTime))
} else {
assertEquals(baselineTime, preference.adultContentVisibilityChangedAt)
}
if (contentTypeChanged) {
assertTrue(preference.contentTypeChangedAt.isAfter(baselineTime))
} else {
assertEquals(baselineTime, preference.contentTypeChangedAt)
}
if (adultVisibilityChanged || contentTypeChanged) {
verifyRecommendLiveCacheEvicted(34L)
} else {
verifyRecommendLiveCacheNotEvicted(34L)
}
}
@ParameterizedTest
@ValueSource(booleans = [true, false])
@DisplayName("인증 선호값 변경은 커밋 후에만 캐시를 제거하고 롤백 시 캐시를 유지한다")
fun shouldEvictAuthPreferenceCacheOnlyAfterCommit(committed: Boolean) {
val member = createMember(id = 35L)
val preference = createPreference(member)
Mockito.`when`(memberRepository.findById(35L)).thenReturn(Optional.of(member))
Mockito.`when`(repository.findByMemberId(35L)).thenReturn(preference)
val realCacheManager = ConcurrentMapCacheManager("cache_ttl_3_hours")
val cache = realCacheManager.getCache("cache_ttl_3_hours")!!
val keys = listOf("getRecommendLive:35:false", "getRecommendLive:35:true", "getRecommendLive:35")
keys.forEach { cache.put(it, "cached") }
service = MemberContentPreferenceService(repository, memberRepository, countryContext, realCacheManager)
TransactionSynchronizationManager.initSynchronization()
try {
service.markAdultVisibleAfterAuthVerify(35L, false, ContentType.MALE)
assertFalse(preference.isAdultContentVisible)
assertEquals(ContentType.MALE, preference.contentType)
keys.forEach { assertEquals("cached", cache.get(it)?.get()) }
val synchronizations = TransactionSynchronizationManager.getSynchronizations()
assertEquals(1, synchronizations.size)
if (committed) {
synchronizations.forEach { it.afterCommit() }
}
val status = if (committed) {
TransactionSynchronization.STATUS_COMMITTED
} else {
TransactionSynchronization.STATUS_ROLLED_BACK
}
synchronizations.forEach { it.afterCompletion(status) }
keys.forEach { assertEquals(if (committed) null else "cached", cache.get(it)?.get()) }
} finally {
TransactionSynchronizationManager.clearSynchronization()
}
}
@Test @Test
@DisplayName("기존 row가 있으면 legacy 조회 파라미터를 무시하고 저장값을 그대로 사용한다") @DisplayName("기존 row가 있으면 legacy 조회 파라미터를 무시하고 저장값을 그대로 사용한다")
fun shouldIgnoreLegacyParamsWhenPreferenceAlreadyExists() { fun shouldIgnoreLegacyParamsWhenPreferenceAlreadyExists() {