Skip to content
Open
Show file tree
Hide file tree
Changes from 9 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
16 changes: 16 additions & 0 deletions app/src/main/java/umc/OnAirMate/data/api/NicknameService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package umc.onairmate.data.api
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

패키지 경로 대소문자 불일치 가능성 점검

파일 시스템 경로(app/src/main/java/umc/OnAirMate/…)와 선언 패키지(package umc.onairmate…)의 대소문자 불일치가 보입니다. CI(리눅스 등 대소문자 구분) 환경에서 중복 패키지/클래스 인식 문제를 유발할 수 있어 디렉터리명을 패키지 규약(all-lowercase)으로 통일하는 것을 권장합니다.

다음 스크립트로 대소문자 혼재 여부와 해당 파일들의 package 선언을 확인해 주세요.


🏁 Script executed:

#!/bin/bash
# 대소문자 혼재 디렉터리 나열
echo "[디렉터리 확인]"
fd -t d -a 'OnAirMate|onairmate' app/src/main/java/umc | sort

# OnAirMate 경로를 사용하는 파일들의 package 선언 확인
echo
echo "[OnAirMate 경로 내 Kotlin 파일의 package 선언]"
fd -a -e kt 'app/src/main/java/umc/OnAirMate' | while read -r f; do
  echo "=== $f"
  rg -n '^package ' "$f" -n || true
done

Length of output: 779


추가로 OnAirMate 디렉터리 내 Kotlin 파일들의 패키지 선언을 확인합니다:


🏁 Script executed:

#!/bin/bash
echo "[OnAirMate 경로 내 Kotlin 파일 목록]"
find app/src/main/java/umc/OnAirMate -type f -name '*.kt' | sort

echo
echo "[OnAirMate 경로 내 Kotlin 파일들의 package 선언]"
find app/src/main/java/umc/OnAirMate -type f -name '*.kt' | while IFS= read -r f; do
  echo "=== $f"
  grep -n '^package ' "$f" || true
done

Length of output: 15518


패키지 디렉터리명 소문자 통일 필요

app/src/main/java/umc 경로에 대문자 혼용 디렉터리(OnAirMate)가 존재하여 선언된 패키지 umc.onairmate와 불일치합니다. Linux CI 환경에서 클래스 인식 문제를 방지하려면 디렉터리명을 모두 소문자로 변경하세요.

  • 변경 전: app/src/main/java/umc/OnAirMate/...
  • 변경 후: app/src/main/java/umc/onairmate/...
🤖 Prompt for AI Agents
In app/src/main/java/umc/OnAirMate/data/api/NicknameService.kt around line 1,
the package declaration uses lowercase "umc.onairmate" but the filesystem
directory is "OnAirMate" (mixed case), causing mismatch on case-sensitive CI;
rename the directory app/src/main/java/umc/OnAirMate to
app/src/main/java/umc/onairmate (use git mv to preserve history), ensure all
package declarations and imports still match "umc.onairmate", commit the change,
and run a build to verify no remaining case-related path errors.


import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.PUT
import retrofit2.http.Path
import umc.onairmate.data.model.response.NicknameResponse
import umc.onairmate.data.model.response.RawDefaultResponse

interface NicknameService {
@GET("auth/check-nickname/{nickname}")
suspend fun checkNickname(
@Path("nickname") nickname: String
): NicknameResponse
}
11 changes: 11 additions & 0 deletions app/src/main/java/umc/OnAirMate/data/model/entity/NicknameData.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package umc.onairmate.data.model.entity

import com.google.gson.annotations.SerializedName

