From b1c9c3e124dee4b2c55b553ce8c5ef16d533f9a8 Mon Sep 17 00:00:00 2001 From: klaus Date: Tue, 5 Aug 2025 02:01:19 +0900 Subject: [PATCH] =?UTF-8?q?feat(ui):=20=ED=86=A1=20=ED=83=AD=20-=20api,=20?= =?UTF-8?q?viewmodel,=20repository=20=EC=97=B0=EA=B2=B0=20-=20=EC=B1=84?= =?UTF-8?q?=ED=8C=85=EB=B0=A9=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20UI=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vividnext/sodalive/chat/talk/TalkApi.kt | 13 +++ .../vividnext/sodalive/chat/talk/TalkRoom.kt | 10 ++ .../sodalive/chat/talk/TalkRoomResponse.kt | 6 ++ .../sodalive/chat/talk/TalkTabAdapter.kt | 71 ++++++++++++++ .../sodalive/chat/talk/TalkTabFragment.kt | 58 +++++++++++- .../sodalive/chat/talk/TalkTabRepository.kt | 7 ++ .../sodalive/chat/talk/TalkTabViewModel.kt | 47 ++++++++++ .../java/kr/co/vividnext/sodalive/di/AppDI.kt | 6 ++ .../bg_character_type_badge_character.xml | 6 ++ .../bg_character_type_badge_clone.xml | 6 ++ .../bg_character_type_badge_creator.xml | 6 ++ app/src/main/res/layout/fragment_talk_tab.xml | 13 ++- app/src/main/res/layout/item_talk.xml | 92 +++++++++++++++++++ 13 files changed, 333 insertions(+), 8 deletions(-) create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/chat/talk/TalkApi.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/chat/talk/TalkRoom.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/chat/talk/TalkRoomResponse.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/chat/talk/TalkTabAdapter.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/chat/talk/TalkTabRepository.kt create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/chat/talk/TalkTabViewModel.kt create mode 100644 app/src/main/res/drawable/bg_character_type_badge_character.xml create mode 100644 app/src/main/res/drawable/bg_character_type_badge_clone.xml create mode 100644 app/src/main/res/drawable/bg_character_type_badge_creator.xml create mode 100644 app/src/main/res/layout/item_talk.xml diff --git a/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/TalkApi.kt b/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/TalkApi.kt new file mode 100644 index 00000000..c81a4fb2 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/TalkApi.kt @@ -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> +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/TalkRoom.kt b/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/TalkRoom.kt new file mode 100644 index 00000000..f458511b --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/TalkRoom.kt @@ -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 +) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/TalkRoomResponse.kt b/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/TalkRoomResponse.kt new file mode 100644 index 00000000..c38b036a --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/TalkRoomResponse.kt @@ -0,0 +1,6 @@ +package kr.co.vividnext.sodalive.chat.talk + +data class TalkRoomResponse( + val totalCount: Int, + val items: List +) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/TalkTabAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/TalkTabAdapter.kt new file mode 100644 index 00000000..ef9c4199 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/TalkTabAdapter.kt @@ -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(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() { + override fun areItemsTheSame(oldItem: TalkRoom, newItem: TalkRoom): Boolean { + return oldItem.id == newItem.id + } + + override fun areContentsTheSame(oldItem: TalkRoom, newItem: TalkRoom): Boolean { + return oldItem == newItem + } + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/TalkTabFragment.kt b/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/TalkTabFragment.kt index b65f5f0e..eaa3c7fe 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/TalkTabFragment.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/TalkTabFragment.kt @@ -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::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() + } } } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/TalkTabRepository.kt b/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/TalkTabRepository.kt new file mode 100644 index 00000000..98693f4f --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/TalkTabRepository.kt @@ -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) +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/TalkTabViewModel.kt b/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/TalkTabViewModel.kt new file mode 100644 index 00000000..fa2d8d7c --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/chat/talk/TalkTabViewModel.kt @@ -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>() + val talkRooms: LiveData> = _talkRooms + + private val _isLoading = MutableLiveData() + val isLoading: LiveData = _isLoading + + private val _toastLiveData = MutableLiveData() + val toastLiveData: LiveData + 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 = "알 수 없는 오류가 발생했습니다. 다시 시도해 주세요." + } + ) + ) + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt b/app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt index a614ffd6..15d76b50 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/di/AppDI.kt @@ -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()) } } diff --git a/app/src/main/res/drawable/bg_character_type_badge_character.xml b/app/src/main/res/drawable/bg_character_type_badge_character.xml new file mode 100644 index 00000000..281afcb4 --- /dev/null +++ b/app/src/main/res/drawable/bg_character_type_badge_character.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/app/src/main/res/drawable/bg_character_type_badge_clone.xml b/app/src/main/res/drawable/bg_character_type_badge_clone.xml new file mode 100644 index 00000000..fe7136b3 --- /dev/null +++ b/app/src/main/res/drawable/bg_character_type_badge_clone.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/app/src/main/res/drawable/bg_character_type_badge_creator.xml b/app/src/main/res/drawable/bg_character_type_badge_creator.xml new file mode 100644 index 00000000..496c3869 --- /dev/null +++ b/app/src/main/res/drawable/bg_character_type_badge_creator.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/app/src/main/res/layout/fragment_talk_tab.xml b/app/src/main/res/layout/fragment_talk_tab.xml index 68bef76e..23b6cb79 100644 --- a/app/src/main/res/layout/fragment_talk_tab.xml +++ b/app/src/main/res/layout/fragment_talk_tab.xml @@ -5,15 +5,14 @@ android:layout_height="match_parent" android:background="@color/black"> - - diff --git a/app/src/main/res/layout/item_talk.xml b/app/src/main/res/layout/item_talk.xml new file mode 100644 index 00000000..42887dfb --- /dev/null +++ b/app/src/main/res/layout/item_talk.xml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +