fix(home): 메인 홈 추천 탭 지금 라이브 중 입장을 연결한다

This commit is contained in:
2026-07-06 02:34:41 +09:00
parent b1f605fda4
commit 5489c86cdc
3 changed files with 157 additions and 45 deletions

View File

@@ -3,6 +3,7 @@ package kr.co.vividnext.sodalive.v2.creator.channel
import android.content.Intent import android.content.Intent
import android.view.LayoutInflater import android.view.LayoutInflater
import android.widget.Toast import android.widget.Toast
import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import kr.co.vividnext.sodalive.R import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.audio_content.AudioContentPlayService import kr.co.vividnext.sodalive.audio_content.AudioContentPlayService
@@ -27,7 +28,7 @@ import java.util.Locale
import java.util.TimeZone import java.util.TimeZone
class CreatorChannelLiveCoordinator( class CreatorChannelLiveCoordinator(
private val activity: CreatorChannelActivity, private val activity: FragmentActivity,
private val layoutInflater: LayoutInflater, private val layoutInflater: LayoutInflater,
private val fragmentManager: FragmentManager, private val fragmentManager: FragmentManager,
private val liveViewModel: LiveViewModel, private val liveViewModel: LiveViewModel,
@@ -65,11 +66,38 @@ class CreatorChannelLiveCoordinator(
} }
liveViewModel.getRoomDetail(roomId) { liveViewModel.getRoomDetail(roomId) {
if (!it.channelName.isNullOrBlank()) { enterLiveRoom(roomId, it, onEnterRoomSuccess)
if (it.manager.id == SharedPreferenceManager.userId) { }
}
fun enterLiveRoom(roomId: Long, roomDetail: GetRoomDetailResponse) {
activity.startService(
Intent(activity, AudioContentPlayService::class.java).apply {
action = AudioContentPlayService.MusicAction.STOP.name
}
)
activity.startService(
Intent(activity, AudioContentPlayerService::class.java).apply {
action = "STOP_SERVICE"
}
)
val onEnterRoomSuccess = {
activity.runOnUiThread { openLiveRoom(roomId) }
}
enterLiveRoom(roomId, roomDetail, onEnterRoomSuccess)
}
private fun enterLiveRoom(
roomId: Long,
roomDetail: GetRoomDetailResponse,
onEnterRoomSuccess: () -> Unit
) {
if (!roomDetail.channelName.isNullOrBlank()) {
if (roomDetail.manager.id == SharedPreferenceManager.userId) {
liveViewModel.enterRoom(roomId, onEnterRoomSuccess) liveViewModel.enterRoom(roomId, onEnterRoomSuccess)
} else if (it.price == 0 || it.isPaid) { } else if (roomDetail.price == 0 || roomDetail.isPaid) {
if (it.isPrivateRoom) { if (roomDetail.isPrivateRoom) {
LiveRoomPasswordDialog( LiveRoomPasswordDialog(
activity = activity, activity = activity,
layoutInflater = layoutInflater, layoutInflater = layoutInflater,
@@ -88,9 +116,9 @@ class CreatorChannelLiveCoordinator(
} else { } else {
showPaidLiveEntryDialog( showPaidLiveEntryDialog(
roomId = roomId, roomId = roomId,
beginDateTimeUtc = it.beginDateTimeUtc, beginDateTimeUtc = roomDetail.beginDateTimeUtc,
price = it.price, price = roomDetail.price,
isPrivateRoom = it.isPrivateRoom, isPrivateRoom = roomDetail.isPrivateRoom,
onEnterRoomSuccess = onEnterRoomSuccess onEnterRoomSuccess = onEnterRoomSuccess
) )
} }
@@ -98,7 +126,6 @@ class CreatorChannelLiveCoordinator(
showLiveRoomDetail(roomId) showLiveRoomDetail(roomId)
} }
} }
}
private fun reservationRoom(roomId: Long) { private fun reservationRoom(roomId: Long) {
liveViewModel.getRoomDetail(roomId) { liveViewModel.getRoomDetail(roomId) {

View File

@@ -17,9 +17,11 @@ import kr.co.vividnext.sodalive.explorer.profile.creator_community.all.CreatorCo
import kr.co.vividnext.sodalive.following.FollowingCreatorActivity import kr.co.vividnext.sodalive.following.FollowingCreatorActivity
import kr.co.vividnext.sodalive.chat.talk.room.ChatRoomActivity import kr.co.vividnext.sodalive.chat.talk.room.ChatRoomActivity
import kr.co.vividnext.sodalive.home.pushnotification.PushNotificationListActivity import kr.co.vividnext.sodalive.home.pushnotification.PushNotificationListActivity
import kr.co.vividnext.sodalive.live.LiveViewModel
import kr.co.vividnext.sodalive.mypage.can.charge.CanChargeActivity import kr.co.vividnext.sodalive.mypage.can.charge.CanChargeActivity
import kr.co.vividnext.sodalive.search.SearchActivity import kr.co.vividnext.sodalive.search.SearchActivity
import kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivity import kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelActivity
import kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelLiveCoordinator
import kr.co.vividnext.sodalive.v2.live.onair.HomeOnAirLiveActivity import kr.co.vividnext.sodalive.v2.live.onair.HomeOnAirLiveActivity
import kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomActivity import kr.co.vividnext.sodalive.v2.main.chat.dm.DmChatRoomActivity
import kr.co.vividnext.sodalive.v2.main.chat.model.ChatRoomListUiItem import kr.co.vividnext.sodalive.v2.main.chat.model.ChatRoomListUiItem
@@ -74,6 +76,7 @@ import kr.co.vividnext.sodalive.v2.main.home.ui.HomeRecentDebutCreatorAdapter
import kr.co.vividnext.sodalive.v2.widget.creatorranking.CreatorRankingAdapter import kr.co.vividnext.sodalive.v2.widget.creatorranking.CreatorRankingAdapter
import kr.co.vividnext.sodalive.v2.widget.creatorranking.CreatorRankingItem import kr.co.vividnext.sodalive.v2.widget.creatorranking.CreatorRankingItem
import kr.co.vividnext.sodalive.v2.widget.feed.FeedItem import kr.co.vividnext.sodalive.v2.widget.feed.FeedItem
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.androidx.viewmodel.ext.android.viewModel
class HomeMainFragment : BaseFragment<FragmentV2MainHomeBinding>( class HomeMainFragment : BaseFragment<FragmentV2MainHomeBinding>(
@@ -82,7 +85,18 @@ class HomeMainFragment : BaseFragment<FragmentV2MainHomeBinding>(
private val homeRecommendationViewModel: HomeRecommendationViewModel by viewModel() private val homeRecommendationViewModel: HomeRecommendationViewModel by viewModel()
private val homeCreatorRankingViewModel: HomeCreatorRankingViewModel by viewModel() private val homeCreatorRankingViewModel: HomeCreatorRankingViewModel by viewModel()
private val homeFollowingViewModel: HomeFollowingViewModel by viewModel() private val homeFollowingViewModel: HomeFollowingViewModel by viewModel()
private val liveViewModel: LiveViewModel by inject()
private val loadingDialog: LoadingDialog by lazy { LoadingDialog(requireActivity(), layoutInflater) } private val loadingDialog: LoadingDialog by lazy { LoadingDialog(requireActivity(), layoutInflater) }
private val liveCoordinator: CreatorChannelLiveCoordinator by lazy {
CreatorChannelLiveCoordinator(
activity = requireActivity(),
layoutInflater = layoutInflater,
fragmentManager = requireActivity().supportFragmentManager,
liveViewModel = liveViewModel,
screenWidthProvider = { screenWidth },
refreshHome = { homeRecommendationViewModel.loadRecommendations() }
)
}
private val liveAdapter = HomeLiveAdapter() private val liveAdapter = HomeLiveAdapter()
private val recentActivityCreatorAdapter = HomeRecentActivityCreatorAdapter { onRecentActivityClick(it) } private val recentActivityCreatorAdapter = HomeRecentActivityCreatorAdapter { onRecentActivityClick(it) }
private val recentDebutCreatorAdapter = HomeRecentDebutCreatorAdapter { openCreatorProfile(it.creatorId) } private val recentDebutCreatorAdapter = HomeRecentDebutCreatorAdapter { openCreatorProfile(it.creatorId) }
@@ -111,6 +125,10 @@ class HomeMainFragment : BaseFragment<FragmentV2MainHomeBinding>(
private var hasLoadedCreatorRankings = false private var hasLoadedCreatorRankings = false
private var hasLoadedFollowing = false private var hasLoadedFollowing = false
private var currentHomeTabIndex = HOME_TAB_RECOMMENDATION private var currentHomeTabIndex = HOME_TAB_RECOMMENDATION
private var isRecommendationLoading = false
private var isCreatorRankingLoading = false
private var isFollowingLoading = false
private var isLiveEntryLoading = false
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
@@ -138,6 +156,7 @@ class HomeMainFragment : BaseFragment<FragmentV2MainHomeBinding>(
bindHomeRecommendationObservers() bindHomeRecommendationObservers()
bindHomeCreatorRankingObservers() bindHomeCreatorRankingObservers()
bindHomeFollowingObservers() bindHomeFollowingObservers()
bindLiveEntryObservers()
homeRecommendationViewModel.loadRecommendations() homeRecommendationViewModel.loadRecommendations()
} }
@@ -252,11 +271,8 @@ class HomeMainFragment : BaseFragment<FragmentV2MainHomeBinding>(
} }
} }
homeCreatorRankingViewModel.isLoading.observe(viewLifecycleOwner) { isLoading -> homeCreatorRankingViewModel.isLoading.observe(viewLifecycleOwner) { isLoading ->
if (isLoading) { isCreatorRankingLoading = isLoading
loadingDialog.show(screenWidth) updateLoadingDialog()
} else {
loadingDialog.dismiss()
}
} }
homeCreatorRankingViewModel.toastLiveData.observe(viewLifecycleOwner) { toastMessage -> homeCreatorRankingViewModel.toastLiveData.observe(viewLifecycleOwner) { toastMessage ->
toastMessage?.let(::showToast) toastMessage?.let(::showToast)
@@ -315,11 +331,8 @@ class HomeMainFragment : BaseFragment<FragmentV2MainHomeBinding>(
} }
} }
homeFollowingViewModel.isLoading.observe(viewLifecycleOwner) { isLoading -> homeFollowingViewModel.isLoading.observe(viewLifecycleOwner) { isLoading ->
if (isLoading) { isFollowingLoading = isLoading
loadingDialog.show(screenWidth) updateLoadingDialog()
} else {
loadingDialog.dismiss()
}
} }
homeFollowingViewModel.toastLiveData.observe(viewLifecycleOwner) { toastMessage -> homeFollowingViewModel.toastLiveData.observe(viewLifecycleOwner) { toastMessage ->
toastMessage?.let(::showToast) toastMessage?.let(::showToast)
@@ -337,17 +350,32 @@ class HomeMainFragment : BaseFragment<FragmentV2MainHomeBinding>(
} }
} }
homeRecommendationViewModel.isLoading.observe(viewLifecycleOwner) { isLoading -> homeRecommendationViewModel.isLoading.observe(viewLifecycleOwner) { isLoading ->
if (isLoading) { isRecommendationLoading = isLoading
loadingDialog.show(screenWidth) updateLoadingDialog()
} else {
loadingDialog.dismiss()
}
} }
homeRecommendationViewModel.toastLiveData.observe(viewLifecycleOwner) { toastMessage -> homeRecommendationViewModel.toastLiveData.observe(viewLifecycleOwner) { toastMessage ->
toastMessage?.let(::showToast) toastMessage?.let(::showToast)
} }
} }
private fun bindLiveEntryObservers() {
liveViewModel.isLoading.observe(viewLifecycleOwner) { isLoading ->
isLiveEntryLoading = isLoading
updateLoadingDialog()
}
liveViewModel.toastLiveData.observe(viewLifecycleOwner) { message ->
message?.let { showToast(it) }
}
}
private fun updateLoadingDialog() {
if (isRecommendationLoading || isCreatorRankingLoading || isFollowingLoading || isLiveEntryLoading) {
loadingDialog.show(screenWidth)
} else {
loadingDialog.dismiss()
}
}
private fun bindHomeRecommendationContent(content: HomeRecommendationUiState.Content) { private fun bindHomeRecommendationContent(content: HomeRecommendationUiState.Content) {
bindLiveSection(content.lives) bindLiveSection(content.lives)
bindBannerSection(content.banners) bindBannerSection(content.banners)
@@ -514,7 +542,16 @@ class HomeMainFragment : BaseFragment<FragmentV2MainHomeBinding>(
ivSectionTitleChevron.visibility = if (showMore) View.VISIBLE else View.GONE ivSectionTitleChevron.visibility = if (showMore) View.VISIBLE else View.GONE
} }
private fun onLiveClick(item: HomeRecommendationLiveUiModel) = Unit private fun onLiveClick(item: HomeRecommendationLiveUiModel) {
ensureMainV2NavigationAllowed {
liveViewModel.getRoomDetail(item.roomId) { roomDetail ->
if (!isAdded || view == null) return@getRoomDetail
ensureMainV2NavigationAllowed(requiresAdultContentAccess = roomDetail.isAdult) {
liveCoordinator.enterLiveRoom(item.roomId, roomDetail)
}
}
}
}
private fun openHomeOnAirLive() { private fun openHomeOnAirLive() {
ensureMainV2NavigationAllowed { ensureMainV2NavigationAllowed {

View File

@@ -77,6 +77,46 @@ class HomeMainFragmentLoginGuardSourceTest {
assertTrue(showHomeTabSource.contains("currentHomeTabIndex = index")) assertTrue(showHomeTabSource.contains("currentHomeTabIndex = index"))
} }
@Test
fun `HomeMainFragment 추천 라이브 아이템은 로그인과 성인 가드 후 기존 입장 흐름을 실행한다`() {
val source = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragment.kt"
).readText()
val clickSource = source.substringFrom("private fun onLiveClick(item: HomeRecommendationLiveUiModel)")
assertTrue(clickSource.contains("ensureMainV2NavigationAllowed"))
assertTrue(clickSource.contains("liveViewModel.getRoomDetail(item.roomId) { roomDetail ->"))
assertBefore(clickSource, "if (!isAdded || view == null) return@getRoomDetail", "requiresAdultContentAccess = roomDetail.isAdult")
assertBefore(clickSource, "liveViewModel.getRoomDetail(item.roomId) { roomDetail ->", "requiresAdultContentAccess = roomDetail.isAdult")
assertBefore(clickSource, "requiresAdultContentAccess = roomDetail.isAdult", "liveCoordinator.enterLiveRoom(item.roomId, roomDetail)")
assertFalse(clickSource.contains("= Unit"))
assertTrue(source.contains("import kr.co.vividnext.sodalive.v2.creator.channel.CreatorChannelLiveCoordinator"))
assertTrue(source.contains("private val liveCoordinator: CreatorChannelLiveCoordinator by lazy"))
}
@Test
fun `HomeMainFragment loading dialog는 통합 loading state로 제어한다`() {
val source = projectFile(
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragment.kt"
).readText()
assertTrue(source.contains("private var isRecommendationLoading = false"))
assertTrue(source.contains("private var isCreatorRankingLoading = false"))
assertTrue(source.contains("private var isFollowingLoading = false"))
assertTrue(source.contains("private var isLiveEntryLoading = false"))
val updateSource = source.substringFrom("private fun updateLoadingDialog()")
assertTrue(updateSource.contains("isRecommendationLoading || isCreatorRankingLoading || isFollowingLoading || isLiveEntryLoading"))
assertTrue(updateSource.contains("loadingDialog.show(screenWidth)"))
assertTrue(updateSource.contains("loadingDialog.dismiss()"))
assertLoadingObserverUsesUpdate(source, "homeRecommendationViewModel.isLoading.observe(viewLifecycleOwner)", "isRecommendationLoading = isLoading")
assertLoadingObserverUsesUpdate(source, "homeCreatorRankingViewModel.isLoading.observe(viewLifecycleOwner)", "isCreatorRankingLoading = isLoading")
assertLoadingObserverUsesUpdate(source, "homeFollowingViewModel.isLoading.observe(viewLifecycleOwner)", "isFollowingLoading = isLoading")
assertLoadingObserverUsesUpdate(source, "liveViewModel.isLoading.observe(viewLifecycleOwner)", "isLiveEntryLoading = isLoading")
}
private fun assertGuardedStartActivity(source: String, functionSignature: String) { private fun assertGuardedStartActivity(source: String, functionSignature: String) {
val functionSource = source.substringFrom(functionSignature) val functionSource = source.substringFrom(functionSignature)
assertTrue( assertTrue(
@@ -101,6 +141,14 @@ class HomeMainFragmentLoginGuardSourceTest {
assertTrue("Missing source after $expectedBefore: $expectedAfter", afterIndex > beforeIndex) assertTrue("Missing source after $expectedBefore: $expectedAfter", afterIndex > beforeIndex)
} }
private fun assertLoadingObserverUsesUpdate(source: String, observerMarker: String, loadingAssignment: String) {
val observerSource = source.substringFrom(observerMarker)
assertTrue(observerSource.contains(loadingAssignment))
assertTrue(observerSource.contains("updateLoadingDialog()"))
assertFalse(observerSource.contains("loadingDialog.show(screenWidth)"))
assertFalse(observerSource.contains("loadingDialog.dismiss()"))
}
private fun String.substringFrom(marker: String): String { private fun String.substringFrom(marker: String): String {
val startIndex = indexOf(marker) val startIndex = indexOf(marker)
assertTrue("Missing function: $marker", startIndex >= 0) assertTrue("Missing function: $marker", startIndex >= 0)