diff --git a/app/src/main/java/umc/OnAirMate/ui/profile/BlockListAdapter.kt b/app/src/main/java/umc/OnAirMate/ui/profile/BlockListAdapter.kt new file mode 100644 index 00000000..b5b530c0 --- /dev/null +++ b/app/src/main/java/umc/OnAirMate/ui/profile/BlockListAdapter.kt @@ -0,0 +1,63 @@ +package umc.onairmate.ui.profile + +import android.view.LayoutInflater +import android.view.ViewGroup +import android.widget.PopupMenu +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import umc.onairmate.R +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(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}]" + + // btnMore 클릭 시 팝업 메뉴 표시 + binding.btnMore.setOnClickListener { view -> + val popup = PopupMenu(view.context, view) + popup.menuInflater.inflate(R.menu.menu_block_option, popup.menu) + popup.setOnMenuItemClickListener { item -> + when (item.itemId) { + R.id.menuUnblock -> { + onUnblockClick(user) // 콜백 호출 + true + } + else -> false + } + } + popup.show() + } + } + } + + companion object { + val diffCallback = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: BlockedUser, newItem: BlockedUser) = + oldItem.nickname == newItem.nickname + + override fun areContentsTheSame(oldItem: BlockedUser, newItem: BlockedUser) = + oldItem == newItem + } + } +} \ No newline at end of file diff --git a/app/src/main/java/umc/OnAirMate/ui/profile/BlockListFragment.kt b/app/src/main/java/umc/OnAirMate/ui/profile/BlockListFragment.kt new file mode 100644 index 00000000..987c8abd --- /dev/null +++ b/app/src/main/java/umc/OnAirMate/ui/profile/BlockListFragment.kt @@ -0,0 +1,69 @@ +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 android.widget.ImageButton +import android.widget.PopupMenu +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() + } + + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} \ No newline at end of file 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/module/NetworkModule.kt b/app/src/main/java/umc/onairmate/module/NetworkModule.kt index 7cce93bb..62b15c29 100644 --- a/app/src/main/java/umc/onairmate/module/NetworkModule.kt +++ b/app/src/main/java/umc/onairmate/module/NetworkModule.kt @@ -70,25 +70,6 @@ object NetworkModule { private inline fun Retrofit.buildService(): T{ return this.create(T::class.java) } - - @Module - @InstallIn(SingletonComponent::class) - object NetworkModule { - - @Provides - @Singleton - fun provideRetrofit(): Retrofit { - return Retrofit.Builder() - .baseUrl(OnAirMateApplication.getString(R.string.base_url)) // 수정 필요 - .addConverterFactory(GsonConverterFactory.create()) - .build() - } - - @Provides - @Singleton - fun provideJoinService(retrofit: Retrofit): JoinService { - return retrofit.create(JoinService::class.java) - } - } + } diff --git a/app/src/main/java/umc/onairmate/module/RepositoryModule.kt b/app/src/main/java/umc/onairmate/module/RepositoryModule.kt index 64f569a7..0226c144 100644 --- a/app/src/main/java/umc/onairmate/module/RepositoryModule.kt +++ b/app/src/main/java/umc/onairmate/module/RepositoryModule.kt @@ -13,6 +13,8 @@ import umc.onairmate.data.api.CollectionService import umc.onairmate.data.api.HomeService import umc.onairmate.data.api.JoinService import umc.onairmate.data.repository.repository.BookmarkRepository +import umc.onairmate.data.api.NicknameService +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.CollectionRepository @@ -26,6 +28,7 @@ import umc.onairmate.data.repository.repositoryImpl.CollectionRepositoryImpl import umc.onairmate.data.repository.repositoryImpl.HomeRepositoryImpl import umc.onairmate.data.repository.repositoryImpl.JoinRepositoryImpl import umc.onairmate.data.repository.repositoryImpl.AuthRepositoryImpl +import umc.onairmate.data.repository.repositoryImpl.NicknameRepositoryImpl @Module @InstallIn(ViewModelComponent::class) @@ -35,7 +38,7 @@ object RepositoryModule { @Provides fun providesHomeRepository( homeService: HomeService - ) : HomeRepository = HomeRepositoryImpl(homeService) + ): HomeRepository = HomeRepositoryImpl(homeService) @ViewModelScoped @Provides @@ -47,13 +50,13 @@ object RepositoryModule { @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 @@ -67,16 +70,16 @@ object RepositoryModule { collectionService: CollectionService ) : CollectionRepository = CollectionRepositoryImpl(collectionService) - @Module - @InstallIn(ViewModelComponent::class) - object RepositoryModule { + @ViewModelScoped + @Provides + fun provideJoinRepository( + joinService: JoinService + ): JoinRepository = JoinRepositoryImpl(joinService) + + @ViewModelScoped + @Provides + fun provideNicknameRepository( + nicknameService: NicknameService + ): NicknameRepository = NicknameRepositoryImpl(nicknameService) - @Provides - @ViewModelScoped - fun provideJoinRepository( - joinService: JoinService - ): JoinRepository { - return JoinRepositoryImpl(joinService) - } - } -} +} \ 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 2557b881..5dbcefa3 100644 --- a/app/src/main/java/umc/onairmate/module/ServiceModule.kt +++ b/app/src/main/java/umc/onairmate/module/ServiceModule.kt @@ -11,6 +11,8 @@ import umc.onairmate.data.api.ChatRoomService import umc.onairmate.data.api.CollectionService import umc.onairmate.data.api.FriendService import umc.onairmate.data.api.HomeService +import umc.onairmate.data.api.JoinService +import umc.onairmate.data.api.NicknameService import javax.inject.Singleton @@ -46,7 +48,6 @@ object ServiceModule { fun friendApi(@NetworkModule.BaseRetrofit retrofit: Retrofit): FriendService{ return retrofit.buildService() } - @Provides @Singleton fun bookmarkApi(@NetworkModule.BaseRetrofit retrofit: Retrofit): BookmarkService{ @@ -58,4 +59,15 @@ object ServiceModule { fun collectionApi(@NetworkModule.BaseRetrofit retrofit: Retrofit): CollectionService{ return retrofit.buildService() } + + @Provides + @Singleton + fun nicknameApi(@NetworkModule.BaseRetrofit retrofit: Retrofit): NicknameService { + return retrofit.buildService() + } + @Provides + @Singleton + fun joinApi(@NetworkModule.BaseRetrofit retrofit: Retrofit): JoinService { + return retrofit.buildService() + } } \ No newline at end of file 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..15e78786 --- /dev/null +++ b/app/src/main/java/umc/onairmate/ui/pop_up/ChangeNicknamePopup.kt @@ -0,0 +1,120 @@ +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.fragment.app.DialogFragment +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 : DialogFragment() { + + 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/ui/profile/ProfileFragment.kt b/app/src/main/java/umc/onairmate/ui/profile/ProfileFragment.kt index 83c7b985..f65a17fb 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,38 @@ 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.Button +import android.widget.EditText +import android.widget.ImageView import android.widget.Toast +import androidx.appcompat.app.AlertDialog +import androidx.core.content.ContentProviderCompat.requireContext import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import androidx.navigation.fragment.findNavController import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import umc.onairmate.R 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 +49,29 @@ 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.layoutOpinion.setOnClickListener { + showFeedbackDialog() + } + + // 기존에 있던 다른 클릭 리스너들 유지 binding.btnChangeProfile.setOnClickListener { Toast.makeText(requireContext(), "프로필 사진 변경 클릭", Toast.LENGTH_SHORT).show() } @@ -47,56 +80,38 @@ class ProfileFragment : Fragment() { Toast.makeText(requireContext(), "추천 및 제재에 따라 인기도가 조정됩니다.", Toast.LENGTH_LONG).show() } - binding.layoutMyRooms.setOnClickListener { - // 참여한 방 이동 + binding.layoutBlock.setOnClickListener { + findNavController().navigate(R.id.action_profileFragment_to_blockListFragment) + } + } + private fun showFeedbackDialog() { + val dialogView = layoutInflater.inflate(R.layout.dialog_opinion, null) + val dialog = AlertDialog.Builder(requireContext()) + .setView(dialogView) + .create() + + // 뷰 바인딩 + val etFeedback = dialogView.findViewById(R.id.etFeedback) + val btnClose = dialogView.findViewById(R.id.btnClose) + val btnSubmit = dialogView.findViewById