feat(home): 추천 배너 이동을 연결한다

This commit is contained in:
2026-06-05 23:04:40 +09:00
parent cdcd938cdf
commit e160a10708
3 changed files with 191 additions and 1 deletions

View File

@@ -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.HomeRecommendationRecentlyActiveCreatorUiModel
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationRecentDebutCreatorSection 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.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.model.visibleHomeGenreCreatorGroups
import kr.co.vividnext.sodalive.v2.main.home.ui.HomeAiCharacterAdapter import kr.co.vividnext.sodalive.v2.main.home.ui.HomeAiCharacterAdapter
import kr.co.vividnext.sodalive.v2.main.home.ui.HomeBannerBinder import kr.co.vividnext.sodalive.v2.main.home.ui.HomeBannerBinder
@@ -266,7 +268,10 @@ class HomeMainFragment : BaseFragment<FragmentV2MainHomeBinding>(
private fun onLiveClick(item: HomeRecommendationLiveUiModel) = Unit 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) { private fun openCreatorProfile(creatorId: Long) {
startActivity( startActivity(

View File

@@ -1,9 +1,18 @@
package kr.co.vividnext.sodalive.v2.main.home.model 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.settings.event.EventItem
import kr.co.vividnext.sodalive.v2.widget.AudioContentTag import kr.co.vividnext.sodalive.v2.widget.AudioContentTag
import kr.co.vividnext.sodalive.v2.widget.characterchatthumbnail.CharacterChatThumbnailItem import kr.co.vividnext.sodalive.v2.widget.characterchatthumbnail.CharacterChatThumbnailItem
import kr.co.vividnext.sodalive.v2.widget.feed.FeedItem import kr.co.vividnext.sodalive.v2.widget.feed.FeedItem
import java.util.Locale
data class HomeRecommendationLiveSection( data class HomeRecommendationLiveSection(
val items: List<HomeRecommendationLiveUiModel> val items: List<HomeRecommendationLiveUiModel>
@@ -65,6 +74,60 @@ data class HomeRecommendationBannerUiModel(
val link: String? 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( data class HomeRecommendationRecentlyActiveCreatorUiModel(
val nickname: String, val nickname: String,
val profileImage: String, val profileImage: String,

View File

@@ -22,7 +22,12 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.test.core.app.ApplicationProvider import androidx.test.core.app.ApplicationProvider
import kr.co.vividnext.sodalive.R 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.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.settings.event.EventItem
import kr.co.vividnext.sodalive.v2.main.home.data.HomeActiveCreatorItem import kr.co.vividnext.sodalive.v2.main.home.data.HomeActiveCreatorItem
import kr.co.vividnext.sodalive.v2.main.home.data.HomeBannerItem 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.HomeGenreCreatorGroupItem
import kr.co.vividnext.sodalive.v2.main.home.data.HomeLiveItem 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.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.HomeRecommendationBannerUiModel
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationFirstAudioContentUiModel import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationFirstAudioContentUiModel
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationCreatorUiModel 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.HomeRecommendationPopularCommunityPostSection
import kr.co.vividnext.sodalive.v2.main.home.model.HomeRecommendationPopularCommunityPostUiModel 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.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.visibleHomePopularCommunityPosts
import kr.co.vividnext.sodalive.v2.main.home.model.visibleHomeGenreCreatorGroups import kr.co.vividnext.sodalive.v2.main.home.model.visibleHomeGenreCreatorGroups
import kr.co.vividnext.sodalive.v2.main.home.data.HomePopularCommunityPostItem import kr.co.vividnext.sodalive.v2.main.home.data.HomePopularCommunityPostItem
@@ -884,6 +892,106 @@ class HomeMainFragmentLayoutTest {
assertEquals(listOf("second"), clickedItems) 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<Context>()
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 @Test
fun `home popular community adapter does not load original image for locked paid post`() { fun `home popular community adapter does not load original image for locked paid post`() {
val context = ApplicationProvider.getApplicationContext<Context>() val context = ApplicationProvider.getApplicationContext<Context>()
@@ -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 { private fun popularCommunityPost(id: Long): HomeRecommendationPopularCommunityPostUiModel {
return HomeRecommendationPopularCommunityPostUiModel( return HomeRecommendationPopularCommunityPostUiModel(
item = FeedItem.Community( item = FeedItem.Community(