feat(character-comment): 답글 리스트 API 연동 및 커서 기반 무한 스크롤 적용
feat(character-comment): 답글 작성 API 연동 및 성공 시 낙관적 UI 반영 - CharacterCommentReplyFragment에 listReplies API 연동 - 초기 1회 로드 허용, 이후 cursor != null일 때만 추가 로드 - isLoading 플래그로 중복 요청 방지 - 어댑터 헤더(원본 댓글) 유지, replies만 순차 추가 - CharacterCommentReplyFragment에서 createReply API 호출로 스텁 제거 - 요청 중 로딩 다이얼로그 표시, 성공 시 입력 초기화 및 리스트에 즉시 추가 - 에러 처리(토스트) 적용
This commit is contained in:
@@ -12,14 +12,14 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
|||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import coil.load
|
import coil.load
|
||||||
import coil.transform.CircleCropTransformation
|
import coil.transform.CircleCropTransformation
|
||||||
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
|
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||||
import kr.co.vividnext.sodalive.R
|
import kr.co.vividnext.sodalive.R
|
||||||
import kr.co.vividnext.sodalive.base.BaseFragment
|
import kr.co.vividnext.sodalive.base.BaseFragment
|
||||||
import kr.co.vividnext.sodalive.common.LoadingDialog
|
import kr.co.vividnext.sodalive.common.LoadingDialog
|
||||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||||
import kr.co.vividnext.sodalive.databinding.FragmentCharacterCommentReplyBinding
|
import kr.co.vividnext.sodalive.databinding.FragmentCharacterCommentReplyBinding
|
||||||
import kr.co.vividnext.sodalive.extensions.dpToPx
|
import kr.co.vividnext.sodalive.extensions.dpToPx
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -93,7 +93,21 @@ class CharacterCommentReplyFragment : BaseFragment<FragmentCharacterCommentReply
|
|||||||
hideKeyboard()
|
hideKeyboard()
|
||||||
val text = binding.etComment.text.toString()
|
val text = binding.etComment.text.toString()
|
||||||
if (text.isBlank()) return@setOnClickListener
|
if (text.isBlank()) return@setOnClickListener
|
||||||
// 스텁: 로컬에 즉시 추가
|
val originalId = original?.commentId ?: return@setOnClickListener
|
||||||
|
val token = "Bearer ${SharedPreferenceManager.token}"
|
||||||
|
loadingDialog.show(screenWidth)
|
||||||
|
val d = repository.createReply(
|
||||||
|
characterId = characterId,
|
||||||
|
commentId = originalId,
|
||||||
|
comment = text,
|
||||||
|
token = token
|
||||||
|
)
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.doFinally { loadingDialog.dismiss() }
|
||||||
|
.subscribe({ resp ->
|
||||||
|
if (resp.success) {
|
||||||
|
// 성공 시 낙관적 UI 반영: 기존 스텁과 동일하게 즉시 목록에 추가
|
||||||
val me = CharacterReplyResponse(
|
val me = CharacterReplyResponse(
|
||||||
replyId = System.currentTimeMillis(),
|
replyId = System.currentTimeMillis(),
|
||||||
memberId = SharedPreferenceManager.userId,
|
memberId = SharedPreferenceManager.userId,
|
||||||
@@ -107,7 +121,21 @@ class CharacterCommentReplyFragment : BaseFragment<FragmentCharacterCommentReply
|
|||||||
adapter.notifyItemInserted(insertAt)
|
adapter.notifyItemInserted(insertAt)
|
||||||
binding.rvCommentReply.scrollToPosition(adapter.items.size - 1)
|
binding.rvCommentReply.scrollToPosition(adapter.items.size - 1)
|
||||||
binding.etComment.setText("")
|
binding.etComment.setText("")
|
||||||
Toast.makeText(requireContext(), "등록되었습니다 (stub)", Toast.LENGTH_SHORT).show()
|
} else {
|
||||||
|
Toast.makeText(
|
||||||
|
requireContext(),
|
||||||
|
resp.message ?: "요청 중 오류가 발생했습니다",
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
}
|
||||||
|
}, { e ->
|
||||||
|
Toast.makeText(
|
||||||
|
requireContext(),
|
||||||
|
e.message ?: "요청 중 오류가 발생했습니다",
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
).show()
|
||||||
|
})
|
||||||
|
compositeDisposable.add(d)
|
||||||
}
|
}
|
||||||
|
|
||||||
adapter = CharacterCommentReplyAdapter(
|
adapter = CharacterCommentReplyAdapter(
|
||||||
@@ -134,16 +162,30 @@ class CharacterCommentReplyFragment : BaseFragment<FragmentCharacterCommentReply
|
|||||||
|
|
||||||
val recyclerView = binding.rvCommentReply
|
val recyclerView = binding.rvCommentReply
|
||||||
recyclerView.setHasFixedSize(true)
|
recyclerView.setHasFixedSize(true)
|
||||||
recyclerView.layoutManager = LinearLayoutManager(activity, LinearLayoutManager.VERTICAL, false)
|
recyclerView.layoutManager =
|
||||||
|
LinearLayoutManager(activity, LinearLayoutManager.VERTICAL, false)
|
||||||
recyclerView.addItemDecoration(object : RecyclerView.ItemDecoration() {
|
recyclerView.addItemDecoration(object : RecyclerView.ItemDecoration() {
|
||||||
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
|
override fun getItemOffsets(
|
||||||
|
outRect: Rect,
|
||||||
|
view: View,
|
||||||
|
parent: RecyclerView,
|
||||||
|
state: RecyclerView.State
|
||||||
|
) {
|
||||||
super.getItemOffsets(outRect, view, parent, state)
|
super.getItemOffsets(outRect, view, parent, state)
|
||||||
outRect.left = 13.3f.dpToPx().toInt()
|
outRect.left = 13.3f.dpToPx().toInt()
|
||||||
outRect.right = 13.3f.dpToPx().toInt()
|
outRect.right = 13.3f.dpToPx().toInt()
|
||||||
when (parent.getChildAdapterPosition(view)) {
|
when (parent.getChildAdapterPosition(view)) {
|
||||||
0 -> { outRect.top = 13.3f.dpToPx().toInt(); outRect.bottom = 12f.dpToPx().toInt() }
|
0 -> {
|
||||||
adapter.itemCount - 1 -> { outRect.top = 12f.dpToPx().toInt(); outRect.bottom = 13.3f.dpToPx().toInt() }
|
outRect.top = 13.3f.dpToPx().toInt(); outRect.bottom = 12f.dpToPx().toInt()
|
||||||
else -> { outRect.top = 12f.dpToPx().toInt(); outRect.bottom = 12f.dpToPx().toInt() }
|
}
|
||||||
|
|
||||||
|
adapter.itemCount - 1 -> {
|
||||||
|
outRect.top = 12f.dpToPx().toInt(); outRect.bottom = 13.3f.dpToPx().toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
outRect.top = 12f.dpToPx().toInt(); outRect.bottom = 12f.dpToPx().toInt()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -218,12 +260,16 @@ class CharacterCommentReplyFragment : BaseFragment<FragmentCharacterCommentReply
|
|||||||
private const val EXTRA_CHARACTER_ID = "extra_character_id"
|
private const val EXTRA_CHARACTER_ID = "extra_character_id"
|
||||||
private const val EXTRA_ORIGINAL_COMMENT_ID = "extra_original_comment_id"
|
private const val EXTRA_ORIGINAL_COMMENT_ID = "extra_original_comment_id"
|
||||||
private const val EXTRA_ORIGINAL_MEMBER_ID = "extra_original_member_id"
|
private const val EXTRA_ORIGINAL_MEMBER_ID = "extra_original_member_id"
|
||||||
private const val EXTRA_ORIGINAL_MEMBER_PROFILE_IMAGE = "extra_original_member_profile_image"
|
private const val EXTRA_ORIGINAL_MEMBER_PROFILE_IMAGE =
|
||||||
|
"extra_original_member_profile_image"
|
||||||
private const val EXTRA_ORIGINAL_MEMBER_NICKNAME = "extra_original_member_nickname"
|
private const val EXTRA_ORIGINAL_MEMBER_NICKNAME = "extra_original_member_nickname"
|
||||||
private const val EXTRA_ORIGINAL_CREATED_AT = "extra_original_created_at"
|
private const val EXTRA_ORIGINAL_CREATED_AT = "extra_original_created_at"
|
||||||
private const val EXTRA_ORIGINAL_REPLY_COUNT = "extra_original_reply_count"
|
private const val EXTRA_ORIGINAL_REPLY_COUNT = "extra_original_reply_count"
|
||||||
private const val EXTRA_ORIGINAL_COMMENT_TEXT = "extra_original_comment_text"
|
private const val EXTRA_ORIGINAL_COMMENT_TEXT = "extra_original_comment_text"
|
||||||
fun newInstance(characterId: Long, original: CharacterCommentResponse): CharacterCommentReplyFragment {
|
fun newInstance(
|
||||||
|
characterId: Long,
|
||||||
|
original: CharacterCommentResponse
|
||||||
|
): CharacterCommentReplyFragment {
|
||||||
val args = Bundle().apply {
|
val args = Bundle().apply {
|
||||||
putLong(EXTRA_CHARACTER_ID, characterId)
|
putLong(EXTRA_CHARACTER_ID, characterId)
|
||||||
putLong(EXTRA_ORIGINAL_COMMENT_ID, original.commentId)
|
putLong(EXTRA_ORIGINAL_COMMENT_ID, original.commentId)
|
||||||
|
|||||||
Reference in New Issue
Block a user