data class NicknameData(
@SerializedName("available")
val available: Boolean,

@SerializedName("message")
val message: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package umc.onairmate.data.model.response
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

패키지-디렉터리 대소문자 일치 검증 요청

해당 파일 경로는 …/umc/OnAirMate/…인데 package는 umc.onairmate…입니다. 대소문자 혼재는 환경에 따라 문제를 유발할 수 있으니 디렉터리 표준화(all-lowercase) 여부 확인 바랍니다.

다음 스크립트로 확인 가능합니다:


🏁 Script executed:

#!/bin/bash
echo "[NicknameResponse.kt package 확인]"
fd -a 'NicknameResponse.kt' | while read -r f; do
  echo "=== $f"
  rg -n '^package ' "$f" -n || true
done

Length of output: 483


디렉터리 이름 소문자 일치 필요: OnAirMateonairmate로 변경

JVM 패키지-디렉터리 매핑에서 대소문자 불일치는 빌드 또는 런타임 오류를 유발할 수 있습니다. package umc.onairmate.data.model.response 선언과 실제 경로가 일치하도록 디렉터리를 모두 소문자로 표준화해주세요.

  • 수정 대상
    • 기존 경로:
      app/src/main/java/umc/OnAirMate/data/model/response/NicknameResponse.kt
    • 변경 경로 예시:
      app/src/main/java/umc/onairmate/data/model/response/NicknameResponse.kt
  • 작업 내용
    1. umc/OnAirMate/… 디렉터리 이름을 umc/onairmate/…로 변경
    2. 변경된 경로에 맞춰 프로젝트 내 패키지 선언과 import 경로 검토 및 업데이트

다른 패키지 경로에도 동일한 대소문자 불일치가 없는지 추가 확인 바랍니다.

🤖 Prompt for AI Agents
In app/src/main/java/umc/OnAirMate/data/model/response/NicknameResponse.kt
around line 1, the directory name casing (`OnAirMate`) does not match the
package declaration `umc.onairmate...`, which can break JVM package-directory
mapping; rename the directory path from umc/OnAirMate/... to umc/onairmate/...
and move the file to
app/src/main/java/umc/onairmate/data/model/response/NicknameResponse.kt, then
update any affected package/import references across the project to use the
all-lowercase `umc.onairmate` path and verify there are no other casing
mismatches in source directories.


import com.google.gson.annotations.SerializedName
import umc.onairmate.data.model.entity.NicknameData

class NicknameResponse (
@SerializedName("success")
val success: Boolean,

@SerializedName("data")
val data: NicknameData
)
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

DTO는 data class 사용 권장

동등성/복사/디버깅에 유리하도록 data class로 선언을 권장합니다.

다음 diff를 적용해 주세요:

-class NicknameResponse (
+data class NicknameResponse (
     @SerializedName("success")
     val success: Boolean,

     @SerializedName("data")
     val data: NicknameData
 )
📝 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
class NicknameResponse (
@SerializedName("success")
val success: Boolean,
@SerializedName("data")
val data: NicknameData
)
data class NicknameResponse (
@SerializedName("success")
val success: Boolean,
@SerializedName("data")
val data: NicknameData
)
🤖 Prompt for AI Agents
In app/src/main/java/umc/OnAirMate/data/model/response/NicknameResponse.kt
around lines 6 to 12, the DTO is declared as a regular class but should be a
data class to get automatic equals/hashCode/toString/copy; change the
declaration from "class NicknameResponse (...)" to "data class NicknameResponse
(...)" leaving the existing @SerializedName annotations and property definitions
unchanged so it becomes a Kotlin data class.

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package umc.onairmate.data.repository

interface NicknameRepository {
suspend fun isNicknameDuplicated(nickname: String): Boolean
}
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

패키지-디렉터리 경로 불일치 가능성(OnAirMate vs onairmate, repository/repository)

파일 경로가 .../data/repository/repository/NicknameRepository.kt인데, 패키지는 umc.onairmate.data.repository입니다. 대소문자(OnAirMate vs onairmate)와 디렉터리 깊이 차이(repository/repository)가 섞여 있으면, OS에 따라 빌드/IDE 인덱싱 이슈가 발생할 수 있습니다. 전체 모듈에서 일관된 디렉터리 구조 사용을 권장합니다.

다음 스크립트로 대소문자 혼용 및 중복 디렉터리 여부를 점검해 주세요:


🏁 Script executed:

#!/bin/bash
# 대소문자 혼용 디렉터리 탐지
echo "Directories containing 'OnAirMate' (capitalized):"
fd OnAirMate -t d

echo
echo "Directories containing 'onairmate' (lowercase):"
fd onairmate -t d

echo
echo "Check for duplicated 'repository/repository' paths:"
fd -t f 'NicknameRepository.kt' -x dirname {} | sort -u

Length of output: 691


패키지 선언 및 디렉터리 구조 정합성 확보 필요

현재 NicknameRepository.kt 파일이 아래 경로에 위치하면서 패키지 선언(umc.onairmate.data.repository)과 실제 디렉터리 구조가 일치하지 않습니다.
OS나 IDE 인덱싱 이슈를 방지하려면 다음 작업이 필요합니다:

  • 대상 파일
    • app/src/main/java/umc/OnAirMate/data/repository/repository/NicknameRepository.kt
  • 조치 사항
    1. umc/OnAirMate 디렉터리명을 기존 테스트 코드(app/src/test/java/umc/onairmate, app/src/androidTest/java/umc/onairmate)와 동일하게 모두 소문자 onairmate로 변경
    2. 중복된 repository/repository 폴더 구조를 제거하여 최종 경로를
      app/src/main/java/umc/onairmate/data/repository/NicknameRepository.kt
      형태로 통일
    3. 변경 후 패키지 선언(package umc.onairmate.data.repository)이 실제 경로와 완전히 일치하는지 확인

위 조치로 대소문자 혼용 및 중복 디렉터리 문제를 해소하고, 빌드/IDE 환경 전반의 안정성을 확보할 수 있습니다.

🤖 Prompt for AI Agents
In
app/src/main/java/umc/OnAirMate/data/repository/repository/NicknameRepository.kt
lines 1-5, the package declaration and filesystem path do not match due to a
capitalized folder and duplicate "repository" directory; rename the directory
"OnAirMate" to all-lowercase "onairmate", remove the extra nested "repository"
folder so the file resides at
app/src/main/java/umc/onairmate/data/repository/NicknameRepository.kt, and
verify the file's package declaration remains "package
umc.onairmate.data.repository" (update it if necessary) so the package and path
are fully consistent.

Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package umc.onairmate.data.repository.repositoryImpl
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

패키지-디렉터리 대소문자 불일치 가능성 점검

파일 경로에는 'OnAirMate'(대문자) 디렉터리가 보이고, 패키지는 'umc.onairmate'(소문자)입니다. Linux와 같이 대소문자 구분 파일시스템 환경에서 빌드 이슈가 발생할 수 있으니, 디렉터리/패키지 네이밍 일관성을 확인해 주세요.

점검 스크립트:


🏁 Script executed:

#!/bin/bash
# 패키지 선언 케이스 통계
echo "[package umc.onairmate] count:"
rg -n '^package umc\.onairmate' | wc -l
echo "[package umc.OnAirMate] count:"
rg -n '^package umc\.OnAirMate' | wc -l

# 디렉터리 케이스 확인
echo "[Directories named onairmate]"
fd -t d -a "onairmate" app/src/main/java/umc || true
echo "[Directories named OnAirMate]"
fd -t d -a "OnAirMate" app/src/main/java/umc || true

# import/참조에서 케이스 혼용 여부
rg -n 'umc\.OnAirMate' -S || true

Length of output: 650


패키지/디렉터리 네이밍 불일치 해결

Linux 환경에서 빌드 오류를 방지하려면 app/src/main/java/umc 경로에 중복으로 존재하는 OnAirMate/onairmate/ 디렉터리 중 하나를 삭제하거나 이름을 통일하세요. 모든 package umc.onairmate 선언이 소문자를 전제하므로:

  • OnAirMate/ 디렉터리를 onairmate/로 리네임
  • 또는 불필요한 디렉터리(중복 경로) 삭제

수정 후 import 및 패키지 경로가 모두 umc.onairmate(소문자)로 참조되는지 재확인 바랍니다.

🤖 Prompt for AI Agents
In
app/src/main/java/umc/OnAirMate/data/repository/repositoryImpl/NicknameRepositoryImpl.kt
around line 1, there is a package/directory name mismatch between `OnAirMate`
and `onairmate` causing Linux build errors; resolve it by renaming the
filesystem directory `OnAirMate` to `onairmate` (or removing the duplicate
directory) so the physical path matches the package declaration `package
umc.onairmate...`, then update and verify all affected package declarations and
import statements across the project to use the lowercase `umc.onairmate` path.


import android.util.Log
import umc.onairmate.data.api.NicknameService
import umc.onairmate.data.repository.NicknameRepository
import javax.inject.Inject

class NicknameRepositoryImpl @Inject constructor(
private val api: NicknameService
): NicknameRepository {

override suspend fun isNicknameDuplicated(nickname: String): Boolean {
return try {
val response = api.checkNickname(nickname)
// available == true 면 사용 가능한 닉네임, 중복 아님 → 따라서 중복 여부는 반대(!)
!response.data.available
} catch (e: Exception) {
Log.e("NicknameRepository", "닉네임 중복 검사 실패", e)
// 실패 시 기본값 false 또는 true 선택 가능 (보통 실패는 중복 아님 false로 처리)
false
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

예외 삼키기와 성공/응답 검증 누락으로 잘못된 판정/크래시 가능성이 있습니다

  • 실패 시 무조건 false 반환 → UI가 “중복 아님”으로 오판할 수 있어 UX/정합성에 치명적입니다.
  • response.success(서버 성공 플래그) 미검증 → 서버가 실패를 내려도 data를 신뢰하고 접근할 수 있음.
  • 널 가능성(data/message) 미대비 → 서버 스펙 변경 시 NPE 위험.
  • catch(Exception) 범용 포착 → 네트워크/HTTP 오류를 구분하지 못해 복구 전략 수립이 어렵습니다.

최소 수정안: 서버 성공 플래그 체크 + 오류 타입별 로깅/처리 분기 추가(기존 Boolean 시그니처 유지).

아래 패치 제안 적용을 고려해 주세요:

-        return try {
-            val response = api.checkNickname(nickname)
-            // available == true 면 사용 가능한 닉네임, 중복 아님 → 따라서 중복 여부는 반대(!)
-            !response.data.available
-        } catch (e: Exception) {
-            Log.e("NicknameRepository", "닉네임 중복 검사 실패", e)
-            // 실패 시 기본값 false 또는 true 선택 가능 (보통 실패는 중복 아님 false로 처리)
-            false
-        }
+        return try {
+            val response = api.checkNickname(nickname)
+            if (!response.success) {
+                Log.w(TAG, "닉네임 중복 검사 API가 success=false를 반환했습니다. message=${response.data?.message}")
+                return false
+            }
+            // available == true 면 사용 가능한 닉네임, 중복 아님 → 따라서 중복 여부는 반대(!)
+            !response.data.available
+        } catch (e: IOException) {
+            Log.e(TAG, "네트워크 오류로 닉네임 중복 검사 실패", e)
+            false
+        } catch (e: HttpException) {
+            Log.e(TAG, "HTTP 오류로 닉네임 중복 검사 실패", e)
+            false
+        } catch (e: Exception) {
+            Log.e(TAG, "알 수 없는 오류로 닉네임 중복 검사 실패", e)
+            false
+        }

추가로 필요한 외부 변경(선택 적용):

  • import 추가
import retrofit2.HttpException
import java.io.IOException
  • 로깅 태그 상수화(클래스 내부에 추가)
companion object {
    private const val TAG = "NicknameRepository"
}

장기적으로는 Boolean 반환 대신 Result-스타일 도메인 결과(예: Available/Duplicated/Error(메시지 포함))로 표현하여 UI에서 오류를 명확히 처리하도록 전환하는 것을 권장합니다.

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ class ChatRoomFragment : Fragment() {
lifecycle.addObserver(youtubePlayer)

youtubePlayer.addYouTubePlayerListener(object : AbstractYouTubePlayerListener() {
override fun onReady(youTubePlayer: YouTubePlayer) {\
override fun onReady(youTubePlayer: YouTubePlayer) {
val videoId = roomData.videoId ?: "CgCVZdcKcqY"
youTubePlayer.loadVideo(videoId, 0f) // todo: RoomData duration 연동
}
Expand Down
119 changes: 119 additions & 0 deletions app/src/main/java/umc/OnAirMate/ui/pop_up/ChangeNicknamePopup.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package umc.onairmate.ui.pop_up

import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.lifecycle.lifecycleScope
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import umc.onairmate.R
import umc.onairmate.data.repository.NicknameRepository
import umc.onairmate.databinding.PopupChangeNicknameBinding
import javax.inject.Inject

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

대소문자 혼용 디렉터리 구조 점검(OnAirMate vs onairmate)

이 파일(디렉터리 경로의 OnAirMate 대문자)과 패키지명(umc.onairmate 소문자)의 불일치가 프로젝트 전반에 존재할 경우, 케이스-센시티브 환경에서 문제를 유발할 수 있습니다.

다음 스크립트로 모듈 전체의 디렉터리 대소문자 혼용을 점검해 주세요:


🏁 Script executed:

#!/bin/bash
echo "Check Kotlin/Java source directories with 'OnAirMate' vs 'onairmate':"
fd -t d 'OnAirMate'
fd -t d 'onairmate'

echo
echo "List files under both to inspect duplicates:"
fd -t f '.kt' OnAirMate
fd -t f '.kt' onairmate

Length of output: 721


대소문자 디렉터리 경로 일관성 확인 및 수정 필요
app/src/main/java/umc/OnAirMate 디렉터리가 소문자 umc/onairmate 경로와 혼용되어 있어, 케이스-센시티브 파일 시스템에서 빌드 오류를 유발할 수 있습니다. 다음 경로를 모두 소문자 onairmate로 통일해주세요:

  • app/src/main/java/umc/OnAirMate → app/src/main/java/umc/onairmate
  • app/src/main/java/umc/onairmate
  • app/src/androidTest/java/umc/onairmate
  • app/src/test/java/umc/onairmate

리팩토링 후에는 패키지 선언(package umc.onairmate…)과 import 구문이 모두 올바르게 변경되었는지 꼭 검증 부탁드립니다.

🤖 Prompt for AI Agents
In app/src/main/java/umc/OnAirMate/ui/pop_up/ChangeNicknamePopup.kt around lines
1 to 20, the directory name uses mixed case ("OnAirMate") which conflicts with
the package declaration and will break builds on case-sensitive filesystems;
rename the filesystem directories to lowercase (app/src/main/java/umc/onairmate,
app/src/androidTest/java/umc/onairmate, app/src/test/java/umc/onairmate) and
update all affected files so their package declarations and import statements
use "umc.onairmate" consistently, then run a full project build and update any
references/IDE caches to ensure no leftover incorrect paths remain.

@AndroidEntryPoint
class ChangeNicknamePopup : BottomSheetDialogFragment() {

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

// 외부에서 중복 체크를 처리하기 위한 콜백 람다 추가
var onCheckNickname: ((String, (Boolean) -> Unit) -> Unit)? = null

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

onCheckNickname == null일 때 동작 정의 및 버튼 상태 처리 필요

콜백이 설정되지 않은 경우 클릭 시 아무 일도 발생하지 않습니다. 초기/텍스트 변경 시 onCheckNickname != null을 고려해 버튼 활성화 여부를 제어하거나, null일 때 안내 토스트를 표시하세요.

-    var onCheckNickname: ((String, (Boolean) -> Unit) -> Unit)? = null
+    var onCheckNickname: ((String, (Boolean) -> Unit) -> Unit)? = null

추가 변경은 아래 텍스트 변경/클릭 로직에서 제안합니다.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In app/src/main/java/umc/OnAirMate/ui/pop_up/ChangeNicknamePopup.kt around lines
27 to 29, the onCheckNickname nullable callback is not handled so clicks do
nothing; update the UI logic to (1) initialize the "중복확인" button enabled state
based on onCheckNickname != null and current text (disable if callback is null
or text empty), (2) in the text-change listener re-evaluate button enabled state
using both text non-empty and onCheckNickname != null, and (3) in the button
click handler, if onCheckNickname is null show a Toast informing the user to try
again or that the check is unavailable, otherwise invoke the callback normally;
ensure the initial state is set when the view is created and whenever
onCheckNickname is assigned.

// @Inject
// lateinit var repository: NicknameRepository


override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = PopupChangeNicknameBinding.inflate(inflater, container, false)
return binding.root
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

binding.editNickname.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) {
val input = s.toString()
val isValid = input.length in 3..10

binding.checkNickname.setBackgroundResource(
if (isValid) R.drawable.bg_btn_main
else R.drawable.bg_btn_disabled
)
binding.checkNickname.isEnabled = isValid
}

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

버튼 활성화 조건에 콜백 존재 여부 포함

중복 체크 콜백이 없으면 버튼을 활성화하지 않는 것이 안전합니다.

-                binding.checkNickname.setBackgroundResource(
-                    if (isValid) R.drawable.bg_btn_main
-                    else R.drawable.bg_btn_disabled
-                )
-                binding.checkNickname.isEnabled = isValid
+                val canCheck = isValid && onCheckNickname != null
+                binding.checkNickname.setBackgroundResource(
+                    if (canCheck) R.drawable.bg_btn_main else R.drawable.bg_btn_disabled
+                )
+                binding.checkNickname.isEnabled = canCheck
📝 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
binding.editNickname.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) {
val input = s.toString()
val isValid = input.length in 3..10
binding.checkNickname.setBackgroundResource(
if (isValid) R.drawable.bg_btn_main
else R.drawable.bg_btn_disabled
)
binding.checkNickname.isEnabled = isValid
}
binding.editNickname.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) {
val input = s.toString()
val isValid = input.length in 3..10
val canCheck = isValid && onCheckNickname != null
binding.checkNickname.setBackgroundResource(
if (canCheck) R.drawable.bg_btn_main else R.drawable.bg_btn_disabled
)
binding.checkNickname.isEnabled = canCheck
}
🤖 Prompt for AI Agents
In app/src/main/java/umc/OnAirMate/ui/pop_up/ChangeNicknamePopup.kt around lines
45–56, the TextWatcher currently enables the "checkNickname" button based only
on length; update it to also require that a duplicate-check callback has been
performed and succeeded. Add a boolean field (e.g., hasPassedDuplicateCheck) to
the popup, reset it to false whenever editNickname text changes, set it true
only when the duplicate-check callback confirms uniqueness, and change the
isValid condition to require both length in 3..10 and hasPassedDuplicateCheck
before setting background and enabling the button; ensure the duplicate-check
setter is invoked from the existing duplicate-check flow and UI updates reflect
the combined condition.

override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
})

binding.checkNickname.setOnClickListener {
val nickname = binding.editNickname.text.toString()

if (nickname.length !in 3..10) {
Toast.makeText(requireContext(), "닉네임은 3자 이상 10자 이하여야 합니다.", Toast.LENGTH_SHORT).show()
return@setOnClickListener
}

// 외부 콜백 호출해서 중복 검사 요청
onCheckNickname?.invoke(nickname) { isDuplicated ->
if (isDuplicated) {
Toast.makeText(requireContext(), "이미 사용 중인 닉네임입니다.", Toast.LENGTH_SHORT).show()
binding.editNickname.text?.clear()
binding.checkNickname.setBackgroundResource(R.drawable.bg_btn_disabled)
binding.checkNickname.isEnabled = false
} else {
Toast.makeText(requireContext(), "사용 가능한 닉네임입니다!", Toast.LENGTH_SHORT).show()
}
}

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

비동기 콜백에서의 UI 업데이트: 생명주기/스레드 안전성 보장

현재 콜백 람다 내에서 binding을 직접 접근합니다. 콜백이 비동기적으로 늦게 호출되면, onDestroyView 이후 _binding이 null이 되어 NPE 위험이 있습니다. 또한 콜백 스레드가 메인 스레드가 아닐 수 있습니다.

아래와 같이 진행 중 상태 처리, 메인 스레드 전환, 안전한 바인딩 접근을 적용하세요.

         binding.checkNickname.setOnClickListener {
-            val nickname = binding.editNickname.text.toString()
+            val nickname = binding.editNickname.text.toString().trim()

             if (nickname.length !in 3..10) {
-                Toast.makeText(requireContext(), "닉네임은 3자 이상 10자 이하여야 합니다.", Toast.LENGTH_SHORT).show()
+                Toast.makeText(requireContext(), "닉네임은 3자 이상 10자 이하여야 합니다.", Toast.LENGTH_SHORT).show()
                 return@setOnClickListener
             }

             // 외부 콜백 호출해서 중복 검사 요청
-            onCheckNickname?.invoke(nickname) { isDuplicated ->
-                if (isDuplicated) {
-                    Toast.makeText(requireContext(), "이미 사용 중인 닉네임입니다.", Toast.LENGTH_SHORT).show()
-                    binding.editNickname.text?.clear()
-                    binding.checkNickname.setBackgroundResource(R.drawable.bg_btn_disabled)
-                    binding.checkNickname.isEnabled = false
-                } else {
-                    Toast.makeText(requireContext(), "사용 가능한 닉네임입니다!", Toast.LENGTH_SHORT).show()
-                }
-            }
+            // 진행 중 상태 표시: 버튼 비활성화
+            binding.checkNickname.isEnabled = false
+            onCheckNickname?.invoke(nickname) { isDuplicated ->
+                viewLifecycleOwner.lifecycleScope.launch {
+                    val b = _binding ?: return@launch
+                    if (isDuplicated) {
+                        Toast.makeText(requireContext(), "이미 사용 중인 닉네임입니다.", Toast.LENGTH_SHORT).show()
+                        b.editNickname.text?.clear()
+                        b.checkNickname.setBackgroundResource(R.drawable.bg_btn_disabled)
+                        b.checkNickname.isEnabled = false
+                    } else {
+                        Toast.makeText(requireContext(), "사용 가능한 닉네임입니다!", Toast.LENGTH_SHORT).show()
+                        // 성공 시 버튼을 재활성화(다시 검사 허용)하거나 완료 버튼 활성화 등 후속 상태 전환
+                        b.checkNickname.setBackgroundResource(R.drawable.bg_btn_main)
+                        b.checkNickname.isEnabled = true
+                    }
+                }
+            }

추가로 "완료" 흐름을 위해 중복 확인 성공 시에만 완료 버튼 활성화 등 상태 머신을 명확히 하면 UX가 좋아집니다.

📝 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
// 외부 콜백 호출해서 중복 검사 요청
onCheckNickname?.invoke(nickname) { isDuplicated ->
if (isDuplicated) {
Toast.makeText(requireContext(), "이미 사용 중인 닉네임입니다.", Toast.LENGTH_SHORT).show()
binding.editNickname.text?.clear()
binding.checkNickname.setBackgroundResource(R.drawable.bg_btn_disabled)
binding.checkNickname.isEnabled = false
} else {
Toast.makeText(requireContext(), "사용 가능한 닉네임입니다!", Toast.LENGTH_SHORT).show()
}
}
binding.checkNickname.setOnClickListener {
val nickname = binding.editNickname.text.toString().trim()
if (nickname.length !in 3..10) {
Toast.makeText(requireContext(), "닉네임은 3자 이상 10자 이하여야 합니다.", Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
// 진행 중 상태 표시: 버튼 비활성화
binding.checkNickname.isEnabled = false
onCheckNickname?.invoke(nickname) { isDuplicated ->
viewLifecycleOwner.lifecycleScope.launch {
val b = _binding ?: return@launch
if (isDuplicated) {
Toast.makeText(requireContext(), "이미 사용 중인 닉네임입니다.", Toast.LENGTH_SHORT).show()
b.editNickname.text?.clear()
b.checkNickname.setBackgroundResource(R.drawable.bg_btn_disabled)
b.checkNickname.isEnabled = false
} else {
Toast.makeText(requireContext(), "사용 가능한 닉네임입니다!", Toast.LENGTH_SHORT).show()
// 성공 시 버튼을 재활성화하거나 완료 버튼 활성화 등 후속 상태 전환
b.checkNickname.setBackgroundResource(R.drawable.bg_btn_main)
b.checkNickname.isEnabled = true
}
}
}
}
🤖 Prompt for AI Agents
In app/src/main/java/umc/OnAirMate/ui/pop_up/ChangeNicknamePopup.kt around lines
69-80, the asynchronous nickname-check callback updates binding directly which
can cause NPE if the view is destroyed and may run off the main thread; modify
the callback to switch to the main thread and safely access the view binding
(e.g., use viewLifecycleOwner.lifecycleScope.launch(Dispatchers.Main) or post to
requireActivity().runOnUiThread) and guard with a null-check or safe-call on
binding/_binding (return early if the view is destroyed), move all Toast and
view updates into that main-thread block, and additionally update the form state
(enable the complete/confirm button only when duplicate check succeeds) so the
UI state reflects the validation result.

/*
viewLifecycleOwner.lifecycleScope.launch {
val isDuplicated = withContext(Dispatchers.IO) {
repository.isNicknameDuplicated(nickname)
}

if (isDuplicated) {
Toast.makeText(requireContext(), "이미 사용 중인 닉네임입니다.", Toast.LENGTH_SHORT).show()
binding.editNickname.text.clear()
binding.checkNickname.setBackgroundResource(R.drawable.bg_btn_disabled)
binding.checkNickname.isEnabled = false
} else {
Toast.makeText(requireContext(), "사용 가능한 닉네임입니다!", Toast.LENGTH_SHORT).show()
}
}
*/

// viewLifecycleOwner.lifecycleScope.launch {
// val isDuplicated = withContext(Dispatchers.IO) {
// repository.isNicknameDuplicated(nickname)
// }
//
// if (isDuplicated) {
// Toast.makeText(requireContext(), "이미 사용 중인 닉네임입니다.", Toast.LENGTH_SHORT).show()
// binding.editNickname.text.clear()
// binding.checkNickname.setBackgroundResource(R.drawable.bg_btn_disabled)
// binding.checkNickname.isEnabled = false
// } else {
// Toast.makeText(requireContext(), "사용 가능한 닉네임입니다!", Toast.LENGTH_SHORT).show()
// }
// }
}
}

override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package umc.onairmate.ui.pop_up

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject
import umc.onairmate.data.repository.NicknameRepository

@HiltViewModel
class ChangeNicknameViewModel @Inject constructor(
private val nicknameRepository: NicknameRepository
) : ViewModel() {

// 닉네임 중복 확인 결과를 콜백으로 전달하거나 LiveData로 관리 가능
fun checkNickname(
nickname: String,
onResult: (Boolean) -> Unit
) {
viewModelScope.launch {
try {
val isDuplicated = nicknameRepository.isNicknameDuplicated(nickname)
onResult(isDuplicated)
} catch (e: Exception) {
// 에러 처리 필요하면 여기서
onResult(false) // 중복 아님으로 처리하거나 별도 처리
}
}
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

에러 처리 시 중복 아님(false)으로 반환 — 잘못된 UX/로직 가능성

네트워크/서버 에러가 발생해도 onResult(false)로 내려 중복 아님으로 처리됩니다. 이는 검증 실패를 유효로 오인하게 만들어, 이미 사용 중인 닉네임을 통과시킬 수 있습니다. 또한 detekt가 지적하듯 예외를 삼켜 원인 추적이 어렵습니다.

다음과 같이 삼진 결과로 분리해 주세요: 성공(isDuplicated), 실패(Error). 콜백 시그니처 변경이 가능하다면 권장합니다.

-    fun checkNickname(
-        nickname: String,
-        onResult: (Boolean) -> Unit
-    ) {
+    fun checkNickname(
+        nickname: String,
+        onResult: (NicknameCheckResult) -> Unit
+    ) {
         viewModelScope.launch {
             try {
-                val isDuplicated = nicknameRepository.isNicknameDuplicated(nickname)
-                onResult(isDuplicated)
+                val isDuplicated = nicknameRepository.isNicknameDuplicated(nickname)
+                onResult(NicknameCheckResult.Success(isDuplicated))
             } catch (e: Exception) {
-                // 에러 처리 필요하면 여기서
-                onResult(false) // 중복 아님으로 처리하거나 별도 처리
+                // 로깅 및 에러 전달
+                // Timber.e(e, "checkNickname failed") // Timber 사용 시
+                onResult(NicknameCheckResult.Error(e))
             }
         }
     }

다음 보조 타입을 추가해 주세요:

sealed class NicknameCheckResult {
    data class Success(val isDuplicated: Boolean) : NicknameCheckResult()
    data class Error(val throwable: Throwable) : NicknameCheckResult()
}

콜백 시그니처 변경이 어렵다면, 최소한 에러 시 안전 불허 정책으로 바꿔 주세요:

- onResult(false)
+ onResult(true) // 실패 시 중복으로 간주해 통과를 막음

detekt(예외 삼킴)도 함께 해소됩니다.

🧰 Tools
🪛 detekt (1.23.8)

[warning] 24-24: The caught exception is swallowed. The original exception could be lost.

(detekt.exceptions.SwallowedException)

🤖 Prompt for AI Agents
In app/src/main/java/umc/OnAirMate/ui/pop_up/ChangeNicknameViewModel.kt around
lines 16–29, the current catch block swallows exceptions and returns
onResult(false) which misrepresents errors as "not duplicated"; replace this by
propagating a clear error result: add the suggested sealed class
NicknameCheckResult (Success(isDuplicated: Boolean) and Error(throwable:
Throwable)) and change the onResult callback to accept NicknameCheckResult so
the coroutine returns Success on normal flow and Error(e) in the catch; if
changing the callback signature is not possible, change the error branch to a
safe-deny policy (call onResult(true) to disallow nickname on error) and
log/propagate the exception instead of swallowing it to satisfy detekt.

}
45 changes: 45 additions & 0 deletions app/src/main/java/umc/OnAirMate/ui/profile/BlockListAdapter.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package umc.onairmate.ui.profile

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import umc.onairmate.databinding.ItemBlockedUserBinding

data class BlockedUser(val nickname: String, val reason: String, val date: String)

class BlockListAdapter(
private val onUnblockClick: (BlockedUser) -> Unit
) : ListAdapter<BlockedUser, BlockListAdapter.BlockViewHolder>(diffCallback) {

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BlockViewHolder {
val binding = ItemBlockedUserBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return BlockViewHolder(binding)
}

override fun onBindViewHolder(holder: BlockViewHolder, position: Int) {
holder.bind(getItem(position))
}

inner class BlockViewHolder(private val binding: ItemBlockedUserBinding) :
RecyclerView.ViewHolder(binding.root) {

fun bind(user: BlockedUser) {
binding.tvNickname.text = "[${user.nickname}]"
binding.tvReason.text = user.reason
binding.tvDate.text = user.date

binding.btnUnblock.setOnClickListener {
onUnblockClick(user)
}
}
}

companion object {
val diffCallback = object : DiffUtil.ItemCallback<BlockedUser>() {
override fun areItemsTheSame(oldItem: BlockedUser, newItem: BlockedUser) = oldItem.nickname == newItem.nickname
override fun areContentsTheSame(oldItem: BlockedUser, newItem: BlockedUser) = oldItem == newItem
}
}
}
66 changes: 66 additions & 0 deletions app/src/main/java/umc/OnAirMate/ui/profile/BlockListFragment.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package umc.onairmate.ui.profile

import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import umc.onairmate.R
import umc.onairmate.databinding.FragmentBlockListBinding

class BlockListFragment : Fragment() {

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

private lateinit var blockListAdapter: BlockListAdapter

override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentBlockListBinding.inflate(inflater, container, false)
return binding.root
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

Log.d("BlockListFragment", "view created, binding.ivBack=${binding.ivBack}")
setupRecyclerView()
setupListeners()
}

private fun setupRecyclerView() {
blockListAdapter = BlockListAdapter { blockedUser ->
// 차단 해제 클릭 시 처리
}
binding.rvBlockList.apply {
layoutManager = LinearLayoutManager(requireContext())
adapter = blockListAdapter
}

// 테스트 데이터
blockListAdapter.submitList(
listOf(
BlockedUser("차단한 사용자 닉네임", "차단 사유", "0000년 00월 00일"),
BlockedUser("차단한 사용자 닉네임", "차단 사유", "0000년 00월 00일"),
BlockedUser("차단한 사용자 닉네임", "차단 사유", "0000년 00월 00일")
)
)
}

private fun setupListeners() {
binding.ivBack.setOnClickListener {
requireActivity().onBackPressed()
}
Comment on lines +59 to +61
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

백버튼 처리: Navigation API 사용 권장

Activity.onBackPressed()는 deprecated입니다. NavController를 통해 navigateUp/popBackStack을 호출하는 방식이 안전합니다.

-        binding.ivBack.setOnClickListener {
-            requireActivity().onBackPressed()
-        }
+        binding.ivBack.setOnClickListener {
+            findNavController().navigateUp()
+        }

추가로 파일 상단에 다음 import가 필요합니다:

import androidx.navigation.fragment.findNavController
📝 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
binding.ivBack.setOnClickListener {
requireActivity().onBackPressed()
}
binding.ivBack.setOnClickListener {
findNavController().navigateUp()
}
🤖 Prompt for AI Agents
In app/src/main/java/umc/OnAirMate/ui/profile/BlockListFragment.kt around lines
56 to 58, replace the deprecated requireActivity().onBackPressed() call with the
Navigation API by calling findNavController().navigateUp() (or
findNavController().popBackStack() if preferred) inside the ivBack click
listener, and add the required import line import
androidx.navigation.fragment.findNavController at the top of the file.


}

override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
Comment on lines +65 to +68
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

onDestroyView에서 RecyclerView 어댑터 해제

Fragment 뷰 생명주기와 어댑터/뷰홀더 참조가 얽혀 메모리 누수가 발생할 수 있습니다. 뷰 파괴 시 어댑터를 null로 해제하세요.

     override fun onDestroyView() {
         super.onDestroyView()
-        _binding = null
+        binding.rvBlockList.adapter = null
+        _binding = 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
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
override fun onDestroyView() {
super.onDestroyView()
binding.rvBlockList.adapter = null
_binding = null
}
🤖 Prompt for AI Agents
In app/src/main/java/umc/OnAirMate/ui/profile/BlockListFragment.kt around lines
62 to 65, onDestroyView currently only nulls the binding which can leak
RecyclerView adapters/viewHolders; before setting _binding = null, clear the
RecyclerView reference by setting binding.recyclerView.adapter = null (and
remove any itemListeners/scrollListeners) so the adapter and its ViewHolders can
be garbage-collected, then call super and null the binding.

}
Loading