feat(home): 팔로잉 최근 소식 응답을 중첩 구조로 바꾼다

This commit is contained in:
2026-06-30 21:34:59 +09:00
parent 941dd3c3a8
commit 17b1305a95
4 changed files with 240 additions and 48 deletions

View File

@@ -5,7 +5,11 @@ 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.HomeFollowingCommunityPostNews
import kr.co.vividnext.sodalive.v2.home.following.domain.HomeFollowingContentNews
import kr.co.vividnext.sodalive.v2.home.following.domain.HomeFollowingContentRankingNews
import kr.co.vividnext.sodalive.v2.home.following.domain.HomeFollowingCreator
import kr.co.vividnext.sodalive.v2.home.following.domain.HomeFollowingCreatorRankingNews
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
@@ -112,31 +116,91 @@ data class FollowingScheduleResponse(
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?
val creatorRanking: FollowingCreatorRankingNewsResponse?,
val audioContent: FollowingContentNewsResponse?,
val photoContent: FollowingContentNewsResponse?,
val contentRanking: FollowingContentRankingNewsResponse?,
val communityPost: FollowingCommunityPostNewsResponse?
) {
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
creatorRanking = news.creatorRanking?.toResponse(),
audioContent = news.audioContent?.toContentResponse(),
photoContent = news.photoContent?.toContentResponse(),
contentRanking = news.contentRanking?.toResponse(),
communityPost = news.communityPost?.toResponse()
)
}
}
}
data class FollowingCreatorRankingNewsResponse(
val rank: Int,
val creatorId: Long,
val nickname: String,
val profileImageUrl: String
)
data class FollowingContentNewsResponse(
val contentId: Long,
val contentImageUrl: String?,
val title: String,
val creatorProfileImageUrl: String,
val creatorNickname: String
)
data class FollowingContentRankingNewsResponse(
val rank: Int,
val contentId: Long,
val contentImageUrl: String?,
val title: String
)
data class FollowingCommunityPostNewsResponse(
val postId: Long,
val creatorProfileImage: String,
val creatorNickname: String,
val imageUrl: String?,
val content: String,
val createdAt: String,
val likeCount: Int,
val commentCount: Int
)
private fun HomeFollowingCreatorRankingNews.toResponse() = FollowingCreatorRankingNewsResponse(
rank = rank,
creatorId = creatorId,
nickname = nickname,
profileImageUrl = profileImageUrl
)
private fun HomeFollowingContentNews.toContentResponse() = FollowingContentNewsResponse(
contentId = contentId,
contentImageUrl = contentImageUrl,
title = title,
creatorProfileImageUrl = creatorProfileImageUrl,
creatorNickname = creatorNickname
)
private fun HomeFollowingContentRankingNews.toResponse() = FollowingContentRankingNewsResponse(
rank = rank,
contentId = contentId,
contentImageUrl = contentImageUrl,
title = title
)
private fun HomeFollowingCommunityPostNews.toResponse() = FollowingCommunityPostNewsResponse(
postId = postId,
creatorProfileImage = creatorProfileImage,
creatorNickname = creatorNickname,
imageUrl = imageUrl,
content = content,
createdAt = createdAt,
likeCount = likeCount,
commentCount = commentCount
)

View File

@@ -40,13 +40,43 @@ data class HomeFollowingSchedule(
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?
val creatorRanking: HomeFollowingCreatorRankingNews? = null,
val audioContent: HomeFollowingContentNews? = null,
val photoContent: HomeFollowingContentNews? = null,
val contentRanking: HomeFollowingContentRankingNews? = null,
val communityPost: HomeFollowingCommunityPostNews? = null
)
data class HomeFollowingCreatorRankingNews(
val rank: Int,
val creatorId: Long,
val nickname: String,
val profileImageUrl: String
)
data class HomeFollowingContentNews(
val contentId: Long,
val contentImageUrl: String?,
val title: String,
val creatorProfileImageUrl: String,
val creatorNickname: String
)
data class HomeFollowingContentRankingNews(
val rank: Int,
val contentId: Long,
val contentImageUrl: String?,
val title: String
)
data class HomeFollowingCommunityPostNews(
val postId: Long,
val creatorProfileImage: String,
val creatorNickname: String,
val imageUrl: String?,
val content: String,
val createdAt: String,
val likeCount: Int,
val commentCount: Int
)

