feat(home-following): 팔로잉 탭 응답 모델을 추가한다

This commit is contained in:
2026-06-25 22:14:21 +09:00
parent 3add66ff7a
commit e4052d097a
4 changed files with 317 additions and 0 deletions

View File

@@ -0,0 +1,142 @@
package kr.co.vividnext.sodalive.v2.api.home.following.dto
import com.fasterxml.jackson.annotation.JsonProperty
import kr.co.vividnext.sodalive.v2.chat.dto.ChatRoomListItemResponse
import kr.co.vividnext.sodalive.v2.common.domain.CreatorActivityType
import kr.co.vividnext.sodalive.v2.home.following.domain.FollowingNewsType
import kr.co.vividnext.sodalive.v2.home.following.domain.HomeFollowing
import kr.co.vividnext.sodalive.v2.home.following.domain.HomeFollowingCreator
import kr.co.vividnext.sodalive.v2.home.following.domain.HomeFollowingLive
import kr.co.vividnext.sodalive.v2.home.following.domain.HomeFollowingNews
import kr.co.vividnext.sodalive.v2.home.following.domain.HomeFollowingSchedule
data class HomeFollowingTabResponse(
@JsonProperty("isLoginRequired")
val isLoginRequired: Boolean,
val followingCreators: List<FollowingCreatorResponse>,
val onAirLives: List<FollowingLiveResponse>,
val recentChats: List<ChatRoomListItemResponse>,
val monthlySchedules: List<FollowingScheduleResponse>,
val recentNews: List<FollowingNewsResponse>
) {
companion object {
fun loginRequired(): HomeFollowingTabResponse {
return HomeFollowingTabResponse(
isLoginRequired = true,
followingCreators = emptyList(),
onAirLives = emptyList(),
recentChats = emptyList(),
monthlySchedules = emptyList(),
recentNews = emptyList()
)
}
fun from(home: HomeFollowing): HomeFollowingTabResponse {
return HomeFollowingTabResponse(
isLoginRequired = false,
followingCreators = home.followingCreators.map(FollowingCreatorResponse::from),
onAirLives = home.onAirLives.map(FollowingLiveResponse::from),
recentChats = home.recentChats,
monthlySchedules = home.monthlySchedules.map(FollowingScheduleResponse::from),
recentNews = home.recentNews.map(FollowingNewsResponse::from)
)
}
}
}
data class FollowingCreatorResponse(
val creatorId: Long,
val creatorNickname: String,
val creatorProfileImageUrl: String
) {
companion object {
fun from(creator: HomeFollowingCreator): FollowingCreatorResponse {
return FollowingCreatorResponse(
creatorId = creator.creatorId,
creatorNickname = creator.creatorNickname,
creatorProfileImageUrl = creator.creatorProfileImageUrl
)
}
}
}
data class FollowingLiveResponse(
val liveId: Long,
val creatorProfileImageUrl: String,
val creatorNickname: String,
val title: String,
val startedAtUtc: String
) {
companion object {
fun from(live: HomeFollowingLive): FollowingLiveResponse {
return FollowingLiveResponse(
liveId = live.liveId,
creatorProfileImageUrl = live.creatorProfileImageUrl,
creatorNickname = live.creatorNickname,
title = live.title,
startedAtUtc = live.startedAtUtc
)
}
}
}
data class FollowingScheduleResponse(
val scheduleId: String,
val creatorId: Long,
val creatorProfileImageUrl: String,
val creatorNickname: String,
val title: String,
val type: CreatorActivityType,
val targetId: Long,
val scheduledAtUtc: String,
@JsonProperty("isOnAir")
val isOnAir: Boolean
) {
companion object {
fun from(schedule: HomeFollowingSchedule): FollowingScheduleResponse {
return FollowingScheduleResponse(
scheduleId = schedule.scheduleId,
creatorId = schedule.creatorId,
creatorProfileImageUrl = schedule.creatorProfileImageUrl,
creatorNickname = schedule.creatorNickname,
title = schedule.title,
type = schedule.type,
targetId = schedule.targetId,
scheduledAtUtc = schedule.scheduledAtUtc,
isOnAir = schedule.isOnAir
)
}
}
}
data class FollowingNewsResponse(
val newsId: String,
val type: FollowingNewsType,
val creatorProfileImageUrl: String,
val creatorNickname: String,
val title: String,
val body: String,
val thumbnailImageUrl: String?,
val targetId: Long,
val occurredAtUtc: String,
val visibleFromAtUtc: String,
val rank: Int?
) {
companion object {
fun from(news: HomeFollowingNews): FollowingNewsResponse {
return FollowingNewsResponse(
newsId = news.newsId,
type = news.type,
creatorProfileImageUrl = news.creatorProfileImageUrl,
creatorNickname = news.creatorNickname,
title = news.title,
body = news.body,
thumbnailImageUrl = news.thumbnailImageUrl,
targetId = news.targetId,
occurredAtUtc = news.occurredAtUtc,
visibleFromAtUtc = news.visibleFromAtUtc,
rank = news.rank
)
}
}
}

