From c715c890b51a3bcd319446520bd62195e74da915 Mon Sep 17 00:00:00 2001 From: klaus Date: Sat, 11 Jul 2026 22:02:13 +0900 Subject: [PATCH] =?UTF-8?q?feat(chat):=20=ED=83=AD=EB=B3=84=20empty=20stat?= =?UTF-8?q?e=EB=A5=BC=20=ED=91=9C=EC=8B=9C=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sodalive/v2/main/chat/ChatMainFragment.kt | 32 ++++++++++++++----- .../v2/main/chat/model/ChatRoomFilter.kt | 10 ++++++ .../main/res/layout/fragment_v2_main_chat.xml | 14 ++++++++ .../main/chat/ChatMainFragmentLayoutTest.kt | 27 +++++++++++++++- 4 files changed, 74 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/ChatMainFragment.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/ChatMainFragment.kt index 915cb66a..2aa89a2c 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/ChatMainFragment.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/ChatMainFragment.kt @@ -6,6 +6,7 @@ import android.view.View import android.widget.ImageView import android.widget.LinearLayout import android.widget.Toast +import androidx.core.view.isVisible import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import kr.co.vividnext.sodalive.R @@ -21,6 +22,7 @@ import kr.co.vividnext.sodalive.v2.main.chat.model.ChatRoomFilter import kr.co.vividnext.sodalive.v2.main.chat.model.ChatRoomListUiItem import kr.co.vividnext.sodalive.v2.main.chat.model.ChatRoomListUiState import kr.co.vividnext.sodalive.v2.main.chat.model.ChatRoomType +import kr.co.vividnext.sodalive.v2.main.chat.model.emptyMessageResId import kr.co.vividnext.sodalive.v2.main.chat.ui.ChatRoomListAdapter import org.koin.androidx.viewmodel.ext.android.viewModel @@ -44,22 +46,25 @@ class ChatMainFragment : BaseFragment( private val viewModel: ChatMainViewModel by viewModel() private val chatRoomListAdapter = ChatRoomListAdapter { onChatRoomClick(it) } private lateinit var loadingDialog: LoadingDialog + private var selectedFilter: ChatRoomFilter = ChatRoomFilter.ALL override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) loadingDialog = LoadingDialog(requireActivity(), layoutInflater) setupTitleBar() - val initialFilter = getInitialFilter() - setupFilterTabs(initialFilter ?: ChatRoomFilter.ALL) + val initialFilter = getInitialFilter() ?: ChatRoomFilter.ALL + selectedFilter = initialFilter + setupFilterTabs(initialFilter) setupChatRooms() bindViewModel() binding.btnChatFloating.setOnClickListener { } - viewModel.loadFirstPage(initialFilter ?: ChatRoomFilter.ALL) + viewModel.loadFirstPage(initialFilter) } fun selectFilter(filter: ChatRoomFilter) { + selectedFilter = filter selectFilterTab(filter) viewModel.selectFilter(filter) } @@ -102,7 +107,9 @@ class ChatMainFragment : BaseFragment( selectedIndex = initialFilter.tabIndex ) binding.viewChatFilterTabs.root.setOnTabSelectedListener { index -> - viewModel.selectFilter(ChatRoomFilter.fromTabIndex(index)) + val filter = ChatRoomFilter.fromTabIndex(index) + selectedFilter = filter + viewModel.selectFilter(filter) binding.rvChatRooms.scrollToPosition(0) } } @@ -130,14 +137,14 @@ class ChatMainFragment : BaseFragment( viewModel.chatRoomStateLiveData.observe(viewLifecycleOwner) { state -> when (state) { is ChatRoomListUiState.Content -> { - chatRoomListAdapter.submitItems(state.items) + bindChatRooms(state.items, showEmpty = false) if (!state.isAppending) { binding.rvChatRooms.scrollToPosition(0) } } - ChatRoomListUiState.Empty -> chatRoomListAdapter.submitItems(emptyList()) - is ChatRoomListUiState.Error -> chatRoomListAdapter.submitItems(emptyList()) - ChatRoomListUiState.Loading -> Unit + ChatRoomListUiState.Empty -> bindChatRooms(emptyList(), showEmpty = true) + is ChatRoomListUiState.Error -> bindChatRooms(emptyList(), showEmpty = false) + ChatRoomListUiState.Loading -> binding.tvChatEmptyMessage.isVisible = false } } viewModel.isLoading.observe(viewLifecycleOwner) { @@ -155,6 +162,15 @@ class ChatMainFragment : BaseFragment( } } + private fun bindChatRooms(items: List, showEmpty: Boolean) { + chatRoomListAdapter.submitItems(items) + binding.rvChatRooms.isVisible = !showEmpty + binding.tvChatEmptyMessage.isVisible = showEmpty + if (showEmpty) { + binding.tvChatEmptyMessage.setText(selectedFilter.emptyMessageResId()) + } + } + private fun onChatRoomClick(item: ChatRoomListUiItem) { when (item.chatType) { ChatRoomType.AI -> startActivity(ChatRoomActivity.newIntent(requireContext(), item.roomId)) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/model/ChatRoomFilter.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/model/ChatRoomFilter.kt index 4ca7b207..8194e180 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/model/ChatRoomFilter.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/model/ChatRoomFilter.kt @@ -1,5 +1,8 @@ package kr.co.vividnext.sodalive.v2.main.chat.model +import androidx.annotation.StringRes +import kr.co.vividnext.sodalive.R + enum class ChatRoomFilter(val apiValue: String, val tabIndex: Int) { ALL("ALL", 0), AI("AI", 1), @@ -13,3 +16,10 @@ enum class ChatRoomFilter(val apiValue: String, val tabIndex: Int) { } } } + +@StringRes +fun ChatRoomFilter.emptyMessageResId(): Int = when (this) { + ChatRoomFilter.ALL -> R.string.screen_chat_empty_all + ChatRoomFilter.AI -> R.string.screen_chat_empty_ai + ChatRoomFilter.DM -> R.string.screen_chat_empty_dm +} diff --git a/app/src/main/res/layout/fragment_v2_main_chat.xml b/app/src/main/res/layout/fragment_v2_main_chat.xml index a777aea0..2f7e5f24 100644 --- a/app/src/main/res/layout/fragment_v2_main_chat.xml +++ b/app/src/main/res/layout/fragment_v2_main_chat.xml @@ -37,6 +37,20 @@ app:layout_constraintTop_toBottomOf="@id/view_chat_filter_tabs" tools:listitem="@layout/item_v2_chat_room" /> + + (R.id.view_chat_filter_tabs)) val recyclerView = requireNotNull(root.findViewById(R.id.rv_chat_rooms)) val floatingButton = requireNotNull(root.findViewById(R.id.btn_chat_floating)) + val emptyMessage = requireNotNull(root.findViewById(R.id.tv_chat_empty_message)) assertEquals(Color.BLACK, (root.background as ColorDrawable).color) assertSame(root, titleBar.parent) assertSame(root, tabBar.parent) assertSame(root, recyclerView.parent) assertSame(root, floatingButton.parent) + assertSame(root, emptyMessage.parent) + assertEquals(View.GONE, emptyMessage.visibility) assertFalse(root.containsClassName("com.google.android.material.bottomnavigation.BottomNavigationView")) assertFalse(root.containsViewIdContaining("unread")) } @@ -69,8 +72,10 @@ class ChatMainFragmentLayoutTest { val tabBar = requireNotNull(root.findViewById(R.id.view_chat_filter_tabs)) val recyclerView = requireNotNull(root.findViewById(R.id.rv_chat_rooms)) val floatingButton = requireNotNull(root.findViewById(R.id.btn_chat_floating)) + val emptyMessage = requireNotNull(root.findViewById(R.id.tv_chat_empty_message)) val tabParams = tabBar.layoutParams as ConstraintLayout.LayoutParams val listParams = recyclerView.layoutParams as ConstraintLayout.LayoutParams + val emptyParams = emptyMessage.layoutParams as ConstraintLayout.LayoutParams val buttonParams = floatingButton.layoutParams as ConstraintLayout.LayoutParams assertEquals(60.dpToPx(), titleBar.layoutParams.height) @@ -78,6 +83,8 @@ class ChatMainFragmentLayoutTest { assertEquals(R.id.view_chat_title_bar, tabParams.topToBottom) assertEquals(R.id.view_chat_filter_tabs, listParams.topToBottom) assertEquals(ConstraintLayout.LayoutParams.PARENT_ID, listParams.bottomToBottom) + assertEquals(R.id.view_chat_filter_tabs, emptyParams.topToBottom) + assertEquals(ConstraintLayout.LayoutParams.PARENT_ID, emptyParams.bottomToBottom) assertEquals(false, recyclerView.clipToPadding) assertTrue(recyclerView.paddingBottom >= 28.dpToPx()) assertEquals(ConstraintLayout.LayoutParams.PARENT_ID, buttonParams.endToEnd) @@ -101,7 +108,8 @@ class ChatMainFragmentLayoutTest { assertTrue(source.contains("R.string.screen_chat_filter_all")) assertTrue(source.contains("R.string.screen_chat_filter_ai")) assertTrue(source.contains("R.string.screen_chat_filter_dm")) - assertTrue(source.contains("viewModel.loadFirstPage(initialFilter ?: ChatRoomFilter.ALL)")) + assertTrue(source.contains("val initialFilter = getInitialFilter() ?: ChatRoomFilter.ALL")) + assertTrue(source.contains("viewModel.loadFirstPage(initialFilter)")) } @Test @@ -145,6 +153,23 @@ class ChatMainFragmentLayoutTest { assertTrue(source.contains("!state.isAppending")) } + @Test + fun `채팅 fragment source는 탭별 empty state 문구를 표시한다`() { + val source = chatMainFragmentSource() + val filter = projectFile( + "app/src/main/java/kr/co/vividnext/sodalive/v2/main/chat/model/ChatRoomFilter.kt" + ).readText() + + assertTrue(source.contains("private var selectedFilter: ChatRoomFilter = ChatRoomFilter.ALL")) + assertTrue(source.contains("binding.tvChatEmptyMessage.setText(selectedFilter.emptyMessageResId())")) + assertTrue(source.contains("ChatRoomListUiState.Empty -> bindChatRooms(emptyList(), showEmpty = true)")) + assertTrue(source.contains("is ChatRoomListUiState.Error -> bindChatRooms(emptyList(), showEmpty = false)")) + assertTrue(source.contains("binding.rvChatRooms.isVisible = !showEmpty")) + assertTrue(filter.contains("ChatRoomFilter.ALL -> R.string.screen_chat_empty_all")) + assertTrue(filter.contains("ChatRoomFilter.AI -> R.string.screen_chat_empty_ai")) + assertTrue(filter.contains("ChatRoomFilter.DM -> R.string.screen_chat_empty_dm")) + } + private fun inflateView(layoutResId: Int): View { val context = ApplicationProvider.getApplicationContext() return LayoutInflater.from(context).inflate(layoutResId, null, false)