test #426
@@ -3,6 +3,7 @@ package kr.co.vividnext.sodalive.extensions
|
|||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import java.time.ZoneId
|
import java.time.ZoneId
|
||||||
|
import java.time.ZoneOffset
|
||||||
|
|
||||||
private val DEFAULT_KST_ZONE_ID: ZoneId = ZoneId.of("Asia/Seoul")
|
private val DEFAULT_KST_ZONE_ID: ZoneId = ZoneId.of("Asia/Seoul")
|
||||||
private val UTC_ZONE_ID: ZoneId = ZoneId.of("UTC")
|
private val UTC_ZONE_ID: ZoneId = ZoneId.of("UTC")
|
||||||
@@ -26,3 +27,7 @@ fun LocalDateTime.convertToUtc(timeZone: ZoneId = DEFAULT_KST_ZONE_ID): LocalDat
|
|||||||
.withZoneSameInstant(UTC_ZONE_ID)
|
.withZoneSameInstant(UTC_ZONE_ID)
|
||||||
.toLocalDateTime()
|
.toLocalDateTime()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun LocalDateTime.toUtcIso(): String {
|
||||||
|
return atOffset(ZoneOffset.UTC).toInstant().toString()
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.api.creator.channel.community.application
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.member.Member
|
||||||
|
import kr.co.vividnext.sodalive.v2.api.creator.channel.community.dto.CreatorChannelCommunityTabResponse
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.community.application.CreatorChannelCommunityQueryService
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
import org.springframework.transaction.annotation.Transactional
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
class CreatorChannelCommunityFacade(
|
||||||
|
private val creatorChannelCommunityQueryService: CreatorChannelCommunityQueryService
|
||||||
|
) {
|
||||||
|
fun getCommunityTab(
|
||||||
|
creatorId: Long,
|
||||||
|
viewer: Member,
|
||||||
|
page: Int?,
|
||||||
|
size: Int?,
|
||||||
|
now: LocalDateTime = LocalDateTime.now()
|
||||||
|
): CreatorChannelCommunityTabResponse {
|
||||||
|
return CreatorChannelCommunityTabResponse.from(
|
||||||
|
creatorChannelCommunityQueryService.getCommunityTab(
|
||||||
|
creatorId = creatorId,
|
||||||
|
viewer = viewer,
|
||||||
|
page = page,
|
||||||
|
size = size,
|
||||||
|
now = now
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.api.creator.channel.community.dto
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
import kr.co.vividnext.sodalive.extensions.toUtcIso
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.community.domain.CreatorChannelCommunityPost
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.community.domain.CreatorChannelCommunityTab
|
||||||
|
|
||||||
|
data class CreatorChannelCommunityTabResponse(
|
||||||
|
val communityPostCount: Int,
|
||||||
|
val communityPosts: List<CreatorChannelCommunityPostResponse>,
|
||||||
|
val page: Int,
|
||||||
|
val size: Int,
|
||||||
|
@JsonProperty("hasNext")
|
||||||
|
val hasNext: Boolean
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
fun from(tab: CreatorChannelCommunityTab): CreatorChannelCommunityTabResponse {
|
||||||
|
return CreatorChannelCommunityTabResponse(
|
||||||
|
communityPostCount = tab.communityPostCount,
|
||||||
|
communityPosts = tab.communityPosts.map(CreatorChannelCommunityPostResponse::from),
|
||||||
|
page = tab.page.page,
|
||||||
|
size = tab.page.size,
|
||||||
|
hasNext = tab.hasNext
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class CreatorChannelCommunityPostResponse(
|
||||||
|
val postId: Long,
|
||||||
|
val creatorId: Long,
|
||||||
|
val creatorNickname: String,
|
||||||
|
val creatorProfileUrl: String,
|
||||||
|
val createdAtUtc: String,
|
||||||
|
val content: String,
|
||||||
|
val imageUrl: String?,
|
||||||
|
val audioUrl: String?,
|
||||||
|
val price: Int,
|
||||||
|
@JsonProperty("isCommentAvailable")
|
||||||
|
val isCommentAvailable: Boolean,
|
||||||
|
val existOrdered: Boolean,
|
||||||
|
val likeCount: Int,
|
||||||
|
val commentCount: Int,
|
||||||
|
@JsonProperty("isPinned")
|
||||||
|
val isPinned: Boolean
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
fun from(post: CreatorChannelCommunityPost): CreatorChannelCommunityPostResponse {
|
||||||
|
return CreatorChannelCommunityPostResponse(
|
||||||
|
postId = post.postId,
|
||||||
|
creatorId = post.creatorId,
|
||||||
|
creatorNickname = post.creatorNickname,
|
||||||
|
creatorProfileUrl = post.creatorProfileUrl,
|
||||||
|
createdAtUtc = post.createdAt.toUtcIso(),
|
||||||
|
content = post.content,
|
||||||
|
imageUrl = post.imageUrl,
|
||||||
|
audioUrl = post.audioUrl,
|
||||||
|
price = post.price,
|
||||||
|
isCommentAvailable = post.isCommentAvailable,
|
||||||
|
existOrdered = post.existOrdered,
|
||||||
|
likeCount = post.likeCount,
|
||||||
|
commentCount = post.commentCount,
|
||||||
|
isPinned = post.isPinned
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.api.creator.channel.community.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.community.dto.CreatorChannelCommunityTabResponse
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.community.application.CreatorChannelCommunityQueryService
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.community.domain.CreatorChannelCommunityPost
|
||||||
|
import kr.co.vividnext.sodalive.v2.creator.channel.community.domain.CreatorChannelCommunityTab
|
||||||
|
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.assertNull
|
||||||
|
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 CreatorChannelCommunityFacadeTest {
|
||||||
|
@Test
|
||||||
|
@DisplayName("커뮤니티 탭 응답 DTO는 domain tab 값을 공개 응답 필드로 그대로 매핑한다")
|
||||||
|
fun shouldMapCommunityTabDomainToPublicResponse() {
|
||||||
|
val response = CreatorChannelCommunityTabResponse.from(createTab())
|
||||||
|
|
||||||
|
assertEquals(2, response.communityPostCount)
|
||||||
|
assertEquals(101L, response.communityPosts.first().postId)
|
||||||
|
assertEquals(1L, response.communityPosts.first().creatorId)
|
||||||
|
assertEquals("creator", response.communityPosts.first().creatorNickname)
|
||||||
|
assertEquals("https://cdn.test/profile.png", response.communityPosts.first().creatorProfileUrl)
|
||||||
|
assertEquals("2026-06-21T03:30:00Z", response.communityPosts.first().createdAtUtc)
|
||||||
|
assertEquals("paid content", response.communityPosts.first().content)
|
||||||
|
assertEquals("https://cdn.test/image.png", response.communityPosts.first().imageUrl)
|
||||||
|
assertEquals("https://signed.test/audio", response.communityPosts.first().audioUrl)
|
||||||
|
assertEquals(100, response.communityPosts.first().price)
|
||||||
|
assertTrue(response.communityPosts.first().isCommentAvailable)
|
||||||
|
assertTrue(response.communityPosts.first().existOrdered)
|
||||||
|
assertEquals(7, response.communityPosts.first().likeCount)
|
||||||
|
assertEquals(3, response.communityPosts.first().commentCount)
|
||||||
|
assertTrue(response.communityPosts.first().isPinned)
|
||||||
|
assertNull(response.communityPosts.last().imageUrl)
|
||||||
|
assertNull(response.communityPosts.last().audioUrl)
|
||||||
|
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())
|
||||||
|
assertTrue(json["communityPosts"][0]["isCommentAvailable"].asBoolean())
|
||||||
|
assertTrue(json["communityPosts"][0]["isPinned"].asBoolean())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("커뮤니티 탭 facade는 query service 결과를 공개 응답 DTO로 변환한다")
|
||||||
|
fun shouldMapCommunityTabQueryResultToPublicResponse() {
|
||||||
|
val service = Mockito.mock(CreatorChannelCommunityQueryService::class.java)
|
||||||
|
val facade = CreatorChannelCommunityFacade(service)
|
||||||
|
val viewer = createMember(id = 10L)
|
||||||
|
val now = LocalDateTime.of(2026, 6, 21, 12, 0)
|
||||||
|
Mockito.doReturn(createTab()).`when`(service).getCommunityTab(
|
||||||
|
creatorId = 1L,
|
||||||
|
viewer = viewer,
|
||||||
|
page = -1,
|
||||||
|
size = 100,
|
||||||
|
now = now
|
||||||
|
)
|
||||||
|
|
||||||
|
val response = facade.getCommunityTab(
|
||||||
|
creatorId = 1L,
|
||||||
|
viewer = viewer,
|
||||||
|
page = -1,
|
||||||
|
size = 100,
|
||||||
|
now = now
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(2, response.communityPostCount)
|
||||||
|
assertEquals(101L, response.communityPosts.first().postId)
|
||||||
|
assertEquals("https://cdn.test/profile.png", response.communityPosts.first().creatorProfileUrl)
|
||||||
|
assertTrue(response.communityPosts.first().existOrdered)
|
||||||
|
assertFalse(response.communityPosts.last().isCommentAvailable)
|
||||||
|
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(): CreatorChannelCommunityTab {
|
||||||
|
return CreatorChannelCommunityTab(
|
||||||
|
communityPostCount = 2,
|
||||||
|
communityPosts = listOf(
|
||||||
|
CreatorChannelCommunityPost(
|
||||||
|
postId = 101L,
|
||||||
|
creatorId = 1L,
|
||||||
|
creatorNickname = "creator",
|
||||||
|
creatorProfileUrl = "https://cdn.test/profile.png",
|
||||||
|
imageUrl = "https://cdn.test/image.png",
|
||||||
|
audioUrl = "https://signed.test/audio",
|
||||||
|
content = "paid content",
|
||||||
|
price = 100,
|
||||||
|
createdAt = LocalDateTime.of(2026, 6, 21, 3, 30),
|
||||||
|
existOrdered = true,
|
||||||
|
isCommentAvailable = true,
|
||||||
|
likeCount = 7,
|
||||||
|
commentCount = 3,
|
||||||
|
isPinned = true
|
||||||
|
),
|
||||||
|
CreatorChannelCommunityPost(
|
||||||
|
postId = 102L,
|
||||||
|
creatorId = 1L,
|
||||||
|
creatorNickname = "creator",
|
||||||
|
creatorProfileUrl = "https://cdn.test/profile.png",
|
||||||
|
imageUrl = null,
|
||||||
|
audioUrl = null,
|
||||||
|
content = "masked...",
|
||||||
|
price = 50,
|
||||||
|
createdAt = LocalDateTime.of(2026, 6, 21, 3, 0),
|
||||||
|
existOrdered = false,
|
||||||
|
isCommentAvailable = false,
|
||||||
|
likeCount = 1,
|
||||||
|
commentCount = 0,
|
||||||
|
isPinned = false
|
||||||
|
)
|
||||||
|
),
|
||||||
|
page = CreatorChannelPage(page = 1, size = 20),
|
||||||
|
hasNext = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user