diff --git a/app/src/main/java/umc/OnAirMate/data/api/NicknameService.kt b/app/src/main/java/umc/OnAirMate/data/api/NicknameService.kt new file mode 100644 index 00000000..c847b99c --- /dev/null +++ b/app/src/main/java/umc/OnAirMate/data/api/NicknameService.kt @@ -0,0 +1,16 @@ +package umc.onairmate.data.api + +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 +} \ No newline at end of file diff --git a/app/src/main/java/umc/OnAirMate/data/model/entity/NicknameData.kt b/app/src/main/java/umc/OnAirMate/data/model/entity/NicknameData.kt new file mode 100644 index 00000000..764419d7 --- /dev/null +++ b/app/src/main/java/umc/OnAirMate/data/model/entity/NicknameData.kt @@ -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 +) \ No newline at end of file diff --git a/app/src/main/java/umc/OnAirMate/data/model/response/NicknameResponse.kt b/app/src/main/java/umc/OnAirMate/data/model/response/NicknameResponse.kt new file mode 100644 index 00000000..b005976b --- /dev/null +++ b/app/src/main/java/umc/OnAirMate/data/model/response/NicknameResponse.kt @@ -0,0 +1,12 @@ +package umc.onairmate.data.model.response + +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 +) \ No newline at end of file diff --git a/app/src/main/java/umc/OnAirMate/data/repository/repository/NicknameRepository.kt b/app/src/main/java/umc/OnAirMate/data/repository/repository/NicknameRepository.kt new file mode 100644 index 00000000..928a2029 --- /dev/null +++ b/app/src/main/java/umc/OnAirMate/data/repository/repository/NicknameRepository.kt @@ -0,0 +1,5 @@ +package umc.onairmate.data.repository + +interface NicknameRepository { + suspend fun isNicknameDuplicated(nickname: String): Boolean +} \ No newline at end of file diff --git a/app/src/main/java/umc/OnAirMate/data/repository/repositoryImpl/NicknameRepositoryImpl.kt b/app/src/main/java/umc/OnAirMate/data/repository/repositoryImpl/NicknameRepositoryImpl.kt new file mode 100644 index 00000000..def93ae2 --- /dev/null +++ b/app/src/main/java/umc/OnAirMate/data/repository/repositoryImpl/NicknameRepositoryImpl.kt @@ -0,0 +1,23 @@ +package umc.onairmate.data.repository.repositoryImpl + +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 + } + } +} \ No newline at end of file diff --git a/app/src/main/java/umc/OnAirMate/ui/chat_room/ChatRoomFragment.kt b/app/src/main/java/umc/OnAirMate/ui/chat_room/ChatRoomFragment.kt index 358de4b1..92d61afa 100644 --- a/app/src/main/java/umc/OnAirMate/ui/chat_room/ChatRoomFragment.kt +++ b/app/src/main/java/umc/OnAirMate/ui/chat_room/ChatRoomFragment.kt @@ -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 연동 } diff --git a/app/src/main/java/umc/OnAirMate/ui/pop_up/ChangeNicknamePopup.kt b/app/src/main/java/umc/OnAirMate/ui/pop_up/ChangeNicknamePopup.kt new file mode 100644 index 00000000..c687f1cf --- /dev/null +++ b/app/src/main/java/umc/OnAirMate/ui/pop_up/ChangeNicknamePopup.kt @@ -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 + +@AndroidEntryPoint +class ChangeNicknamePopup : BottomSheetDialogFragment() { + + private var _binding: PopupChangeNicknameBinding? = null + private val binding get() = _binding!! + + // 외부에서 중복 체크를 처리하기 위한 콜백 람다 추가 + var onCheckNickname: ((String, (Boolean) -> Unit) -> Unit)? = null + +// @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 + } + + 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() + } + } + + /* + 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 + } +} \ No newline at end of file diff --git a/app/src/main/java/umc/OnAirMate/ui/pop_up/ChangeNicknameViewModel.kt b/app/src/main/java/umc/OnAirMate/ui/pop_up/ChangeNicknameViewModel.kt new file mode 100644 index 00000000..e300b4ef --- /dev/null +++ b/app/src/main/java/umc/OnAirMate/ui/pop_up/ChangeNicknameViewModel.kt @@ -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) // 중복 아님으로 처리하거나 별도 처리 + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/umc/onairmate/module/RepositoryModule.kt b/app/src/main/java/umc/onairmate/module/RepositoryModule.kt index aa7a3e80..417968cd 100644 --- a/app/src/main/java/umc/onairmate/module/RepositoryModule.kt +++ b/app/src/main/java/umc/onairmate/module/RepositoryModule.kt @@ -8,7 +8,9 @@ import dagger.hilt.android.scopes.ViewModelScoped import umc.onairmate.data.api.FriendService import umc.onairmate.data.api.ChatRoomService import umc.onairmate.data.api.HomeService +import umc.onairmate.data.api.NicknameService import umc.onairmate.data.api.TestService +import umc.onairmate.data.repository.NicknameRepository import umc.onairmate.data.repository.repository.FriendRepository import umc.onairmate.data.repository.repository.ChatRoomRepository import umc.onairmate.data.repository.repository.HomeRepository @@ -16,6 +18,7 @@ import umc.onairmate.data.repository.repository.TestRepository import umc.onairmate.data.repository.repositoryImpl.FriendRepositoryImpl import umc.onairmate.data.repository.repositoryImpl.ChatRoomRepositoryImpl import umc.onairmate.data.repository.repositoryImpl.HomeRepositoryImpl +import umc.onairmate.data.repository.repositoryImpl.NicknameRepositoryImpl import umc.onairmate.data.repository.repositoryImpl.TestRepositoryImpl @Module @@ -26,22 +29,29 @@ object RepositoryModule { @Provides fun providesHomeRepository( homeService: HomeService - ) : HomeRepository = HomeRepositoryImpl(homeService) + ): HomeRepository = HomeRepositoryImpl(homeService) @ViewModelScoped @Provides fun providesTestRepository( testService: TestService - ) : TestRepository = TestRepositoryImpl(testService) + ): TestRepository = TestRepositoryImpl(testService) @ViewModelScoped @Provides fun providesFriendRepository( friendService: FriendService - ) : FriendRepository = FriendRepositoryImpl(friendService) + ): FriendRepository = FriendRepositoryImpl(friendService) + @ViewModelScoped @Provides fun providesChatRoomRepository( chatRoomService: ChatRoomService - ) : ChatRoomRepository = ChatRoomRepositoryImpl(chatRoomService) -} + ): ChatRoomRepository = ChatRoomRepositoryImpl(chatRoomService) + + @ViewModelScoped + @Provides + fun providesNicknameRepository( + nicknameService: NicknameService + ): NicknameRepository = NicknameRepositoryImpl(nicknameService) +} \ No newline at end of file diff --git a/app/src/main/java/umc/onairmate/module/ServiceModule.kt b/app/src/main/java/umc/onairmate/module/ServiceModule.kt index 4d0e045c..ec8553d0 100644 --- a/app/src/main/java/umc/onairmate/module/ServiceModule.kt +++ b/app/src/main/java/umc/onairmate/module/ServiceModule.kt @@ -8,6 +8,7 @@ import retrofit2.Retrofit import umc.onairmate.data.api.ChatRoomService import umc.onairmate.data.api.FriendService import umc.onairmate.data.api.HomeService +import umc.onairmate.data.api.NicknameService import umc.onairmate.data.api.TestService import javax.inject.Singleton @@ -44,4 +45,10 @@ object ServiceModule { fun friendApi(@NetworkModule.BaseRetrofit retrofit: Retrofit): FriendService{ return retrofit.buildService() } + + @Provides + @Singleton + fun nicknameApi(@NetworkModule.BaseRetrofit retrofit: Retrofit): NicknameService { + return retrofit.buildService() + } } \ No newline at end of file diff --git a/app/src/main/java/umc/onairmate/ui/profile/ProfileFragment.kt b/app/src/main/java/umc/onairmate/ui/profile/ProfileFragment.kt index 83c7b985..e8b7693d 100644 --- a/app/src/main/java/umc/onairmate/ui/profile/ProfileFragment.kt +++ b/app/src/main/java/umc/onairmate/ui/profile/ProfileFragment.kt @@ -1,28 +1,32 @@ package umc.onairmate.ui.profile - import android.content.Context -import android.graphics.Color -import android.graphics.drawable.ColorDrawable import android.os.Bundle -import android.view.Gravity import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.PopupWindow -import android.widget.TextView import android.widget.Toast +import androidx.core.content.ContentProviderCompat.requireContext import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope import dagger.hilt.android.AndroidEntryPoint -import umc.onairmate.R +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import umc.onairmate.databinding.FragmentProfileBinding - +import umc.onairmate.ui.pop_up.ChangeNicknamePopup +import umc.onairmate.ui.pop_up.ChangeNicknameViewModel @AndroidEntryPoint class ProfileFragment : Fragment() { private var _binding: FragmentProfileBinding? = null private val binding get() = _binding!! + + // ChangeNicknameViewModel 선언 + private val changeNicknameViewModel: ChangeNicknameViewModel by viewModels() + private var nickname = "" override fun onCreateView( @@ -39,6 +43,25 @@ class ProfileFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + binding.tvNicknameValue.text = nickname + + // 닉네임 영역 클릭 시 팝업 띄우기 + binding.layoutNickname.setOnClickListener { + val popup = ChangeNicknamePopup() + + // 팝업에 중복 검사 콜백 연결 + popup.onCheckNickname = { newNickname, callback -> + // 뷰모델의 checkNickname 함수 사용 + changeNicknameViewModel.checkNickname(newNickname) { isDuplicated -> + // 결과 콜백 호출 + callback(isDuplicated) + } + } + + popup.show(childFragmentManager, "ChangeNicknamePopup") + } + + // 기존에 있던 다른 클릭 리스너들 유지 binding.btnChangeProfile.setOnClickListener { Toast.makeText(requireContext(), "프로필 사진 변경 클릭", Toast.LENGTH_SHORT).show() } @@ -46,57 +69,10 @@ class ProfileFragment : Fragment() { binding.ivTooltip.setOnClickListener { Toast.makeText(requireContext(), "추천 및 제재에 따라 인기도가 조정됩니다.", Toast.LENGTH_LONG).show() } - - binding.layoutMyRooms.setOnClickListener { - // 참여한 방 이동 - } - - binding.tvNicknameValue.text = nickname - - // 다른 버튼들에 대한 clickListener도 동일하게 설정 } override fun onDestroyView() { super.onDestroyView() _binding = null } - - //도움말 클릭시 글귀 표시 - private fun showTooltip(anchorView: View, message: String) { - val inflater = LayoutInflater.from(anchorView.context) - val popupView = inflater.inflate(R.layout.popup_tooltip, null) - - // 텍스트 설정 - val tooltipText = popupView.findViewById(R.id.tooltip_text) - tooltipText.text = message - - // PopupWindow 생성 - val popupWindow = PopupWindow( - popupView, - ViewGroup.LayoutParams.WRAP_CONTENT, - ViewGroup.LayoutParams.WRAP_CONTENT, - true - ).apply { - isOutsideTouchable = true - setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) - } - - // 위치 계산 - val location = IntArray(2) - anchorView.getLocationOnScreen(location) - - val anchorX = location[0] - val anchorY = location[1] - - // anchorView 위에 말풍선 위치 조정 - popupWindow.showAtLocation( - anchorView, Gravity.NO_GRAVITY, - anchorX - popupView.measuredWidth / 2 + anchorView.width / 2, - anchorY - anchorView.height - 20 // 말풍선 높이 조절 - ) - - binding.ivTooltip.setOnClickListener { - showTooltip(it, "추천 및 제재에 따라 인기도가 조정됩니다.") - } - } } \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_btn_edit.xml b/app/src/main/res/drawable/bg_btn_edit.xml new file mode 100644 index 00000000..0b5686a4 --- /dev/null +++ b/app/src/main/res/drawable/bg_btn_edit.xml @@ -0,0 +1,4 @@ + + + + diff --git a/app/src/main/res/drawable/bg_input_edit_text.xml b/app/src/main/res/drawable/bg_input_edit_text.xml new file mode 100644 index 00000000..48d5829d --- /dev/null +++ b/app/src/main/res/drawable/bg_input_edit_text.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/btn_logout_outline.xml b/app/src/main/res/drawable/btn_logout_outline.xml new file mode 100644 index 00000000..7ebd933d --- /dev/null +++ b/app/src/main/res/drawable/btn_logout_outline.xml @@ -0,0 +1,10 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_profile.xml b/app/src/main/res/layout/fragment_profile.xml index 2466bd1a..f22cb0dc 100644 --- a/app/src/main/res/layout/fragment_profile.xml +++ b/app/src/main/res/layout/fragment_profile.xml @@ -4,7 +4,9 @@ xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="@color/white"> + android:background="@color/white" + android:layout_marginTop="20dp" + android:id="@+id/fragment_profile"> + android:layout_marginTop="50dp" />