View File

@@ -0,0 +1,9 @@
package kr.co.vividnext.sodalive.v2.home.following.domain
enum class FollowingNewsType {
CREATOR_RANKING,
CONTENT_RANKING,
COMMUNITY_POST,
AUDIO_CONTENT,
PHOTO_CONTENT
}

View File

@@ -0,0 +1,52 @@
package kr.co.vividnext.sodalive.v2.home.following.domain
import kr.co.vividnext.sodalive.v2.chat.dto.ChatRoomListItemResponse
import kr.co.vividnext.sodalive.v2.common.domain.CreatorActivityType
data class HomeFollowing(
val followingCreators: List<HomeFollowingCreator>,
val onAirLives: List<HomeFollowingLive>,
val recentChats: List<ChatRoomListItemResponse>,
val monthlySchedules: List<HomeFollowingSchedule>,
val recentNews: List<HomeFollowingNews>
)
data class HomeFollowingCreator(
val creatorId: Long,
val creatorNickname: String,
val creatorProfileImageUrl: String
)
data class HomeFollowingLive(
val liveId: Long,
val creatorProfileImageUrl: String,
val creatorNickname: String,
val title: String,
val startedAtUtc: String
)
data class HomeFollowingSchedule(
val scheduleId: String,
val creatorId: Long,
val creatorProfileImageUrl: String,
val creatorNickname: String,
val title: String,
val type: CreatorActivityType,
val targetId: Long,
val scheduledAtUtc: String,
val isOnAir: Boolean
)
data class HomeFollowingNews(
val newsId: String,
val type: FollowingNewsType,
val creatorProfileImageUrl: String,
val creatorNickname: String,
val title: String,
val body: String,
val thumbnailImageUrl: String?,
val targetId: Long,
val occurredAtUtc: String,
val visibleFromAtUtc: String,
val rank: Int?
)

View File

