Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.PopupWindow
import androidx.recyclerview.widget.RecyclerView
import umc.onairmate.R
import umc.onairmate.data.model.entity.ParticipantData
import umc.onairmate.databinding.PopupParticipantOptionsBinding
import umc.onairmate.databinding.RvItemChatRoomUserBinding
Expand Down Expand Up @@ -72,8 +73,18 @@ class ChatRoomParticipantRVAdapter(
popupWindow.dismiss()
}
popupBinding.tvBlock.setOnClickListener {
itemClick.clickBlock(data)
//itemClick.clickBlock(data)
popupWindow.dismiss()

// 다이얼로그 뷰 inflate
val dialogView = LayoutInflater.from(anchorView.context)
.inflate(R.layout.dialog_block, null)

val dialog = android.app.AlertDialog.Builder(anchorView.context)
.setView(dialogView)
.create()

dialog.show()
}
Comment on lines +76 to 88
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Block 클릭 시 실제 차단/신고 로직이 호출되지 않음

현재 다이얼로그만 표시되고 어떤 콜백도 호출되지 않아 기능적으로 회귀가 발생합니다. 최소한 제출 버튼에서 기존 clickBlock(data)를 호출하거나, 사유까지 전달할 새로운 콜백을 추가해야 합니다.

아래와 같이 다이얼로그 바인딩을 사용하고 제출 버튼에서 검증 후 기존 로직을 호출하도록 제안합니다. (MaterialAlertDialogBuilder 사용 권장)

-            popupBinding.tvBlock.setOnClickListener {
-                //itemClick.clickBlock(data)
-                popupWindow.dismiss()
-
-                // 다이얼로그 뷰 inflate
-                val dialogView = LayoutInflater.from(anchorView.context)
-                    .inflate(R.layout.dialog_block, null)
-
-                val dialog = android.app.AlertDialog.Builder(anchorView.context)
-                    .setView(dialogView)
-                    .create()
-
-                dialog.show()
-            }
+            popupBinding.tvBlock.setOnClickListener {
+                popupWindow.dismiss()
+
+                val inflater = LayoutInflater.from(anchorView.context)
+                val dialogBinding = umc.onairmate.databinding.DialogBlockBinding.inflate(inflater)
+
+                // TODO: 피그마 스펙에 맞는 스타일 적용 필요 시 theme 확인
+                val dialog = com.google.android.material.dialog.MaterialAlertDialogBuilder(anchorView.context)
+                    .setView(dialogBinding.root)
+                    .create()
+
+                // 제목에 닉네임 반영 (strings.xml: dialog_block_title = "%1$s님의 신고 사유")
+                runCatching {
+                    dialogBinding.tvTitle.text =
+                        anchorView.context.getString(umc.onairmate.R.string.dialog_block_title, data.nickname)
+                }
+
+                dialogBinding.btnSubmit.setOnClickListener {
+                    val anyChecked =
+                        (dialogBinding.cbSpam.isChecked
+                         || dialogBinding.cbSports.isChecked
+                         || dialogBinding.cbAbuse.isChecked
+                         || dialogBinding.cbHate.isChecked
+                         || dialogBinding.cbNickname.isChecked)
+                    val hasText = !dialogBinding.etExtraReason.text.isNullOrBlank()
+
+                    if (!anyChecked && !hasText) {
+                        android.widget.Toast
+                            .makeText(anchorView.context,
+                                      umc.onairmate.R.string.dialog_block_reason_required,
+                                      android.widget.Toast.LENGTH_SHORT)
+                            .show()
+                        return@setOnClickListener
+                    }
+
+                    // 최소한 기존 기능 유지: 제출 시 차단 실행
+                    itemClick.clickBlock(data)
+                    dialog.dismiss()
+                }
+
+                dialog.show()
+            }

필요한 외부 import(파일 상단에 추가):

import com.google.android.material.dialog.MaterialAlertDialogBuilder
import umc.onairmate.databinding.DialogBlockBinding
import android.widget.Toast

추가 제안:

  • 사유를 전달하려면 ParticipantItemClickListener에 e.g. clickBlockConfirm(data, reasons: List, extra: String) 같은 시그니처를 신설하고, 뷰모델/도메인으로 위임하는 구조가 더 바람직합니다.
