From 14f648cd10425a5db8d6453a5ddbc0d7b63b91ba Mon Sep 17 00:00:00 2001 From: Klaus Date: Mon, 22 Jun 2026 17:59:41 +0900 Subject: [PATCH] =?UTF-8?q?feat(creator-channel):=20=ED=9B=84=EC=9B=90=20?= =?UTF-8?q?=ED=83=AD=20=EC=9D=91=EB=8B=B5=20=EC=A1=B0=EB=A6=BD=EC=9D=84=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CreatorChannelDonationFacade.kt | 32 +++++ .../dto/CreatorChannelDonationTabResponse.kt | 68 ++++++++++ .../CreatorChannelDonationFacadeTest.kt | 120 ++++++++++++++++++ 3 files changed, 220 insertions(+) create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/donation/application/CreatorChannelDonationFacade.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/donation/dto/CreatorChannelDonationTabResponse.kt create mode 100644 src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/donation/application/CreatorChannelDonationFacadeTest.kt diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/donation/application/CreatorChannelDonationFacade.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/donation/application/CreatorChannelDonationFacade.kt new file mode 100644 index 00000000..13a96348 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/donation/application/CreatorChannelDonationFacade.kt @@ -0,0 +1,32 @@ +package kr.co.vividnext.sodalive.v2.api.creator.channel.donation.application + +import kr.co.vividnext.sodalive.member.Member +import kr.co.vividnext.sodalive.v2.api.creator.channel.donation.dto.CreatorChannelDonationTabResponse +import kr.co.vividnext.sodalive.v2.creator.channel.donation.application.CreatorChannelDonationQueryService +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.time.LocalDateTime + +@Service +@Transactional(readOnly = true) +class CreatorChannelDonationFacade( + private val creatorChannelDonationQueryService: CreatorChannelDonationQueryService +) { + fun getDonationTab( + creatorId: Long, + viewer: Member, + page: Int?, + size: Int?, + now: LocalDateTime = LocalDateTime.now() + ): CreatorChannelDonationTabResponse { + return CreatorChannelDonationTabResponse.from( + creatorChannelDonationQueryService.getDonationTab( + creatorId = creatorId, + viewer = viewer, + page = page, + size = size, + now = now + ) + ) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/donation/dto/CreatorChannelDonationTabResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/donation/dto/CreatorChannelDonationTabResponse.kt new file mode 100644 index 00000000..bf0a535d --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/donation/dto/CreatorChannelDonationTabResponse.kt @@ -0,0 +1,68 @@ +package kr.co.vividnext.sodalive.v2.api.creator.channel.donation.dto + +import com.fasterxml.jackson.annotation.JsonProperty +import kr.co.vividnext.sodalive.extensions.toUtcIso +import kr.co.vividnext.sodalive.v2.creator.channel.donation.domain.CreatorChannelDonation +import kr.co.vividnext.sodalive.v2.creator.channel.donation.domain.CreatorChannelDonationRanking +import kr.co.vividnext.sodalive.v2.creator.channel.donation.domain.CreatorChannelDonationTab + +data class CreatorChannelDonationTabResponse( + val donationCount: Int, + val rankings: List, + val donations: List, + val page: Int, + val size: Int, + @JsonProperty("hasNext") + val hasNext: Boolean +) { + companion object { + fun from(tab: CreatorChannelDonationTab): CreatorChannelDonationTabResponse { + return CreatorChannelDonationTabResponse( + donationCount = tab.donationCount, + rankings = tab.rankings.map(MemberDonationRankingResponse::from), + donations = tab.donations.map(CreatorChannelDonationResponse::from), + page = tab.page.page, + size = tab.page.size, + hasNext = tab.hasNext + ) + } + } +} + +data class MemberDonationRankingResponse( + @JsonProperty("userId") val userId: Long, + @JsonProperty("nickname") val nickname: String, + @JsonProperty("profileImage") val profileImage: String, + @JsonProperty("donationCan") val donationCan: Int +) { + companion object { + fun from(ranking: CreatorChannelDonationRanking): MemberDonationRankingResponse { + return MemberDonationRankingResponse( + userId = ranking.userId, + nickname = ranking.nickname, + profileImage = ranking.profileImage, + donationCan = ranking.donationCan + ) + } + } +} + +data class CreatorChannelDonationResponse( + val nickname: String, + val profileImageUrl: String, + val can: Int, + val message: String, + val createdAtUtc: String +) { + companion object { + fun from(donation: CreatorChannelDonation): CreatorChannelDonationResponse { + return CreatorChannelDonationResponse( + nickname = donation.nickname, + profileImageUrl = donation.profileImageUrl, + can = donation.can, + message = donation.message, + createdAtUtc = donation.createdAt.toUtcIso() + ) + } + } +} diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/donation/application/CreatorChannelDonationFacadeTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/donation/application/CreatorChannelDonationFacadeTest.kt new file mode 100644 index 00000000..d04fe573 --- /dev/null +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/donation/application/CreatorChannelDonationFacadeTest.kt @@ -0,0 +1,120 @@ +package kr.co.vividnext.sodalive.v2.api.creator.channel.donation.application + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.module.kotlin.KotlinModule +import kr.co.vividnext.sodalive.member.Member +import kr.co.vividnext.sodalive.member.MemberRole +import kr.co.vividnext.sodalive.v2.api.creator.channel.donation.dto.CreatorChannelDonationTabResponse +import kr.co.vividnext.sodalive.v2.creator.channel.donation.application.CreatorChannelDonationQueryService +import kr.co.vividnext.sodalive.v2.creator.channel.donation.domain.CreatorChannelDonation +import kr.co.vividnext.sodalive.v2.creator.channel.donation.domain.CreatorChannelDonationRanking +import kr.co.vividnext.sodalive.v2.creator.channel.donation.domain.CreatorChannelDonationTab +import kr.co.vividnext.sodalive.v2.creator.channel.live.domain.CreatorChannelPage +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.mockito.Mockito +import java.time.LocalDateTime + +class CreatorChannelDonationFacadeTest { + @Test + @DisplayName("후원 탭 응답 DTO는 domain tab 값을 공개 응답 필드와 UTC 문자열로 매핑한다") + fun shouldMapDonationTabDomainToPublicResponse() { + val response = CreatorChannelDonationTabResponse.from(createTab()) + + assertEquals(3, response.donationCount) + assertEquals(10L, response.rankings.first().userId) + assertEquals("fan", response.rankings.first().nickname) + assertEquals("https://cdn.test/fan.png", response.rankings.first().profileImage) + assertEquals(100, response.rankings.first().donationCan) + assertEquals("donor", response.donations.first().nickname) + assertEquals("https://cdn.test/donor.png", response.donations.first().profileImageUrl) + assertEquals(50, response.donations.first().can) + assertEquals("thanks", response.donations.first().message) + assertEquals("2026-06-21T03:30:00Z", response.donations.first().createdAtUtc) + assertEquals(1, response.page) + assertEquals(20, response.size) + assertTrue(response.hasNext) + + val mapper = ObjectMapper().registerModule(KotlinModule.Builder().build()) + val json = mapper.readTree(mapper.writeValueAsString(response)) + assertEquals(10L, json["rankings"][0]["userId"].asLong()) + assertEquals("fan", json["rankings"][0]["nickname"].asText()) + assertEquals("https://cdn.test/fan.png", json["rankings"][0]["profileImage"].asText()) + assertEquals(100, json["rankings"][0]["donationCan"].asInt()) + assertEquals("donor", json["donations"][0]["nickname"].asText()) + assertEquals("https://cdn.test/donor.png", json["donations"][0]["profileImageUrl"].asText()) + assertEquals(50, json["donations"][0]["can"].asInt()) + assertEquals("thanks", json["donations"][0]["message"].asText()) + assertEquals("2026-06-21T03:30:00Z", json["donations"][0]["createdAtUtc"].asText()) + assertTrue(json["hasNext"].asBoolean()) + assertFalse(json.has("languageCode")) + } + + @Test + @DisplayName("후원 탭 facade는 query service 결과를 공개 응답 DTO로 변환한다") + fun shouldMapDonationTabQueryResultToPublicResponse() { + val service = Mockito.mock(CreatorChannelDonationQueryService::class.java) + val facade = CreatorChannelDonationFacade(service) + val viewer = createMember(id = 10L) + val now = LocalDateTime.of(2026, 6, 21, 12, 0) + Mockito.doReturn(createTab()).`when`(service).getDonationTab( + creatorId = 1L, + viewer = viewer, + page = -1, + size = 100, + now = now + ) + + val response = facade.getDonationTab( + creatorId = 1L, + viewer = viewer, + page = -1, + size = 100, + now = now + ) + + assertEquals(3, response.donationCount) + assertEquals(10L, response.rankings.first().userId) + assertEquals("https://cdn.test/donor.png", response.donations.first().profileImageUrl) + assertEquals(1, response.page) + assertEquals(20, response.size) + assertTrue(response.hasNext) + } + + private fun createMember(id: Long): Member { + return Member( + email = "viewer$id@test.com", + password = "password", + nickname = "viewer$id", + role = MemberRole.USER + ).apply { this.id = id } + } + + private fun createTab(): CreatorChannelDonationTab { + return CreatorChannelDonationTab( + donationCount = 3, + rankings = listOf( + CreatorChannelDonationRanking( + userId = 10L, + nickname = "fan", + profileImage = "https://cdn.test/fan.png", + donationCan = 100 + ) + ), + donations = listOf( + CreatorChannelDonation( + nickname = "donor", + profileImageUrl = "https://cdn.test/donor.png", + can = 50, + message = "thanks", + createdAt = LocalDateTime.of(2026, 6, 21, 3, 30) + ) + ), + page = CreatorChannelPage(page = 1, size = 20), + hasNext = true + ) + } +}