feat(home): 팔로잉 응답 UI 매핑을 추가한다

This commit is contained in:
2026-06-25 22:22:22 +09:00
parent 21d3ed4603
commit 502bbd4f35
4 changed files with 404 additions and 0 deletions

View File

@@ -0,0 +1,173 @@
package kr.co.vividnext.sodalive.v2.main.home
import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.common.UtcRelativeTimeTextFormatter
import kr.co.vividnext.sodalive.v2.common.CreatorActivityType
import kr.co.vividnext.sodalive.v2.main.chat.data.ChatRoomListItemResponse
import kr.co.vividnext.sodalive.v2.main.home.data.FollowingCreatorResponse
import kr.co.vividnext.sodalive.v2.main.home.data.FollowingLiveResponse
import kr.co.vividnext.sodalive.v2.main.home.data.FollowingNewsResponse
import kr.co.vividnext.sodalive.v2.main.home.data.FollowingNewsType
import kr.co.vividnext.sodalive.v2.main.home.data.FollowingScheduleResponse
import kr.co.vividnext.sodalive.v2.main.home.data.HomeFollowingTabResponse
import kr.co.vividnext.sodalive.v2.main.home.model.HomeFollowingNewsUiItem
import kr.co.vividnext.sodalive.v2.main.home.model.HomeFollowingUiState
import kr.co.vividnext.sodalive.v2.main.home.model.toUiState
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
class HomeFollowingMapperTest {
private val formatter = UtcRelativeTimeTextFormatter { "relative:$it" }
@Test
fun `isLoginRequired true 응답은 LoginRequired로 매핑된다`() {
val state = response(isLoginRequired = true, followingCreators = listOf(creator())).toUiState(formatter)
assertTrue(state is HomeFollowingUiState.LoginRequired)
}
@Test
fun `로그인이 필요 없고 모든 섹션이 비면 Empty로 매핑된다`() {
val state = response().toUiState(formatter)
assertTrue(state is HomeFollowingUiState.Empty)
}
@Test
fun `응답 섹션은 팔로잉 UI model로 매핑된다`() {
val state = response(
followingCreators = listOf(creator()),
onAirLives = listOf(live()),
recentChats = listOf(chat()),
monthlySchedules = listOf(schedule(scheduleId = "schedule-1")),
recentNews = listOf(news(newsId = "news-1", type = FollowingNewsType.PHOTO_CONTENT))
).toUiState(formatter) as HomeFollowingUiState.Content
assertEquals(1L, state.followingCreators.items.single().creatorId)
assertEquals(2L, state.onAirLives.items.single().liveId)
assertEquals(3L, state.recentChats.items.single().roomId)
assertEquals("schedule-1", state.monthlySchedules.items.single().scheduleId)
assertEquals("news-1", state.recentNews.items.single().newsId)
}
@Test
fun `recentChats는 toUiItem 결과가 null인 항목을 제외한다`() {
val state = response(
recentChats = listOf(chat(roomId = 1L, chatType = "UNKNOWN"), chat(roomId = 2L, chatType = "DM"))
).toUiState(formatter) as HomeFollowingUiState.Content
assertEquals(listOf(2L), state.recentChats.items.map { it.roomId })
}
@Test
fun `monthlySchedules는 서버 응답 순서를 유지한다`() {
val state = response(
monthlySchedules = listOf(schedule(scheduleId = "second"), schedule(scheduleId = "first"))
).toUiState(formatter) as HomeFollowingUiState.Content
assertEquals(listOf("second", "first"), state.monthlySchedules.items.map { it.scheduleId })
}
@Test
fun `PHOTO_CONTENT는 photo content label로 매핑된다`() {
val state = response(
recentNews = listOf(news(type = FollowingNewsType.PHOTO_CONTENT))
).toUiState(formatter) as HomeFollowingUiState.Content
val item = state.recentNews.items.single() as HomeFollowingNewsUiItem.Content
assertEquals(R.string.screen_home_following_photo_content, item.labelResId)
}
@Test
fun `ranking news의 rank null 항목은 제외된다`() {
val state = response(
recentNews = listOf(
news(newsId = "hidden", type = FollowingNewsType.CREATOR_RANKING, rank = null),
news(newsId = "shown", type = FollowingNewsType.CONTENT_RANKING, rank = 1)
)
).toUiState(formatter) as HomeFollowingUiState.Content
assertEquals(listOf("shown"), state.recentNews.items.map { it.newsId })
}
@Test
fun `news 상대 시간은 visibleFromAtUtc 값을 formatter에 전달한다`() {
val state = response(
recentNews = listOf(news(visibleFromAtUtc = "2026-06-25T00:00:00Z"))
).toUiState(formatter) as HomeFollowingUiState.Content
assertEquals("relative:2026-06-25T00:00:00Z", state.recentNews.items.single().visibleFromText)
}
private fun response(
isLoginRequired: Boolean = false,
followingCreators: List<FollowingCreatorResponse> = emptyList(),
onAirLives: List<FollowingLiveResponse> = emptyList(),
recentChats: List<ChatRoomListItemResponse> = emptyList(),
monthlySchedules: List<FollowingScheduleResponse> = emptyList(),
recentNews: List<FollowingNewsResponse> = emptyList()
) = HomeFollowingTabResponse(
isLoginRequired = isLoginRequired,
followingCreators = followingCreators,
onAirLives = onAirLives,
recentChats = recentChats,
monthlySchedules = monthlySchedules,
recentNews = recentNews
)
private fun creator() = FollowingCreatorResponse(
creatorId = 1L,
creatorNickname = "creator",
creatorProfileImageUrl = "https://example.com/creator.png"
)
private fun live() = FollowingLiveResponse(
liveId = 2L,
creatorProfileImageUrl = "https://example.com/live.png",
creatorNickname = "creator",
title = "live",
startedAtUtc = "2026-06-25T00:00:00Z"
)
private fun chat(roomId: Long = 3L, chatType: String = "AI") = ChatRoomListItemResponse(
roomId = roomId,
chatType = chatType,
targetName = "target",
targetImageUrl = "https://example.com/chat.png",
lastMessage = "message",
lastMessageAt = "2026-06-25T00:00:00Z"
)
private fun schedule(scheduleId: String) = FollowingScheduleResponse(
scheduleId = scheduleId,
creatorId = 4L,
creatorProfileImageUrl = "https://example.com/schedule.png",
creatorNickname = "creator",
title = "schedule",
type = CreatorActivityType.Live,
targetId = 5L,
scheduledAtUtc = "2026-06-25T00:00:00Z",
isOnAir = false
)
private fun news(
newsId: String = "news",
type: FollowingNewsType = FollowingNewsType.AUDIO_CONTENT,
visibleFromAtUtc: String = "2026-06-25T01:00:00Z",
rank: Int? = null
) = FollowingNewsResponse(
newsId = newsId,
type = type,
creatorProfileImageUrl = "https://example.com/news.png",
creatorNickname = "creator",
title = "title",
body = "body",
thumbnailImageUrl = null,
targetId = 6L,
occurredAtUtc = "2026-06-25T00:00:00Z",
visibleFromAtUtc = visibleFromAtUtc,
rank = rank
)
}