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")
|
@Column(columnDefinition = "TEXT")
|
||||||
var additionalMessage: String? = null
|
var additionalMessage: String? = null
|
||||||
) : BaseEntity() {
|
) : BaseEntity() {
|
||||||
|
@Column(nullable = false)
|
||||||
|
var isHidden: Boolean = false
|
||||||
|
|
||||||
@ManyToOne(fetch = FetchType.LAZY)
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
@JoinColumn(name = "member_id", nullable = false)
|
@JoinColumn(name = "member_id", nullable = false)
|
||||||
var member: Member? = null
|
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