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
fun markAdultVisibleAfterAuthVerify(memberId: Long) {
fun markAdultVisibleAfterAuthVerify(
memberId: Long,
isAdultContentVisible: Boolean = true,
contentType: ContentType = ContentType.ALL
) {
val member = memberRepository.findByIdOrNull(memberId)
?: throw SodaException(messageKey = "common.error.bad_credentials")
val preference = initializeDefaultPreference(member)
if (!preference.isAdultContentVisible) {
preference.isAdultContentVisible = true
preference.adultContentVisibilityChangedAt = LocalDateTime.now()
val now = LocalDateTime.now()
var hasChanged = false
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)
}
}
@@ -14,6 +14,8 @@ 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.params.ParameterizedTest
import org.junit.jupiter.params.provider.CsvSource
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
import org.springframework.cache.concurrent.ConcurrentMapCacheManager
@@ -166,16 +168,50 @@ class MemberContentPreferenceIntegrationTest @Autowired constructor(
}
@Test
@DisplayName("authVerify 성공 후 markAdultVisibleAfterAuthVerify를 호출하면 저장값이 true로 반영된다")
@DisplayName("인증 후 입력이 없으면 기존 false/FEMALE 행이 true/ALL로 저장된다")
fun shouldMarkAdultVisibleAfterAuthVerify() {
val member = saveNonForcedMember("auth-verified-user")
countryContext.setCountryCode("US")
service.updatePreference(member, isAdultContentVisible = false, contentType = ContentType.ALL)
service.updatePreference(member, isAdultContentVisible = false, contentType = ContentType.FEMALE)
service.markAdultVisibleAfterAuthVerify(member.id!!)
entityManager.flush()
entityManager.clear()
val stored = service.getStoredPreference(member)
val stored = preferenceRepository.findByMemberId(member.id!!)!!
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
@@ -13,10 +13,16 @@ 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.params.ParameterizedTest
import org.junit.jupiter.params.provider.CsvSource
import org.junit.jupiter.params.provider.ValueSource
import org.mockito.Mockito
import org.springframework.cache.Cache
import org.springframework.cache.CacheManager
import org.springframework.cache.concurrent.ConcurrentMapCacheManager
import org.springframework.dao.DataIntegrityViolationException
import org.springframework.transaction.support.TransactionSynchronization
import org.springframework.transaction.support.TransactionSynchronizationManager
import java.time.LocalDateTime
import java.util.Optional
@@ -405,6 +411,108 @@ class MemberContentPreferenceServiceTest {
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
@DisplayName("기존 row가 있으면 legacy 조회 파라미터를 무시하고 저장값을 그대로 사용한다")
fun shouldIgnoreLegacyParamsWhenPreferenceAlreadyExists() {