feat(chat): 탭별 empty state를 표시한다

This commit is contained in:
2026-07-11 22:02:13 +09:00
parent 9925a954d1
commit c715c890b5
4 changed files with 74 additions and 9 deletions

View File

@@ -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<FragmentV2MainChatBinding>(
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<FragmentV2MainChatBinding>(
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<FragmentV2MainChatBinding>(
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<FragmentV2MainChatBinding>(
}
}
private fun bindChatRooms(items: List<ChatRoomListUiItem>, 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))

View File

@@ -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
}

View File

@@ -37,6 +37,20 @@
app:layout_constraintTop_toBottomOf="@id/view_chat_filter_tabs"
tools:listitem="@layout/item_v2_chat_room" />
<TextView
android:id="@+id/tv_chat_empty_message"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:gravity="center"
android:textColor="@color/color_777777"
android:textSize="14sp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/view_chat_filter_tabs"
tools:text="아직 나눈 대화가 없어요.\n크리에이터와 이야기를 시작해 보세요." />
<ImageView
android:id="@+id/btn_chat_floating"
android:layout_width="@dimen/spacing_48"

View File

@@ -52,12 +52,15 @@ class ChatMainFragmentLayoutTest {
val tabBar = requireNotNull(root.findViewById<CapsuleTabBarView>(R.id.view_chat_filter_tabs))
val recyclerView = requireNotNull(root.findViewById<RecyclerView>(R.id.rv_chat_rooms))
val floatingButton = requireNotNull(root.findViewById<ImageView>(R.id.btn_chat_floating))
val emptyMessage = requireNotNull(root.findViewById<TextView>(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<CapsuleTabBarView>(R.id.view_chat_filter_tabs))
val recyclerView = requireNotNull(root.findViewById<RecyclerView>(R.id.rv_chat_rooms))
val floatingButton = requireNotNull(root.findViewById<ImageView>(R.id.btn_chat_floating))
val emptyMessage = requireNotNull(root.findViewById<TextView>(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<Context>()
return LayoutInflater.from(context).inflate(layoutResId, null, false)