feat(channel-donation): 원본을 보존하는 후원 숨김 저장 모델을 추가한다
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE channel_donation_message
|
||||
ADD COLUMN is_hidden TINYINT(1) NOT NULL DEFAULT 0 COMMENT '채널 후원 표시 숨김 여부';
|
||||
+3
@@ -15,6 +15,9 @@ class ChannelDonationMessage(
|
||||
@Column(columnDefinition = "TEXT")
|
||||
var additionalMessage: String? = null
|
||||
) : BaseEntity() {
|
||||
@Column(nullable = false)
|
||||
var isHidden: Boolean = false
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "member_id", nullable = false)
|
||||
var member: Member? = null
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package kr.co.vividnext.sodalive.v2.creator.channel.donation.adapter.out.persistence
|
||||
|
||||
import com.querydsl.jpa.impl.JPAQueryFactory
|
||||
import kr.co.vividnext.sodalive.explorer.profile.channelDonation.QChannelDonationMessage.channelDonationMessage
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.donation.port.out.HideCreatorChannelDonationPort
|
||||
import org.springframework.stereotype.Repository
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
|
||||
@Repository
|
||||
class DefaultHideCreatorChannelDonationRepository(
|
||||
private val queryFactory: JPAQueryFactory
|
||||
) : HideCreatorChannelDonationPort {
|
||||
@Transactional
|
||||
override fun hideOwnedDonation(creatorId: Long, donationId: Long): Boolean {
|
||||
val ownedDonation = channelDonationMessage.id.eq(donationId)
|
||||
.and(channelDonationMessage.creator.id.eq(creatorId))
|
||||
val updatedCount = queryFactory
|
||||
.update(channelDonationMessage)
|
||||
.set(channelDonationMessage.isHidden, true)
|
||||
.set(channelDonationMessage.updatedAt, channelDonationMessage.updatedAt)
|
||||
.where(ownedDonation, channelDonationMessage.isHidden.isFalse)
|
||||
.execute()
|
||||
|
||||
return updatedCount > 0 || queryFactory
|
||||
.selectOne()
|
||||
.from(channelDonationMessage)
|
||||
.where(ownedDonation)
|
||||
.fetchFirst() != null
|
||||
}
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
package kr.co.vividnext.sodalive.v2.creator.channel.donation.port.out
|
||||
|
||||
interface HideCreatorChannelDonationPort {
|
||||
fun hideOwnedDonation(creatorId: Long, donationId: Long): Boolean
|
||||
}
|
||||
+200
@@ -0,0 +1,200 @@
|
||||
package kr.co.vividnext.sodalive.v2.creator.channel.donation.adapter.out.persistence
|
||||
|
||||
import kr.co.vividnext.sodalive.configs.QueryDslConfig
|
||||
import kr.co.vividnext.sodalive.explorer.profile.channelDonation.ChannelDonationMessage
|
||||
import kr.co.vividnext.sodalive.member.Member
|
||||
import kr.co.vividnext.sodalive.member.MemberRole
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.donation.port.out.HideCreatorChannelDonationPort
|
||||
import org.hibernate.resource.jdbc.spi.StatementInspector
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
import org.junit.jupiter.params.provider.ValueSource
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.autoconfigure.orm.jpa.HibernatePropertiesCustomizer
|
||||
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
|
||||
import org.springframework.boot.test.context.TestConfiguration
|
||||
import org.springframework.context.annotation.Import
|
||||
import java.time.LocalDateTime
|
||||
import javax.persistence.EntityManager
|
||||
|
||||
@DataJpaTest(properties = ["spring.cache.type=none"])
|
||||
@Import(
|
||||
QueryDslConfig::class,
|
||||
DefaultHideCreatorChannelDonationRepository::class,
|
||||
DefaultHideCreatorChannelDonationRepositoryTest.SqlCaptureConfiguration::class
|
||||
)
|
||||
class DefaultHideCreatorChannelDonationRepositoryTest @Autowired constructor(
|
||||
private val entityManager: EntityManager,
|
||||
private val repository: HideCreatorChannelDonationPort,
|
||||
private val sqlCapture: SqlCaptureConfiguration
|
||||
) {
|
||||
@Test
|
||||
@DisplayName("새 후원은 기본 비숨김 상태로 저장된다")
|
||||
fun shouldPersistDonationAsVisibleByDefault() {
|
||||
val creator = saveMember("hide-default-creator", MemberRole.CREATOR)
|
||||
val donor = saveMember("hide-default-donor", MemberRole.USER)
|
||||
val donationId = saveDonation(creator, donor)
|
||||
|
||||
val donation = reloadDonation(donationId)
|
||||
|
||||
assertFalse(donation.isHidden)
|
||||
assertOriginalValues(donation, donationId, creator.id!!, donor.id!!, false)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("숨김 UPDATE는 MySQL 자동 갱신을 막도록 수정 시각을 자기 대입한다")
|
||||
fun shouldPreserveUpdatedAtWithSelfAssignmentInGeneratedHideSql() {
|
||||
val creator = saveMember("hide-sql-creator", MemberRole.CREATOR)
|
||||
val donor = saveMember("hide-sql-donor", MemberRole.USER)
|
||||
val donationId = saveDonation(creator, donor)
|
||||
val updatedAt = requireNotNull(reloadDonation(donationId).updatedAt)
|
||||
sqlCapture.statements.clear()
|
||||
|
||||
assertTrue(repository.hideOwnedDonation(creator.id!!, donationId))
|
||||
|
||||
val updateSql = sqlCapture.statements.single { it.startsWith("update channel_donation_message ", ignoreCase = true) }
|
||||
assertTrue(Regex("""(?i)\bupdated_at\s*=\s*(?:\w+\.)?updated_at\b""").containsMatchIn(updateSql), updateSql)
|
||||
val donation = reloadDonation(donationId)
|
||||
assertTrue(donation.isHidden)
|
||||
assertEquals(updatedAt, donation.updatedAt)
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(booleans = [false, true])
|
||||
@DisplayName("소유 채널의 선택한 공개·비밀 후원 한 건만 숨기고 원본 값을 보존한다")
|
||||
fun shouldHideOnlyDonationOwnedByCreatorAndRetainOriginalValues(isSecret: Boolean) {
|
||||
val creator = saveMember("hide-owned-creator", MemberRole.CREATOR)
|
||||
val otherCreator = saveMember("hide-other-creator", MemberRole.CREATOR)
|
||||
val donor = saveMember("hide-owned-donor", MemberRole.USER)
|
||||
val donationId = saveDonation(creator, donor, isSecret)
|
||||
val sameChannelDonationId = saveDonation(creator, donor)
|
||||
val otherChannelDonationId = saveDonation(otherCreator, donor)
|
||||
assertFalse(reloadDonation(donationId).isHidden)
|
||||
|
||||
assertTrue(repository.hideOwnedDonation(creator.id!!, donationId))
|
||||
|
||||
val donation = reloadDonation(donationId)
|
||||
assertTrue(donation.isHidden)
|
||||
assertOriginalValues(donation, donationId, creator.id!!, donor.id!!, isSecret)
|
||||
val sameChannelDonation = reloadDonation(sameChannelDonationId)
|
||||
assertFalse(sameChannelDonation.isHidden)
|
||||
assertOriginalValues(sameChannelDonation, sameChannelDonationId, creator.id!!, donor.id!!, false)
|
||||
val otherChannelDonation = reloadDonation(otherChannelDonationId)
|
||||
assertFalse(otherChannelDonation.isHidden)
|
||||
assertOriginalValues(otherChannelDonation, otherChannelDonationId, otherCreator.id!!, donor.id!!, false)
|
||||
assertEquals(3L, donationCount())
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("다른 채널의 후원은 숨기지 않고 실패를 반환한다")
|
||||
fun shouldRejectDonationFromAnotherCreatorWithoutChangingIt() {
|
||||
val creator = saveMember("hide-reject-creator", MemberRole.CREATOR)
|
||||
val otherCreator = saveMember("hide-reject-other", MemberRole.CREATOR)
|
||||
val donor = saveMember("hide-reject-donor", MemberRole.USER)
|
||||
val donationId = saveDonation(creator, donor, isSecret = true)
|
||||
reloadDonation(donationId)
|
||||
|
||||
assertFalse(repository.hideOwnedDonation(otherCreator.id!!, donationId))
|
||||
|
||||
val donation = reloadDonation(donationId)
|
||||
assertFalse(donation.isHidden)
|
||||
assertOriginalValues(donation, donationId, creator.id!!, donor.id!!, true)
|
||||
assertEquals(1L, donationCount())
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("존재하지 않는 후원 ID는 기존 후원을 바꾸지 않고 실패를 반환한다")
|
||||
fun shouldRejectMissingDonationWithoutChangingExistingDonation() {
|
||||
val creator = saveMember("hide-missing-creator", MemberRole.CREATOR)
|
||||
val donor = saveMember("hide-missing-donor", MemberRole.USER)
|
||||
val donationId = saveDonation(creator, donor)
|
||||
reloadDonation(donationId)
|
||||
|
||||
assertFalse(repository.hideOwnedDonation(creator.id!!, Long.MAX_VALUE))
|
||||
|
||||
val donation = reloadDonation(donationId)
|
||||
assertFalse(donation.isHidden)
|
||||
assertOriginalValues(donation, donationId, creator.id!!, donor.id!!, false)
|
||||
assertEquals(1L, donationCount())
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("같은 후원의 반복 숨김은 성공하며 다른 채널 요청은 거부하고 원본 값을 보존한다")
|
||||
fun shouldSucceedWhenHidingAgainWithoutChangingOriginalValues() {
|
||||
val creator = saveMember("hide-repeat-creator", MemberRole.CREATOR)
|
||||
val otherCreator = saveMember("hide-repeat-other", MemberRole.CREATOR)
|
||||
val donor = saveMember("hide-repeat-donor", MemberRole.USER)
|
||||
val donationId = saveDonation(creator, donor, isSecret = true)
|
||||
reloadDonation(donationId)
|
||||
|
||||
assertTrue(repository.hideOwnedDonation(creator.id!!, donationId))
|
||||
assertTrue(reloadDonation(donationId).isHidden)
|
||||
assertTrue(repository.hideOwnedDonation(creator.id!!, donationId))
|
||||
assertFalse(repository.hideOwnedDonation(otherCreator.id!!, donationId))
|
||||
|
||||
val donation = reloadDonation(donationId)
|
||||
assertTrue(donation.isHidden)
|
||||
assertOriginalValues(donation, donationId, creator.id!!, donor.id!!, true)
|
||||
assertEquals(1L, donationCount())
|
||||
}
|
||||
|
||||
private fun saveMember(nickname: String, role: MemberRole): Member {
|
||||
val member = Member(email = "$nickname@test.com", password = "password", nickname = nickname, role = role)
|
||||
entityManager.persist(member)
|
||||
return member
|
||||
}
|
||||
|
||||
private fun saveDonation(creator: Member, donor: Member, isSecret: Boolean = false): Long {
|
||||
val donation = ChannelDonationMessage(can = 120, isSecret = isSecret, additionalMessage = "original message")
|
||||
donation.creator = creator
|
||||
donation.member = donor
|
||||
entityManager.persist(donation)
|
||||
donation.createdAt = LocalDateTime.of(2026, 11, 5, 0, 0)
|
||||
return requireNotNull(donation.id)
|
||||
}
|
||||
|
||||
private fun reloadDonation(donationId: Long): ChannelDonationMessage {
|
||||
entityManager.flush()
|
||||
entityManager.clear()
|
||||
return requireNotNull(entityManager.find(ChannelDonationMessage::class.java, donationId))
|
||||
}
|
||||
|
||||
private fun assertOriginalValues(
|
||||
donation: ChannelDonationMessage,
|
||||
donationId: Long,
|
||||
creatorId: Long,
|
||||
donorId: Long,
|
||||
isSecret: Boolean
|
||||
) {
|
||||
assertEquals(donationId, donation.id)
|
||||
assertEquals(120, donation.can)
|
||||
assertEquals("original message", donation.additionalMessage)
|
||||
assertEquals(isSecret, donation.isSecret)
|
||||
assertEquals(creatorId, donation.creator?.id)
|
||||
assertEquals(donorId, donation.member?.id)
|
||||
assertEquals(LocalDateTime.of(2026, 11, 5, 0, 0), donation.createdAt)
|
||||
}
|
||||
|
||||
private fun donationCount(): Long {
|
||||
return entityManager.createQuery("select count(d) from ChannelDonationMessage d", java.lang.Long::class.java)
|
||||
.singleResult.toLong()
|
||||
}
|
||||
|
||||
@TestConfiguration
|
||||
class SqlCaptureConfiguration : HibernatePropertiesCustomizer, StatementInspector {
|
||||
val statements = mutableListOf<String>()
|
||||
|
||||
override fun customize(hibernateProperties: MutableMap<String, Any>) {
|
||||
hibernateProperties["hibernate.session_factory.statement_inspector"] = this
|
||||
}
|
||||
|
||||
override fun inspect(sql: String): String {
|
||||
statements.add(sql)
|
||||
return sql
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user