feat(creator-channel): 커뮤니티 상세 조회 계약을 추가한다

This commit is contained in:
2026-07-09 00:52:59 +09:00
parent e0dc0c2cb7
commit 9e524d626a
4 changed files with 252 additions and 0 deletions

View File

@@ -0,0 +1,63 @@
package kr.co.vividnext.sodalive.v2.creator.channel.community.detail.data
import androidx.annotation.Keep
import com.google.gson.annotations.SerializedName
@Keep
data class CreatorChannelCommunityPostDetailResponse(
@SerializedName("postId") val postId: Long,
@SerializedName("creatorId") val creatorId: Long,
@SerializedName("creatorNickname") val creatorNickname: String,
@SerializedName("creatorProfileUrl") val creatorProfileUrl: String?,
@SerializedName("createdAtUtc") val createdAtUtc: String,
@SerializedName("content") val content: String,
@SerializedName("imageUrl") val imageUrl: String?,
@SerializedName("audioUrl") val audioUrl: String?,
@SerializedName("price") val price: Int,
@SerializedName("existOrdered") val existOrdered: Boolean,
@SerializedName("isCommentAvailable") val isCommentAvailable: Boolean,
@SerializedName("likeCount") val likeCount: Int,
@SerializedName("commentCount") val commentCount: Int,
@SerializedName("isLiked") val isLiked: Boolean,
@SerializedName("isPinned") val isPinned: Boolean,
@SerializedName("comments") val comments: CreatorChannelCommunityCommentsResponse
)
@Keep
data class CreatorChannelCommunityCommentsResponse(
@SerializedName("commentCount") val commentCount: Int,
@SerializedName("comments") val comments: List<CreatorChannelCommunityCommentResponse>,
@SerializedName("page") val page: Int,
@SerializedName("size") val size: Int,
@SerializedName("hasNext") val hasNext: Boolean
)
@Keep
data class CreatorChannelCommunityCommentResponse(
@SerializedName("commentId") val commentId: Long,
@SerializedName("writerId") val writerId: Long,
@SerializedName("writerProfileImageUrl") val writerProfileImageUrl: String,
@SerializedName("writerNickname") val writerNickname: String,
@SerializedName("content") val content: String,
@SerializedName("isSecret") val isSecret: Boolean,
@SerializedName("createdAtUtc") val createdAtUtc: String,
@SerializedName("latestReply") val latestReply: CreatorChannelCommunityReplyResponse?
)
@Keep
data class CreatorChannelCommunityReplyResponse(
@SerializedName("commentId") val commentId: Long,
@SerializedName("writerId") val writerId: Long,
@SerializedName("writerProfileImageUrl") val writerProfileImageUrl: String,
@SerializedName("writerNickname") val writerNickname: String,
@SerializedName("content") val content: String,
@SerializedName("createdAtUtc") val createdAtUtc: String
)
@Keep
data class CreatorChannelCommunityRepliesResponse(
@SerializedName("replies") val replies: List<CreatorChannelCommunityReplyResponse>,
@SerializedName("page") val page: Int,
@SerializedName("size") val size: Int,
@SerializedName("hasNext") val hasNext: Boolean
)

View File