🤖 Prompt for AI Agents
In
app/src/main/java/umc/onairmate/ui/chat_room/drawer/participants/ChatRoomParticipantRVAdapter.kt
around lines 76 to 88, the AlertDialog only shows but never calls the
block/report callback causing a regression; update the dialog to inflate
DialogBlockBinding (or use MaterialAlertDialogBuilder), wire the submit button
to validate inputs and call the existing clickBlock(data) (or a new
clickBlockConfirm(...) signature if you need to pass reasons/extra), then
dismiss the dialog; also add the suggested imports at the top
(MaterialAlertDialogBuilder, DialogBlockBinding, Toast) and ensure the adapter
forwards the new callback to viewModel/domain if you introduce a new listener
method.


popupWindow.showAsDropDown(anchorView, offsetX, offsetY)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
package umc.onairmate.ui.friend.list

import android.content.Context
import android.content.Intent
import androidx.fragment.app.Fragment
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.LinearLayoutManager
Expand All @@ -18,26 +16,24 @@ import umc.onairmate.data.model.entity.RequestedFriendData
import umc.onairmate.data.model.entity.UserData
import umc.onairmate.databinding.FragmentFriendListTabBinding
import umc.onairmate.ui.friend.FriendViewModel
import umc.onairmate.ui.friend.chat.FriendChatActivity
import umc.onairmate.ui.friend.chat.FriendChatViewModel
import umc.onairmate.ui.pop_up.PopupClick
import umc.onairmate.ui.pop_up.TwoButtonPopup
import umc.onairmate.ui.util.SharedPrefUtil
import kotlin.getValue

