fix(comment): 답글 더보기 Bottom Sheet 적용 및 삭제/신고 API 연동

답글 리스트에서 PopupMenu를 Bottom Sheet로 통일하고, 내 답글은 삭제, 타인 답글은 신고 메뉴만 노출하도록 변경.
삭제는 원 댓글 삭제와 동일한 API(deleteComment)를 사용하며, 신고는 reportComment로 연동.
This commit is contained in:
2025-08-20 15:55:59 +09:00
parent d1c62fd2b6
commit fdc9ba80e0
2 changed files with 98 additions and 44 deletions

View File

@@ -1,10 +1,8 @@
package kr.co.vividnext.sodalive.chat.character.comment package kr.co.vividnext.sodalive.chat.character.comment
import android.content.Context
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.PopupMenu
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
import coil.load import coil.load
@@ -15,8 +13,7 @@ import kr.co.vividnext.sodalive.databinding.ItemCharacterCommentReplyBinding
class CharacterCommentReplyAdapter( class CharacterCommentReplyAdapter(
private val currentUserId: Long, private val currentUserId: Long,
private val onModify: (id: Long, newText: String, isReply: Boolean) -> Unit, private val onMore: (data: CharacterReplyResponse, isOwner: Boolean) -> Unit
private val onDelete: (id: Long, isReply: Boolean) -> Unit
) : RecyclerView.Adapter<CharacterReplyVH>() { ) : RecyclerView.Adapter<CharacterReplyVH>() {
// 첫 번째 아이템은 항상 원본 댓글 // 첫 번째 아이템은 항상 원본 댓글
@@ -27,15 +24,21 @@ class CharacterCommentReplyAdapter(
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CharacterReplyVH { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CharacterReplyVH {
return if (viewType == 0) { return if (viewType == 0) {
CharacterReplyHeaderVH( CharacterReplyHeaderVH(
ItemCharacterCommentBinding.inflate(LayoutInflater.from(parent.context), parent, false) ItemCharacterCommentBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
) )
} else { } else {
CharacterReplyItemVH( CharacterReplyItemVH(
context = parent.context, binding = ItemCharacterCommentReplyBinding.inflate(
binding = ItemCharacterCommentReplyBinding.inflate(LayoutInflater.from(parent.context), parent, false), LayoutInflater.from(parent.context),
parent,
false
),
currentUserId = currentUserId, currentUserId = currentUserId,
onModify = onModify, onMoreCallback = onMore
onDelete = onDelete
) )
} }
} }
@@ -76,11 +79,9 @@ class CharacterReplyHeaderVH(
} }
class CharacterReplyItemVH( class CharacterReplyItemVH(
private val context: Context,
private val binding: ItemCharacterCommentReplyBinding, private val binding: ItemCharacterCommentReplyBinding,
private val currentUserId: Long, private val currentUserId: Long,
private val onModify: (id: Long, newText: String, isReply: Boolean) -> Unit, private val onMoreCallback: (data: CharacterReplyResponse, isOwner: Boolean) -> Unit
private val onDelete: (id: Long, isReply: Boolean) -> Unit
) : CharacterReplyVH(binding) { ) : CharacterReplyVH(binding) {
override fun bind(item: Any) { override fun bind(item: Any) {
@@ -103,25 +104,8 @@ class CharacterReplyItemVH(
val isOwner = data.memberId == currentUserId val isOwner = data.memberId == currentUserId
binding.ivMenu.visibility = View.VISIBLE binding.ivMenu.visibility = View.VISIBLE
binding.ivMenu.setOnClickListener { binding.ivMenu.setOnClickListener {
val popup = PopupMenu(context, it) // 답글의 더보기는 PopupMenu 대신 BottomSheet를 사용하기 위해 Fragment 측 콜백으로 위임
if (isOwner) { onMoreCallback(data, isOwner)
popup.menuInflater.inflate(R.menu.content_comment_option_menu, popup.menu)
} else {
popup.menuInflater.inflate(R.menu.content_comment_option_menu2, popup.menu)
}
popup.setOnMenuItemClickListener { mi ->
when (mi.itemId) {
R.id.menu_review_modify -> {
// 간단화: 수정은 현재 텍스트 그대로 콜백 (실서비스는 인라인 에디트 UI 구성)
onModify(data.replyId, data.comment, true)
}
R.id.menu_review_delete -> {
onDelete(data.replyId, true)
}
}
true
}
popup.show()
} }
} }
} }

View File

@@ -8,6 +8,7 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import coil.load import coil.load
@@ -140,20 +141,89 @@ class CharacterCommentReplyFragment : BaseFragment<FragmentCharacterCommentReply
adapter = CharacterCommentReplyAdapter( adapter = CharacterCommentReplyAdapter(
currentUserId = SharedPreferenceManager.userId, currentUserId = SharedPreferenceManager.userId,
onModify = { id, newText, isReply -> onMore = { reply, isOwner ->
Toast.makeText(requireContext(), "수정 스텁: $id", Toast.LENGTH_SHORT).show() CharacterCommentMoreBottomSheet.newInstance(isOwner).apply {
}, onReport = {
onDelete = { id, isReply -> val reportSheet = CharacterCommentReportBottomSheet.newInstance()
val index = adapter.items.indexOfFirst { reportSheet.onSubmit = { reason ->
when (it) { val token = "Bearer ${SharedPreferenceManager.token}"
is CharacterReplyResponse -> it.replyId == id val d = repository.reportComment(
else -> false characterId = characterId,
commentId = reply.replyId,
reason = reason,
token = token
).subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ resp ->
if (resp.success) {
Toast.makeText(
requireContext(),
"신고가 접수되었습니다.",
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)
} }
if (index > 0) { // 0은 원본 댓글이므로 제외 reportSheet.show(parentFragmentManager, "reply_report")
}
onDelete = {
AlertDialog.Builder(requireContext())
.setTitle(getString(R.string.confirm_delete_title))
.setMessage(getString(R.string.confirm_delete_message))
.setPositiveButton(getString(R.string.confirm)) { _, _ ->
val token = "Bearer ${SharedPreferenceManager.token}"
loadingDialog.show(screenWidth)
val d = repository.deleteComment(
characterId = characterId,
commentId = reply.replyId,
token = token
).subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doFinally { loadingDialog.dismiss() }
.subscribe({ resp ->
if (resp.success) {
val index = adapter.items
.indexOfFirst {
it is CharacterReplyResponse
&& it.replyId == reply.replyId
}
if (index > 0) {
adapter.items.removeAt(index) adapter.items.removeAt(index)
adapter.notifyItemRemoved(index) adapter.notifyItemRemoved(index)
} }
} else {
Toast.makeText(
requireActivity(),
resp.message ?: "요청 중 오류가 발생했습니다",
Toast.LENGTH_SHORT
).show()
}
}, { e ->
Toast.makeText(
requireActivity(),
e.message ?: "요청 중 오류가 발생했습니다",
Toast.LENGTH_SHORT
).show()
})
compositeDisposable.add(d)
}
.setNegativeButton(getString(R.string.cancel), null)
.show()
}
}.show(childFragmentManager, "reply_more")
} }
).apply { ).apply {
items.clear() items.clear()