feat(creator-channel): FanTalk 탭 응답 조립을 추가한다
This commit is contained in:
@@ -0,0 +1,32 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.api.creator.channel.fantalk.application
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.member.Member
|
||||||
|
import kr.co.vividnext.sodalive.v2.api.creator.channel.fantalk.dto.CreatorChannelFanTalkTabResponse
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.application.CreatorChannelFanTalkQueryService
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
import org.springframework.transaction.annotation.Transactional
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
class CreatorChannelFanTalkFacade(
|
||||||
|
private val creatorChannelFanTalkQueryService: CreatorChannelFanTalkQueryService
|
||||||
|
) {
|
||||||
|
fun getFanTalkTab(
|
||||||
|
creatorId: Long,
|
||||||
|
viewer: Member,
|
||||||
|
page: Int?,
|
||||||
|
size: Int?,
|
||||||
|
now: LocalDateTime = LocalDateTime.now()
|
||||||
|
): CreatorChannelFanTalkTabResponse {
|
||||||
|
return CreatorChannelFanTalkTabResponse.from(
|
||||||
|
creatorChannelFanTalkQueryService.getFanTalkTab(
|
||||||
|
creatorId = creatorId,
|
||||||
|
viewer = viewer,
|
||||||
|
page = page,
|
||||||
|
size = size,
|
||||||
|
now = now
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.api.creator.channel.fantalk.dto
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
import kr.co.vividnext.sodalive.extensions.toUtcIso
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.domain.CreatorChannelFanTalk
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.domain.CreatorChannelFanTalkReply
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.domain.CreatorChannelFanTalkTab
|
||||||
|
|
||||||
|
data class CreatorChannelFanTalkTabResponse(
|
||||||
|
val fanTalkCount: Int,
|
||||||
|
val fanTalks: List<CreatorChannelFanTalkResponse>,
|
||||||
|
val page: Int,
|
||||||
|
val size: Int,
|
||||||
|
@JsonProperty("hasNext")
|
||||||
|
val hasNext: Boolean
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
fun from(tab: CreatorChannelFanTalkTab): CreatorChannelFanTalkTabResponse {
|
||||||
|
return CreatorChannelFanTalkTabResponse(
|
||||||
|
fanTalkCount = tab.fanTalkCount,
|
||||||
|
fanTalks = tab.fanTalks.map(CreatorChannelFanTalkResponse::from),
|
||||||
|
page = tab.page.page,
|
||||||
|
size = tab.page.size,
|
||||||
|
hasNext = tab.hasNext
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class CreatorChannelFanTalkResponse(
|
||||||
|
val fanTalkId: Long,
|
||||||
|
val writerId: Long,
|
||||||
|
val writerNickname: String,
|
||||||
|
val writerProfileImageUrl: String,
|
||||||
|
val content: String,
|
||||||
|
val createdAtUtc: String,
|
||||||
|
val creatorReplies: List<CreatorChannelFanTalkReplyResponse>
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
fun from(fanTalk: CreatorChannelFanTalk): CreatorChannelFanTalkResponse {
|
||||||
|
return CreatorChannelFanTalkResponse(
|
||||||
|
fanTalkId = fanTalk.fanTalkId,
|
||||||
|
writerId = fanTalk.writerId,
|
||||||
|
writerNickname = fanTalk.writerNickname,
|
||||||
|
writerProfileImageUrl = fanTalk.writerProfileImageUrl,
|
||||||
|
content = fanTalk.content,
|
||||||
|
createdAtUtc = fanTalk.createdAt.toUtcIso(),
|
||||||
|
creatorReplies = fanTalk.creatorReplies.map(CreatorChannelFanTalkReplyResponse::from)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class CreatorChannelFanTalkReplyResponse(
|
||||||
|
val fanTalkId: Long,
|
||||||
|
val writerId: Long,
|
||||||
|
val writerNickname: String,
|
||||||
|
val writerProfileImageUrl: String,
|
||||||
|
val content: String,
|
||||||
|
val createdAtUtc: String
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
fun from(reply: CreatorChannelFanTalkReply): CreatorChannelFanTalkReplyResponse {
|
||||||
|
return CreatorChannelFanTalkReplyResponse(
|
||||||
|
fanTalkId = reply.fanTalkId,
|
||||||
|
writerId = reply.writerId,
|
||||||
|
writerNickname = reply.writerNickname,
|
||||||
|
writerProfileImageUrl = reply.writerProfileImageUrl,
|
||||||
|
content = reply.content,
|
||||||
|
createdAtUtc = reply.createdAt.toUtcIso()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.creator.channel.fantalk.application
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.member.Member
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.domain.CreatorChannelFanTalkTab
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
import org.springframework.transaction.annotation.Transactional
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
class CreatorChannelFanTalkQueryService {
|
||||||
|
fun getFanTalkTab(
|
||||||
|
creatorId: Long,
|
||||||
|
viewer: Member,
|
||||||
|
page: Int?,
|
||||||
|
size: Int?,
|
||||||
|
now: LocalDateTime = LocalDateTime.now()
|
||||||
|
): CreatorChannelFanTalkTab {
|
||||||
|
throw UnsupportedOperationException("CreatorChannelFanTalkQueryService is implemented in Phase 3")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.api.creator.channel.fantalk.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.fantalk.dto.CreatorChannelFanTalkTabResponse
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.application.CreatorChannelFanTalkQueryService
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.domain.CreatorChannelFanTalk
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.domain.CreatorChannelFanTalkReply
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.domain.CreatorChannelFanTalkTab
|
||||||
|
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 CreatorChannelFanTalkFacadeTest {
|
||||||
|
@Test
|
||||||
|
@DisplayName("FanTalk 탭 응답 DTO는 domain tab 값을 공개 응답 필드와 UTC 문자열로 매핑한다")
|
||||||
|
fun shouldMapFanTalkTabDomainToPublicResponse() {
|
||||||
|
val response = CreatorChannelFanTalkTabResponse.from(createTab())
|
||||||
|
|
||||||
|
assertEquals(2, response.fanTalkCount)
|
||||||
|
assertEquals(101L, response.fanTalks.first().fanTalkId)
|
||||||
|
assertEquals(10L, response.fanTalks.first().writerId)
|
||||||
|
assertEquals("fan", response.fanTalks.first().writerNickname)
|
||||||
|
assertEquals("https://cdn.test/fan.png", response.fanTalks.first().writerProfileImageUrl)
|
||||||
|
assertEquals("fan talk", response.fanTalks.first().content)
|
||||||
|
assertEquals("2026-06-21T03:30:00Z", response.fanTalks.first().createdAtUtc)
|
||||||
|
assertEquals(201L, response.fanTalks.first().creatorReplies.first().fanTalkId)
|
||||||
|
assertEquals(1L, response.fanTalks.first().creatorReplies.first().writerId)
|
||||||
|
assertEquals("creator", response.fanTalks.first().creatorReplies.first().writerNickname)
|
||||||
|
assertEquals("https://cdn.test/creator.png", response.fanTalks.first().creatorReplies.first().writerProfileImageUrl)
|
||||||
|
assertEquals("creator reply", response.fanTalks.first().creatorReplies.first().content)
|
||||||
|
assertEquals("2026-06-21T03:35:00Z", response.fanTalks.first().creatorReplies.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))
|
||||||
|
assertTrue(json["hasNext"].asBoolean())
|
||||||
|
assertFalse(json.has("languageCode"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("FanTalk 탭 facade는 query service 결과를 공개 응답 DTO로 변환한다")
|
||||||
|
fun shouldMapFanTalkTabQueryResultToPublicResponse() {
|
||||||
|
val service = Mockito.mock(CreatorChannelFanTalkQueryService::class.java)
|
||||||
|
val facade = CreatorChannelFanTalkFacade(service)
|
||||||
|
val viewer = createMember(id = 10L)
|
||||||
|
val now = LocalDateTime.of(2026, 6, 21, 12, 0)
|
||||||
|
Mockito.doReturn(createTab()).`when`(service).getFanTalkTab(
|
||||||
|
creatorId = 1L,
|
||||||
|
viewer = viewer,
|
||||||
|
page = -1,
|
||||||
|
size = 100,
|
||||||
|
now = now
|
||||||
|
)
|
||||||
|
|
||||||
|
val response = facade.getFanTalkTab(
|
||||||
|
creatorId = 1L,
|
||||||
|
viewer = viewer,
|
||||||
|
page = -1,
|
||||||
|
size = 100,
|
||||||
|
now = now
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(2, response.fanTalkCount)
|
||||||
|
assertEquals(101L, response.fanTalks.first().fanTalkId)
|
||||||
|
assertEquals("https://cdn.test/fan.png", response.fanTalks.first().writerProfileImageUrl)
|
||||||
|
assertEquals(201L, response.fanTalks.first().creatorReplies.first().fanTalkId)
|
||||||
|
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(): CreatorChannelFanTalkTab {
|
||||||
|
return CreatorChannelFanTalkTab(
|
||||||
|
fanTalkCount = 2,
|
||||||
|
fanTalks = listOf(
|
||||||
|
CreatorChannelFanTalk(
|
||||||
|
fanTalkId = 101L,
|
||||||
|
writerId = 10L,
|
||||||
|
writerNickname = "fan",
|
||||||
|
writerProfileImageUrl = "https://cdn.test/fan.png",
|
||||||
|
content = "fan talk",
|
||||||
|
createdAt = LocalDateTime.of(2026, 6, 21, 3, 30),
|
||||||
|
creatorReplies = listOf(
|
||||||
|
CreatorChannelFanTalkReply(
|
||||||
|
fanTalkId = 201L,
|
||||||
|
writerId = 1L,
|
||||||
|
writerNickname = "creator",
|
||||||
|
writerProfileImageUrl = "https://cdn.test/creator.png",
|
||||||
|
content = "creator reply",
|
||||||
|
createdAt = LocalDateTime.of(2026, 6, 21, 3, 35)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
page = CreatorChannelPage(page = 1, size = 20),
|
||||||
|
hasNext = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user