@AndroidEntryPoint
class FriendListTabFragment() : Fragment() {
private val TAG = this.javaClass.simpleName

private var _binding: FragmentFriendListTabBinding? = null
private val binding get() = _binding!!

private lateinit var adapter : FriendListRVAdapter
private lateinit var adapter: FriendListRVAdapter
private val viewModel: FriendViewModel by viewModels()
private val friendChatViewModel: FriendChatViewModel by viewModels()

private var user : UserData = UserData()
private var friendId : Int = 0
private var type : Int = 0
private var user: UserData = UserData()
private var friendId: Int = 0
private var type: Int = 0

companion object {
private const val ARG_POSITION = "arg_position"
Expand All @@ -52,7 +48,6 @@ class FriendListTabFragment() : Fragment() {

const val LIST_TYPE = 0
const val REQUEST_TYPE = 1

}

override fun onCreateView(
Expand All @@ -61,13 +56,13 @@ class FriendListTabFragment() : Fragment() {
savedInstanceState: Bundle?
): View {
_binding = FragmentFriendListTabBinding.inflate(inflater, container, false)
val root: View = binding.root
type = arguments?.getInt(ARG_POSITION) ?: 0

setAdapter()
setObservers()
setBindings()

return root
return binding.root
}

override fun onResume() {
Expand All @@ -80,92 +75,95 @@ class FriendListTabFragment() : Fragment() {
_binding = null
}

private fun initData(){
private fun initData() {
if (type == LIST_TYPE) viewModel.getFriendList()
if (type == REQUEST_TYPE) viewModel.getRequestedFriendList()
user = SharedPrefUtil.getData("user_info")?: UserData()
user = SharedPrefUtil.getData("user_info") ?: UserData()
}

private fun setObservers() {
viewModel.friendList.observe(viewLifecycleOwner, Observer { list ->
if (list == null) return@Observer
binding.tvEmptyMessage.text = "아직 친구가 없습니다.\n함께할 친구를 초대해보세요!"
binding.layoutEmpty.visibility = if (list.isEmpty()) View.VISIBLE else View.GONE
adapter.initFriendList(list)
binding.layoutEmpty.visibility = if (list.isNullOrEmpty()) View.VISIBLE else View.GONE
adapter.initFriendList(list ?: emptyList())
})

viewModel.requestedFriendList.observe(viewLifecycleOwner, Observer { list ->
if (list == null) return@Observer
binding.tvEmptyMessage.text = "받은 친구 요청이 아직 없어요."
binding.layoutEmpty.visibility = if (list.isEmpty()) View.VISIBLE else View.GONE
adapter.initRequestList(list)
binding.layoutEmpty.visibility = if (list.isNullOrEmpty()) View.VISIBLE else View.GONE
adapter.initRequestList(list ?: emptyList())
})

viewModel.result.observe(viewLifecycleOwner, Observer { message ->
if (message == null) return@Observer
if (message == "친구가 삭제되었습니다.") friendChatViewModel.deleteFriend(friendId,user.userId)
message ?: return@Observer
if (message == "친구가 삭제되었습니다.") friendChatViewModel.deleteFriend(friendId, user.userId)
initData()
Toast.makeText(requireContext(),message, Toast.LENGTH_SHORT).show()
Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show()
viewModel.clearResult()
})
Comment on lines 97 to 103
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

문자열 비교 기반 로직은 취약하며 userId=0 가드 필요

  • 서버/VM 메시지와 한글 문자열을 직접 비교하면 다국어/문구 변경에 취약합니다. 결과는 code/sealed class 등으로 분기하세요.
  • user.userId가 기본값 0일 수 있어 잘못된 삭제 동기화 호출 위험이 있습니다. 가드를 추가하세요.

예시 수정(문자열 리소스로 이전 + userId 가드):

-            if (message == "친구가 삭제되었습니다.") friendChatViewModel.deleteFriend(friendId, user.userId)
+            if (message == getString(R.string.friend_deleted)) {
+                if (user.userId != 0) {
+                    friendChatViewModel.deleteFriend(friendId, user.userId)
+                }
+            }

strings.xml(파일 외 추가):

<resources>
    <string name="friend_deleted">친구가 삭제되었습니다.</string>
</resources>

또는 더 바람직하게는 viewModel.resultsealed class Result { object FriendDeleted : Result ... } 형태로 바꿔 타입 안정적으로 분기해 주세요. 변경 원하시면 제안 드리겠습니다.


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== repo root: ==="
pwd

echo
echo "=== Find FriendListTabFragment file ==="
rg --hidden -n "FriendListTabFragment" || true

echo
echo "=== Print FriendListTabFragment (first 400 lines) if exists ==="
if [ -f "app/src/main/java/umc/onairmate/ui/friend/list/FriendListTabFragment.kt" ]; then
  sed -n '1,400p' app/src/main/java/umc/onairmate/ui/friend/list/FriendListTabFragment.kt || true
else
  echo "File not found at expected path."
fi

echo
echo "=== Search for viewModel.result, deleteFriend, userId, and User model ==="
rg --hidden -n "viewModel.result" || true
rg --hidden -n "deleteFriend" || true
rg --hidden -n "friendChatViewModel" || true
rg --hidden -n "data class .*User|class .*User" || true
rg --hidden -n "userId" || true

Length of output: 17676


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== FriendViewModel.kt ==="
if [ -f "app/src/main/java/umc/onairmate/ui/friend/FriendViewModel.kt" ]; then
  sed -n '1,400p' app/src/main/java/umc/onairmate/ui/friend/FriendViewModel.kt
else
  echo "FriendViewModel.kt not found"
fi

echo
echo "=== FriendChatViewModel.kt ==="
if [ -f "app/src/main/java/umc/onairmate/ui/friend/chat/FriendChatViewModel.kt" ]; then
  sed -n '1,400p' app/src/main/java/umc/onairmate/ui/friend/chat/FriendChatViewModel.kt
else
  echo "FriendChatViewModel.kt not found"
fi

echo
echo "=== SharedPrefUtil.kt ==="
if [ -f "app/src/main/java/umc/onairmate/ui/util/SharedPrefUtil.kt" ]; then
  sed -n '1,400p' app/src/main/java/umc/onairmate/ui/util/SharedPrefUtil.kt
else
  rg --hidden -n "object SharedPrefUtil|class SharedPrefUtil" || true
fi

echo
echo "=== UserData.kt ==="
if [ -f "app/src/main/java/umc/onairmate/data/model/entity/UserData.kt" ]; then
  sed -n '1,200p' app/src/main/java/umc/onairmate/data/model/entity/UserData.kt
else
  echo "UserData.kt not found"
fi

echo
echo "=== Search for literal message '친구가 삭제되었습니다.' ==="
rg --hidden -n "친구가 삭제되었습니다." || true

echo
echo "=== Search for LiveData result in FriendViewModel ==="
rg --hidden -n "val result" app/src/main/java/umc/onairmate/ui/friend || true
rg --hidden -n "MutableLiveData.*result|MutableLiveData<.*>.*result" || true

Length of output: 15434


친구 삭제 분기: 문자열 비교 취약 및 userId=0 가드 필요

간단히: viewModel.result가 문자열("친구가 삭제되었습니다.")로 분기되고 있으며, SharedPref에서 유저를 못 불러오면 user.userId가 기본값 0이 되어 잘못된 소켓 호출을 할 수 있습니다. 수정이 필요합니다.

수정이 필요한 위치:

  • app/src/main/java/umc/onairmate/ui/friend/list/FriendListTabFragment.kt — viewModel.result.observe 내부 (현재 message == "친구가 삭제되었습니다.")
  • app/src/main/java/umc/onairmate/ui/friend/search/SearchFriendTabFragment.kt — viewModel.result 관찰부(유사한 패턴)
  • app/src/main/java/umc/onairmate/ui/friend/FriendViewModel.kt — _result: MutableLiveData<String?> (문자열 메시지 전달 방식 자체)

권장 최소 수정 예시(간단 패치):

-        viewModel.result.observe(viewLifecycleOwner, Observer { message ->
-            message ?: return@Observer
-            if (message == "친구가 삭제되었습니다.") friendChatViewModel.deleteFriend(friendId, user.userId)
-            initData()
-            Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show()
-            viewModel.clearResult()
-        })
+        viewModel.result.observe(viewLifecycleOwner, Observer { message ->
+            message ?: return@Observer
+            // 문자열 리터럴 대신 리소스(또는 더 나은 타입)를 사용하고 userId 가드 추가
+            if (message == getString(R.string.friend_deleted)) {
+                if (user.userId != 0) {
+                    friendChatViewModel.deleteFriend(friendId, user.userId)
+                }
+            }
+            initData()
+            Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show()
+            viewModel.clearResult()
+        })

strings.xml(추가):

<string name="friend_deleted">친구가 삭제되었습니다.</string>

더 권장되는 개선(우선순위):

  1. FriendViewModel.result를 문자열이 아닌 sealed class / enum / 상태 코드 형태로 변경(Result.FriendDeleted 등).
  2. UI는 메시지(Toast)와 별개로 결과 타입을 보고 소켓 호출 등 후속 작업을 수행.
  3. 위 변경을 SearchFriendTabFragment 등 모든 관찰부에 적용.

태그:

🤖 Prompt for AI Agents
In app/src/main/java/umc/onairmate/ui/friend/list/FriendListTabFragment.kt
around lines 97-103, the observer currently compares viewModel.result to a raw
string and calls friendChatViewModel.deleteFriend with user.userId that may be
0; change the branching to use a typed result (preferably a sealed class/enum
like Result.FriendDeleted or at minimum compare against a string resource
R.string.friend_deleted) and add a guard to only call
friendChatViewModel.deleteFriend when user.userId is valid (non-zero); also
apply the same pattern to
app/src/main/java/umc/onairmate/ui/friend/search/SearchFriendTabFragment.kt and
refactor FriendViewModel._result from MutableLiveData<String?> to a typed result
LiveData so UI reacts to result types (Toast can still display localized string
resources).

}

private fun setAdapter(){
private fun setAdapter() {
adapter = FriendListRVAdapter(requireContext())
adapter.setItemClickListener(object: FriendItemClickListener{
adapter.setItemClickListener(object : FriendItemClickListener {
override fun clickMessage(data: FriendData) {
val bundle = Bundle().apply {
putParcelable("friendData", data)
}
val bundle = Bundle().apply { putParcelable("friendData", data) }
parentFragmentManager.setFragmentResult("open_friend_chat_activity", bundle)
}

override fun acceptRequest(data: RequestedFriendData) {
val text = data.nickname+"님의 친구요청을 수락하시겠습니까?"
val textList = listOf(text,"수락","거절")
showPopup(
text =textList,
left = { viewModel.acceptFriend(data.userId, "REJECT") },
right = { viewModel.acceptFriend(data.userId, "ACCEPT")} )
val textList = listOf("${data.nickname}님의 친구요청을 수락하시겠습니까?", "수락", "거절")
showPopup(textList,
left = { viewModel.acceptFriend(data.userId, "REJECT") },
right = { viewModel.acceptFriend(data.userId, "ACCEPT") })
}
Comment on lines 114 to 119
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

수락/거절 라벨과 동작이 반대로 매핑된 것으로 보입니다

textList는 ["수락","거절"]로 라벨링하는데, left는 REJECT, right는 ACCEPT 실행입니다. TwoButtonPopup이 [메시지, left 라벨, right 라벨] 순서를 사용한다면, 현재 구성은 사용자가 “수락”을 눌러도 거절이 수행되는 UX 오류가 됩니다.

라벨과 동작을 맞추도록 하나를 정정하세요. 예시(라벨 순서를 “거절”, “수락”으로 교체):

-                val textList = listOf("${data.nickname}님의 친구요청을 수락하시겠습니까?", "수락", "거절")
+                val textList = listOf("${data.nickname}님의 친구요청을 수락하시겠습니까?", "거절", "수락")
                 showPopup(textList,
-                    left = { viewModel.acceptFriend(data.userId, "REJECT") },
-                    right = { viewModel.acceptFriend(data.userId, "ACCEPT") })
+                    left = { viewModel.acceptFriend(data.userId, "REJECT") },
+                    right = { viewModel.acceptFriend(data.userId, "ACCEPT") })

또는 라벨은 유지하고 left/right 동작을 서로 바꾸는 방식으로 수정해도 됩니다.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
override fun acceptRequest(data: RequestedFriendData) {
val text = data.nickname+"님의 친구요청을 수락하시겠습니까?"
val textList = listOf(text,"수락","거절")
showPopup(
text =textList,
left = { viewModel.acceptFriend(data.userId, "REJECT") },
right = { viewModel.acceptFriend(data.userId, "ACCEPT")} )
val textList = listOf("${data.nickname}님의 친구요청을 수락하시겠습니까?", "수락", "거절")
showPopup(textList,
left = { viewModel.acceptFriend(data.userId, "REJECT") },
right = { viewModel.acceptFriend(data.userId, "ACCEPT") })
}
override fun acceptRequest(data: RequestedFriendData) {
val textList = listOf("${data.nickname}님의 친구요청을 수락하시겠습니까?", "거절", "수락")
showPopup(textList,
left = { viewModel.acceptFriend(data.userId, "REJECT") },
right = { viewModel.acceptFriend(data.userId, "ACCEPT") })
}
🤖 Prompt for AI Agents
In app/src/main/java/umc/onairmate/ui/friend/list/FriendListTabFragment.kt
around lines 114 to 119, the popup button labels and their actions are reversed:
the textList labels imply left="수락" right="거절" but left currently calls REJECT
and right calls ACCEPT; fix by making labels and handlers match — either swap
the textList order to ["거절", "수락"] so left=REJECT right=ACCEPT stays correct, or
keep textList as ["수락", "거절"] and swap the left/right lambdas so left calls
ACCEPT and right calls REJECT.


override fun clickCollection(data: FriendData) {
// 인탠트 필요
}
override fun clickCollection(data: FriendData) {}
override fun clickDelete(data: FriendData) {
val text = data.nickname+"님을 친구 목록에서 삭제하시겠습니까?"
val textList = listOf(text,"예","아니오")
friendId = data.userId
showPopup(text =textList, left = {
viewModel.deleteFriend(data.userId) }, right = {} )
val textList = listOf("${data.nickname}님을 친구 목록에서 삭제하시겠습니까?", "예", "아니오")
showPopup(textList, left = { viewModel.deleteFriend(data.userId) }, right = {})
}

override fun clickBlock(data: FriendData) {
val text = data.nickname+"님을 차단하시겠습니까?"
val textList = listOf(text,"예","아니오")
showPopup(text =textList, left = { }, right = {} )
val textList = listOf("${data.nickname}님을 차단하시겠습니까?", "예", "아니오")
showPopup(textList, left = { /* 차단 로직 */ }, right = { /* 취소 */ })
}

override fun clickReport(data: FriendData) {
val text = data.nickname+"님을 신고하시겠습니까?"
val textList = listOf(text,"예","아니오")
showPopup(text =textList, left = { }, right = {} )
val textList = listOf("${data.nickname}님을 신고하시겠습니까?", "예", "아니오")
showPopup(textList, left = { /* 신고 로직 */ }, right = { /* 취소 */ })
}

})
binding.rvFriendList.adapter = adapter
binding.rvFriendList.layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
}


private fun showPopup(text : List<String>, right : ()-> Unit?, left: () -> Unit?) {
val dialog = TwoButtonPopup(text,object : PopupClick{
private fun showPopup(text: List<String>, right: () -> Unit?, left: () -> Unit?) {
val dialog = TwoButtonPopup(text, object : PopupClick {
override fun rightClickFunction() { right() }
override fun leftClickFunction() {
left()
}
}, false) // 뒤로 가기 막고 싶으면 false 넣어주세요, 아니면 생략가능합니다.
override fun leftClickFunction() { left() }
}, false)
dialog.show(activity?.supportFragmentManager!!, "FriendPopup")
}
Comment on lines +142 to 148
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

NPE 위험과 불필요한 nullable 람다 — childFragmentManager 사용 및 타입 정교화

activity?.supportFragmentManager!!는 lifecycle 타이밍에 따라 NPE 위험이 있습니다. Fragment에서 DialogFragment를 띄울 때는 childFragmentManager(또는 parentFragmentManager)를 사용하세요. 또한 콜백은 nullable일 필요가 없습니다.

-    private fun showPopup(text: List<String>, right: () -> Unit?, left: () -> Unit?) {
+    private fun showPopup(text: List<String>, right: () -> Unit, left: () -> Unit) {
         val dialog = TwoButtonPopup(text, object : PopupClick {
             override fun rightClickFunction() { right() }
             override fun leftClickFunction() { left() }
         }, false)
-        dialog.show(activity?.supportFragmentManager!!, "FriendPopup")
+        dialog.show(childFragmentManager, "FriendPopup")
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private fun showPopup(text: List<String>, right: () -> Unit?, left: () -> Unit?) {
val dialog = TwoButtonPopup(text, object : PopupClick {
override fun rightClickFunction() { right() }
override fun leftClickFunction() {
left()
}
}, false) // 뒤로 가기 막고 싶으면 false 넣어주세요, 아니면 생략가능합니다.
override fun leftClickFunction() { left() }
}, false)
dialog.show(activity?.supportFragmentManager!!, "FriendPopup")
}
private fun showPopup(text: List<String>, right: () -> Unit, left: () -> Unit) {
val dialog = TwoButtonPopup(text, object : PopupClick {
override fun rightClickFunction() { right() }
override fun leftClickFunction() { left() }
}, false)
dialog.show(childFragmentManager, "FriendPopup")
}
🤖 Prompt for AI Agents
In app/src/main/java/umc/onairmate/ui/friend/list/FriendListTabFragment.kt
around lines 142 to 148, replace the risky use of
activity?.supportFragmentManager!! and nullable lambda types: change the
function signature to accept non-null callbacks (right: () -> Unit, left: () ->
Unit), update the PopupClick implementation to call those non-null lambdas
directly, and show the dialog with childFragmentManager (or
parentFragmentManager if appropriate) instead of force-unwrapping activity; this
removes the NPE risk and unnecessary nullable types.


// btn_block 클릭 시 popup 생성
private fun setBindings() {
// 빈 화면 버튼이 보일 때만 작동
binding.btnBlock.setOnClickListener {
val nickname = "친구" // 빈 화면 상태용 닉네임
val textList = listOf("$nickname 님을 차단하시겠습니까?", "예", "아니오")
showPopup(textList,
left = {
Toast.makeText(
requireContext(),
"$nickname 님을 차단했습니다.",
Toast.LENGTH_SHORT
).show()
},
right = {
Toast.makeText(requireContext(), "차단 취소", Toast.LENGTH_SHORT).show()
})
}
}
Comment on lines +150 to +168
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

팝업 문구 하드코딩 — 문자열 리소스화 및 문구 일관성

"예"/"아니오", "차단했습니다/취소" 등은 strings.xml로 이동하세요. 또한 "${data.nickname}님" 형태와 "$nickname 님" 형태가 혼재합니다(공백 유무). 하나로 통일하세요.

예시(strings.xml):

<resources>
    <string name="common_yes">예</string>
    <string name="common_no">아니오</string>
    <string name="block_confirm_title">%1$s님을 차단하시겠습니까?</string>
    <string name="block_done">%1$s님을 차단했습니다.</string>
    <string name="block_cancel">차단 취소</string>
</resources>

사용:

val nickname = "친구"
val textList = listOf(
    getString(R.string.block_confirm_title, nickname),
    getString(R.string.common_yes),
    getString(R.string.common_no)
)
🤖 Prompt for AI Agents
In app/src/main/java/umc/onairmate/ui/friend/list/FriendListTabFragment.kt
around lines 150 to 168, the popup strings and Toast text are hardcoded and
inconsistent (some use "$nickname 님" with a space, others use
"${data.nickname}님" without); move all user-visible literals
("예","아니오","차단했습니다","차단 취소", and the confirmation title) into strings.xml as
formatted resources (e.g., block_confirm_title="%1$s님을 차단하시겠습니까?",
block_done="%1$s님을 차단했습니다.", common_yes, common_no, block_cancel) and replace
the inline literals with getString(...) calls using the nickname as the format
arg; ensure you standardize the spacing by using the same "%1$s님" format
everywhere (no extra space) and update both the popup textList and the Toast
messages to use these string resources.

}
63 changes: 63 additions & 0 deletions app/src/main/res/layout/dialog_block.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:padding="20dp"
android:layout_width="320dp"
android:layout_height="494dp">
Comment on lines +5 to +6
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

고정 크기(320dp x 494dp)로 인한 화면 깨짐/키보드 가림 가능성

다양한 해상도/회전/키보드 상황에서 다이얼로그 콘텐츠가 잘리는 리스크가 큽니다. 스크롤 가능 영역으로 감싸고, 다이얼로그 콘텐츠는 wrap_content로 두는 구성이 안전합니다.

아래처럼 ScrollView + 내부 LinearLayout으로 변경을 제안합니다:

-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    android:orientation="vertical"
-    android:padding="20dp"
-    android:layout_width="320dp"
-    android:layout_height="494dp">
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content">
+
+    <LinearLayout
+        android:orientation="vertical"
+        android:padding="20dp"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content">
...
-</LinearLayout>
+</LinearLayout>
+</ScrollView>


<TextView
android:id="@+id/tvTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="[닉네임]의 신고 사유"
android:textSize="16sp"
android:textStyle="bold"
android:gravity="center"
android:paddingBottom="16dp"/>

<CheckBox
android:id="@+id/cbSpam"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="스팸, 도배"/>

<CheckBox
android:id="@+id/cbSports"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="스포성 채팅"/>

<CheckBox
android:id="@+id/cbAbuse"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="욕설"/>

<CheckBox
android:id="@+id/cbHate"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="혐오 발언"/>

<CheckBox
android:id="@+id/cbNickname"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="부적절한 닉네임 사용"/>

<EditText
android:id="@+id/etExtraReason"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="추가적인 신고 사유를 작성해주세요."
android:minHeight="80dp"
android:padding="10dp"
android:layout_marginTop="10dp"/>

<Button
android:id="@+id/btnSubmit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="제출"
android:layout_marginTop="16dp"/>
</LinearLayout>
57 changes: 54 additions & 3 deletions app/src/main/res/layout/fragment_friend_list_tab.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

루트 ConstraintLayout의 height=wrap_content로 인해 RV/오버레이 제약 해석 오류 가능 — match_parent로 변경 권장

자식이 0dp(height)로 상하 제약을 사용하는 구조에서는 부모가 고정 크기여야 합니다. 현재 wrap_content면 측정이 불안정해 오버레이가 꽉 차지 않거나 RV가 0 높이로 렌더링될 수 있습니다.

다음 변경을 권장합니다:

-    android:layout_height="wrap_content"
+    android:layout_height="match_parent"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
android:layout_height="wrap_content"
android:layout_height="match_parent"
🤖 Prompt for AI Agents
In app/src/main/res/layout/fragment_friend_list_tab.xml around line 4, the root
ConstraintLayout currently uses android:layout_height="wrap_content", which can
cause RecyclerView/overlay constraint measurement errors when child views use
0dp with top/bottom constraints; change the root layout's height to match_parent
so the parent has a fixed size and children using 0dp can measure correctly,
keeping other constraints the same.

xmlns:app="http://schemas.android.com/apk/res-auto">

<!--
<LinearLayout
android:id="@+id/layout_empty"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center"
app:layout_constraintTop_toTopOf="parent"
android:visibility="gone">
app:layout_constraintTop_toTopOf="parent">
<ImageView
android:id="@+id/iv_empty_image"
android:layout_width="58dp"
Expand All @@ -30,7 +31,15 @@
app:layout_constraintTop_toBottomOf="@id/iv_empty_image"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>

<Button
android:id="@+id/btn_block"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=""
android:textAppearance="@style/TextAppearance.App.Medium.14sp"/>
</LinearLayout>

<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_friend_list"
android:layout_width="match_parent"
Expand All @@ -39,5 +48,47 @@
android:layout_marginTop="20dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
-->

</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_friend_list"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>

<LinearLayout
android:id="@+id/layout_empty"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
android:background="@android:color/transparent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">

Comment on lines +60 to +71
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

빈 상태 오버레이가 기본 VISIBLE — 초기 화면 가림/터치 혼선 유발

layout_empty가 기본값(VISIBLE)이라 화면 진입 시 목록 유무 판단 전까지 전체를 덮습니다. 초기 플래시/깜빡임 및 뒤의 RV 클릭 혼선이 생길 수 있습니다. 기본을 GONE으로 두고, 필요 시만 VISIBLE로 토글하세요. 또한 오버레이가 보일 때 뒤의 RV 터치를 막으려면 clickable/focusable 설정이 필요합니다.

 <LinearLayout
     android:id="@+id/layout_empty"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    android:orientation="vertical"
-    android:gravity="center"
-    android:background="@android:color/transparent"
+    android:orientation="vertical"
+    android:gravity="center"
+    android:background="@android:color/transparent"
+    android:visibility="gone"
+    android:clickable="true"
+    android:focusable="true"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<LinearLayout
android:id="@+id/layout_empty"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
android:background="@android:color/transparent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<LinearLayout
android:id="@+id/layout_empty"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
android:background="@android:color/transparent"
android:visibility="gone"
android:clickable="true"
android:focusable="true"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
🤖 Prompt for AI Agents
In app/src/main/res/layout/fragment_friend_list_tab.xml around lines 60 to 71,
the layout_empty overlay is VISIBLE by default which covers the screen until the
list presence is determined; change its default visibility to GONE, and add
android:clickable="true" and android:focusable="true" so when you set it VISIBLE
it will block touches to the RecyclerView behind it; ensure the code that
shows/hides this view toggles visibility to VISIBLE only when the empty state is
confirmed and back to GONE when data is present.

<ImageView
android:id="@+id/iv_empty_image"
android:layout_width="58dp"
android:layout_height="58dp"
android:layout_marginTop="66dp"
android:src="@drawable/ic_empty_room"/>

Comment on lines +72 to +78
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

접근성: 빈 상태 이미지에 contentDescription 누락

장식용 이미지면 스크린리더 무시를 위해 @null을 지정하세요. 대체 텍스트가 필요한 경우 적절한 문자열 리소스를 사용하세요.

 <ImageView
     android:id="@+id/iv_empty_image"
     android:layout_width="58dp"
     android:layout_height="58dp"
     android:layout_marginTop="66dp"
-    android:src="@drawable/ic_empty_room"/>
+    android:src="@drawable/ic_empty_room"
+    android:contentDescription="@null"/>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<ImageView
android:id="@+id/iv_empty_image"
android:layout_width="58dp"
android:layout_height="58dp"
android:layout_marginTop="66dp"
android:src="@drawable/ic_empty_room"/>
<ImageView
android:id="@+id/iv_empty_image"
android:layout_width="58dp"
android:layout_height="58dp"
android:layout_marginTop="66dp"
android:src="@drawable/ic_empty_room"
android:contentDescription="@null"/>
🤖 Prompt for AI Agents
In app/src/main/res/layout/fragment_friend_list_tab.xml around lines 72 to 78
the ImageView for the empty state is missing a contentDescription; if the image
is purely decorative set the contentDescription attribute to the Android null
resource (i.e., @null) so screen readers ignore it, otherwise set
contentDescription to an appropriate string resource (e.g., a descriptive entry
in strings.xml) and reference that resource.

<TextView
android:id="@+id/tv_empty_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=""
android:gravity="center"
android:layout_marginTop="13dp"
android:layout_marginBottom="48dp"/>

<Button
android:id="@+id/btn_block"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="친구 차단"/>
</LinearLayout>
Comment on lines +88 to +93
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

하드코딩 문자열 사용 — 문자열 리소스로 분리하여 i18n/일관성 확보

Button 텍스트는 리소스로 관리하세요.

 <Button
     android:id="@+id/btn_block"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
-    android:text="친구 차단"/>
+    android:text="@string/friend_block"/>

strings.xml에 다음을 추가하세요(파일 외 변경):

<resources>
    <string name="friend_block">친구 차단</string>
</resources>
🤖 Prompt for AI Agents
In app/src/main/res/layout/fragment_friend_list_tab.xml around lines 88 to 93
the Button uses a hardcoded android:text value; replace the literal text with a
string resource reference (e.g. @string/friend_block) and add the corresponding
entry named "friend_block" to your module's strings.xml resource file containing
"친구 차단" so the UI text is localized and consistent.

</androidx.constraintlayout.widget.ConstraintLayout>
Loading