feat(ui): 톡 탭
- api, viewmodel, repository 연결 - 채팅방 리스트 UI 추가
This commit is contained in:
		@@ -0,0 +1,13 @@
 | 
			
		||||
package kr.co.vividnext.sodalive.chat.talk
 | 
			
		||||
 | 
			
		||||
import io.reactivex.rxjava3.core.Single
 | 
			
		||||
import kr.co.vividnext.sodalive.common.ApiResponse
 | 
			
		||||
import retrofit2.http.GET
 | 
			
		||||
import retrofit2.http.Header
 | 
			
		||||
 | 
			
		||||
interface TalkApi {
 | 
			
		||||
    @GET("/api/chat/talk/rooms")
 | 
			
		||||
    fun getTalkRooms(
 | 
			
		||||
        @Header("Authorization") authHeader: String
 | 
			
		||||
    ): Single<ApiResponse<TalkRoomResponse>>
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,10 @@
 | 
			
		||||
package kr.co.vividnext.sodalive.chat.talk
 | 
			
		||||
 | 
			
		||||
data class TalkRoom(
 | 
			
		||||
    val id: Long,
 | 
			
		||||
    val profileImageUrl: String,
 | 
			
		||||
    val characterName: String,
 | 
			
		||||
    val characterType: String,
 | 
			
		||||
    val lastMessageTime: String,
 | 
			
		||||
    val lastMessage: String
 | 
			
		||||
)
 | 
			
		||||
@@ -0,0 +1,6 @@
 | 
			
		||||
package kr.co.vividnext.sodalive.chat.talk
 | 
			
		||||
 | 
			
		||||
data class TalkRoomResponse(
 | 
			
		||||
    val totalCount: Int,
 | 
			
		||||
    val items: List<TalkRoom>
 | 
			
		||||
)
 | 
			
		||||
@@ -0,0 +1,71 @@
 | 
			
		||||
package kr.co.vividnext.sodalive.chat.talk
 | 
			
		||||
 | 
			
		||||
import android.view.LayoutInflater
 | 
			
		||||
import android.view.ViewGroup
 | 
			
		||||
import androidx.recyclerview.widget.DiffUtil
 | 
			
		||||
import androidx.recyclerview.widget.ListAdapter
 | 
			
		||||
import androidx.recyclerview.widget.RecyclerView
 | 
			
		||||
import com.bumptech.glide.Glide
 | 
			
		||||
import kr.co.vividnext.sodalive.databinding.ItemTalkBinding
 | 
			
		||||
 | 
			