@@ -5,6 +5,9 @@ import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.v2.common.data.ContentSort import kr.co.vividnext.sodalive.v2.common.data.ContentSort
import kr.co.vividnext.sodalive.v2.creator.channel.audio.data.CreatorChannelAudioTabResponse import kr.co.vividnext.sodalive.v2.creator.channel.audio.data.CreatorChannelAudioTabResponse
import kr.co.vividnext.sodalive.v2.creator.channel.community.data.CreatorChannelCommunityTabResponse import kr.co.vividnext.sodalive.v2.creator.channel.community.data.CreatorChannelCommunityTabResponse
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.data.CreatorChannelCommunityCommentsResponse
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.data.CreatorChannelCommunityPostDetailResponse
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.data.CreatorChannelCommunityRepliesResponse
import kr.co.vividnext.sodalive.v2.creator.channel.donation.data.CreatorChannelDonationTabResponse import kr.co.vividnext.sodalive.v2.creator.channel.donation.data.CreatorChannelDonationTabResponse
import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.data.CreatorChannelFanTalkTabResponse import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.data.CreatorChannelFanTalkTabResponse
import kr.co.vividnext.sodalive.v2.creator.channel.live.data.CreatorChannelLiveTabResponse import kr.co.vividnext.sodalive.v2.creator.channel.live.data.CreatorChannelLiveTabResponse
@@ -57,6 +60,28 @@ interface CreatorChannelApi {
@Header("Authorization") authHeader: String @Header("Authorization") authHeader: String
): Single<ApiResponse<CreatorChannelCommunityTabResponse>> ): Single<ApiResponse<CreatorChannelCommunityTabResponse>>
@GET("/api/v2/creator-channels/community-posts/{postId}")
fun getCommunityPostDetail(
@Path("postId") postId: Long,
@Header("Authorization") authHeader: String
): Single<ApiResponse<CreatorChannelCommunityPostDetailResponse>>
@GET("/api/v2/creator-channels/community-posts/{postId}/comments")
fun getCommunityPostComments(
@Path("postId") postId: Long,
@Query("page") page: Int,
@Query("size") size: Int,
@Header("Authorization") authHeader: String
): Single<ApiResponse<CreatorChannelCommunityCommentsResponse>>
@GET("/api/v2/creator-channels/community-comments/{commentId}/replies")
fun getCommunityCommentReplies(
@Path("commentId") commentId: Long,
@Query("page") page: Int,
@Query("size") size: Int,
@Header("Authorization") authHeader: String
): Single<ApiResponse<CreatorChannelCommunityRepliesResponse>>
@GET("/api/v2/creator-channels/{creatorId}/fan-talks") @GET("/api/v2/creator-channels/{creatorId}/fan-talks")
fun getFanTalks( fun getFanTalks(
@Path("creatorId") creatorId: Long, @Path("creatorId") creatorId: Long,

View File

@@ -79,6 +79,35 @@ class CreatorChannelRepository(
authHeader = token authHeader = token
) )
fun getCommunityPostDetail(postId: Long, token: String) = api.getCommunityPostDetail(
postId = postId,
authHeader = token
)
fun getCommunityPostComments(
postId: Long,
page: Int = 0,
size: Int = 20,
token: String
) = api.getCommunityPostComments(
postId = postId,
page = page,
size = size,
authHeader = token
)
fun getCommunityCommentReplies(
commentId: Long,
page: Int = 0,
size: Int = 20,
token: String
) = api.getCommunityCommentReplies(
commentId = commentId,
page = page,
size = size,
authHeader = token
)
fun getFanTalks( fun getFanTalks(
creatorId: Long, creatorId: Long,
page: Int, page: Int,

View File

@@ -0,0 +1,135 @@
package kr.co.vividnext.sodalive.v2.creator.channel.community
import com.google.gson.annotations.SerializedName
import io.reactivex.rxjava3.core.Single
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.data.CreatorChannelCommunityCommentResponse
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.data.CreatorChannelCommunityCommentsResponse
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.data.CreatorChannelCommunityPostDetailResponse
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.data.CreatorChannelCommunityRepliesResponse
import kr.co.vividnext.sodalive.v2.creator.channel.community.detail.data.CreatorChannelCommunityReplyResponse
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelApi
import kr.co.vividnext.sodalive.v2.creator.channel.data.CreatorChannelRepository
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Test
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
import java.io.File
class CreatorChannelCommunityDetailContractTest {
@Test
fun `커뮤니티 상세 API source는 신규 v2 endpoint와 ApiResponse Single 계약을 제공한다`() {
val api = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/data/CreatorChannelApi.kt"
).readText()
assertTrue(api.contains("@GET(\"/api/v2/creator-channels/community-posts/{postId}\")"))
assertTrue(api.contains("@GET(\"/api/v2/creator-channels/community-posts/{postId}/comments\")"))
assertTrue(api.contains("@GET(\"/api/v2/creator-channels/community-comments/{commentId}/replies\")"))
assertTrue(api.contains("Single<ApiResponse<CreatorChannelCommunityPostDetailResponse>>"))
assertTrue(api.contains("Single<ApiResponse<CreatorChannelCommunityCommentsResponse>>"))
assertTrue(api.contains("Single<ApiResponse<CreatorChannelCommunityRepliesResponse>>"))
}
@Test
fun `커뮤니티 상세 repository는 댓글과 답글 기본 page 0 size 20 계약을 제공한다`() {
val api = mock<CreatorChannelApi>()
val repository = CreatorChannelRepository(api, mock(), mock(), mock(), mock())
whenever(api.getCommunityPostDetail(11L, "Bearer token")).thenReturn(Single.never())
whenever(api.getCommunityPostComments(11L, 0, 20, "Bearer token")).thenReturn(Single.never())
whenever(api.getCommunityCommentReplies(22L, 0, 20, "Bearer token")).thenReturn(Single.never())
repository.getCommunityPostDetail(11L, "Bearer token")
repository.getCommunityPostComments(11L, token = "Bearer token")
repository.getCommunityCommentReplies(22L, token = "Bearer token")
verify(api).getCommunityPostDetail(11L, "Bearer token")
verify(api).getCommunityPostComments(11L, 0, 20, "Bearer token")
verify(api).getCommunityCommentReplies(22L, 0, 20, "Bearer token")
}
@Test
fun `커뮤니티 상세 응답 모델은 SerializedName과 nullable latestReply 계약을 제공한다`() {
val reply = CreatorChannelCommunityReplyResponse(
commentId = 44L,
writerId = 55L,
writerProfileImageUrl = "reply.png",
writerNickname = "reply member",
content = "reply",
createdAtUtc = "2026-07-08T00:02:00Z"
)
val comment = CreatorChannelCommunityCommentResponse(
commentId = 22L,
writerId = 33L,
writerProfileImageUrl = "member.png",
writerNickname = "member",
content = "comment",
isSecret = false,
createdAtUtc = "2026-07-08T00:01:00Z",
latestReply = reply
)
val comments = CreatorChannelCommunityCommentsResponse(
commentCount = 1,
comments = listOf(comment),
page = 0,
size = 20,
hasNext = false
)
val detail = CreatorChannelCommunityPostDetailResponse(
postId = 11L,
creatorId = 100L,
creatorNickname = "creator",
creatorProfileUrl = null,
createdAtUtc = "2026-07-08T00:00:00Z",
content = "content",
imageUrl = null,
audioUrl = null,
price = 0,
existOrdered = true,
isCommentAvailable = true,
likeCount = 1,
commentCount = 2,
isLiked = false,
isPinned = false,
comments = comments
)
assertEquals(11L, detail.postId)
assertEquals(comments, detail.comments)
assertEquals(reply, comment.latestReply)
assertEquals(44L, reply.commentId)
assertNotNull(
CreatorChannelCommunityPostDetailResponse::class.java.getDeclaredField("postId")
.getAnnotation(SerializedName::class.java)
)
assertNotNull(
CreatorChannelCommunityPostDetailResponse::class.java.getDeclaredField("comments")
.getAnnotation(SerializedName::class.java)
)
assertNotNull(
CreatorChannelCommunityCommentResponse::class.java.getDeclaredField("latestReply")
.getAnnotation(SerializedName::class.java)
)
assertNotNull(
CreatorChannelCommunityCommentResponse::class.java.getDeclaredField("writerId")
.getAnnotation(SerializedName::class.java)
)
assertNotNull(
CreatorChannelCommunityReplyResponse::class.java.getDeclaredField("writerId")
.getAnnotation(SerializedName::class.java)
)
assertEquals(CreatorChannelCommunityCommentsResponse(1, listOf(comment), 0, 20, false).comments.first(), comment)
assertEquals(CreatorChannelCommunityRepliesResponse(listOf(reply), 0, 20, false).replies.first(), reply)
}
private fun projectFile(path: String): File {
var current = File(checkNotNull(System.getProperty("user.dir"))).absoluteFile
while (current.parentFile != null && !File(current, "settings.gradle").exists()) {
current = checkNotNull(current.parentFile)
}
return File(current, path)
}
}