From e160a107086f1c72aac368f7e0ee572757237b8a Mon Sep 17 00:00:00 2001 From: klaus Date: Fri, 5 Jun 2026 23:04:40 +0900 Subject: [PATCH] =?UTF-8?q?feat(home):=20=EC=B6=94=EC=B2=9C=20=EB=B0=B0?= =?UTF-8?q?=EB=84=88=20=EC=9D=B4=EB=8F=99=EC=9D=84=20=EC=97=B0=EA=B2=B0?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sodalive/v2/main/HomeMainFragment.kt | 7 +- .../home/model/HomeRecommendationUiModels.kt | 63 +++++++++ .../main/home/HomeMainFragmentLayoutTest.kt | 122 ++++++++++++++++++ 3 files changed, 191 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/main/HomeMainFragment.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/main/HomeMainFragment.kt index 6d6b4df7..21c6a696 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/v2/main/HomeMainFragment.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/main/HomeMainFragment.kt @@ -33,6 +33,8 @@ import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationRecentlyAct import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationRecentlyActiveCreatorUiModel import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationRecentDebutCreatorSection import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationUiState +import kr.co.vividnext.sodalive.v2.main.home.model.toHomeRecommendationBannerIntent +import kr.co.vividnext.sodalive.v2.main.home.model.toHomeRecommendationBannerRoute import kr.co.vividnext.sodalive.v2.main.home.model.visibleHomeGenreCreatorGroups import kr.co.vividnext.sodalive.v2.main.home.ui.HomeAiCharacterAdapter import kr.co.vividnext.sodalive.v2.main.home.ui.HomeBannerBinder @@ -266,7 +268,10 @@ class HomeMainFragment : BaseFragment( private fun onLiveClick(item: HomeRecommendationLiveUiModel) = Unit - private fun onBannerClick(item: HomeRecommendationBannerUiModel) = Unit + private fun onBannerClick(item: HomeRecommendationBannerUiModel) { + val route = item.toHomeRecommendationBannerRoute() ?: return + startActivity(route.toHomeRecommendationBannerIntent(requireContext())) + } private fun openCreatorProfile(creatorId: Long) { startActivity( diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/model/HomeRecommendationUiModels.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/model/HomeRecommendationUiModels.kt index f311ff7f..98a7efb2 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/model/HomeRecommendationUiModels.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/model/HomeRecommendationUiModels.kt @@ -1,9 +1,18 @@ package kr.co.vividnext.sodalive.v2.main.home.model +import android.content.Context +import android.content.Intent +import android.net.Uri +import kr.co.vividnext.sodalive.BuildConfig +import kr.co.vividnext.sodalive.audio_content.series.detail.SeriesDetailActivity +import kr.co.vividnext.sodalive.common.Constants +import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity +import kr.co.vividnext.sodalive.settings.event.EventDetailActivity import kr.co.vividnext.sodalive.settings.event.EventItem import kr.co.vividnext.sodalive.v2.widget.AudioContentTag import kr.co.vividnext.sodalive.v2.widget.characterchatthumbnail.CharacterChatThumbnailItem import kr.co.vividnext.sodalive.v2.widget.feed.FeedItem +import java.util.Locale data class HomeRecommendationLiveSection( val items: List @@ -65,6 +74,60 @@ data class HomeRecommendationBannerUiModel( val link: String? ) +sealed interface HomeRecommendationBannerRoute { + data class Event(val eventItem: EventItem) : HomeRecommendationBannerRoute + + data class Creator(val creatorId: Long) : HomeRecommendationBannerRoute + + data class Series(val seriesId: Long) : HomeRecommendationBannerRoute + + data class Link(val url: String, val isWebUrl: Boolean) : HomeRecommendationBannerRoute +} + +fun HomeRecommendationBannerUiModel.toHomeRecommendationBannerRoute(): HomeRecommendationBannerRoute? { + eventItem?.let { return HomeRecommendationBannerRoute.Event(it) } + creatorId?.takeIf { it > 0 }?.let { return HomeRecommendationBannerRoute.Creator(it) } + seriesId?.takeIf { it > 0 }?.let { return HomeRecommendationBannerRoute.Series(it) } + + val routeLink = link?.trim()?.takeIf { it.isNotBlank() } ?: return null + val uri = runCatching { Uri.parse(routeLink) }.getOrNull() ?: return null + val scheme = uri.scheme?.lowercase(Locale.ROOT) ?: return null + val isWebUrl = scheme == "http" || scheme == "https" + val isInternalDeepLink = scheme == BuildConfig.APPSCHEME || + (isWebUrl && uri.host.equals(homeRecommendationAppLinkHost(), ignoreCase = true)) + if (!isWebUrl && !isInternalDeepLink) return null + return HomeRecommendationBannerRoute.Link( + url = routeLink, + isWebUrl = isWebUrl && !isInternalDeepLink + ) +} + +fun HomeRecommendationBannerRoute.toHomeRecommendationBannerIntent(context: Context): Intent { + return when (this) { + is HomeRecommendationBannerRoute.Event -> { + Intent(context, EventDetailActivity::class.java).apply { + putExtra(Constants.EXTRA_EVENT, eventItem) + } + } + + is HomeRecommendationBannerRoute.Creator -> { + Intent(context, UserProfileActivity::class.java).apply { + putExtra(Constants.EXTRA_USER_ID, creatorId) + } + } + + is HomeRecommendationBannerRoute.Series -> { + Intent(context, SeriesDetailActivity::class.java).apply { + putExtra(Constants.EXTRA_SERIES_ID, seriesId) + } + } + + is HomeRecommendationBannerRoute.Link -> Intent(Intent.ACTION_VIEW, Uri.parse(url)) + } +} + +private fun homeRecommendationAppLinkHost(): String = "${BuildConfig.APPSCHEME}.onelink.me" + data class HomeRecommendationRecentlyActiveCreatorUiModel( val nickname: String, val profileImage: String, diff --git a/app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragmentLayoutTest.kt b/app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragmentLayoutTest.kt index 745ce257..5842cd4c 100644 --- a/app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragmentLayoutTest.kt +++ b/app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragmentLayoutTest.kt @@ -22,7 +22,12 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.test.core.app.ApplicationProvider import kr.co.vividnext.sodalive.R +import kr.co.vividnext.sodalive.BuildConfig +import kr.co.vividnext.sodalive.audio_content.series.detail.SeriesDetailActivity +import kr.co.vividnext.sodalive.common.Constants import kr.co.vividnext.sodalive.common.formatUtcRelativeTimeText +import kr.co.vividnext.sodalive.explorer.profile.UserProfileActivity +import kr.co.vividnext.sodalive.settings.event.EventDetailActivity import kr.co.vividnext.sodalive.settings.event.EventItem import kr.co.vividnext.sodalive.v2.main.home.data.HomeActiveCreatorItem import kr.co.vividnext.sodalive.v2.main.home.data.HomeBannerItem @@ -31,6 +36,7 @@ import kr.co.vividnext.sodalive.v2.main.home.data.HomeFirstAudioContentItem import kr.co.vividnext.sodalive.v2.main.home.data.HomeGenreCreatorGroupItem import kr.co.vividnext.sodalive.v2.main.home.data.HomeLiveItem import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationBannerSection +import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationBannerRoute import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationBannerUiModel import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationFirstAudioContentUiModel import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationCreatorUiModel @@ -40,6 +46,8 @@ import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationLiveUiModel import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationPopularCommunityPostSection import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationPopularCommunityPostUiModel import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationPaidStatus +import kr.co.vividnext.sodalive.v2.main.home.model.toHomeRecommendationBannerIntent +import kr.co.vividnext.sodalive.v2.main.home.model.toHomeRecommendationBannerRoute import kr.co.vividnext.sodalive.v2.main.home.model.visibleHomePopularCommunityPosts import kr.co.vividnext.sodalive.v2.main.home.model.visibleHomeGenreCreatorGroups import kr.co.vividnext.sodalive.v2.main.home.data.HomePopularCommunityPostItem @@ -884,6 +892,106 @@ class HomeMainFragmentLayoutTest { assertEquals(listOf("second"), clickedItems) } + @Test + fun `home banner route uses event creator series link priority`() { + val eventItem = EventItem(id = 1L, thumbnailImageUrl = "https://example.com/event.png") + val banner = HomeRecommendationBannerUiModel( + imageUrl = "https://example.com/banner.png", + eventItem = eventItem, + creatorId = 2L, + seriesId = 3L, + link = "https://example.com" + ) + + val route = banner.toHomeRecommendationBannerRoute() + + assertEquals(HomeRecommendationBannerRoute.Event(eventItem), route) + } + + @Test + fun `home banner route maps creator series web and internal deeplink`() { + assertEquals( + HomeRecommendationBannerRoute.Creator(2L), + homeBanner(creatorId = 2L).toHomeRecommendationBannerRoute() + ) + assertEquals( + HomeRecommendationBannerRoute.Series(3L), + homeBanner(seriesId = 3L).toHomeRecommendationBannerRoute() + ) + assertEquals( + HomeRecommendationBannerRoute.Link(url = "https://example.com", isWebUrl = true), + homeBanner(link = "https://example.com").toHomeRecommendationBannerRoute() + ) + assertEquals( + HomeRecommendationBannerRoute.Link(url = "${BuildConfig.APPSCHEME}://series/3", isWebUrl = false), + homeBanner(link = "${BuildConfig.APPSCHEME}://series/3").toHomeRecommendationBannerRoute() + ) + assertEquals( + HomeRecommendationBannerRoute.Link( + url = "https://${BuildConfig.APPSCHEME}.onelink.me/RkTm?deep_link_value=series&deep_link_sub5=3", + isWebUrl = false + ), + homeBanner( + link = "https://${BuildConfig.APPSCHEME}.onelink.me/RkTm?deep_link_value=series&deep_link_sub5=3" + ).toHomeRecommendationBannerRoute() + ) + } + + @Test + fun `home banner route uses creator over series and series over link priority`() { + assertEquals( + HomeRecommendationBannerRoute.Creator(2L), + homeBanner( + creatorId = 2L, + seriesId = 3L, + link = "https://example.com" + ).toHomeRecommendationBannerRoute() + ) + assertEquals( + HomeRecommendationBannerRoute.Series(3L), + homeBanner( + seriesId = 3L, + link = "https://example.com" + ).toHomeRecommendationBannerRoute() + ) + } + + @Test + fun `home banner route creates activity and link intents with expected extras`() { + val context = ApplicationProvider.getApplicationContext() + val eventItem = EventItem(id = 1L, thumbnailImageUrl = "https://example.com/event.png") + val eventIntent = HomeRecommendationBannerRoute.Event(eventItem).toHomeRecommendationBannerIntent(context) + val creatorIntent = HomeRecommendationBannerRoute.Creator(2L).toHomeRecommendationBannerIntent(context) + val seriesIntent = HomeRecommendationBannerRoute.Series(3L).toHomeRecommendationBannerIntent(context) + val webIntent = HomeRecommendationBannerRoute.Link( + url = "https://example.com", + isWebUrl = true + ).toHomeRecommendationBannerIntent(context) + val deepLinkIntent = HomeRecommendationBannerRoute.Link( + url = "${BuildConfig.APPSCHEME}://series/3", + isWebUrl = false + ).toHomeRecommendationBannerIntent(context) + + assertEquals(EventDetailActivity::class.java.name, eventIntent.component?.className) + assertEquals(eventItem, eventIntent.getParcelableExtra(Constants.EXTRA_EVENT)) + assertEquals(UserProfileActivity::class.java.name, creatorIntent.component?.className) + assertEquals(2L, creatorIntent.getLongExtra(Constants.EXTRA_USER_ID, 0L)) + assertEquals(SeriesDetailActivity::class.java.name, seriesIntent.component?.className) + assertEquals(3L, seriesIntent.getLongExtra(Constants.EXTRA_SERIES_ID, 0L)) + assertEquals(android.content.Intent.ACTION_VIEW, webIntent.action) + assertEquals("https://example.com", webIntent.data.toString()) + assertEquals(android.content.Intent.ACTION_VIEW, deepLinkIntent.action) + assertEquals("${BuildConfig.APPSCHEME}://series/3", deepLinkIntent.data.toString()) + } + + @Test + fun `home banner route ignores blank malformed and scheme-less links`() { + assertEquals(null, homeBanner(link = "").toHomeRecommendationBannerRoute()) + assertEquals(null, homeBanner(link = "not a url").toHomeRecommendationBannerRoute()) + assertEquals(null, homeBanner(link = "example.com/path").toHomeRecommendationBannerRoute()) + assertEquals(null, homeBanner(link = "mailto:test@example.com").toHomeRecommendationBannerRoute()) + } + @Test fun `home popular community adapter does not load original image for locked paid post`() { val context = ApplicationProvider.getApplicationContext() @@ -1211,6 +1319,20 @@ class HomeMainFragmentLayoutTest { ) } + private fun homeBanner( + creatorId: Long? = null, + seriesId: Long? = null, + link: String? = null + ): HomeRecommendationBannerUiModel { + return HomeRecommendationBannerUiModel( + imageUrl = "https://example.com/banner.png", + eventItem = null, + creatorId = creatorId, + seriesId = seriesId, + link = link + ) + } + private fun popularCommunityPost(id: Long): HomeRecommendationPopularCommunityPostUiModel { return HomeRecommendationPopularCommunityPostUiModel( item = FeedItem.Community(