View File

@@ -10,6 +10,7 @@ import kr.co.vividnext.sodalive.v2.home.following.application.HomeFollowingQuery
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.HomeFollowingCreatorRankingNews
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
@@ -109,15 +110,13 @@ class HomeFollowingFacadeTest {
HomeFollowingNews(
newsId = "news-5",
type = FollowingNewsType.CREATOR_RANKING,
creatorProfileImageUrl = "https://cdn.test/news.png",
creatorNickname = "creator",
title = "news",
body = "body",
thumbnailImageUrl = null,
targetId = 1L,
occurredAtUtc = "2026-06-25T03:00:00Z",
visibleFromAtUtc = "2026-06-25T04:00:00Z",
rank = 7
creatorRanking = HomeFollowingCreatorRankingNews(
rank = 7,
creatorId = 1L,
nickname = "creator",
profileImageUrl = "https://cdn.test/news.png"
)
)
)
)

View File

@@ -5,7 +5,11 @@ 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.HomeFollowingCommunityPostNews
import kr.co.vividnext.sodalive.v2.home.following.domain.HomeFollowingContentNews
import kr.co.vividnext.sodalive.v2.home.following.domain.HomeFollowingContentRankingNews
import kr.co.vividnext.sodalive.v2.home.following.domain.HomeFollowingCreator
import kr.co.vividnext.sodalive.v2.home.following.domain.HomeFollowingCreatorRankingNews
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
@@ -39,24 +43,71 @@ class HomeFollowingTabResponseTest {
}
@Test
@DisplayName("팔로잉 탭 도메인은 creatorId 없는 최근 소식과 nullable rank 응답으로 변환한다")
fun shouldMapDomainToResponseWithoutCreatorIdInRecentNews() {
@DisplayName("팔로잉 탭 도메인은 타입별 nested 최근 소식 응답으로 변환한다")
fun shouldMapDomainToNestedRecentNewsResponseByType() {
val response = HomeFollowingTabResponse.from(createHomeFollowing())
val json = objectMapper.readTree(objectMapper.writeValueAsString(response))
val recentNews = json["recentNews"]
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())
val removedTopLevelFields = listOf(
"creatorProfileImageUrl",
"creatorNickname",
"title",
"body",
"thumbnailImageUrl",
"targetId",
"occurredAtUtc",
"rank"
)
removedTopLevelFields.forEach { field ->
assertFalse(recentNews[0].has(field), "recentNews top-level must not expose $field")
}
assertEquals("30", recentNews[0]["newsId"].asText())
assertEquals("CREATOR_RANKING", recentNews[0]["type"].asText())
assertEquals("2026-06-25T09:00:00Z", recentNews[0]["visibleFromAtUtc"].asText())
assertEquals(3, recentNews[0]["creatorRanking"]["rank"].asInt())
assertEquals(1L, recentNews[0]["creatorRanking"]["creatorId"].asLong())
assertEquals("news-creator", recentNews[0]["creatorRanking"]["nickname"].asText())
assertTrue(recentNews[0]["audioContent"].isNull)
assertTrue(recentNews[0]["photoContent"].isNull)
assertTrue(recentNews[0]["contentRanking"].isNull)
assertTrue(recentNews[0]["communityPost"].isNull)
assertEquals("AUDIO_CONTENT", recentNews[1]["type"].asText())
assertEquals(200L, recentNews[1]["audioContent"]["contentId"].asLong())
assertEquals("audio title", recentNews[1]["audioContent"]["title"].asText())
assertFalse(recentNews[1]["audioContent"].has("releaseDate"))
assertTrue(recentNews[1]["creatorRanking"].isNull)
assertTrue(recentNews[1]["communityPost"].isNull)
assertEquals("PHOTO_CONTENT", recentNews[2]["type"].asText())
assertEquals(300L, recentNews[2]["photoContent"]["contentId"].asLong())
assertEquals("photo title", recentNews[2]["photoContent"]["title"].asText())
assertTrue(recentNews[2]["audioContent"].isNull)
assertEquals("CONTENT_RANKING", recentNews[3]["type"].asText())
assertEquals(5, recentNews[3]["contentRanking"]["rank"].asInt())
assertEquals(400L, recentNews[3]["contentRanking"]["contentId"].asLong())
assertTrue(recentNews[3]["creatorRanking"].isNull)
assertEquals("COMMUNITY_POST", recentNews[4]["type"].asText())
assertEquals(500L, recentNews[4]["communityPost"]["postId"].asLong())
assertEquals("https://cdn/community-profile.jpg", recentNews[4]["communityPost"]["creatorProfileImage"].asText())
assertEquals("community creator", recentNews[4]["communityPost"]["creatorNickname"].asText())
assertTrue(recentNews[4]["communityPost"]["imageUrl"].isNull)
assertEquals("community body", recentNews[4]["communityPost"]["content"].asText())
assertEquals("2026-06-25T03:00:00Z", recentNews[4]["communityPost"]["createdAt"].asText())
assertEquals(11, recentNews[4]["communityPost"]["likeCount"].asInt())
assertEquals(2, recentNews[4]["communityPost"]["commentCount"].asInt())
}
private fun createHomeFollowing(): HomeFollowing {
@@ -98,15 +149,63 @@ class HomeFollowingTabResponseTest {
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
creatorRanking = HomeFollowingCreatorRankingNews(
rank = 3,
creatorId = 1L,
nickname = "news-creator",
profileImageUrl = "https://cdn/news-profile.jpg"
)
),
HomeFollowingNews(
newsId = "31",
type = FollowingNewsType.AUDIO_CONTENT,
visibleFromAtUtc = "2026-06-26T00:00:00Z",
audioContent = HomeFollowingContentNews(
contentId = 200L,
contentImageUrl = "https://cdn/audio.jpg",
title = "audio title",
creatorProfileImageUrl = "https://cdn/audio-profile.jpg",
creatorNickname = "audio creator"
)
),
HomeFollowingNews(
newsId = "32",
type = FollowingNewsType.PHOTO_CONTENT,
visibleFromAtUtc = "2026-06-27T00:00:00Z",
photoContent = HomeFollowingContentNews(
contentId = 300L,
contentImageUrl = "https://cdn/photo.jpg",
title = "photo title",
creatorProfileImageUrl = "https://cdn/photo-profile.jpg",
creatorNickname = "photo creator"
)
),
HomeFollowingNews(
newsId = "33",
type = FollowingNewsType.CONTENT_RANKING,
visibleFromAtUtc = "2026-06-28T00:00:00Z",
contentRanking = HomeFollowingContentRankingNews(
rank = 5,
contentId = 400L,
contentImageUrl = "https://cdn/ranking.jpg",
title = "content ranking"
)
),
HomeFollowingNews(
newsId = "34",
type = FollowingNewsType.COMMUNITY_POST,
visibleFromAtUtc = "2026-06-25T03:00:00Z",
communityPost = HomeFollowingCommunityPostNews(
postId = 500L,
creatorProfileImage = "https://cdn/community-profile.jpg",
creatorNickname = "community creator",
imageUrl = null,
content = "community body",
createdAt = "2026-06-25T03:00:00Z",
likeCount = 11,
commentCount = 2
)
)
)
)