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 coil.load
|
||||
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.base.BaseFragment
|
||||
import kr.co.vividnext.sodalive.common.LoadingDialog
|
||||
import kr.co.vividnext.sodalive.common.SharedPreferenceManager
|
||||
import kr.co.vividnext.sodalive.databinding.FragmentCharacterCommentReplyBinding
|
||||
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
|
||||
|
||||
/**
|
||||
@@ -93,21 +93,49 @@ class CharacterCommentReplyFragment : BaseFragment<FragmentCharacterCommentReply
|
||||
hideKeyboard()
|
||||
val text = binding.etComment.text.toString()
|
||||
if (text.isBlank()) return@setOnClickListener
|
||||
// 스텁: 로컬에 즉시 추가
|
||||
val me = CharacterReplyResponse(
|
||||
replyId = System.currentTimeMillis(),
|
||||
memberId = SharedPreferenceManager.userId,
|
||||
memberProfileImage = SharedPreferenceManager.profileImage,
|
||||
memberNickname = SharedPreferenceManager.nickname,
|
||||
createdAt = System.currentTimeMillis(),
|
||||
comment = text
|
||||
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
|
||||
)
|
||||
val insertAt = adapter.items.size
|
||||
adapter.items.add(me)
|
||||
adapter.notifyItemInserted(insertAt)
|
||||
binding.rvCommentReply.scrollToPosition(adapter.items.size - 1)
|
||||
binding.etComment.setText("")
|
||||
Toast.makeText(requireContext(), "등록되었습니다 (stub)", Toast.LENGTH_SHORT).show()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doFinally { loadingDialog.dismiss() }
|
||||
.subscribe({ resp ->
|
||||
if (resp.success) {
|
||||
// 성공 시 낙관적 UI 반영: 기존 스텁과 동일하게 즉시 목록에 추가
|
||||
val me = CharacterReplyResponse(
|
||||
replyId = System.currentTimeMillis(),
|
||||
memberId = SharedPreferenceManager.userId,
|
||||
memberProfileImage = SharedPreferenceManager.profileImage,
|
||||
memberNickname = SharedPreferenceManager.nickname,
|
||||
createdAt = System.currentTimeMillis(),
|
||||
comment = text
|
||||
)
|
||||
val insertAt = adapter.items.size
|
||||
adapter.items.add(me)
|
||||
adapter.notifyItemInserted(insertAt)
|
||||
binding.rvCommentReply.scrollToPosition(adapter.items.size - 1)
|
||||
binding.etComment.setText("")
|
||||
} 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(
|
||||
@@ -134,16 +162,30 @@ class CharacterCommentReplyFragment : BaseFragment<FragmentCharacterCommentReply
|
||||
|
||||
val recyclerView = binding.rvCommentReply
|
||||
recyclerView.setHasFixedSize(true)
|
||||
recyclerView.layoutManager = LinearLayoutManager(activity, LinearLayoutManager.VERTICAL, false)
|
||||
recyclerView.layoutManager =
|
||||
LinearLayoutManager(activity, LinearLayoutManager.VERTICAL, false)
|
||||
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)
|
||||
outRect.left = 13.3f.dpToPx().toInt()
|
||||
outRect.right = 13.3f.dpToPx().toInt()
|
||||
when (parent.getChildAdapterPosition(view)) {
|
||||
0 -> { outRect.top = 13.3f.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() }
|
||||
0 -> {
|
||||
outRect.top = 13.3f.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_ORIGINAL_COMMENT_ID = "extra_original_comment_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_CREATED_AT = "extra_original_created_at"
|
||||
private const val EXTRA_ORIGINAL_REPLY_COUNT = "extra_original_reply_count"
|
||||
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 {
|
||||
putLong(EXTRA_CHARACTER_ID, characterId)
|
||||
putLong(EXTRA_ORIGINAL_COMMENT_ID, original.commentId)
|
||||
|
||||
Reference in New Issue
Block a user