feat(community): 게시물 댓글 조회 서비스를 추가한다
This commit is contained in:
+202
-2
@@ -10,6 +10,8 @@ import kr.co.vividnext.sodalive.member.MemberProvider
|
||||
import kr.co.vividnext.sodalive.member.MemberRole
|
||||
import kr.co.vividnext.sodalive.member.contentpreference.MemberContentPreferenceService
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.domain.CreatorChannelCommunityQueryPolicy
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.port.out.CreatorChannelCommunityCommentContextRecord
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.port.out.CreatorChannelCommunityCommentRecord
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.port.out.CreatorChannelCommunityCreatorRecord
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.port.out.CreatorChannelCommunityPostRecord
|
||||
import kr.co.vividnext.sodalive.v2.creator.channel.community.port.out.CreatorChannelCommunityQueryPort
|
||||
@@ -173,6 +175,119 @@ class CreatorChannelCommunityQueryServiceTest {
|
||||
assertEquals(true, posts.single().isLiked)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("커뮤니티 상세는 게시글과 초기 댓글 및 최신 답글을 조립한다")
|
||||
fun shouldAssembleCommunityPostDetailWithInitialComments() {
|
||||
val port = FakeCreatorChannelCommunityQueryPort().apply {
|
||||
communityPost = communityPostRecord(1L, price = 0, isLiked = true)
|
||||
communityCommentCount = 21
|
||||
communityComments = (1L..21L).map { communityCommentRecord(it) }
|
||||
latestReplies = listOf(communityCommentRecord(100L, parentCommentId = 1L, writerNickname = "deleted_reply-writer"))
|
||||
}
|
||||
val service = createService(port)
|
||||
val viewer = createMember(id = 10L)
|
||||
|
||||
val detail = service.getCommunityPostDetail(1L, viewer, LocalDateTime.of(2026, 7, 6, 10, 0))
|
||||
|
||||
assertEquals(1L, detail.post.postId)
|
||||
assertEquals(true, detail.post.isLiked)
|
||||
assertEquals(21, detail.comments.commentCount)
|
||||
assertEquals(20, detail.comments.comments.size)
|
||||
assertEquals(true, detail.comments.hasNext)
|
||||
assertEquals("reply-writer", detail.comments.comments.first().latestReply?.writerNickname)
|
||||
assertEquals("https://cdn.test/profile/default-profile.png", detail.comments.comments.first().writerProfileImageUrl)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("댓글 불가 게시글은 댓글 port를 호출하지 않고 빈 댓글 응답을 반환한다")
|
||||
fun shouldReturnEmptyCommentsWhenCommentUnavailable() {
|
||||
val port = FakeCreatorChannelCommunityQueryPort().apply {
|
||||
communityPost = communityPostRecord(1L, price = 0, isCommentAvailable = false)
|
||||
}
|
||||
val service = createService(port)
|
||||
|
||||
val comments = service.getCommunityComments(1L, createMember(id = 10L), -1, 100, LocalDateTime.of(2026, 7, 6, 10, 0))
|
||||
|
||||
assertEquals(0, comments.commentCount)
|
||||
assertEquals(0, comments.comments.size)
|
||||
assertEquals(0, comments.page.page)
|
||||
assertEquals(50, comments.page.size)
|
||||
assertEquals(false, comments.hasNext)
|
||||
assertEquals(false, port.commentListCalled)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("커뮤니티 댓글은 page fallback과 최신 답글을 적용한다")
|
||||
fun shouldApplyCommentPageFallbackAndLatestReply() {
|
||||
val port = FakeCreatorChannelCommunityQueryPort().apply {
|
||||
communityPost = communityPostRecord(1L, price = 0)
|
||||
communityCommentCount = 60
|
||||
communityComments = (1L..51L).map { communityCommentRecord(it, writerProfilePath = "profile/$it.png") }
|
||||
latestReplies = listOf(communityCommentRecord(100L, parentCommentId = 2L))
|
||||
}
|
||||
val service = createService(port)
|
||||
|
||||
val comments = service.getCommunityComments(1L, createMember(id = 10L), -1, 100, LocalDateTime.of(2026, 7, 6, 10, 0))
|
||||
|
||||
assertEquals(0L, port.commentListOffset)
|
||||
assertEquals(51, port.commentListLimit)
|
||||
assertEquals((1L..50L).toList(), port.latestReplyCommentIds)
|
||||
assertEquals(50, comments.comments.size)
|
||||
assertEquals(true, comments.hasNext)
|
||||
assertEquals("https://cdn.test/profile/1.png", comments.comments.first().writerProfileImageUrl)
|
||||
assertEquals(100L, comments.comments[1].latestReply?.commentId)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("커뮤니티 답글은 부모 댓글 context를 확인하고 page fallback으로 조립한다")
|
||||
fun shouldAssembleCommunityReplies() {
|
||||
val port = FakeCreatorChannelCommunityQueryPort().apply {
|
||||
rootCommentContext = CreatorChannelCommunityCommentContextRecord(1L, 10L, 1L, true)
|
||||
communityReplyCount = 51
|
||||
communityReplies = (1L..51L).map { communityCommentRecord(it, parentCommentId = 1L) }
|
||||
}
|
||||
val service = createService(port)
|
||||
|
||||
val replies = service.getCommunityReplies(1L, createMember(id = 10L), -1, 100, LocalDateTime.of(2026, 7, 6, 10, 0))
|
||||
|
||||
assertEquals(51, replies.replyCount)
|
||||
assertEquals(50, replies.replies.size)
|
||||
assertEquals(0L, port.replyListOffset)
|
||||
assertEquals(51, port.replyListLimit)
|
||||
assertEquals(true, replies.hasNext)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("부모 댓글 context가 보이지 않으면 커뮤니티 답글 조회는 예외를 던진다")
|
||||
fun shouldThrowWhenRootCommentContextNotVisible() {
|
||||
val port = FakeCreatorChannelCommunityQueryPort().apply { rootCommentContext = null }
|
||||
val service = createService(port)
|
||||
|
||||
val exception = assertThrows(SodaException::class.java) {
|
||||
service.getCommunityReplies(1L, createMember(id = 10L), null, null, LocalDateTime.of(2026, 7, 6, 10, 0))
|
||||
}
|
||||
|
||||
assertEquals("creator.community.invalid_request_retry", exception.messageKey)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("댓글 불가 게시글의 답글 직접 조회는 빈 응답을 반환한다")
|
||||
fun shouldReturnEmptyRepliesWhenCommentUnavailable() {
|
||||
val port = FakeCreatorChannelCommunityQueryPort().apply {
|
||||
rootCommentContext = CreatorChannelCommunityCommentContextRecord(1L, 10L, 1L, false)
|
||||
communityReplyCount = 1
|
||||
communityReplies = listOf(communityCommentRecord(2L, parentCommentId = 1L))
|
||||
}
|
||||
val service = createService(port)
|
||||
|
||||
val replies = service.getCommunityReplies(1L, createMember(id = 10L), null, null, LocalDateTime.of(2026, 7, 6, 10, 0))
|
||||
|
||||
assertEquals(0, replies.replyCount)
|
||||
assertEquals(0, replies.replies.size)
|
||||
assertEquals(false, replies.hasNext)
|
||||
assertEquals(null, port.replyListOffset)
|
||||
}
|
||||
|
||||
private fun createService(
|
||||
port: FakeCreatorChannelCommunityQueryPort,
|
||||
audioContentCloudFront: AudioContentCloudFront = Mockito.mock(AudioContentCloudFront::class.java),
|
||||
@@ -226,11 +341,29 @@ private class FakeCreatorChannelCommunityQueryPort : CreatorChannelCommunityQuer
|
||||
var blocked = false
|
||||
var communityPostCount = 1
|
||||
var communityPosts = listOf(communityPostRecord(1L, price = 0))
|
||||
var communityPost: CreatorChannelCommunityPostRecord? = communityPostRecord(1L, price = 0)
|
||||
var homeCommunityPosts = listOf(communityPostRecord(1L, price = 0))
|
||||
var communityCommentCount = 0
|
||||
var communityComments = emptyList<CreatorChannelCommunityCommentRecord>()
|
||||
var latestReplies = emptyList<CreatorChannelCommunityCommentRecord>()
|
||||
var rootCommentContext: CreatorChannelCommunityCommentContextRecord? = CreatorChannelCommunityCommentContextRecord(
|
||||
commentId = 1L,
|
||||
postId = 1L,
|
||||
creatorId = 1L,
|
||||
isCommentAvailable = true
|
||||
)
|
||||
var communityReplyCount = 0
|
||||
var communityReplies = emptyList<CreatorChannelCommunityCommentRecord>()
|
||||
var countCanViewAdultContent: Boolean? = null
|
||||
var listCanViewAdultContent: Boolean? = null
|
||||
var listOffset: Long? = null
|
||||
var listLimit: Int? = null
|
||||
var commentListCalled = false
|
||||
var commentListOffset: Long? = null
|
||||
var commentListLimit: Int? = null
|
||||
var latestReplyCommentIds = emptyList<Long>()
|
||||
var replyListOffset: Long? = null
|
||||
var replyListLimit: Int? = null
|
||||
var homeCreatorId: Long? = null
|
||||
var homeViewerId: Long? = null
|
||||
var homeIsPinned: Boolean? = null
|
||||
@@ -277,6 +410,54 @@ private class FakeCreatorChannelCommunityQueryPort : CreatorChannelCommunityQuer
|
||||
homeLimit = limit
|
||||
return homeCommunityPosts
|
||||
}
|
||||
|
||||
override fun findCommunityPost(
|
||||
postId: Long,
|
||||
viewerId: Long,
|
||||
canViewAdultContent: Boolean
|
||||
): CreatorChannelCommunityPostRecord? = communityPost
|
||||
|
||||
override fun countCommunityComments(postId: Long, viewerId: Long, creatorId: Long): Int = communityCommentCount
|
||||
|
||||
override fun findCommunityComments(
|
||||
postId: Long,
|
||||
viewerId: Long,
|
||||
creatorId: Long,
|
||||
offset: Long,
|
||||
limit: Int
|
||||
): List<CreatorChannelCommunityCommentRecord> {
|
||||
commentListCalled = true
|
||||
commentListOffset = offset
|
||||
commentListLimit = limit
|
||||
return communityComments
|
||||
}
|
||||
|
||||
override fun findLatestRepliesByCommentIds(
|
||||
commentIds: List<Long>,
|
||||
viewerId: Long
|
||||
): List<CreatorChannelCommunityCommentRecord> {
|
||||
latestReplyCommentIds = commentIds
|
||||
return latestReplies
|
||||
}
|
||||
|
||||
override fun findRootCommentContext(
|
||||
commentId: Long,
|
||||
viewerId: Long,
|
||||
canViewAdultContent: Boolean
|
||||
): CreatorChannelCommunityCommentContextRecord? = rootCommentContext
|
||||
|
||||
override fun countCommunityReplies(commentId: Long, viewerId: Long): Int = communityReplyCount
|
||||
|
||||
override fun findCommunityReplies(
|
||||
commentId: Long,
|
||||
viewerId: Long,
|
||||
offset: Long,
|
||||
limit: Int
|
||||
): List<CreatorChannelCommunityCommentRecord> {
|
||||
replyListOffset = offset
|
||||
replyListLimit = limit
|
||||
return communityReplies
|
||||
}
|
||||
}
|
||||
|
||||
private fun communityPostRecord(
|
||||
@@ -287,7 +468,8 @@ private fun communityPostRecord(
|
||||
creatorProfilePath: String? = "profile/$postId.png",
|
||||
imagePath: String? = "image/$postId.png",
|
||||
audioPath: String? = "audio/$postId.mp3",
|
||||
isLiked: Boolean = false
|
||||
isLiked: Boolean = false,
|
||||
isCommentAvailable: Boolean = true
|
||||
): CreatorChannelCommunityPostRecord {
|
||||
return CreatorChannelCommunityPostRecord(
|
||||
postId = postId,
|
||||
@@ -300,10 +482,28 @@ private fun communityPostRecord(
|
||||
price = price,
|
||||
createdAt = LocalDateTime.of(2026, 6, 21, 10, 0).plusMinutes(postId),
|
||||
existOrdered = existOrdered,
|
||||
isCommentAvailable = true,
|
||||
isCommentAvailable = isCommentAvailable,
|
||||
likeCount = postId.toInt(),
|
||||
commentCount = postId.toInt() + 1,
|
||||
isPinned = postId == 1L,
|
||||
isLiked = isLiked
|
||||
)
|
||||
}
|
||||
|
||||
private fun communityCommentRecord(
|
||||
commentId: Long,
|
||||
parentCommentId: Long? = null,
|
||||
writerProfilePath: String? = null,
|
||||
writerNickname: String = "writer-$commentId"
|
||||
): CreatorChannelCommunityCommentRecord {
|
||||
return CreatorChannelCommunityCommentRecord(
|
||||
commentId = commentId,
|
||||
parentCommentId = parentCommentId,
|
||||
postId = 1L,
|
||||
writerId = commentId + 100L,
|
||||
writerProfilePath = writerProfilePath,
|
||||
writerNickname = writerNickname,
|
||||
content = "comment-$commentId",
|
||||
createdAt = LocalDateTime.of(2026, 7, 6, 10, 0).plusMinutes(commentId)
|
||||
)
|
||||
}
|
||||
|
||||
+36
@@ -742,4 +742,40 @@ private class FakeCreatorChannelCommunityQueryPort : CreatorChannelCommunityQuer
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun findCommunityPost(
|
||||
postId: Long,
|
||||
viewerId: Long,
|
||||
canViewAdultContent: Boolean
|
||||
): CreatorChannelCommunityPostRecord? = null
|
||||
|
||||
override fun countCommunityComments(postId: Long, viewerId: Long, creatorId: Long): Int = 0
|
||||
|
||||
override fun findCommunityComments(
|
||||
postId: Long,
|
||||
viewerId: Long,
|
||||
creatorId: Long,
|
||||
offset: Long,
|
||||
limit: Int
|
||||
) = emptyList<kr.co.vividnext.sodalive.v2.creator.channel.community.port.out.CreatorChannelCommunityCommentRecord>()
|
||||
|
||||
override fun findLatestRepliesByCommentIds(
|
||||
commentIds: List<Long>,
|
||||
viewerId: Long
|
||||
) = emptyList<kr.co.vividnext.sodalive.v2.creator.channel.community.port.out.CreatorChannelCommunityCommentRecord>()
|
||||
|
||||
override fun findRootCommentContext(
|
||||
commentId: Long,
|
||||
viewerId: Long,
|
||||
canViewAdultContent: Boolean
|
||||
): kr.co.vividnext.sodalive.v2.creator.channel.community.port.out.CreatorChannelCommunityCommentContextRecord? = null
|
||||
|
||||
override fun countCommunityReplies(commentId: Long, viewerId: Long): Int = 0
|
||||
|
||||
override fun findCommunityReplies(
|
||||
commentId: Long,
|
||||
viewerId: Long,
|
||||
offset: Long,
|
||||
limit: Int
|
||||
) = emptyList<kr.co.vividnext.sodalive.v2.creator.channel.community.port.out.CreatorChannelCommunityCommentRecord>()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user