diff --git a/app/src/main/java/kr/co/vividnext/sodalive/chat/ChatFragment.kt b/app/src/main/java/kr/co/vividnext/sodalive/chat/ChatFragment.kt index 5bd72fe3..5c706c23 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/chat/ChatFragment.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/chat/ChatFragment.kt @@ -7,6 +7,7 @@ import com.google.android.material.tabs.TabLayout import kr.co.vividnext.sodalive.R import kr.co.vividnext.sodalive.base.BaseFragment import kr.co.vividnext.sodalive.chat.character.CharacterTabFragment +import kr.co.vividnext.sodalive.chat.original.OriginalTabFragment import kr.co.vividnext.sodalive.chat.talk.TalkTabFragment import kr.co.vividnext.sodalive.common.SharedPreferenceManager import kr.co.vividnext.sodalive.databinding.FragmentChatBinding @@ -52,6 +53,7 @@ class ChatFragment : BaseFragment(FragmentChatBinding::infl private fun setupTabs() { // 탭 추가 binding.tabLayout.addTab(binding.tabLayout.newTab().setText("캐릭터")) + binding.tabLayout.addTab(binding.tabLayout.newTab().setText("작품별")) binding.tabLayout.addTab(binding.tabLayout.newTab().setText("톡")) // 탭 선택 리스너 설정 @@ -86,7 +88,8 @@ class ChatFragment : BaseFragment(FragmentChatBinding::infl // 선택된 탭에 따라 프래그먼트 표시 val fragment = when (position) { 0 -> CharacterTabFragment() - 1 -> TalkTabFragment() + 1 -> OriginalTabFragment() + 2 -> TalkTabFragment() else -> CharacterTabFragment() } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/chat/original/OriginalTabFragment.kt b/app/src/main/java/kr/co/vividnext/sodalive/chat/original/OriginalTabFragment.kt new file mode 100644 index 00000000..0a20db74 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/chat/original/OriginalTabFragment.kt @@ -0,0 +1,61 @@ +package kr.co.vividnext.sodalive.chat.original + +import android.os.Bundle +import android.view.View +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import kr.co.vividnext.sodalive.base.BaseFragment +import kr.co.vividnext.sodalive.common.GridSpacingItemDecoration +import kr.co.vividnext.sodalive.databinding.FragmentOriginalTabBinding +import kr.co.vividnext.sodalive.extensions.dpToPx +import org.koin.android.ext.android.inject + +class OriginalTabFragment : + BaseFragment(FragmentOriginalTabBinding::inflate) { + + private val viewModel: OriginalWorkViewModel by inject() + + private lateinit var adapter: OriginalWorkListAdapter + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setupRecycler() + bind() + viewModel.loadMore() + } + + private fun setupRecycler() { + val spanCount = 3 + val spacingPx = 16f.dpToPx().toInt() + adapter = OriginalWorkListAdapter { /* TODO: 상세 페이지 이동 정의 시 연결 */ } + binding.rvOriginal.layoutManager = GridLayoutManager(requireContext(), spanCount) + binding.rvOriginal.addItemDecoration( + GridSpacingItemDecoration( + spanCount, + spacingPx, + true + ) + ) + binding.rvOriginal.addOnScrollListener(object : RecyclerView.OnScrollListener() { + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + super.onScrolled(recyclerView, dx, dy) + val lastVisibleItemPosition = (recyclerView.layoutManager as LinearLayoutManager) + .findLastVisibleItemPosition() + val totalItemCount = recyclerView.adapter?.itemCount ?: 0 + if (!recyclerView.canScrollVertically(1) && lastVisibleItemPosition >= totalItemCount - 1) { + viewModel.loadMore() + } + } + }) + binding.rvOriginal.adapter = adapter + } + + private fun bind() { + viewModel.items.observe(viewLifecycleOwner) { list -> + // 누적 리스트를 어댑터에 추가 + adapter.addItems(list.drop(adapter.itemCount)) + } + // 필요 시 로딩/토스트 처리 추가 + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/chat/original/OriginalWorkApi.kt b/app/src/main/java/kr/co/vividnext/sodalive/chat/original/OriginalWorkApi.kt new file mode 100644 index 00000000..3b2ba0b0 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/chat/original/OriginalWorkApi.kt @@ -0,0 +1,16 @@ +package kr.co.vividnext.sodalive.chat.original + +import io.reactivex.rxjava3.core.Single +import kr.co.vividnext.sodalive.common.ApiResponse +import retrofit2.http.GET +import retrofit2.http.Header +import retrofit2.http.Query + +interface OriginalWorkApi { + @GET("/api/chat/original/list") + fun getOriginalWorkList( + @Header("Authorization") authHeader: String, + @Query("page") page: Int, + @Query("size") size: Int + ): Single> +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/chat/original/OriginalWorkListAdapter.kt b/app/src/main/java/kr/co/vividnext/sodalive/chat/original/OriginalWorkListAdapter.kt new file mode 100644 index 00000000..4cfd0c4b --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/chat/original/OriginalWorkListAdapter.kt @@ -0,0 +1,61 @@ +package kr.co.vividnext.sodalive.chat.original + +import android.annotation.SuppressLint +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import coil.load +import coil.transform.RoundedCornersTransformation +import kr.co.vividnext.sodalive.R +import kr.co.vividnext.sodalive.databinding.ItemOriginalWorkBinding +import kr.co.vividnext.sodalive.extensions.dpToPx + +class OriginalWorkListAdapter( + private val onClick: (Long) -> Unit +) : RecyclerView.Adapter() { + + private val items = mutableListOf() + + inner class VH(val binding: ItemOriginalWorkBinding) : RecyclerView.ViewHolder(binding.root) { + fun bind(item: OriginalWorkListItemResponse) { + binding.tvTitle.text = item.title + binding.tvContentType.text = item.contentType + binding.ivCover.load(item.imageUrl) { + crossfade(true) + placeholder(R.drawable.ic_logo_service_center) + transformations(RoundedCornersTransformation(16f.dpToPx())) + } + binding.root.setOnClickListener { onClick(item.id) } + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH { + val binding = ItemOriginalWorkBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return VH(binding) + } + + override fun getItemCount(): Int = items.size + + override fun onBindViewHolder(holder: VH, position: Int) { + holder.bind(items[position]) + } + + @SuppressLint("NotifyDataSetChanged") + fun submitList(newItems: List) { + items.clear() + items.addAll(newItems) + notifyDataSetChanged() + } + + fun addItems(newItems: List) { + val start = items.size + items.addAll(newItems) + notifyItemRangeInserted(start, newItems.size) + } + + @SuppressLint("NotifyDataSetChanged") + fun clear() { + items.clear() + notifyDataSetChanged() + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/chat/original/OriginalWorkListResponse.kt b/app/src/main/java/kr/co/vividnext/sodalive/chat/original/OriginalWorkListResponse.kt new file mode 100644 index 00000000..400e390a --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/chat/original/OriginalWorkListResponse.kt @@ -0,0 +1,17 @@ +package kr.co.vividnext.sodalive.chat.original + +import androidx.annotation.Keep + +@Keep +data class OriginalWorkListResponse( + val totalCount: Long, + val content: List +) + +@Keep +data class OriginalWorkListItemResponse( + val id: Long, + val imageUrl: String?, + val title: String, + val contentType: String +) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/chat/original/OriginalWorkRepository.kt b/app/src/main/java/kr/co/vividnext/sodalive/chat/original/OriginalWorkRepository.kt new file mode 100644 index 00000000..fbdede9a --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/chat/original/OriginalWorkRepository.kt @@ -0,0 +1,12 @@ +package kr.co.vividnext.sodalive.chat.original + +import io.reactivex.rxjava3.core.Single +import kr.co.vividnext.sodalive.common.ApiResponse + +class OriginalWorkRepository( + private val api: OriginalWorkApi +) { + fun getOriginalWorks(token: String, page: Int, size: Int): Single> { + return api.getOriginalWorkList(token, page, size) + } +} diff --git a/app/src/main/java/kr/co/vividnext/sodalive/chat/original/OriginalWorkViewModel.kt b/app/src/main/java/kr/co/vividnext/sodalive/chat/original/OriginalWorkViewModel.kt new file mode 100644 index 00000000..13b514d7 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/chat/original/OriginalWorkViewModel.kt @@ -0,0 +1,71 @@ +package kr.co.vividnext.sodalive.chat.original + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +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 OriginalWorkViewModel( + private val repository: OriginalWorkRepository +) : BaseViewModel() { + + private val _isLoading = MutableLiveData(false) + val isLoading: LiveData get() = _isLoading + + private val _toast = MutableLiveData(null) + val toast: LiveData get() = _toast + + private val _totalCount = MutableLiveData(0) + val totalCount: LiveData get() = _totalCount + + private val _items = MutableLiveData>(emptyList()) + val items: LiveData> get() = _items + + private var page = 0 + private val size = 20 + private var isLast = false + + fun loadMore() { + if (_isLoading.value == true || isLast) return + _isLoading.value = true + + compositeDisposable.add( + repository.getOriginalWorks( + token = "Bearer ${SharedPreferenceManager.token}", + page = page, + size = size + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ response -> + val data = response.data + if (response.success && data != null) { + val current = _items.value ?: emptyList() + val next = current + data.content + _items.value = next + _totalCount.value = data.totalCount + if (data.content.isNotEmpty()) { + page += 1 + } else { + isLast = true + } + } else { + _toast.value = response.message ?: "알 수 없는 오류가 발생했습니다." + } + _isLoading.value = false + }, { e -> + _isLoading.value = false + _toast.value = e.message ?: "알 수 없는 오류가 발생했습니다." + }) + ) + } + + fun refresh() { + page = 0 + isLast = false + _items.value = emptyList() + loadMore() + } +} 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 cd42f689..9e13fe3f 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 @@ -79,6 +79,9 @@ 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.chat.talk.room.chatTalkRoomModule +import kr.co.vividnext.sodalive.chat.original.OriginalWorkApi +import kr.co.vividnext.sodalive.chat.original.OriginalWorkRepository +import kr.co.vividnext.sodalive.chat.original.OriginalWorkViewModel import kr.co.vividnext.sodalive.common.ApiBuilder import kr.co.vividnext.sodalive.common.ObjectBox import kr.co.vividnext.sodalive.explorer.ExplorerApi @@ -263,6 +266,7 @@ class AppDI(private val context: Context, isDebugMode: Boolean) { single { ApiBuilder().build(get(), CharacterApi::class.java) } single { ApiBuilder().build(get(), TalkApi::class.java) } single { ApiBuilder().build(get(), CharacterCommentApi::class.java) } + single { ApiBuilder().build(get(), OriginalWorkApi::class.java) } } private val viewModelModule = module { @@ -367,6 +371,7 @@ class AppDI(private val context: Context, isDebugMode: Boolean) { viewModel { kr.co.vividnext.sodalive.chat.character.comment.CharacterCommentListViewModel(get()) } viewModel { kr.co.vividnext.sodalive.chat.character.comment.CharacterCommentReplyViewModel(get()) } viewModel { NewCharactersAllViewModel(get()) } + viewModel { OriginalWorkViewModel(get()) } } private val repositoryModule = module { @@ -417,6 +422,7 @@ class AppDI(private val context: Context, isDebugMode: Boolean) { factory { TalkTabRepository(get()) } factory { CharacterCommentRepository(get()) } factory { NewCharactersRepository(get()) } + factory { OriginalWorkRepository(get()) } } diff --git a/app/src/main/res/layout/fragment_original_tab.xml b/app/src/main/res/layout/fragment_original_tab.xml new file mode 100644 index 00000000..1c7732b6 --- /dev/null +++ b/app/src/main/res/layout/fragment_original_tab.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/app/src/main/res/layout/item_original_work.xml b/app/src/main/res/layout/item_original_work.xml new file mode 100644 index 00000000..eb4fa623 --- /dev/null +++ b/app/src/main/res/layout/item_original_work.xml @@ -0,0 +1,48 @@ + + + + + + + + + +