From 5489c86cdc03a9ae651966b8231b21c2b2b5a997 Mon Sep 17 00:00:00 2001 From: klaus Date: Mon, 6 Jul 2026 02:34:41 +0900 Subject: [PATCH] =?UTF-8?q?fix(home):=20=EB=A9=94=EC=9D=B8=20=ED=99=88=20?= =?UTF-8?q?=EC=B6=94=EC=B2=9C=20=ED=83=AD=20=EC=A7=80=EA=B8=88=20=EB=9D=BC?= =?UTF-8?q?=EC=9D=B4=EB=B8=8C=20=EC=A4=91=20=EC=9E=85=EC=9E=A5=EC=9D=84=20?= =?UTF-8?q?=EC=97=B0=EA=B2=B0=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../channel/CreatorChannelLiveCoordinator.kt | 85 ++++++++++++------- .../sodalive/v2/main/home/HomeMainFragment.kt | 69 +++++++++++---- .../HomeMainFragmentLoginGuardSourceTest.kt | 48 +++++++++++ 3 files changed, 157 insertions(+), 45 deletions(-) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelLiveCoordinator.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelLiveCoordinator.kt index 4de3c76a..71c86117 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelLiveCoordinator.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/creator/channel/CreatorChannelLiveCoordinator.kt @@ -3,6 +3,7 @@ package kr.co.vividnext.sodalive.v2.creator.channel import android.content.Intent import android.view.LayoutInflater import android.widget.Toast +import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentManager import kr.co.vividnext.sodalive.R import kr.co.vividnext.sodalive.audio_content.AudioContentPlayService @@ -27,7 +28,7 @@ import java.util.Locale import java.util.TimeZone class CreatorChannelLiveCoordinator( - private val activity: CreatorChannelActivity, + private val activity: FragmentActivity, private val layoutInflater: LayoutInflater, private val fragmentManager: FragmentManager, private val liveViewModel: LiveViewModel, @@ -65,38 +66,64 @@ class CreatorChannelLiveCoordinator( } liveViewModel.getRoomDetail(roomId) { - if (!it.channelName.isNullOrBlank()) { - if (it.manager.id == SharedPreferenceManager.userId) { - liveViewModel.enterRoom(roomId, onEnterRoomSuccess) - } else if (it.price == 0 || it.isPaid) { - if (it.isPrivateRoom) { - LiveRoomPasswordDialog( - activity = activity, - layoutInflater = layoutInflater, - can = 0, - confirmButtonClick = { password -> - liveViewModel.enterRoom( - roomId = roomId, - onSuccess = onEnterRoomSuccess, - password = password - ) - } - ).show(screenWidthProvider()) - } else { - liveViewModel.enterRoom(roomId, onEnterRoomSuccess) - } + enterLiveRoom(roomId, it, onEnterRoomSuccess) + } + } + + 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) + } else if (roomDetail.price == 0 || roomDetail.isPaid) { + if (roomDetail.isPrivateRoom) { + LiveRoomPasswordDialog( + activity = activity, + layoutInflater = layoutInflater, + can = 0, + confirmButtonClick = { password -> + liveViewModel.enterRoom( + roomId = roomId, + onSuccess = onEnterRoomSuccess, + password = password + ) + } + ).show(screenWidthProvider()) } else { - showPaidLiveEntryDialog( - roomId = roomId, - beginDateTimeUtc = it.beginDateTimeUtc, - price = it.price, - isPrivateRoom = it.isPrivateRoom, - onEnterRoomSuccess = onEnterRoomSuccess - ) + liveViewModel.enterRoom(roomId, onEnterRoomSuccess) } } else { - showLiveRoomDetail(roomId) + showPaidLiveEntryDialog( + roomId = roomId, + beginDateTimeUtc = roomDetail.beginDateTimeUtc, + price = roomDetail.price, + isPrivateRoom = roomDetail.isPrivateRoom, + onEnterRoomSuccess = onEnterRoomSuccess + ) } + } else { + showLiveRoomDetail(roomId) } } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragment.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragment.kt index 9edd61fb..4d628d76 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragment.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragment.kt @@ -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.chat.talk.room.ChatRoomActivity 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.search.SearchActivity 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.main.chat.dm.DmChatRoomActivity 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.CreatorRankingItem import kr.co.vividnext.sodalive.v2.widget.feed.FeedItem +import org.koin.android.ext.android.inject import org.koin.androidx.viewmodel.ext.android.viewModel class HomeMainFragment : BaseFragment( @@ -82,7 +85,18 @@ class HomeMainFragment : BaseFragment( private val homeRecommendationViewModel: HomeRecommendationViewModel by viewModel() private val homeCreatorRankingViewModel: HomeCreatorRankingViewModel 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 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 recentActivityCreatorAdapter = HomeRecentActivityCreatorAdapter { onRecentActivityClick(it) } private val recentDebutCreatorAdapter = HomeRecentDebutCreatorAdapter { openCreatorProfile(it.creatorId) } @@ -111,6 +125,10 @@ class HomeMainFragment : BaseFragment( private var hasLoadedCreatorRankings = false private var hasLoadedFollowing = false 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?) { super.onViewCreated(view, savedInstanceState) @@ -138,6 +156,7 @@ class HomeMainFragment : BaseFragment( bindHomeRecommendationObservers() bindHomeCreatorRankingObservers() bindHomeFollowingObservers() + bindLiveEntryObservers() homeRecommendationViewModel.loadRecommendations() } @@ -252,11 +271,8 @@ class HomeMainFragment : BaseFragment( } } homeCreatorRankingViewModel.isLoading.observe(viewLifecycleOwner) { isLoading -> - if (isLoading) { - loadingDialog.show(screenWidth) - } else { - loadingDialog.dismiss() - } + isCreatorRankingLoading = isLoading + updateLoadingDialog() } homeCreatorRankingViewModel.toastLiveData.observe(viewLifecycleOwner) { toastMessage -> toastMessage?.let(::showToast) @@ -315,11 +331,8 @@ class HomeMainFragment : BaseFragment( } } homeFollowingViewModel.isLoading.observe(viewLifecycleOwner) { isLoading -> - if (isLoading) { - loadingDialog.show(screenWidth) - } else { - loadingDialog.dismiss() - } + isFollowingLoading = isLoading + updateLoadingDialog() } homeFollowingViewModel.toastLiveData.observe(viewLifecycleOwner) { toastMessage -> toastMessage?.let(::showToast) @@ -337,17 +350,32 @@ class HomeMainFragment : BaseFragment( } } homeRecommendationViewModel.isLoading.observe(viewLifecycleOwner) { isLoading -> - if (isLoading) { - loadingDialog.show(screenWidth) - } else { - loadingDialog.dismiss() - } + isRecommendationLoading = isLoading + updateLoadingDialog() } homeRecommendationViewModel.toastLiveData.observe(viewLifecycleOwner) { toastMessage -> 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) { bindLiveSection(content.lives) bindBannerSection(content.banners) @@ -514,7 +542,16 @@ class HomeMainFragment : BaseFragment( 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() { ensureMainV2NavigationAllowed { diff --git a/app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragmentLoginGuardSourceTest.kt b/app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragmentLoginGuardSourceTest.kt index 1b8f509d..073d04b9 100644 --- a/app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragmentLoginGuardSourceTest.kt +++ b/app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragmentLoginGuardSourceTest.kt @@ -77,6 +77,46 @@ class HomeMainFragmentLoginGuardSourceTest { 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) { val functionSource = source.substringFrom(functionSignature) assertTrue( @@ -101,6 +141,14 @@ class HomeMainFragmentLoginGuardSourceTest { 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 { val startIndex = indexOf(marker) assertTrue("Missing function: $marker", startIndex >= 0)