		||||
class TalkTabAdapter(
 | 
			
		||||
    private val onItemClick: (TalkRoom) -> Unit
 | 
			
		||||
) : ListAdapter<TalkRoom, TalkTabAdapter.TalkViewHolder>(TalkDiffCallback()) {
 | 
			
		||||
 | 
			
		||||
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TalkViewHolder {
 | 
			
		||||
        val binding = ItemTalkBinding.inflate(LayoutInflater.from(parent.context), parent, false)
 | 
			
		||||
        return TalkViewHolder(binding)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onBindViewHolder(holder: TalkViewHolder, position: Int) {
 | 
			
		||||
        holder.bind(getItem(position))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    inner class TalkViewHolder(
 | 
			
		||||
        private val binding: ItemTalkBinding
 | 
			
		||||
    ) : RecyclerView.ViewHolder(binding.root) {
 | 
			
		||||
 | 
			
		||||
        init {
 | 
			
		||||
            binding.root.setOnClickListener {
 | 
			
		||||
                val position = bindingAdapterPosition
 | 
			
		||||
                if (position != RecyclerView.NO_POSITION) {
 | 
			
		||||
                    onItemClick(getItem(position))
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        fun bind(talkRoom: TalkRoom) {
 | 
			
		||||
            binding.apply {
 | 
			
		||||
                // 프로필 이미지 로드
 | 
			
		||||
                Glide.with(ivProfile.context)
 | 
			
		||||
                    .load(talkRoom.profileImageUrl)
 | 
			
		||||
                    .into(ivProfile)
 | 
			
		||||
 | 
			
		||||
                // 텍스트 설정
 | 
			
		||||
                tvCharacterName.text = talkRoom.characterName
 | 
			
		||||
                tvCharacterType.text = talkRoom.characterType
 | 
			
		||||
                tvLastTime.text = talkRoom.lastMessageTime
 | 
			
		||||
                tvLastMessage.text = talkRoom.lastMessage
 | 
			
		||||
 | 
			
		||||
                // 캐릭터 유형에 따른 배경 설정
 | 
			
		||||
                val backgroundResId = when (talkRoom.characterType.lowercase()) {
 | 
			
		||||
                    "character" -> kr.co.vividnext.sodalive.R.drawable.bg_character_type_badge_character
 | 
			
		||||
                    "clone" -> kr.co.vividnext.sodalive.R.drawable.bg_character_type_badge_clone
 | 
			
		||||
                    "creator" -> kr.co.vividnext.sodalive.R.drawable.bg_character_type_badge_creator
 | 
			
		||||
                    else -> kr.co.vividnext.sodalive.R.drawable.bg_character_type_badge_character
 | 
			
		||||
                }
 | 
			
		||||
                tvCharacterType.setBackgroundResource(backgroundResId)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private class TalkDiffCallback : DiffUtil.ItemCallback<TalkRoom>() {
 | 
			
		||||
        override fun areItemsTheSame(oldItem: TalkRoom, newItem: TalkRoom): Boolean {
 | 
			
		||||
            return oldItem.id == newItem.id
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        override fun areContentsTheSame(oldItem: TalkRoom, newItem: TalkRoom): Boolean {
 | 
			
		||||
            return oldItem == newItem
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -2,14 +2,70 @@ package kr.co.vividnext.sodalive.chat.talk
 | 
			
		||||
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import android.view.View
 | 
			
		||||
import android.widget.Toast
 | 
			
		||||
import androidx.recyclerview.widget.LinearLayoutManager
 | 
			
		||||
import kr.co.vividnext.sodalive.base.BaseFragment
 | 
			
		||||
import kr.co.vividnext.sodalive.common.LoadingDialog
 | 
			
		||||
import kr.co.vividnext.sodalive.databinding.FragmentTalkTabBinding
 | 
			
		||||
import org.koin.android.ext.android.inject
 | 
			
		||||
 | 
			
		||||
class TalkTabFragment : BaseFragment<FragmentTalkTabBinding>(
 | 
			
		||||
    FragmentTalkTabBinding::inflate
 | 
			
		||||
) {
 | 
			
		||||
    private val viewModel: TalkTabViewModel by inject()
 | 
			
		||||
 | 
			
		||||
    private lateinit var adapter: TalkTabAdapter
 | 
			
		||||
 | 
			
		||||
    private lateinit var loadingDialog: LoadingDialog
 | 
			
		||||
 | 
			
		||||
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
 | 
			
		||||
        super.onViewCreated(view, savedInstanceState)
 | 
			
		||||
        // 톡 탭 초기화 로직
 | 
			
		||||
 | 
			
		||||
        loadingDialog = LoadingDialog(requireActivity(), layoutInflater)
 | 
			
		||||
        setupRecyclerView()
 | 
			
		||||
        observeViewModel()
 | 
			
		||||
 | 
			
		||||
        // 데이터 로드
 | 
			
		||||
        viewModel.loadTalkRooms()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun setupRecyclerView() {
 | 
			
		||||
        adapter = TalkTabAdapter { talkRoom ->
 | 
			
		||||
            // 대화방 클릭 이벤트 처리
 | 
			
		||||
            // TODO: 대화방 상세 화면으로 이동
 | 
			
		||||
            Toast.makeText(
 | 
			
		||||
                requireContext(),
 | 
			
		||||
                "대화방 ${talkRoom.characterName} 클릭됨",
 | 
			
		||||
                Toast.LENGTH_SHORT
 | 
			
		||||
            ).show()
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        binding.rvTalk.apply {
 | 
			
		||||
            layoutManager = LinearLayoutManager(requireContext())
 | 
			
		||||
            adapter = this@TalkTabFragment.adapter
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun observeViewModel() {
 | 
			
		||||
        // 대화방 목록 관찰
 | 
			
		||||
        viewModel.talkRooms.observe(viewLifecycleOwner) {
 | 
			
		||||
            if (it.isNotEmpty()) {
 | 
			
		||||
                adapter.submitList(it)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // 로딩 상태 관찰
 | 
			
		||||
        viewModel.isLoading.observe(viewLifecycleOwner) {
 | 
			
		||||
            if (it) {
 | 
			
		||||
                loadingDialog.show(screenWidth)
 | 
			
		||||
            } else {
 | 
			
		||||
                loadingDialog.dismiss()
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // 오류 관찰
 | 
			
		||||
        viewModel.toastLiveData.observe(viewLifecycleOwner) {
 | 
			
		||||
            Toast.makeText(requireContext(), it, Toast.LENGTH_LONG).show()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,7 @@
 | 
			
		||||
package kr.co.vividnext.sodalive.chat.talk
 | 
			
		||||
 | 
			
		||||
class TalkTabRepository(private val api: TalkApi) {
 | 
			
		||||
    fun getTalkRooms(
 | 
			
		||||
        token: String
 | 
			
		||||
    ) = api.getTalkRooms(authHeader = token)
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,47 @@
 | 
			
		||||
package kr.co.vividnext.sodalive.chat.talk
 | 
			
		||||
 | 
			
		||||
import androidx.lifecycle.LiveData
 | 
			
		||||
import androidx.lifecycle.MutableLiveData
 | 
			
		||||
import com.orhanobut.logger.Logger
 | 
			
		||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
 | 
			
		||||
import io.reactivex.rxjava3.schedulers.Schedulers
 | 
			
		||||
import kr.co.vividnext.sodalive.base.BaseViewModel
 | 
			
		||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
 | 
			
		||||
 | 
			
		||||
class TalkTabViewModel(private val repository: TalkTabRepository) : BaseViewModel() {
 | 
			
		||||
 | 
			
		||||
    private val _talkRooms = MutableLiveData<List<TalkRoom>>()
 | 
			
		||||
    val talkRooms: LiveData<List<TalkRoom>> = _talkRooms
 | 
			
		||||
 | 
			
		||||
    private val _isLoading = MutableLiveData<Boolean>()
 | 
			
		||||
    val isLoading: LiveData<Boolean> = _isLoading
 | 
			
		||||
 | 
			
		||||
    private val _toastLiveData = MutableLiveData<String?>()
 | 
			
		||||
    val toastLiveData: LiveData<String?>
 | 
			
		||||
        get() = _toastLiveData
 | 
			
		||||
 | 
			
		||||
    fun loadTalkRooms() {
 | 
			
		||||
        _isLoading.value = true
 | 
			
		||||
        compositeDisposable.add(
 | 
			
		||||
            repository.getTalkRooms(token = "Bearer ${SharedPreferenceManager.token}")
 | 
			
		||||
                .subscribeOn(Schedulers.io())
 | 
			
		||||
                .observeOn(AndroidSchedulers.mainThread())
 | 
			
		||||
                .subscribe(
 | 
			
		||||
                    { response ->
 | 
			
		||||
                        _isLoading.value = false
 | 
			
		||||
                        if (response.success) {
 | 
			
		||||
                            _talkRooms.value = response.data?.items ?: emptyList()
 | 
			
		||||
                        } else {
 | 
			
		||||
                            _toastLiveData.value =
 | 
			
		||||
                                response.message ?: "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
 | 
			
		||||
                        }
 | 
			
		||||
                    },
 | 
			
		||||
                    {
 | 
			
		||||
                        _isLoading.value = false
 | 
			
		||||
                        it.message?.let { message -> Logger.e(message) }
 | 
			
		||||
                        _toastLiveData.value = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요."
 | 
			
		||||
                    }
 | 
			
		||||
                )
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -67,6 +67,9 @@ import kr.co.vividnext.sodalive.audition.role.AuditionRoleDetailViewModel
 | 
			
		||||
import kr.co.vividnext.sodalive.chat.character.CharacterApi
 | 
			
		||||
import kr.co.vividnext.sodalive.chat.character.CharacterTabRepository
 | 
			
		||||
import kr.co.vividnext.sodalive.chat.character.CharacterTabViewModel
 | 
			
		||||
import kr.co.vividnext.sodalive.chat.talk.TalkApi
 | 
			
		||||
import kr.co.vividnext.sodalive.chat.talk.TalkTabRepository
 | 
			
		||||
import kr.co.vividnext.sodalive.chat.talk.TalkTabViewModel
 | 
			
		||||
import kr.co.vividnext.sodalive.common.ApiBuilder
 | 
			
		||||
import kr.co.vividnext.sodalive.common.ObjectBox
 | 
			
		||||
import kr.co.vividnext.sodalive.explorer.ExplorerApi
 | 
			
		||||
@@ -249,6 +252,7 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
 | 
			
		||||
        single { ApiBuilder().build(get(), PointStatusApi::class.java) }
 | 
			
		||||
        single { ApiBuilder().build(get(), HomeApi::class.java) }
 | 
			
		||||
        single { ApiBuilder().build(get(), CharacterApi::class.java) }
 | 
			
		||||
        single { ApiBuilder().build(get(), TalkApi::class.java) }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private val viewModelModule = module {
 | 
			
		||||
@@ -347,6 +351,7 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
 | 
			
		||||
        viewModel { PointStatusViewModel(get()) }
 | 
			
		||||
        viewModel { HomeViewModel(get(), get()) }
 | 
			
		||||
        viewModel { CharacterTabViewModel(get()) }
 | 
			
		||||
        viewModel { TalkTabViewModel(get()) }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private val repositoryModule = module {
 | 
			
		||||
@@ -392,6 +397,7 @@ class AppDI(private val context: Context, isDebugMode: Boolean) {
 | 
			
		||||
        factory { PointStatusRepository(get()) }
 | 
			
		||||
        factory { HomeRepository(get()) }
 | 
			
		||||
        factory { CharacterTabRepository(get()) }
 | 
			
		||||
        factory { TalkTabRepository(get()) }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,6 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
    android:shape="rectangle">
 | 
			
		||||
    <solid android:color="#009D68" />
 | 
			
		||||
    <corners android:radius="6dp" />
 | 
			
		||||
</shape>
 | 
			
		||||
@@ -0,0 +1,6 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
    android:shape="rectangle">
 | 
			
		||||
    <solid android:color="#0020C9" />
 | 
			
		||||
    <corners android:radius="6dp" />
 | 
			
		||||
</shape>
 | 
			
		||||
@@ -0,0 +1,6 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
    android:shape="rectangle">
 | 
			
		||||
    <solid android:color="#F86660" />
 | 
			
		||||
    <corners android:radius="6dp" />
 | 
			
		||||
</shape>
 | 
			
		||||
@@ -5,15 +5,14 @@
 | 
			
		||||
    android:layout_height="match_parent"
 | 
			
		||||
    android:background="@color/black">
 | 
			
		||||
 | 
			
		||||
    <TextView
 | 
			
		||||
        android:layout_width="wrap_content"
 | 
			
		||||
        android:layout_height="wrap_content"
 | 
			
		||||
        android:text="톡 탭"
 | 
			
		||||
        android:textColor="@color/white"
 | 
			
		||||
        android:textSize="20sp"
 | 
			
		||||
    <androidx.recyclerview.widget.RecyclerView
 | 
			
		||||
        android:id="@+id/rv_talk"
 | 
			
		||||
        android:layout_width="0dp"
 | 
			
		||||
        android:layout_height="0dp"
 | 
			
		||||
        android:clipToPadding="false"
 | 
			
		||||
        android:padding="24dp"
 | 
			
		||||
        app:layout_constraintBottom_toBottomOf="parent"
 | 
			
		||||
        app:layout_constraintEnd_toEndOf="parent"
 | 
			
		||||
        app:layout_constraintStart_toStartOf="parent"
 | 
			
		||||
        app:layout_constraintTop_toTopOf="parent" />
 | 
			
		||||
 | 
			
		||||
</androidx.constraintlayout.widget.ConstraintLayout>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										92
									
								
								app/src/main/res/layout/item_talk.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								app/src/main/res/layout/item_talk.xml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,92 @@
 | 
			
		||||
<?xml version="1.0" encoding="utf-8"?>
 | 
			
		||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
 | 
			
		||||
    xmlns:tools="http://schemas.android.com/tools"
 | 
			
		||||
    android:layout_width="match_parent"
 | 
			
		||||
    android:layout_height="wrap_content"
 | 
			
		||||
    android:background="@color/black"
 | 
			
		||||
    android:gravity="center_vertical"
 | 
			
		||||
    android:orientation="horizontal">
 | 
			
		||||
 | 
			
		||||
    <!-- 프로필 이미지 -->
 | 
			
		||||
    <ImageView
 | 
			
		||||
        android:id="@+id/iv_profile"
 | 
			
		||||
        android:layout_width="76dp"
 | 
			
		||||
        android:layout_height="76dp"
 | 
			
		||||
        android:background="@color/color_777777"
 | 
			
		||||
        android:contentDescription="@null"
 | 
			
		||||
        android:scaleType="centerCrop" />
 | 
			
		||||
 | 
			
		||||
    <!-- 텍스트 영역 -->
 | 
			
		||||
    <LinearLayout
 | 
			
		||||
        android:layout_width="0dp"
 | 
			
		||||
        android:layout_height="wrap_content"
 | 
			
		||||
        android:layout_marginStart="13dp"
 | 
			
		||||
        android:layout_weight="1"
 | 
			
		||||
        android:orientation="vertical">
 | 
			
		||||
 | 
			
		||||
        <!-- 상단 영역: 캐릭터 이름 + 유형 배지 + 시간 -->
 | 
			
		||||
        <LinearLayout
 | 
			
		||||
            android:layout_width="match_parent"
 | 
			
		||||
            android:layout_height="wrap_content"
 | 
			
		||||
            android:gravity="center_vertical"
 | 
			
		||||
            android:orientation="horizontal">
 | 
			
		||||
 | 
			
		||||
            <!-- 왼쪽 영역: 캐릭터 이름 + 유형 배지 -->
 | 
			
		||||
            <LinearLayout
 | 
			
		||||
                android:layout_width="0dp"
 | 
			
		||||
                android:layout_height="wrap_content"
 | 
			
		||||
                android:layout_weight="1"
 | 
			
		||||
                android:gravity="center_vertical"
 | 
			
		||||
                android:orientation="horizontal">
 | 
			
		||||
 | 
			
		||||
                <!-- 캐릭터 이름 -->
 | 
			
		||||
                <TextView
 | 
			
		||||
                    android:id="@+id/tv_character_name"
 | 
			
		||||
                    android:layout_width="wrap_content"
 | 
			
		||||
                    android:layout_height="wrap_content"
 | 
			
		||||
                    android:fontFamily="@font/pretendard_bold"
 | 
			
		||||
                    android:textColor="@color/white"
 | 
			
		||||
                    android:textSize="18sp"
 | 
			
		||||
                    tools:text="정인이" />
 | 
			
		||||
 | 
			
		||||
                <!-- 캐릭터 유형 배지 -->
 | 
			
		||||
                <TextView
 | 
			
		||||
                    android:id="@+id/tv_character_type"
 | 
			
		||||
                    android:layout_width="wrap_content"
 | 
			
		||||
                    android:layout_height="wrap_content"
 | 
			
		||||
                    android:layout_marginStart="4dp"
 | 
			
		||||
                    android:background="@drawable/bg_character_type_badge_character"
 | 
			
		||||
                    android:fontFamily="@font/pretendard_regular"
 | 
			
		||||
                    android:paddingHorizontal="5dp"
 | 
			
		||||
                    android:paddingVertical="1dp"
 | 
			
		||||
                    android:textColor="#D9FCF4"
 | 
			
		||||
                    android:textSize="12sp"
 | 
			
		||||
                    tools:text="Character" />
 | 
			
		||||
 | 
			
		||||
            </LinearLayout>
 | 
			
		||||
 | 
			
		||||
            <!-- 시간 -->
 | 
			
		||||
            <TextView
 | 
			
		||||
                android:id="@+id/tv_last_time"
 | 
			
		||||
                android:layout_width="wrap_content"
 | 
			
		||||
                android:layout_height="wrap_content"
 | 
			
		||||
                android:fontFamily="@font/pretendard_regular"
 | 
			
		||||
                android:textColor="#78909C"
 | 
			
		||||
                android:textSize="12sp"
 | 
			
		||||
                tools:text="6월 15일" />
 | 
			
		||||
        </LinearLayout>
 | 
			
		||||
 | 
			
		||||
        <!-- 마지막 대화 내용 -->
 | 
			
		||||
        <TextView
 | 
			
		||||
            android:id="@+id/tv_last_message"
 | 
			
		||||
            android:layout_width="match_parent"
 | 
			
		||||
            android:layout_height="wrap_content"
 | 
			
		||||
            android:layout_marginTop="6dp"
 | 
			
		||||
            android:ellipsize="end"
 | 
			
		||||
            android:fontFamily="@font/pretendard_regular"
 | 
			
		||||
            android:maxLines="2"
 | 
			
		||||
            android:textColor="@color/color_b0bec5"
 | 
			
		||||
            android:textSize="14sp"
 | 
			
		||||
            tools:text="태풍온다잖아 ㅜㅜ\n조심해 어디 나가지 말고 집에만..." />
 | 
			
		||||
    </LinearLayout>
 | 
			
		||||
</LinearLayout>
 | 
			
		||||
		Reference in New Issue
	
	Block a user