@@ -0,0 +1,114 @@
package kr.co.vividnext.sodalive.v2.api.home.following.dto
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import kr.co.vividnext.sodalive.v2.chat.dto.ChatRoomListItemResponse
import kr.co.vividnext.sodalive.v2.common.domain.CreatorActivityType
import kr.co.vividnext.sodalive.v2.home.following.domain.FollowingNewsType
import kr.co.vividnext.sodalive.v2.home.following.domain.HomeFollowing
import kr.co.vividnext.sodalive.v2.home.following.domain.HomeFollowingCreator
import kr.co.vividnext.sodalive.v2.home.following.domain.HomeFollowingLive
import kr.co.vividnext.sodalive.v2.home.following.domain.HomeFollowingNews
import kr.co.vividnext.sodalive.v2.home.following.domain.HomeFollowingSchedule
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
class HomeFollowingTabResponseTest {
private val objectMapper = jacksonObjectMapper()
@Test
@DisplayName("비로그인 응답은 로그인이 필요하며 모든 섹션을 빈 배열로 반환한다")
fun shouldReturnLoginRequiredResponseWithEmptySections() {
val response = HomeFollowingTabResponse.loginRequired()
val json = objectMapper.readTree(objectMapper.writeValueAsString(response))
assertTrue(response.isLoginRequired)
assertTrue(response.followingCreators.isEmpty())
assertTrue(response.onAirLives.isEmpty())
assertTrue(response.recentChats.isEmpty())
assertTrue(response.monthlySchedules.isEmpty())
assertTrue(response.recentNews.isEmpty())
assertEquals(true, json["isLoginRequired"].asBoolean())
assertTrue(json["followingCreators"].isArray)
assertTrue(json["onAirLives"].isArray)
assertTrue(json["recentChats"].isArray)
assertTrue(json["monthlySchedules"].isArray)
assertTrue(json["recentNews"].isArray)
}
@Test
@DisplayName("팔로잉 탭 도메인은 creatorId 없는 최근 소식과 nullable rank 응답으로 변환한다")
fun shouldMapDomainToResponseWithoutCreatorIdInRecentNews() {
val response = HomeFollowingTabResponse.from(createHomeFollowing())
val json = objectMapper.readTree(objectMapper.writeValueAsString(response))
assertFalse(response.isLoginRequired)
assertEquals(1L, response.followingCreators.first().creatorId)
assertEquals(10L, response.onAirLives.first().liveId)
assertEquals(100L, response.recentChats.first().roomId)
assertEquals("LIVE:20", response.monthlySchedules.first().scheduleId)
assertEquals(3, response.recentNews.first().rank)
assertEquals(false, json["isLoginRequired"].asBoolean())
assertFalse(json["recentNews"][0].has("creatorId"))
assertFalse(json["recentNews"][0].has("ranking"))
assertFalse(json["recentNews"][0].has("rankChange"))
assertFalse(json["recentNews"][0].has("isNew"))
assertEquals(3, json["recentNews"][0]["rank"].asInt())
assertEquals(true, json["monthlySchedules"][0]["isOnAir"].asBoolean())
}
private fun createHomeFollowing(): HomeFollowing {
return HomeFollowing(
followingCreators = listOf(HomeFollowingCreator(1L, "creator", "https://cdn/profile.jpg")),
onAirLives = listOf(
HomeFollowingLive(
liveId = 10L,
creatorProfileImageUrl = "https://cdn/live-profile.jpg",
creatorNickname = "live-creator",
title = "live title",
startedAtUtc = "2026-06-25T00:00:00Z"
)
),
recentChats = listOf(
ChatRoomListItemResponse(
roomId = 100L,
chatType = "DM",
targetName = "creator",
targetImageUrl = "https://cdn/chat.jpg",
lastMessage = "hello",
lastMessageAt = "2026-06-25T00:01:00Z"
)
),
monthlySchedules = listOf(
HomeFollowingSchedule(
scheduleId = "LIVE:20",
creatorId = 1L,
creatorProfileImageUrl = "https://cdn/schedule.jpg",
creatorNickname = "schedule-creator",
title = "schedule title",
type = CreatorActivityType.LIVE,
targetId = 20L,
scheduledAtUtc = "2026-06-26T00:00:00Z",
isOnAir = true
)
),
recentNews = listOf(
HomeFollowingNews(
newsId = "30",
type = FollowingNewsType.CREATOR_RANKING,
creatorProfileImageUrl = "https://cdn/news-profile.jpg",
creatorNickname = "news-creator",
title = "ranking",
body = "ranked",
thumbnailImageUrl = null,
targetId = 1L,
occurredAtUtc = "2026-06-25T00:00:00Z",
visibleFromAtUtc = "2026-06-25T09:00:00Z",
rank = 3
)
)
)
}
}