feat(character-comment): 신고 BottomSheet 추가 및 삭제 확인 팝업 도입
- 신고 BottomSheet(제목/단일선택 리스트/신고 버튼) 구현 및 더보기→신고 흐름 연동 - 삭제 버튼 클릭 시 확인 다이얼로그 표시 후 확정 시 리스트에서 제거 - 신고/삭제 API 호출부는 스텁으로 남겨둠(후속 연동 예정)
This commit is contained in:
		@@ -92,14 +92,29 @@ 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 = {
 | 
				
			||||||
                        val index = adapter.items.indexOfFirst { it.commentId == item.commentId }
 | 
					                        // 삭제 확인 팝업
 | 
				
			||||||
                        if (index >= 0) {
 | 
					                        androidx.appcompat.app.AlertDialog.Builder(requireContext())
 | 
				
			||||||
                            adapter.items.removeAt(index)
 | 
					                            .setTitle(getString(R.string.confirm_delete_title))
 | 
				
			||||||
                            adapter.notifyItemRemoved(index)
 | 
					                            .setMessage(getString(R.string.confirm_delete_message))
 | 
				
			||||||
                        }
 | 
					                            .setPositiveButton(getString(R.string.confirm)) { _, _ ->
 | 
				
			||||||
 | 
					                                // 삭제 API 스텁 호출 지점
 | 
				
			||||||
 | 
					                                val index = adapter.items.indexOfFirst { it.commentId == item.commentId }
 | 
				
			||||||
 | 
					                                if (index >= 0) {
 | 
				
			||||||
 | 
					                                    adapter.items.removeAt(index)
 | 
				
			||||||
 | 
					                                    adapter.notifyItemRemoved(index)
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                            .setNegativeButton(getString(R.string.cancel), null)
 | 
				
			||||||
 | 
					                            .show()
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                }.show(childFragmentManager, "comment_more")
 | 
					                }.show(childFragmentManager, "comment_more")
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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)
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -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 {
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										55
									
								
								app/src/main/res/layout/dialog_character_comment_report.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								app/src/main/res/layout/dialog_character_comment_report.xml
									
									
									
									
									
										Normal 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>
 | 
				
			||||||
							
								
								
									
										24
									
								
								app/src/main/res/layout/item_report_reason.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								app/src/main/res/layout/item_report_reason.xml
									
									
									
									
									
										Normal 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>
 | 
				
			||||||
@@ -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>
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user