From 17b1305a953c2607e21f9ff3631663314f359425 Mon Sep 17 00:00:00 2001 From: Klaus Date: Tue, 30 Jun 2026 21:34:59 +0900 Subject: [PATCH] =?UTF-8?q?feat(home):=20=ED=8C=94=EB=A1=9C=EC=9E=89=20?= =?UTF-8?q?=EC=B5=9C=EA=B7=BC=20=EC=86=8C=EC=8B=9D=20=EC=9D=91=EB=8B=B5?= =?UTF-8?q?=EC=9D=84=20=EC=A4=91=EC=B2=A9=20=EA=B5=AC=EC=A1=B0=EB=A1=9C=20?= =?UTF-8?q?=EB=B0=94=EA=BE=BC=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../following/dto/HomeFollowingTabResponse.kt | 96 ++++++++++--- .../v2/home/following/domain/HomeFollowing.kt | 46 ++++-- .../application/HomeFollowingFacadeTest.kt | 15 +- .../dto/HomeFollowingTabResponseTest.kt | 131 +++++++++++++++--- 4 files changed, 240 insertions(+), 48 deletions(-) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/following/dto/HomeFollowingTabResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/following/dto/HomeFollowingTabResponse.kt index 8bd65e92..b5b1c557 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/following/dto/HomeFollowingTabResponse.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/following/dto/HomeFollowingTabResponse.kt @@ -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 +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/v2/home/following/domain/HomeFollowing.kt b/src/main/kotlin/kr/co/vividnext/sodalive/v2/home/following/domain/HomeFollowing.kt index eefa5a95..d9ecb27f 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/v2/home/following/domain/HomeFollowing.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/v2/home/following/domain/HomeFollowing.kt @@ -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 ) diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/following/application/HomeFollowingFacadeTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/following/application/HomeFollowingFacadeTest.kt index 64d97406..753e03ef 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/following/application/HomeFollowingFacadeTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/following/application/HomeFollowingFacadeTest.kt @@ -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" + ) ) ) ) diff --git a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/following/dto/HomeFollowingTabResponseTest.kt b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/following/dto/HomeFollowingTabResponseTest.kt index 10b7a5b0..93e5cd5c 100644 --- a/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/following/dto/HomeFollowingTabResponseTest.kt +++ b/src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/following/dto/HomeFollowingTabResponseTest.kt @@ -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 + ) ) ) )