From b7eba4c99acac24e5e6821b52080271d03474a31 Mon Sep 17 00:00:00 2001 From: klaus Date: Mon, 22 Jun 2026 16:38:00 +0900 Subject: [PATCH] =?UTF-8?q?feat(creator):=20FanTalk=20=ED=83=AD=20UI=20?= =?UTF-8?q?=EB=AA=A8=EB=8D=B8=20=EB=A7=A4=ED=95=91=EC=9D=84=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../model/CreatorChannelFanTalkMappers.kt | 48 +++++++ .../model/CreatorChannelFanTalkUiModels.kt | 29 ++++ .../CreatorChannelFanTalkMapperTest.kt | 135 ++++++++++++++++++ 3 files changed, 212 insertions(+) create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/model/CreatorChannelFanTalkMappers.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/model/CreatorChannelFanTalkUiModels.kt create mode 100644 app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/CreatorChannelFanTalkMapperTest.kt diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/model/CreatorChannelFanTalkMappers.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/model/CreatorChannelFanTalkMappers.kt new file mode 100644 index 00000000..060f2156 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/model/CreatorChannelFanTalkMappers.kt @@ -0,0 +1,48 @@ +package kr.co.vividnext.sodalive.v2.creator.channel.fantalk.model + +import kr.co.vividnext.sodalive.common.UtcRelativeTimeTextFormatter +import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.data.CreatorChannelFanTalkReplyResponse +import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.data.CreatorChannelFanTalkResponse + +fun List.toFanTalkUiModels( + relativeTimeTextFormatter: UtcRelativeTimeTextFormatter, + isOwner: Boolean, + currentUserId: Long +): List = map { + it.toFanTalkUiModel(relativeTimeTextFormatter, isOwner, currentUserId) +} + +private fun CreatorChannelFanTalkResponse.toFanTalkUiModel( + relativeTimeTextFormatter: UtcRelativeTimeTextFormatter, + isOwner: Boolean, + currentUserId: Long +) = CreatorChannelFanTalkUiModel( + fanTalkId = fanTalkId, + writerId = writerId, + writerNickname = writerNickname, + writerProfileImageUrl = writerProfileImageUrl, + content = content, + createdAtText = relativeTimeTextFormatter.format(createdAtUtc), + reply = creatorReplies.firstOrNull()?.toReplyUiModel(relativeTimeTextFormatter), + rightAction = toRightAction(isOwner = isOwner, currentUserId = currentUserId) +) + +private fun CreatorChannelFanTalkReplyResponse.toReplyUiModel( + relativeTimeTextFormatter: UtcRelativeTimeTextFormatter +) = CreatorChannelFanTalkReplyUiModel( + fanTalkId = fanTalkId, + writerId = writerId, + writerNickname = writerNickname, + writerProfileImageUrl = writerProfileImageUrl, + content = content, + createdAtText = relativeTimeTextFormatter.format(createdAtUtc) +) + +private fun CreatorChannelFanTalkResponse.toRightAction( + isOwner: Boolean, + currentUserId: Long +): CreatorChannelFanTalkRightAction = when { + writerId == currentUserId -> CreatorChannelFanTalkRightAction.OwnerMore(showEdit = true, showDelete = true) + isOwner -> CreatorChannelFanTalkRightAction.OwnerMore(showEdit = false, showDelete = true) + else -> CreatorChannelFanTalkRightAction.Report +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/model/CreatorChannelFanTalkUiModels.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/model/CreatorChannelFanTalkUiModels.kt new file mode 100644 index 00000000..c8078b46 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/model/CreatorChannelFanTalkUiModels.kt @@ -0,0 +1,29 @@ +package kr.co.vividnext.sodalive.v2.creator.channel.fantalk.model + +data class CreatorChannelFanTalkUiModel( + val fanTalkId: Long, + val writerId: Long, + val writerNickname: String, + val writerProfileImageUrl: String, + val content: String, + val createdAtText: String, + val reply: CreatorChannelFanTalkReplyUiModel?, + val rightAction: CreatorChannelFanTalkRightAction +) + +data class CreatorChannelFanTalkReplyUiModel( + val fanTalkId: Long, + val writerId: Long, + val writerNickname: String, + val writerProfileImageUrl: String, + val content: String, + val createdAtText: String +) + +sealed interface CreatorChannelFanTalkRightAction { + data object Report : CreatorChannelFanTalkRightAction + data class OwnerMore( + val showEdit: Boolean, + val showDelete: Boolean + ) : CreatorChannelFanTalkRightAction +} diff --git a/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/CreatorChannelFanTalkMapperTest.kt b/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/CreatorChannelFanTalkMapperTest.kt new file mode 100644 index 00000000..47f667c5 --- /dev/null +++ b/app/src/test/java/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/CreatorChannelFanTalkMapperTest.kt @@ -0,0 +1,135 @@ +package kr.co.vividnext.sodalive.v2.creator.channel.fantalk + +import android.app.Application +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import kr.co.vividnext.sodalive.R +import kr.co.vividnext.sodalive.common.AndroidUtcRelativeTimeTextFormatter +import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.data.CreatorChannelFanTalkReplyResponse +import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.data.CreatorChannelFanTalkResponse +import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.model.CreatorChannelFanTalkRightAction +import kr.co.vividnext.sodalive.v2.creator.channel.fantalk.model.toFanTalkUiModels +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [28], application = Application::class) +class CreatorChannelFanTalkMapperTest { + + private val context: Context = ApplicationProvider.getApplicationContext() + private val relativeTimeTextFormatter = AndroidUtcRelativeTimeTextFormatter(context) + + @Test + fun `기본 필드와 상대 시간을 매핑한다`() { + val item = listOf(fanTalk(createdAtUtc = System.currentTimeMillis().toString())) + .toFanTalkUiModels(relativeTimeTextFormatter, isOwner = false, currentUserId = 99L) + .single() + + assertEquals(1L, item.fanTalkId) + assertEquals(10L, item.writerId) + assertEquals("writer", item.writerNickname) + assertEquals("profile.png", item.writerProfileImageUrl) + assertEquals(context.getString(R.string.character_comment_time_just_now), item.createdAtText) + assertEquals("hello fan talk", item.content) + } + + @Test + fun `creatorReplies가 비어 있으면 reply는 null이다`() { + val item = listOf(fanTalk(creatorReplies = emptyList())) + .toFanTalkUiModels(relativeTimeTextFormatter, isOwner = false, currentUserId = 99L) + .single() + + assertNull(item.reply) + } + + @Test + fun `creatorReplies가 여러 개이면 첫 번째 reply만 매핑한다`() { + val item = listOf( + fanTalk( + creatorReplies = listOf( + reply(fanTalkId = 2L, content = "first reply"), + reply(fanTalkId = 3L, content = "second reply") + ) + ) + ).toFanTalkUiModels(relativeTimeTextFormatter, isOwner = false, currentUserId = 99L).single() + + assertEquals(2L, item.reply?.fanTalkId) + assertEquals("first reply", item.reply?.content) + } + + @Test + fun `내가 쓴 글이면 수정과 삭제 owner more action이다`() { + val item = listOf(fanTalk(writerId = 10L)) + .toFanTalkUiModels(relativeTimeTextFormatter, isOwner = false, currentUserId = 10L) + .single() + + val action = item.rightAction as CreatorChannelFanTalkRightAction.OwnerMore + assertTrue(action.showEdit) + assertTrue(action.showDelete) + } + + @Test + fun `내 채널의 타인 글이면 삭제만 가능한 owner more action이다`() { + val item = listOf(fanTalk(writerId = 11L)) + .toFanTalkUiModels(relativeTimeTextFormatter, isOwner = true, currentUserId = 10L) + .single() + + val action = item.rightAction as CreatorChannelFanTalkRightAction.OwnerMore + assertEquals(false, action.showEdit) + assertTrue(action.showDelete) + } + + @Test + fun `내가 쓴 글도 아니고 내 채널도 아니면 신고 action이다`() { + val item = listOf(fanTalk(writerId = 11L)) + .toFanTalkUiModels(relativeTimeTextFormatter, isOwner = false, currentUserId = 10L) + .single() + + assertTrue(item.rightAction is CreatorChannelFanTalkRightAction.Report) + } + + @Test + fun `원글과 답글 content가 빈 문자열이어도 item을 유지한다`() { + val item = listOf(fanTalk(content = "", creatorReplies = listOf(reply(content = "")))) + .toFanTalkUiModels(relativeTimeTextFormatter, isOwner = false, currentUserId = 99L) + .single() + + assertEquals("", item.content) + assertEquals("", item.reply?.content) + } + + private fun fanTalk( + fanTalkId: Long = 1L, + writerId: Long = 10L, + writerProfileImageUrl: String = "profile.png", + content: String = "hello fan talk", + createdAtUtc: String = "2026-06-21T00:00:00Z", + creatorReplies: List = emptyList() + ) = CreatorChannelFanTalkResponse( + fanTalkId = fanTalkId, + writerId = writerId, + writerNickname = "writer", + writerProfileImageUrl = writerProfileImageUrl, + content = content, + createdAtUtc = createdAtUtc, + creatorReplies = creatorReplies + ) + + private fun reply( + fanTalkId: Long = 2L, + writerId: Long = 20L, + content: String = "reply" + ) = CreatorChannelFanTalkReplyResponse( + fanTalkId = fanTalkId, + writerId = writerId, + writerNickname = "creator", + writerProfileImageUrl = "creator.png", + content = content, + createdAtUtc = "2026-06-21T00:00:00Z" + ) +}