From 68f8d869cc57f4b0a62f4fee84f0e4133f1fef76 Mon Sep 17 00:00:00 2001 From: klaus Date: Tue, 2 Jun 2026 14:51:01 +0900 Subject: [PATCH] =?UTF-8?q?feat(home):=20=ED=99=88=20=EC=B6=94=EC=B2=9C=20?= =?UTF-8?q?=EC=83=81=EB=8B=A8=20UI=20shell=EC=9D=84=20=EC=B6=94=EA=B0=80?= =?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 | 53 +++- .../main/res/layout/fragment_v2_main_home.xml | 256 +++++++++++++++++- .../main/res/layout/view_title_bar_home.xml | 21 +- .../main/home/HomeMainFragmentLayoutTest.kt | 151 +++++++++++ 4 files changed, 476 insertions(+), 5 deletions(-) create mode 100644 app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragmentLayoutTest.kt 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 ee69bde5..782fff7d 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 @@ -1,8 +1,59 @@ package kr.co.vividnext.sodalive.v2.main +import android.os.Bundle +import android.view.View +import kr.co.vividnext.sodalive.R import kr.co.vividnext.sodalive.base.BaseFragment import kr.co.vividnext.sodalive.databinding.FragmentV2MainHomeBinding +import kr.co.vividnext.sodalive.databinding.ViewSectionTitleBinding class HomeMainFragment : BaseFragment( FragmentV2MainHomeBinding::inflate -) +) { + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.textTabBarHome.root.setMenus( + listOf( + getString(R.string.screen_home_tab_recommendation), + getString(R.string.screen_home_tab_ranking), + getString(R.string.screen_home_tab_following) + ), + selectedIndex = 0 + ) + binding.textTabBarHome.root.setOnTabSelectedListener { } + setUpSectionTitles() + } + + private fun setUpSectionTitles() { + binding.viewHomeRecentActivityTitle.setTitle( + R.string.home_recommendation_section_recently_active_creators + ) + binding.viewHomeRecentDebutTitle.setTitle( + R.string.home_recommendation_section_recent_debut_creators, + showMore = true + ) + binding.viewHomeFirstAudioTitle.setTitle( + R.string.home_recommendation_section_first_audio_contents, + showMore = true + ) + binding.viewHomeAiCharacterTitle.setTitle( + R.string.home_recommendation_section_ai_characters, + showMore = true + ) + binding.viewHomeCheerCreatorTitle.setTitle( + R.string.home_recommendation_section_cheer_creators + ) + binding.viewHomePopularCommunityTitle.setTitle( + R.string.home_recommendation_section_popular_community_posts + ) + } + + private fun ViewSectionTitleBinding.setTitle( + titleResId: Int, + showMore: Boolean = false + ) { + tvSectionTitle.setText(titleResId) + ivSectionTitleChevron.visibility = if (showMore) View.VISIBLE else View.GONE + } +} diff --git a/app/src/main/res/layout/fragment_v2_main_home.xml b/app/src/main/res/layout/fragment_v2_main_home.xml index 7c7115b6..bb6b400e 100644 --- a/app/src/main/res/layout/fragment_v2_main_home.xml +++ b/app/src/main/res/layout/fragment_v2_main_home.xml @@ -1,5 +1,257 @@ - + android:background="@color/black"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/view_title_bar_home.xml b/app/src/main/res/layout/view_title_bar_home.xml index 6787bdc0..cf129824 100644 --- a/app/src/main/res/layout/view_title_bar_home.xml +++ b/app/src/main/res/layout/view_title_bar_home.xml @@ -21,8 +21,25 @@ android:layout_weight="1" /> + android:contentDescription="@null" + android:src="@drawable/ic_bar_cash" /> + + + + 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 new file mode 100644 index 00000000..86548d9e --- /dev/null +++ b/app/src/test/java/kr/co/vividnext/sodalive/v2/main/home/HomeMainFragmentLayoutTest.kt @@ -0,0 +1,151 @@ +package kr.co.vividnext.sodalive.v2.main.home + +import android.app.Application +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.TextView +import androidx.core.widget.NestedScrollView +import androidx.recyclerview.widget.RecyclerView +import androidx.test.core.app.ApplicationProvider +import kr.co.vividnext.sodalive.R +import kr.co.vividnext.sodalive.v2.widget.TextTabBarView +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertSame +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.Shadows.shadowOf +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [28], application = Application::class) +class HomeMainFragmentLayoutTest { + + @Test + fun `home layout keeps title and tabs outside recommendation scroll content`() { + val root = inflateView(R.layout.fragment_v2_main_home) + val titleBar = root.findViewById(R.id.view_home_title_bar) + val tabBar = root.findViewById(R.id.text_tab_bar_home) + val scrollView = root.findViewById(R.id.nsv_home_recommendation_content) + + assertNotNull(titleBar) + assertNotNull(tabBar) + assertNotNull(scrollView) + assertSame(root, titleBar.parent) + assertSame(root, tabBar.parent) + assertSame(root, scrollView.parent) + assertEquals(60.dpToPx(), titleBar.layoutParams.height) + assertEquals(52.dpToPx(), tabBar.layoutParams.height) + assertFalse(root.containsClassName("androidx.viewpager2.widget.ViewPager2")) + } + + @Test + fun `home layout contains recommendation section containers with horizontal lists`() { + val root = inflateView(R.layout.fragment_v2_main_home) + val sectionIds = listOf( + R.id.ll_home_live_section, + R.id.ll_home_banner_section, + R.id.ll_home_recent_activity_section, + R.id.ll_home_recent_debut_section, + R.id.ll_home_first_audio_section, + R.id.ll_home_ai_character_section, + R.id.ll_home_genre_creator_section, + R.id.ll_home_cheer_creator_section, + R.id.ll_home_popular_community_section + ) + val listIds = listOf( + R.id.rv_home_lives, + R.id.rv_home_banners, + R.id.rv_home_recent_activity_creators, + R.id.rv_home_recent_debut_creators, + R.id.rv_home_first_audio_contents, + R.id.rv_home_ai_characters, + R.id.rv_home_genre_creators, + R.id.rv_home_cheer_creators, + R.id.rv_home_popular_community_posts + ) + + sectionIds.forEach { sectionId -> assertNotNull(root.findViewById(sectionId)) } + listIds.forEach { listId -> + val recyclerView = root.findViewById(listId) + + assertNotNull(recyclerView) + assertEquals(RecyclerView.HORIZONTAL, recyclerView.layoutManager?.canScrollHorizontally()?.toOrientation()) + } + assertNotNull(root.findViewById(R.id.ll_home_business_info)) + } + + @Test + fun `home layout uses section title components and custom genre title row`() { + val root = inflateView(R.layout.fragment_v2_main_home) + val sectionTitleIds = listOf( + R.id.view_home_recent_activity_title, + R.id.view_home_recent_debut_title, + R.id.view_home_first_audio_title, + R.id.view_home_ai_character_title, + R.id.view_home_cheer_creator_title, + R.id.view_home_popular_community_title + ) + val moreTitleIds = listOf( + R.id.view_home_recent_debut_title, + R.id.view_home_first_audio_title, + R.id.view_home_ai_character_title + ) + + sectionTitleIds.forEach { titleId -> assertNotNull(root.findViewById(titleId)) } + moreTitleIds.forEach { titleId -> + val titleView = root.findViewById(titleId) + val chevron = titleView.findViewById(R.id.iv_section_title_chevron) + + assertNotNull(chevron) + } + assertNotNull(root.findViewById(R.id.ll_home_genre_creator_title)) + assertNotNull(root.findViewById(R.id.tv_home_genre_creator_title_genre)) + assertNotNull(root.findViewById(R.id.tv_home_genre_creator_title_suffix)) + } + + @Test + fun `home title bar exposes right icons in cash search bell order`() { + val titleBar = inflateView(R.layout.view_title_bar_home) + val iconIds = listOf( + R.id.iv_title_bar_cash, + R.id.iv_title_bar_search, + R.id.iv_title_bar_bell + ) + val drawableIds = listOf( + R.drawable.ic_bar_cash, + R.drawable.ic_bar_search, + R.drawable.ic_bar_bell + ) + + iconIds.zip(drawableIds).forEach { (iconId, drawableId) -> + val imageView = titleBar.findViewById(iconId) + + assertNotNull(imageView) + assertEquals(drawableId, shadowOf(imageView.drawable).createdFromResId) + } + } + + private fun inflateView(layoutResId: Int): View { + val context = ApplicationProvider.getApplicationContext() + return LayoutInflater.from(context).inflate(layoutResId, null, false) + } + + private fun View.containsClassName(className: String): Boolean { + if (javaClass.name == className) return true + if (this !is android.view.ViewGroup) return false + return (0 until childCount).any { getChildAt(it).containsClassName(className) } + } + + private fun Boolean.toOrientation(): Int = if (this) RecyclerView.HORIZONTAL else RecyclerView.VERTICAL + + private fun Int.dpToPx(): Int { + val context = ApplicationProvider.getApplicationContext() + return (this * context.resources.displayMetrics.density).toInt() + } +}