feat(home): 팔로잉 탭 로그인 가드를 보강한다
This commit is contained in:
@@ -9,6 +9,7 @@ import kr.co.vividnext.sodalive.audio_content.detail.AudioContentDetailActivity
|
|||||||
import kr.co.vividnext.sodalive.base.BaseFragment
|
import kr.co.vividnext.sodalive.base.BaseFragment
|
||||||
import kr.co.vividnext.sodalive.common.Constants
|
import kr.co.vividnext.sodalive.common.Constants
|
||||||
import kr.co.vividnext.sodalive.common.LoadingDialog
|
import kr.co.vividnext.sodalive.common.LoadingDialog
|
||||||
|
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||||
import kr.co.vividnext.sodalive.common.ToastMessage
|
import kr.co.vividnext.sodalive.common.ToastMessage
|
||||||
import kr.co.vividnext.sodalive.common.formatUtcRelativeTimeText
|
import kr.co.vividnext.sodalive.common.formatUtcRelativeTimeText
|
||||||
import kr.co.vividnext.sodalive.databinding.FragmentV2MainHomeBinding
|
import kr.co.vividnext.sodalive.databinding.FragmentV2MainHomeBinding
|
||||||
@@ -20,6 +21,7 @@ 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
|
||||||
import kr.co.vividnext.sodalive.v2.main.chat.model.ChatRoomType
|
import kr.co.vividnext.sodalive.v2.main.chat.model.ChatRoomType
|
||||||
|
import kr.co.vividnext.sodalive.v2.main.ensureMainV2NavigationAllowed
|
||||||
import kr.co.vividnext.sodalive.v2.main.home.model.HomeFollowingChatSection
|
import kr.co.vividnext.sodalive.v2.main.home.model.HomeFollowingChatSection
|
||||||
import kr.co.vividnext.sodalive.v2.main.home.model.HomeFollowingCreatorSection
|
import kr.co.vividnext.sodalive.v2.main.home.model.HomeFollowingCreatorSection
|
||||||
import kr.co.vividnext.sodalive.v2.main.home.model.HomeFollowingLiveSection
|
import kr.co.vividnext.sodalive.v2.main.home.model.HomeFollowingLiveSection
|
||||||
@@ -106,6 +108,7 @@ class HomeMainFragment : BaseFragment<FragmentV2MainHomeBinding>(
|
|||||||
private var onCheerFollowAllClick: (List<Long>) -> Unit = {}
|
private var onCheerFollowAllClick: (List<Long>) -> Unit = {}
|
||||||
private var hasLoadedCreatorRankings = false
|
private var hasLoadedCreatorRankings = false
|
||||||
private var hasLoadedFollowing = false
|
private var hasLoadedFollowing = false
|
||||||
|
private var currentHomeTabIndex = HOME_TAB_RECOMMENDATION
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
@@ -118,7 +121,11 @@ class HomeMainFragment : BaseFragment<FragmentV2MainHomeBinding>(
|
|||||||
selectedIndex = 0
|
selectedIndex = 0
|
||||||
)
|
)
|
||||||
binding.textTabBarHome.root.setOnTabSelectedListener { index ->
|
binding.textTabBarHome.root.setOnTabSelectedListener { index ->
|
||||||
showHomeTab(index)
|
when (index) {
|
||||||
|
HOME_TAB_FOLLOWING -> openFollowingTab()
|
||||||
|
|
||||||
|
else -> showHomeTab(index)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
setUpSectionTitles()
|
setUpSectionTitles()
|
||||||
setUpRecommendationAdapters()
|
setUpRecommendationAdapters()
|
||||||
@@ -233,7 +240,17 @@ class HomeMainFragment : BaseFragment<FragmentV2MainHomeBinding>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun openFollowingTab() {
|
||||||
|
if (SharedPreferenceManager.token.isBlank()) {
|
||||||
|
binding.textTabBarHome.root.selectTab(currentHomeTabIndex)
|
||||||
|
}
|
||||||
|
ensureMainV2NavigationAllowed {
|
||||||
|
showHomeTab(HOME_TAB_FOLLOWING)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun showHomeTab(index: Int) {
|
private fun showHomeTab(index: Int) {
|
||||||
|
currentHomeTabIndex = index
|
||||||
when (index) {
|
when (index) {
|
||||||
HOME_TAB_RECOMMENDATION -> {
|
HOME_TAB_RECOMMENDATION -> {
|
||||||
binding.nsvHomeRecommendationContent.visibility = View.VISIBLE
|
binding.nsvHomeRecommendationContent.visibility = View.VISIBLE
|
||||||
@@ -487,7 +504,9 @@ class HomeMainFragment : BaseFragment<FragmentV2MainHomeBinding>(
|
|||||||
private fun onLiveClick(item: HomeRecommendationLiveUiModel) = Unit
|
private fun onLiveClick(item: HomeRecommendationLiveUiModel) = Unit
|
||||||
|
|
||||||
private fun openHomeOnAirLive() {
|
private fun openHomeOnAirLive() {
|
||||||
startActivity(HomeOnAirLiveActivity.newIntent(requireContext()))
|
ensureMainV2NavigationAllowed {
|
||||||
|
startActivity(HomeOnAirLiveActivity.newIntent(requireContext()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onFollowingSectionMoreClick(section: HomeFollowingSection) = Unit
|
private fun onFollowingSectionMoreClick(section: HomeFollowingSection) = Unit
|
||||||
@@ -499,25 +518,33 @@ class HomeMainFragment : BaseFragment<FragmentV2MainHomeBinding>(
|
|||||||
private fun onFollowingNewsClick(item: HomeFollowingNewsUiItem) = Unit
|
private fun onFollowingNewsClick(item: HomeFollowingNewsUiItem) = Unit
|
||||||
|
|
||||||
private fun openFollowingChat(item: ChatRoomListUiItem) {
|
private fun openFollowingChat(item: ChatRoomListUiItem) {
|
||||||
when (item.chatType) {
|
ensureMainV2NavigationAllowed {
|
||||||
ChatRoomType.AI -> startActivity(ChatRoomActivity.newIntent(requireContext(), item.roomId))
|
when (item.chatType) {
|
||||||
ChatRoomType.DM -> startActivity(DmChatRoomActivity.newIntentByRoomId(requireContext(), item.roomId))
|
ChatRoomType.AI -> startActivity(ChatRoomActivity.newIntent(requireContext(), item.roomId))
|
||||||
|
ChatRoomType.DM -> startActivity(DmChatRoomActivity.newIntentByRoomId(requireContext(), item.roomId))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onBannerClick(item: HomeRecommendationBannerUiModel) {
|
private fun onBannerClick(item: HomeRecommendationBannerUiModel) {
|
||||||
val route = item.toHomeRecommendationBannerRoute() ?: return
|
val route = item.toHomeRecommendationBannerRoute() ?: return
|
||||||
startActivity(route.toHomeRecommendationBannerIntent(requireContext()))
|
ensureMainV2NavigationAllowed {
|
||||||
|
startActivity(route.toHomeRecommendationBannerIntent(requireContext()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onRecentActivityClick(item: HomeRecommendationRecentlyActiveCreatorUiModel) {
|
private fun onRecentActivityClick(item: HomeRecommendationRecentlyActiveCreatorUiModel) {
|
||||||
val route = item.toHomeRecommendationRecentlyActiveCreatorRoute() ?: return
|
val route = item.toHomeRecommendationRecentlyActiveCreatorRoute() ?: return
|
||||||
startActivity(route.toHomeRecommendationRecentlyActiveCreatorIntent(requireContext()))
|
ensureMainV2NavigationAllowed {
|
||||||
|
startActivity(route.toHomeRecommendationRecentlyActiveCreatorIntent(requireContext()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onAiCharacterClick(item: HomeRecommendationAiCharacterUiModel) {
|
private fun onAiCharacterClick(item: HomeRecommendationAiCharacterUiModel) {
|
||||||
val route = item.toHomeRecommendationAiCharacterRoute() ?: return
|
val route = item.toHomeRecommendationAiCharacterRoute() ?: return
|
||||||
startActivity(route.toHomeRecommendationAiCharacterIntent(requireContext()))
|
ensureMainV2NavigationAllowed {
|
||||||
|
startActivity(route.toHomeRecommendationAiCharacterIntent(requireContext()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun openCreatorRankingProfile(item: CreatorRankingItem) {
|
private fun openCreatorRankingProfile(item: CreatorRankingItem) {
|
||||||
@@ -526,29 +553,35 @@ class HomeMainFragment : BaseFragment<FragmentV2MainHomeBinding>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun openCreatorProfile(creatorId: Long) {
|
private fun openCreatorProfile(creatorId: Long) {
|
||||||
startActivity(
|
ensureMainV2NavigationAllowed {
|
||||||
CreatorChannelActivity.newIntent(requireContext(), creatorId)
|
startActivity(
|
||||||
)
|
CreatorChannelActivity.newIntent(requireContext(), creatorId)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun openAudioContentDetail(item: HomeRecommendationFirstAudioContentUiModel) {
|
private fun openAudioContentDetail(item: HomeRecommendationFirstAudioContentUiModel) {
|
||||||
startActivity(
|
ensureMainV2NavigationAllowed {
|
||||||
Intent(requireContext(), AudioContentDetailActivity::class.java).apply {
|
startActivity(
|
||||||
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, item.contentId)
|
Intent(requireContext(), AudioContentDetailActivity::class.java).apply {
|
||||||
}
|
putExtra(Constants.EXTRA_AUDIO_CONTENT_ID, item.contentId)
|
||||||
)
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun openPopularCommunityPost(item: FeedItem.Community) {
|
private fun openPopularCommunityPost(item: FeedItem.Community) {
|
||||||
val creatorId = item.creatorId.toLongOrNull() ?: return
|
val creatorId = item.creatorId.toLongOrNull() ?: return
|
||||||
val postId = item.postId.toLongOrNull() ?: return
|
val postId = item.postId.toLongOrNull() ?: return
|
||||||
startActivity(
|
ensureMainV2NavigationAllowed {
|
||||||
Intent(requireContext(), CreatorCommunityAllActivity::class.java).apply {
|
startActivity(
|
||||||
putExtra(Constants.EXTRA_COMMUNITY_CREATOR_ID, creatorId)
|
Intent(requireContext(), CreatorCommunityAllActivity::class.java).apply {
|
||||||
putExtra(Constants.EXTRA_COMMUNITY_POST_ID, postId)
|
putExtra(Constants.EXTRA_COMMUNITY_CREATOR_ID, creatorId)
|
||||||
putExtra(Constants.EXTRA_COMMUNITY_EXIST_ORDERED, item.existOrdered)
|
putExtra(Constants.EXTRA_COMMUNITY_POST_ID, postId)
|
||||||
}
|
putExtra(Constants.EXTRA_COMMUNITY_EXIST_ORDERED, item.existOrdered)
|
||||||
)
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showToast(toastMessage: ToastMessage) {
|
private fun showToast(toastMessage: ToastMessage) {
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
package kr.co.vividnext.sodalive.v2.main.home
|
||||||
|
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class HomeMainFragmentLoginGuardSourceTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `HomeMainFragment 실제 이동은 MainV2 로그인 가드를 통과한다`() {
|
||||||
|
val source = projectFile(
|
||||||
|
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragment.kt"
|
||||||
|
).readText()
|
||||||
|
|
||||||
|
assertTrue(source.contains("import kr.co.vividnext.sodalive.v2.main.ensureMainV2NavigationAllowed"))
|
||||||
|
assertGuardedStartActivity(source, "private fun openHomeOnAirLive()")
|
||||||
|
assertGuardedStartActivity(source, "private fun openFollowingChat(item: ChatRoomListUiItem)")
|
||||||
|
assertGuardedStartActivity(source, "private fun onBannerClick(item: HomeRecommendationBannerUiModel)")
|
||||||
|
assertGuardedStartActivity(
|
||||||
|
source,
|
||||||
|
"private fun onRecentActivityClick(item: HomeRecommendationRecentlyActiveCreatorUiModel)"
|
||||||
|
)
|
||||||
|
assertGuardedStartActivity(source, "private fun onAiCharacterClick(item: HomeRecommendationAiCharacterUiModel)")
|
||||||
|
assertGuardedStartActivity(source, "private fun openCreatorProfile(creatorId: Long)")
|
||||||
|
assertGuardedStartActivity(source, "private fun openAudioContentDetail(item: HomeRecommendationFirstAudioContentUiModel)")
|
||||||
|
assertGuardedStartActivity(source, "private fun openPopularCommunityPost(item: FeedItem.Community)")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `HomeMainFragment invalid route와 id return은 로그인 가드보다 먼저 유지된다`() {
|
||||||
|
val source = projectFile(
|
||||||
|
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragment.kt"
|
||||||
|
).readText()
|
||||||
|
|
||||||
|
assertBeforeGuard(source, "val route = item.toHomeRecommendationBannerRoute() ?: return")
|
||||||
|
assertBeforeGuard(source, "val route = item.toHomeRecommendationRecentlyActiveCreatorRoute() ?: return")
|
||||||
|
assertBeforeGuard(source, "val route = item.toHomeRecommendationAiCharacterRoute() ?: return")
|
||||||
|
assertBeforeGuard(source, "val creatorId = item.creatorId.toLongOrNull() ?: return")
|
||||||
|
assertBeforeGuard(source, "val postId = item.postId.toLongOrNull() ?: return")
|
||||||
|
assertFalse(source.contains("requiresAdultContentAccess = true"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `HomeMainFragment 팔로잉 탭 선택은 로그인 가드 통과 후 탭을 전환한다`() {
|
||||||
|
val source = projectFile(
|
||||||
|
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragment.kt"
|
||||||
|
).readText()
|
||||||
|
|
||||||
|
val listenerSource = source.substringFrom("binding.textTabBarHome.root.setOnTabSelectedListener { index ->")
|
||||||
|
assertTrue(listenerSource.contains("HOME_TAB_FOLLOWING -> openFollowingTab()"))
|
||||||
|
assertTrue(listenerSource.contains("else -> showHomeTab(index)"))
|
||||||
|
assertFalse(listenerSource.contains("showHomeTab(index)\n }"))
|
||||||
|
|
||||||
|
val followingTabSource = source.substringFrom("HOME_TAB_FOLLOWING -> {")
|
||||||
|
assertTrue(followingTabSource.contains("homeFollowingViewModel.loadFollowing()"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `HomeMainFragment 팔로잉 탭 미로그인 차단은 이전 탭 선택 상태로 복구한다`() {
|
||||||
|
val source = projectFile(
|
||||||
|
"app/src/main/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragment.kt"
|
||||||
|
).readText()
|
||||||
|
|
||||||
|
assertTrue(source.contains("import kr.co.vividnext.sodalive.common.SharedPreferenceManager"))
|
||||||
|
assertTrue(source.contains("private var currentHomeTabIndex = HOME_TAB_RECOMMENDATION"))
|
||||||
|
|
||||||
|
val followingTabSource = source.substringFrom("private fun openFollowingTab()")
|
||||||
|
assertTrue(followingTabSource.contains("if (SharedPreferenceManager.token.isBlank())"))
|
||||||
|
assertBefore(
|
||||||
|
followingTabSource,
|
||||||
|
"binding.textTabBarHome.root.selectTab(currentHomeTabIndex)",
|
||||||
|
"ensureMainV2NavigationAllowed"
|
||||||
|
)
|
||||||
|
assertTrue(followingTabSource.contains("showHomeTab(HOME_TAB_FOLLOWING)"))
|
||||||
|
|
||||||
|
val showHomeTabSource = source.substringFrom("private fun showHomeTab(index: Int)")
|
||||||
|
assertTrue(showHomeTabSource.contains("currentHomeTabIndex = index"))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun assertGuardedStartActivity(source: String, functionSignature: String) {
|
||||||
|
val functionSource = source.substringFrom(functionSignature)
|
||||||
|
assertTrue(
|
||||||
|
"$functionSignature must call ensureMainV2NavigationAllowed before startActivity.",
|
||||||
|
functionSource.indexOf("ensureMainV2NavigationAllowed") in 0 until functionSource.indexOf("startActivity")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun assertBeforeGuard(source: String, expectedReturn: String) {
|
||||||
|
val returnIndex = source.indexOf(expectedReturn)
|
||||||
|
val guardIndex = source.indexOf("ensureMainV2NavigationAllowed", returnIndex)
|
||||||
|
|
||||||
|
assertTrue("Missing source: $expectedReturn", returnIndex >= 0)
|
||||||
|
assertTrue("$expectedReturn must stay before guard.", guardIndex > returnIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun assertBefore(source: String, expectedBefore: String, expectedAfter: String) {
|
||||||
|
val beforeIndex = source.indexOf(expectedBefore)
|
||||||
|
val afterIndex = source.indexOf(expectedAfter, beforeIndex)
|
||||||
|
|
||||||
|
assertTrue("Missing source: $expectedBefore", beforeIndex >= 0)
|
||||||
|
assertTrue("Missing source after $expectedBefore: $expectedAfter", afterIndex > beforeIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun String.substringFrom(marker: String): String {
|
||||||
|
val startIndex = indexOf(marker)
|
||||||
|
assertTrue("Missing function: $marker", startIndex >= 0)
|
||||||
|
val nextFunctionIndex = indexOf("\n private fun ", startIndex + marker.length).takeIf { it >= 0 } ?: length
|
||||||
|
return substring(startIndex, nextFunctionIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun projectFile(relativePath: String): File {
|
||||||
|
val candidates = listOf(File(relativePath), File("../$relativePath"))
|
||||||
|
return candidates.firstOrNull { it.exists() }
|
||||||
|
?: error("Project file not found: $relativePath")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user