feat(character-comment): 신고 BottomSheet 추가 및 삭제 확인 팝업 도입

- 신고 BottomSheet(제목/단일선택 리스트/신고 버튼) 구현 및 더보기→신고 흐름 연동
- 삭제 버튼 클릭 시 확인 다이얼로그 표시 후 확정 시 리스트에서 제거
- 신고/삭제 API 호출부는 스텁으로 남겨둠(후속 연동 예정)
This commit is contained in:
2025-08-20 01:20:12 +09:00
parent d4ec2fbdef
commit 52ff0c82cb
6 changed files with 208 additions and 12 deletions

View File

@@ -92,15 +92,30 @@ class CharacterCommentListFragment : BaseFragment<FragmentCharacterCommentListBi
onClickMore = { item, isOwner, anchor -> onClickMore = { item, isOwner, anchor ->
CharacterCommentMoreBottomSheet.newInstance(isOwner).apply { CharacterCommentMoreBottomSheet.newInstance(isOwner).apply {
onReport = { onReport = {
Toast.makeText(requireContext(), "신고되었습니다 (stub)", Toast.LENGTH_SHORT).show() // 더보기 닫히고 신고 BottomSheet 열림
val reportSheet = CharacterCommentReportBottomSheet.newInstance()
reportSheet.onSubmit = { reason ->
// 신고 API 스텁 호출 지점
Toast.makeText(requireContext(), "신고 접수: $reason (stub)", Toast.LENGTH_SHORT).show()
}
reportSheet.show(childFragmentManager, "comment_report")
} }
onDelete = { onDelete = {
// 삭제 확인 팝업
androidx.appcompat.app.AlertDialog.Builder(requireContext())
.setTitle(getString(R.string.confirm_delete_title))
.setMessage(getString(R.string.confirm_delete_message))
.setPositiveButton(getString(R.string.confirm)) { _, _ ->
// 삭제 API 스텁 호출 지점
val index = adapter.items.indexOfFirst { it.commentId == item.commentId } val index = adapter.items.indexOfFirst { it.commentId == item.commentId }
if (index >= 0) { if (index >= 0) {
adapter.items.removeAt(index) adapter.items.removeAt(index)
adapter.notifyItemRemoved(index) adapter.notifyItemRemoved(index)
} }
} }
.setNegativeButton(getString(R.string.cancel), null)
.show()
}
}.show(childFragmentManager, "comment_more") }.show(childFragmentManager, "comment_more")
}, },
onClickItem = { item -> onClickItem = { item ->

View File

@@ -0,0 +1,96 @@
package kr.co.vividnext.sodalive.chat.character.comment
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.core.os.bundleOf
import androidx.core.view.isVisible
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import kr.co.vividnext.sodalive.R
/**
* 댓글/답글 신고 BottomSheet (Stub)
* - 제목: 신고
* - 신고 이유 단일 선택 목록(String List 주입 가능, 미주입 시 기본 목록 사용)
* - 최하단 신고 버튼(선택 전 비활성화, 선택 후 활성화)
* - 신고 버튼 클릭 시 onSubmit(reason) 콜백 호출 후 닫기 (API 스텁 호출은 콜백 쪽에서 처리)
*/
class CharacterCommentReportBottomSheet : BottomSheetDialogFragment() {
var onSubmit: ((String) -> Unit)? = null
private var reasons: ArrayList<String>? = null
private var selectedIndex: Int = -1
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
reasons = arguments?.getStringArrayList(ARG_REASONS) ?: DEFAULT_REASONS
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val view = inflater.inflate(R.layout.dialog_character_comment_report, container, false)
val tvTitle = view.findViewById<TextView>(R.id.tv_title)
val llList = view.findViewById<LinearLayout>(R.id.ll_reason_list)
val btnReport = view.findViewById<Button>(R.id.btn_report)
val ivClose = view.findViewById<ImageView>(R.id.iv_close)
tvTitle.text = getString(R.string.report_title)
btnReport.isEnabled = false
val items = reasons ?: DEFAULT_REASONS
// 동적 리스트 구성: 단일 선택
items.forEachIndexed { index, text ->
val itemView = inflater.inflate(R.layout.item_report_reason, llList, false)
val tvText = itemView.findViewById<TextView>(R.id.tv_reason)
val ivCheck = itemView.findViewById<ImageView>(R.id.iv_check)
tvText.text = text
ivCheck.isVisible = index == selectedIndex
itemView.setOnClickListener {
selectedIndex = index
// 전체를 다시 그릴 정도는 아니므로 자식들만 순회하며 체크 상태 갱신
for (i in 0 until llList.childCount) {
val child = llList.getChildAt(i)
val check = child.findViewById<ImageView>(R.id.iv_check)
check?.isVisible = i == selectedIndex
}
btnReport.isEnabled = true
}
llList.addView(itemView)
}
ivClose.setOnClickListener { dismiss() }
btnReport.setOnClickListener {
val idx = selectedIndex
if (idx in items.indices) {
onSubmit?.invoke(items[idx])
dismiss()
}
}
return view
}
companion object {
private const val ARG_REASONS = "arg_reasons"
private val DEFAULT_REASONS = arrayListOf(
"스팸/광고", "욕설/비하", "음란물/불건전", "개인정보 노출", "기타"
)
fun newInstance(reasons: ArrayList<String>? = null): CharacterCommentReportBottomSheet {
return CharacterCommentReportBottomSheet().apply {
arguments = bundleOf(ARG_REASONS to reasons)
}
}
}
}

View File

@@ -13,6 +13,7 @@ import coil.load
import coil.transform.CircleCropTransformation import coil.transform.CircleCropTransformation
import kr.co.vividnext.sodalive.R import kr.co.vividnext.sodalive.R
import kr.co.vividnext.sodalive.base.BaseActivity import kr.co.vividnext.sodalive.base.BaseActivity
import kr.co.vividnext.sodalive.chat.character.comment.CharacterCommentListBottomSheet
import kr.co.vividnext.sodalive.chat.talk.room.ChatRoomActivity import kr.co.vividnext.sodalive.chat.talk.room.ChatRoomActivity
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
@@ -226,14 +227,14 @@ class CharacterDetailActivity : BaseActivity<ActivityCharacterDetailBinding>(
binding.llCommentsSection.setOnClickListener(null) binding.llCommentsSection.setOnClickListener(null)
if (detail.totalComments > 0) { if (detail.totalComments > 0) {
binding.llCommentsSection.setOnClickListener { binding.llCommentsSection.setOnClickListener {
val sheet = kr.co.vividnext.sodalive.chat.character.comment.CharacterCommentListBottomSheet(detail.characterId) val sheet = CharacterCommentListBottomSheet(detail.characterId)
sheet.show(supportFragmentManager, "character_comments") sheet.show(supportFragmentManager, "character_comments")
} }
} }
if ( if (
detail.totalComments > 0 && detail.totalComments > 0 &&
detail.latestComment != null && detail.latestComment != null &&
!detail.latestComment.comment.isNullOrBlank() detail.latestComment.comment.isNotBlank()
) { ) {
binding.llLatestComment.visibility = View.VISIBLE binding.llLatestComment.visibility = View.VISIBLE
binding.llNoComment.visibility = View.GONE binding.llNoComment.visibility = View.GONE
@@ -256,10 +257,7 @@ class CharacterDetailActivity : BaseActivity<ActivityCharacterDetailBinding>(
} }
} }
val commentText = latest.comment binding.tvLatestComment.text = latest.comment.ifBlank {
binding.tvLatestComment.text = if (!commentText.isNullOrBlank()) {
commentText
} else {
latest.memberNickname latest.memberNickname
} }
} else { } else {

View File

@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/color_131313"
android:orientation="vertical"
android:padding="16dp">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/report_title"
android:textColor="@color/white"
android:textSize="16sp"
android:textStyle="bold"
tools:ignore="RelativeOverlap" />
<ImageView
android:id="@+id/iv_close"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_alignParentEnd="true"
android:contentDescription="@null"
android:src="@drawable/ic_circle_x_white" />
</RelativeLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="12dp"
android:background="#78909C" />
<LinearLayout
android:id="@+id/ll_reason_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:orientation="vertical" />
<Button
android:id="@+id/btn_report"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_marginTop="16dp"
android:backgroundTint="@color/color_3bb9f1"
android:enabled="false"
android:text="@string/report_button"
android:textColor="@color/white" />
</LinearLayout>

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingVertical="12dp">
<TextView
android:id="@+id/tv_reason"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textColor="@color/white"
android:textSize="15sp"
android:text=""/>
<ImageView
android:id="@+id/iv_check"
android:layout_width="20dp"
android:layout_height="20dp"
android:contentDescription="@null"
android:src="@android:drawable/checkbox_on_background"
android:visibility="gone"/>
</LinearLayout>

View File

@@ -34,4 +34,12 @@
<string name="status_sending">전송 중</string> <string name="status_sending">전송 중</string>
<string name="status_failed">전송 실패</string> <string name="status_failed">전송 실패</string>
<string name="status_sent">전송 완료</string> <string name="status_sent">전송 완료</string>
<!-- Character comment report strings -->
<string name="report_title">신고</string>
<string name="report_button">신고</string>
<string name="confirm_delete_title">삭제</string>
<string name="confirm_delete_message">삭제 하시겠습니까?</string>
<string name="confirm">확인</string>
<string name="cancel">취소</string>
</resources> </resources>