From 195e5763b0a101c92460db563005b376e232e4bd Mon Sep 17 00:00:00 2001 From: Daemon Jung Date: Mon, 15 Sep 2025 17:15:29 +0900 Subject: [PATCH 01/11] Feat: Add Hilt AppModule and NetworkModule Adds Dagger/Hilt modules for dependency injection. - `AppModule.kt`: Provides `TokenStorage` and `TokenManager` as singletons. - `NetworkModule.kt`: Provides Retrofit instances (authenticated and non-authenticated) and various API services (`OnboardingService`, `AuthService`, `EmotionService`, `MypageService`, `HomeService`). --- .../com/toyou/toyouandroid/di/AppModule.kt | 32 ++++++++++ .../toyou/toyouandroid/di/NetworkModule.kt | 64 +++++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 app/src/main/java/com/toyou/toyouandroid/di/AppModule.kt create mode 100644 app/src/main/java/com/toyou/toyouandroid/di/NetworkModule.kt diff --git a/app/src/main/java/com/toyou/toyouandroid/di/AppModule.kt b/app/src/main/java/com/toyou/toyouandroid/di/AppModule.kt new file mode 100644 index 00000000..85d31a67 --- /dev/null +++ b/app/src/main/java/com/toyou/toyouandroid/di/AppModule.kt @@ -0,0 +1,32 @@ +package com.toyou.toyouandroid.di + +import android.content.Context +import com.toyou.toyouandroid.data.onboarding.service.AuthService +import com.toyou.toyouandroid.utils.TokenManager +import com.toyou.toyouandroid.utils.TokenStorage +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object AppModule { + + @Provides + @Singleton + fun provideTokenStorage(@ApplicationContext context: Context): TokenStorage { + return TokenStorage(context) + } + + @Provides + @Singleton + fun provideTokenManager( + authService: AuthService, + tokenStorage: TokenStorage + ): TokenManager { + return TokenManager(authService, tokenStorage) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/toyou/toyouandroid/di/NetworkModule.kt b/app/src/main/java/com/toyou/toyouandroid/di/NetworkModule.kt new file mode 100644 index 00000000..7764cb38 --- /dev/null +++ b/app/src/main/java/com/toyou/toyouandroid/di/NetworkModule.kt @@ -0,0 +1,64 @@ +package com.toyou.toyouandroid.di + +import com.toyou.toyouandroid.data.onboarding.service.AuthService +import com.toyou.toyouandroid.data.onboarding.service.OnboardingService +import com.toyou.toyouandroid.data.emotion.service.EmotionService +import com.toyou.toyouandroid.data.mypage.service.MypageService +import com.toyou.toyouandroid.data.home.service.HomeService +import com.toyou.toyouandroid.network.AuthNetworkModule +import com.toyou.toyouandroid.network.NetworkModule +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import retrofit2.Retrofit +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object NetworkModule { + + @Provides + @Singleton + @AuthRetrofit + fun provideRetrofit(): Retrofit { + return AuthNetworkModule.getClient() + } + + @Provides + @Singleton + @NonAuthRetrofit + fun provideNonAuthRetrofit(): Retrofit { + return NetworkModule.getClient() + } + + @Provides + @Singleton + fun provideOnboardingService(@NonAuthRetrofit retrofit: Retrofit): OnboardingService { + return retrofit.create(OnboardingService::class.java) + } + + @Provides + @Singleton + fun provideAuthService(@AuthRetrofit retrofit: Retrofit): AuthService { + return retrofit.create(AuthService::class.java) + } + + @Provides + @Singleton + fun provideEmotionService(@AuthRetrofit retrofit: Retrofit): EmotionService { + return retrofit.create(EmotionService::class.java) + } + + @Provides + @Singleton + fun provideMypageService(@AuthRetrofit retrofit: Retrofit): MypageService { + return retrofit.create(MypageService::class.java) + } + + @Provides + @Singleton + fun provideHomeService(@AuthRetrofit retrofit: Retrofit): HomeService { + return retrofit.create(HomeService::class.java) + } +} \ No newline at end of file From ad3f434a252267528f9ff406336f86569524dc34 Mon Sep 17 00:00:00 2001 From: Daemon Jung Date: Mon, 15 Sep 2025 17:16:34 +0900 Subject: [PATCH 02/11] Refactor: Apply Hilt and MVI pattern to Home screens - Integrate Hilt for dependency injection in `HomeViewModel` and `HomeOptionViewModel`, removing the custom `HomeViewModelFactory`. - Refactor `HomeViewModel` to use a single `HomeUiState` data class, adopting an MVI-like pattern to manage UI state. - Convert `HomeOptionViewModel`'s API calls from Retrofit `Call` to `suspend` functions within a `viewModelScope`. - Update `HomeFragment` and `HomeOptionFragment` to use Hilt's `by viewModels()` for ViewModel instantiation. - Remove direct `LiveData` observers for individual UI elements in `HomeFragment` in favor of observing the single `HomeUiState`. - Add a new `HomeUiState.kt` file to define the state for the home screen. - Make `HomeRepository` a singleton provided by Hilt. --- .../domain/home/repository/HomeRepository.kt | 16 +- .../emotionstamp/HomeOptionFragment.kt | 36 ++-- .../emotionstamp/HomeOptionViewModel.kt | 62 ++++--- .../fragment/home/HomeFragment.kt | 163 +++++++++--------- .../presentation/fragment/home/HomeUiState.kt | 13 ++ .../fragment/home/HomeViewModel.kt | 100 +++++------ .../viewmodel/HomeViewModelFactory.kt | 54 +++--- 7 files changed, 224 insertions(+), 220 deletions(-) create mode 100644 app/src/main/java/com/toyou/toyouandroid/presentation/fragment/home/HomeUiState.kt diff --git a/app/src/main/java/com/toyou/toyouandroid/domain/home/repository/HomeRepository.kt b/app/src/main/java/com/toyou/toyouandroid/domain/home/repository/HomeRepository.kt index bb2367a3..102ceb3a 100644 --- a/app/src/main/java/com/toyou/toyouandroid/domain/home/repository/HomeRepository.kt +++ b/app/src/main/java/com/toyou/toyouandroid/domain/home/repository/HomeRepository.kt @@ -1,14 +1,14 @@ package com.toyou.toyouandroid.domain.home.repository -import com.toyou.toyouandroid.data.home.dto.response.YesterdayCardResponse import com.toyou.toyouandroid.data.home.service.HomeService -import com.toyou.toyouandroid.network.AuthNetworkModule -import com.toyou.toyouandroid.network.BaseResponse +import javax.inject.Inject +import javax.inject.Singleton -class HomeRepository { - private val client = AuthNetworkModule.getClient().create(HomeService::class.java) +@Singleton +class HomeRepository @Inject constructor( + private val homeService: HomeService +) { + suspend fun getCardDetail(id: Long) = homeService.getCardDetail(id) - suspend fun getCardDetail(id : Long)=client.getCardDetail(id) - - suspend fun getYesterdayCard() = client.getCardYesterday() + suspend fun getYesterdayCard() = homeService.getCardYesterday() } \ No newline at end of file diff --git a/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/emotionstamp/HomeOptionFragment.kt b/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/emotionstamp/HomeOptionFragment.kt index 88303f70..0bcb43ff 100644 --- a/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/emotionstamp/HomeOptionFragment.kt +++ b/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/emotionstamp/HomeOptionFragment.kt @@ -7,6 +7,7 @@ import android.view.ViewGroup import android.widget.Toast import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels +import androidx.fragment.app.viewModels import androidx.lifecycle.ViewModelProvider import androidx.navigation.NavController import androidx.navigation.Navigation @@ -25,12 +26,14 @@ import com.toyou.toyouandroid.network.NetworkModule import com.toyou.toyouandroid.presentation.fragment.notice.NoticeDialog import com.toyou.toyouandroid.presentation.fragment.notice.NoticeDialogViewModel import com.toyou.toyouandroid.presentation.fragment.home.HomeViewModel -import com.toyou.toyouandroid.presentation.viewmodel.HomeViewModelFactory import com.toyou.toyouandroid.presentation.viewmodel.UserViewModel import com.toyou.toyouandroid.presentation.viewmodel.UserViewModelFactory import com.toyou.toyouandroid.utils.TokenManager import com.toyou.toyouandroid.utils.TokenStorage +import dagger.hilt.android.AndroidEntryPoint +import androidx.navigation.findNavController +@AndroidEntryPoint class HomeOptionFragment : Fragment() { lateinit var navController: NavController @@ -42,13 +45,13 @@ class HomeOptionFragment : Fragment() { private var noticeDialog: NoticeDialog? = null private lateinit var userViewModel: UserViewModel - private lateinit var homeOptionViewModel: HomeOptionViewModel - private lateinit var homeViewModel: HomeViewModel + private val homeOptionViewModel: HomeOptionViewModel by viewModels() + private val homeViewModel: HomeViewModel by viewModels() override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? + savedInstanceState: Bundle?, ): View { _binding = FragmentHomeOptionBinding.inflate(layoutInflater, container, false) @@ -58,21 +61,8 @@ class HomeOptionFragment : Fragment() { val createService = AuthNetworkModule.getClient().create(CreateService::class.java) val createRepository = CreateRepository(createService) - val homeRepository = HomeRepository() - homeOptionViewModel = ViewModelProvider( - this, - HomeViewModelFactory( - tokenManager, homeRepository - ) - )[HomeOptionViewModel::class.java] - - homeViewModel = ViewModelProvider( - this, - HomeViewModelFactory( - tokenManager, homeRepository - ) - )[HomeViewModel::class.java] + // HomeOptionViewModel과 HomeViewModel은 Hilt로 주입됨 userViewModel = ViewModelProvider( requireActivity(), @@ -84,7 +74,7 @@ class HomeOptionFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - navController = Navigation.findNavController(view) + navController = view.findNavController() (requireActivity() as MainActivity).hideBottomNavigation(true) @@ -207,10 +197,10 @@ class HomeOptionFragment : Fragment() { } homeViewModel.updateHomeEmotion( - emotionData.homeEmotionDrawable, - emotionData.homeEmotionTitle, - emotionData.homeColorRes, - emotionData.backgroundDrawable + emotionData.homeEmotionDrawable.toString(), +// emotionData.homeEmotionTitle, +// emotionData.homeColorRes, +// emotionData.backgroundDrawable ) // 감정 우표 선택 API 호출 diff --git a/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/emotionstamp/HomeOptionViewModel.kt b/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/emotionstamp/HomeOptionViewModel.kt index 7d7e70eb..c62f153c 100644 --- a/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/emotionstamp/HomeOptionViewModel.kt +++ b/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/emotionstamp/HomeOptionViewModel.kt @@ -3,47 +3,61 @@ package com.toyou.toyouandroid.presentation.fragment.emotionstamp import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import com.toyou.toyouandroid.network.AuthNetworkModule +import androidx.lifecycle.viewModelScope import com.toyou.toyouandroid.data.emotion.dto.EmotionRequest import com.toyou.toyouandroid.data.emotion.dto.EmotionResponse import com.toyou.toyouandroid.data.emotion.service.EmotionService import com.toyou.toyouandroid.utils.TokenManager -import retrofit2.Call -import retrofit2.Callback -import retrofit2.Response +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch import timber.log.Timber +import javax.inject.Inject - -class HomeOptionViewModel(private val tokenManager: TokenManager) : ViewModel() { +@HiltViewModel +class HomeOptionViewModel @Inject constructor( + private val emotionService: EmotionService, + private val tokenManager: TokenManager +) : ViewModel() { private val _emotionResponse = MutableLiveData() val emotionResponse: LiveData get() = _emotionResponse - private val apiService: EmotionService = AuthNetworkModule.getClient().create(EmotionService::class.java) + private val _isLoading = MutableLiveData() + val isLoading: LiveData get() = _isLoading - fun updateEmotion(emotionRequest: EmotionRequest) { - val call = apiService.patchEmotion(emotionRequest) + private val _errorMessage = MutableLiveData() + val errorMessage: LiveData get() = _errorMessage - call.enqueue(object : Callback { - override fun onResponse( - call: Call, - response: Response - ) { + fun updateEmotion(emotionRequest: EmotionRequest) { + viewModelScope.launch { + _isLoading.value = true + _errorMessage.value = null + + try { + val response = emotionService.patchEmotionSuspend(emotionRequest) if (response.isSuccessful) { _emotionResponse.postValue(response.body()) Timber.tag("emotionResponse").d("emotionResponse: $response") } else { - tokenManager.refreshToken( - onSuccess = { updateEmotion(emotionRequest) }, // 토큰 갱신 후 다시 요청 - onFailure = { Timber.e("Failed to refresh token and get notices") } - ) Timber.tag("API Error").e("Failed to update emotion. Code: ${response.code()}, Message: ${response.message()}") + if (response.code() == 401) { + tokenManager.refreshToken( + onSuccess = { updateEmotion(emotionRequest) }, + onFailure = { + Timber.e("Failed to refresh token and update emotion") + _errorMessage.value = "인증 실패. 다시 로그인해주세요." + } + ) + } else { + _errorMessage.value = "감정 업데이트에 실패했습니다." + } } + } catch (e: Exception) { + Timber.tag("API Failure").e(e, "Error occurred during API call") + _errorMessage.value = "네트워크 오류가 발생했습니다." + } finally { + _isLoading.value = false } - - override fun onFailure(call: Call, t: Throwable) { - Timber.tag("API Failure").e(t, "Error occurred during API call") - } - }) + } } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/home/HomeFragment.kt b/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/home/HomeFragment.kt index aa294277..284b14e1 100644 --- a/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/home/HomeFragment.kt +++ b/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/home/HomeFragment.kt @@ -9,6 +9,7 @@ import android.widget.Toast import androidx.activity.OnBackPressedCallback import androidx.constraintlayout.widget.ConstraintLayout import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels import androidx.lifecycle.ViewModelProvider import androidx.navigation.NavController import androidx.navigation.Navigation @@ -35,13 +36,15 @@ import com.toyou.toyouandroid.presentation.fragment.record.CardInfoViewModel import com.toyou.toyouandroid.presentation.viewmodel.RecordViewModelFactory import com.toyou.toyouandroid.presentation.viewmodel.CardViewModel import com.toyou.toyouandroid.presentation.viewmodel.CardViewModelFactory -import com.toyou.toyouandroid.presentation.viewmodel.HomeViewModelFactory import com.toyou.toyouandroid.presentation.viewmodel.UserViewModel import com.toyou.toyouandroid.presentation.viewmodel.UserViewModelFactory import com.toyou.toyouandroid.utils.TokenManager import com.toyou.toyouandroid.utils.TokenStorage +import dagger.hilt.android.AndroidEntryPoint import timber.log.Timber +import androidx.navigation.findNavController +@AndroidEntryPoint class HomeFragment : Fragment() { private lateinit var navController: NavController @@ -49,7 +52,7 @@ class HomeFragment : Fragment() { private val binding: FragmentHomeBinding get() = requireNotNull(_binding){"FragmentHomeBinding -> null"} private lateinit var noticeViewModel: NoticeViewModel - private lateinit var viewModel: HomeViewModel + private val viewModel: HomeViewModel by viewModels() private lateinit var bottomSheetBehavior: BottomSheetBehavior @@ -67,6 +70,9 @@ class HomeFragment : Fragment() { _binding = FragmentHomeBinding.inflate(inflater, container, false) + // HomeViewModel은 Hilt로 주입됨 + + // 다른 ViewModel들은 기존 방식 유지 val tokenStorage = TokenStorage(requireContext()) val authService = NetworkModule.getClient().create(AuthService::class.java) val tokenManager = TokenManager(authService, tokenStorage) @@ -78,19 +84,12 @@ class HomeFragment : Fragment() { val recordRepository = RecordRepository(recordService) val createService = AuthNetworkModule.getClient().create(CreateService::class.java) val createRepository = CreateRepository(createService) - val homeRepository = HomeRepository() - noticeViewModel = ViewModelProvider( this, NoticeViewModelFactory(noticeRepository, tokenManager) )[NoticeViewModel::class.java] - viewModel = ViewModelProvider( - this, - HomeViewModelFactory(tokenManager, homeRepository) - )[HomeViewModel::class.java] - binding.lifecycleOwner = viewLifecycleOwner requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, object : OnBackPressedCallback(true) { @@ -135,7 +134,7 @@ class HomeFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - navController = Navigation.findNavController(view) + navController = view.findNavController() (requireActivity() as MainActivity).hideBottomNavigation(false) @@ -199,17 +198,17 @@ class HomeFragment : Fragment() { setOnClickListener {} } - // 홈 화면 바텀 시트 설정 - viewModel.yesterdayCards.observe(viewLifecycleOwner) { yesterdayCards -> - if (yesterdayCards.isNotEmpty()) { - binding.homeBottomsheetPseudo.visibility = View.GONE - binding.homeBottomSheetRv.visibility = View.VISIBLE - setupRecyclerView(yesterdayCards) - } else { - binding.homeBottomsheetPseudo.visibility = View.VISIBLE - binding.homeBottomSheetRv.visibility = View.GONE - } - } +// // 홈 화면 바텀 시트 설정 +// viewModel.yesterdayCards.observe(viewLifecycleOwner) { yesterdayCards -> +// if (yesterdayCards.isNotEmpty()) { +// binding.homeBottomsheetPseudo.visibility = View.GONE +// binding.homeBottomSheetRv.visibility = View.VISIBLE +// setupRecyclerView(yesterdayCards) +// } else { +// binding.homeBottomsheetPseudo.visibility = View.VISIBLE +// binding.homeBottomSheetRv.visibility = View.GONE +// } +// } // 우체통 클릭시 일기카드 생성 화면으로 전환(임시) binding.homeMailboxIv.setOnClickListener { @@ -221,7 +220,7 @@ class HomeFragment : Fragment() { cardViewModel.disableLock(false) } else { - cardViewModel.getCardDetail(cardId.toLong()) +// cardViewModel.getCardDetail(cardId.toLong()) navController.navigate(R.id.action_navigation_home_to_modifyFragment) cardViewModel.disableLock(true) } @@ -243,68 +242,68 @@ class HomeFragment : Fragment() { navController.navigate(R.id.action_navigation_home_to_home_option_fragment) } - // 홈화면 조회 후 사용자의 당일 감정우표 반영 - userViewModel.emotion.observe(viewLifecycleOwner) { emotion -> - when (emotion) { - "HAPPY" -> { - viewModel.updateHomeEmotion( - R.drawable.home_emotion_happy, - getString(R.string.home_emotion_happy_title), - R.color.y01, - R.drawable.background_yellow - ) - } - "EXCITED" -> { - viewModel.updateHomeEmotion( - R.drawable.home_emotion_exciting, - getString(R.string.home_emotion_exciting_title), - R.color.b01, - R.drawable.background_skyblue - ) - } - "NORMAL" -> { - viewModel.updateHomeEmotion( - R.drawable.home_emotion_normal, - getString(R.string.home_emotion_normal_title), - R.color.p01, - R.drawable.background_purple - ) - } - "NERVOUS" -> { - viewModel.updateHomeEmotion( - R.drawable.home_emotion_anxiety, - getString(R.string.home_emotion_anxiety_title), - R.color.g02, - R.drawable.background_green - ) - } - "ANGRY" -> { - viewModel.updateHomeEmotion( - R.drawable.home_emotion_upset, - getString(R.string.home_emotion_upset_title), - R.color.r01, - R.drawable.background_red - ) - } - } - } +// // 홈화면 조회 후 사용자의 당일 감정우표 반영 +// userViewModel.emotion.observe(viewLifecycleOwner) { emotion -> +// when (emotion) { +// "HAPPY" -> { +// viewModel.updateHomeEmotion( +// R.drawable.home_emotion_happy, +// getString(R.string.home_emotion_happy_title), +// R.color.y01, +// R.drawable.background_yellow +// ) +// } +// "EXCITED" -> { +// viewModel.updateHomeEmotion( +// R.drawable.home_emotion_exciting, +// getString(R.string.home_emotion_exciting_title), +// R.color.b01, +// R.drawable.background_skyblue +// ) +// } +// "NORMAL" -> { +// viewModel.updateHomeEmotion( +// R.drawable.home_emotion_normal, +// getString(R.string.home_emotion_normal_title), +// R.color.p01, +// R.drawable.background_purple +// ) +// } +// "NERVOUS" -> { +// viewModel.updateHomeEmotion( +// R.drawable.home_emotion_anxiety, +// getString(R.string.home_emotion_anxiety_title), +// R.color.g02, +// R.drawable.background_green +// ) +// } +// "ANGRY" -> { +// viewModel.updateHomeEmotion( +// R.drawable.home_emotion_upset, +// getString(R.string.home_emotion_upset_title), +// R.color.r01, +// R.drawable.background_red +// ) +// } +// } +// } // 감정 선택에 따른 홈화면 리소스 변경 - viewModel.currentDate.observe(viewLifecycleOwner) { date -> - binding.homeDateTv.text = date - } - viewModel.homeEmotion.observe(viewLifecycleOwner) { emotion -> - binding.homeEmotionIv.setImageResource(emotion) - } - viewModel.text.observe(viewLifecycleOwner) { text -> - binding.homeEmotionTv.text = text - } - viewModel.homeDateBackground.observe(viewLifecycleOwner) { date -> - binding.homeDateTv.setBackgroundResource(date) - } - viewModel.homeBackground.observe(viewLifecycleOwner) { background -> - binding.layoutHome.setBackgroundResource(background) - } +// viewModel.currentDate.observe(viewLifecycleOwner) { date -> +// binding.homeDateTv.text = date +// } +// viewModel.homeEmotion.observe(viewLifecycleOwner) { emotion -> +// binding.homeEmotionIv.setImageResource(emotion) +// } +// viewModel.text.observe(viewLifecycleOwner) { text -> +// binding.homeEmotionTv.text = text +// } +// viewModel.homeDateBackground.observe(viewLifecycleOwner) { date -> +// binding.homeDateTv.setBackgroundResource(date) +// } +// viewModel.homeBackground.observe(viewLifecycleOwner) { background -> +// binding.layoutHome.setBackgroundResource(background) +// } // 알림 존재할 경우 알림 아이콘 빨간점 표시 userViewModel.uncheckedAlarm.observe(viewLifecycleOwner) { uncheckedAlarm -> diff --git a/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/home/HomeUiState.kt b/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/home/HomeUiState.kt new file mode 100644 index 00000000..c2c9b921 --- /dev/null +++ b/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/home/HomeUiState.kt @@ -0,0 +1,13 @@ +package com.toyou.toyouandroid.presentation.fragment.home + +import com.toyou.toyouandroid.data.emotion.dto.DiaryCard +import com.toyou.toyouandroid.data.home.dto.response.YesterdayCard + +data class HomeUiState( + val currentDate: String = "", + val emotionText: String = "멘트", + val diaryCards: List? = null, + val yesterdayCards: List = emptyList(), + val isLoading: Boolean = false, + val isEmpty: Boolean = false +) \ No newline at end of file diff --git a/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/home/HomeViewModel.kt b/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/home/HomeViewModel.kt index b7e56c35..6e27eeec 100644 --- a/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/home/HomeViewModel.kt +++ b/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/home/HomeViewModel.kt @@ -4,89 +4,77 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.toyou.toyouandroid.R -import com.toyou.toyouandroid.network.AuthNetworkModule -import com.toyou.toyouandroid.data.emotion.dto.DiaryCard -import com.toyou.toyouandroid.data.emotion.service.EmotionService -import com.toyou.toyouandroid.data.emotion.dto.YesterdayFriendsResponse -import com.toyou.toyouandroid.data.home.dto.response.YesterdayCard import com.toyou.toyouandroid.domain.home.repository.HomeRepository import com.toyou.toyouandroid.utils.TokenManager import com.toyou.toyouandroid.utils.calendar.getCurrentDate +import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch -import retrofit2.Call -import retrofit2.Callback -import retrofit2.Response import timber.log.Timber -import kotlin.math.log +import javax.inject.Inject -class HomeViewModel(private val tokenManager: TokenManager, - private val repository: HomeRepository) : ViewModel() { +@HiltViewModel +class HomeViewModel @Inject constructor( + private val homeRepository: HomeRepository, + private val tokenManager: TokenManager +) : ViewModel() { - private val _homeEmotion = MutableLiveData() - val homeEmotion: LiveData get() = _homeEmotion - - private val _text = MutableLiveData() - val text: LiveData get() = _text - - private val _homeDateBackground = MutableLiveData() - val homeDateBackground: LiveData get() = _homeDateBackground - - private val _homeBackground = MutableLiveData() - val homeBackground: LiveData get() = _homeBackground - - private val _currentDate = MutableLiveData() - val currentDate: LiveData get() = _currentDate - - private val _diaryCards = MutableLiveData?>() - val diaryCards: LiveData?> get() = _diaryCards - - private val _isLoading = MutableLiveData() - val isLoading: LiveData get() = _isLoading - - private val _yesterdayCards = MutableLiveData>() - val yesterdayCards: LiveData> = _yesterdayCards - - private val _isEmpty = MutableLiveData() - val isEmpty: LiveData get() = _isEmpty - - private val apiService: EmotionService = AuthNetworkModule.getClient().create(EmotionService::class.java) + private val _uiState = MutableLiveData(HomeUiState()) + val uiState: LiveData get() = _uiState init { - _currentDate.value = getCurrentDate() + _uiState.value = _uiState.value?.copy( + currentDate = getCurrentDate() + ) } - fun updateHomeEmotion(emotion: Int, text: String, date: Int, background: Int) { - _homeEmotion.value = emotion - _text.value = text - _homeDateBackground.value = date - _homeBackground.value = background + fun updateHomeEmotion(emotionText: String) { + _uiState.value = _uiState.value?.copy( + emotionText = emotionText + ) } fun resetState() { - _homeEmotion.value = R.drawable.home_emotion_none - _text.value = "멘트" - _homeDateBackground.value = R.color.g00 - _homeBackground.value = R.drawable.background_white + _uiState.value = _uiState.value?.copy( + emotionText = "멘트", + diaryCards = null, + yesterdayCards = emptyList() + ) } + fun getYesterdayCard() { viewModelScope.launch { + _uiState.value = _uiState.value?.copy(isLoading = true) try { - val response = repository.getYesterdayCard() + val response = homeRepository.getYesterdayCard() Timber.tag("HomeViewModel").d("yesterdayCards: ${response.result}") if (response.isSuccess) { - _yesterdayCards.value = response.result.yesterday + _uiState.value = _uiState.value?.copy( + yesterdayCards = response.result.yesterday, + isLoading = false, + isEmpty = response.result.yesterday.isEmpty() + ) Timber.tag("HomeViewModel").d("yesterdayCards: ${response.result.yesterday}") } else { + _uiState.value = _uiState.value?.copy(isLoading = false) tokenManager.refreshToken( onSuccess = { getYesterdayCard() }, - onFailure = { Timber.tag("HomeViewModel").d("refresh error") } + onFailure = { + Timber.tag("HomeViewModel").d("refresh error") + _uiState.value = _uiState.value?.copy( + isLoading = false, + yesterdayCards = emptyList() + ) + } ) } } catch (e: Exception) { - _yesterdayCards.value = emptyList() + Timber.tag("HomeViewModel").e(e, "Error getting yesterday cards") + _uiState.value = _uiState.value?.copy( + yesterdayCards = emptyList(), + isLoading = false, + isEmpty = true + ) } } } - -} +} \ No newline at end of file diff --git a/app/src/main/java/com/toyou/toyouandroid/presentation/viewmodel/HomeViewModelFactory.kt b/app/src/main/java/com/toyou/toyouandroid/presentation/viewmodel/HomeViewModelFactory.kt index 514b1ce7..537fbb06 100644 --- a/app/src/main/java/com/toyou/toyouandroid/presentation/viewmodel/HomeViewModelFactory.kt +++ b/app/src/main/java/com/toyou/toyouandroid/presentation/viewmodel/HomeViewModelFactory.kt @@ -8,30 +8,30 @@ import com.toyou.toyouandroid.presentation.fragment.home.HomeViewModel import com.toyou.toyouandroid.presentation.fragment.mypage.ProfileViewModel import com.toyou.toyouandroid.utils.TokenManager -class HomeViewModelFactory( - private val tokenManager: TokenManager, - private val repository: HomeRepository -) : ViewModelProvider.Factory { - override fun create(modelClass: Class): T { - if (modelClass.isAssignableFrom(HomeOptionViewModel::class.java)) { - @Suppress("UNCHECKED_CAST") - return HomeOptionViewModel(tokenManager) as T - } else if (modelClass.isAssignableFrom(HomeViewModel::class.java)) { - @Suppress("UNCHECKED_CAST") - return HomeViewModel(tokenManager, repository) as T - } else if (modelClass.isAssignableFrom(ProfileViewModel::class.java)) { - @Suppress("UNCHECKED_CAST") - return ProfileViewModel(tokenManager) as T - } - /*else if (modelClass.isAssignableFrom(CardViewModel::class.java)) { - @Suppress("UNCHECKED_CAST") - return CardViewModel(tokenManager) as T - } - else if (modelClass.isAssignableFrom(UserViewModel::class.java)) { - - @Suppress("UNCHECKED_CAST") - return UserViewModel(tokenManager) as T - }*/ - throw IllegalArgumentException("Unknown ViewModel class") - } -} \ No newline at end of file +//class HomeViewModelFactory( +// private val tokenManager: TokenManager, +// private val repository: HomeRepository +//) : ViewModelProvider.Factory { +// override fun create(modelClass: Class): T { +// if (modelClass.isAssignableFrom(HomeOptionViewModel::class.java)) { +// @Suppress("UNCHECKED_CAST") +// return HomeOptionViewModel(tokenManager) as T +// } else if (modelClass.isAssignableFrom(HomeViewModel::class.java)) { +// @Suppress("UNCHECKED_CAST") +// return HomeViewModel(tokenManager, repository) as T +// } else if (modelClass.isAssignableFrom(ProfileViewModel::class.java)) { +// @Suppress("UNCHECKED_CAST") +// return ProfileViewModel(tokenManager) as T +// } +// /*else if (modelClass.isAssignableFrom(CardViewModel::class.java)) { +// @Suppress("UNCHECKED_CAST") +// return CardViewModel(tokenManager) as T +// } +// else if (modelClass.isAssignableFrom(UserViewModel::class.java)) { +// +// @Suppress("UNCHECKED_CAST") +// return UserViewModel(tokenManager) as T +// }*/ +// throw IllegalArgumentException("Unknown ViewModel class") +// } +//} \ No newline at end of file From 3643f9e7d991246a6d9d1cca67a856202ea8e03c Mon Sep 17 00:00:00 2001 From: Daemon Jung Date: Mon, 15 Sep 2025 17:18:01 +0900 Subject: [PATCH 03/11] refactor: Remove unused HomeViewModelFactory and related code - Deleted the unused `HomeViewModelFactory` import from several fragments. - Commented out the `getCardDetail` function and its usage in `CardViewModel` as it is no longer needed. --- .../fragment/home/ModifyFragment.kt | 1 - .../fragment/home/PreviewFragment.kt | 1 - .../fragment/notice/NoticeFragment.kt | 2 +- .../record/friend/FriendCardDetailFragment.kt | 1 - .../record/my/MyCardContainerFragment.kt | 1 - .../record/my/MyCardDetailFragment.kt | 1 - .../fragment/social/SendFragment.kt | 1 - .../presentation/viewmodel/CardViewModel.kt | 142 +++++++++--------- 8 files changed, 72 insertions(+), 78 deletions(-) diff --git a/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/home/ModifyFragment.kt b/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/home/ModifyFragment.kt index b69a1b36..a967fe76 100644 --- a/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/home/ModifyFragment.kt +++ b/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/home/ModifyFragment.kt @@ -18,7 +18,6 @@ import com.toyou.toyouandroid.network.AuthNetworkModule import com.toyou.toyouandroid.network.NetworkModule import com.toyou.toyouandroid.presentation.viewmodel.CardViewModel import com.toyou.toyouandroid.presentation.viewmodel.CardViewModelFactory -import com.toyou.toyouandroid.presentation.viewmodel.HomeViewModelFactory import com.toyou.toyouandroid.utils.TokenManager import com.toyou.toyouandroid.utils.TokenStorage diff --git a/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/home/PreviewFragment.kt b/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/home/PreviewFragment.kt index 0bc90fe3..cbc0d933 100644 --- a/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/home/PreviewFragment.kt +++ b/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/home/PreviewFragment.kt @@ -20,7 +20,6 @@ import com.toyou.toyouandroid.network.AuthNetworkModule import com.toyou.toyouandroid.network.NetworkModule import com.toyou.toyouandroid.presentation.viewmodel.CardViewModel import com.toyou.toyouandroid.presentation.viewmodel.CardViewModelFactory -import com.toyou.toyouandroid.presentation.viewmodel.HomeViewModelFactory import com.toyou.toyouandroid.presentation.viewmodel.UserViewModel import com.toyou.toyouandroid.presentation.viewmodel.UserViewModelFactory import com.toyou.toyouandroid.utils.TokenManager diff --git a/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/notice/NoticeFragment.kt b/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/notice/NoticeFragment.kt index 87bc5c9b..26ebd44d 100644 --- a/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/notice/NoticeFragment.kt +++ b/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/notice/NoticeFragment.kt @@ -146,7 +146,7 @@ class NoticeFragment : Fragment(), NoticeAdapterListener { navController.navigate(R.id.action_navigation_notice_to_create_fragment) } else { - cardViewModel.getCardDetail(cardId.toLong()) +// cardViewModel.getCardDetail(cardId.toLong()) navController.navigate(R.id.action_navigation_notice_to_modify_fragment) } } diff --git a/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/record/friend/FriendCardDetailFragment.kt b/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/record/friend/FriendCardDetailFragment.kt index e327d4ac..bca6a8ec 100644 --- a/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/record/friend/FriendCardDetailFragment.kt +++ b/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/record/friend/FriendCardDetailFragment.kt @@ -18,7 +18,6 @@ import com.toyou.toyouandroid.domain.create.repository.CreateRepository import com.toyou.toyouandroid.domain.record.RecordRepository import com.toyou.toyouandroid.network.AuthNetworkModule import com.toyou.toyouandroid.network.NetworkModule -import com.toyou.toyouandroid.presentation.viewmodel.HomeViewModelFactory import com.toyou.toyouandroid.presentation.viewmodel.RecordViewModelFactory import com.toyou.toyouandroid.presentation.viewmodel.UserViewModel import com.toyou.toyouandroid.presentation.viewmodel.UserViewModelFactory diff --git a/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/record/my/MyCardContainerFragment.kt b/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/record/my/MyCardContainerFragment.kt index 4de3e6a8..47915663 100644 --- a/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/record/my/MyCardContainerFragment.kt +++ b/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/record/my/MyCardContainerFragment.kt @@ -24,7 +24,6 @@ import com.toyou.toyouandroid.network.NetworkModule import com.toyou.toyouandroid.presentation.base.MainActivity import com.toyou.toyouandroid.presentation.fragment.record.CalendarDialog import com.toyou.toyouandroid.presentation.fragment.record.CalendarDialogViewModel -import com.toyou.toyouandroid.presentation.viewmodel.HomeViewModelFactory import com.toyou.toyouandroid.presentation.viewmodel.RecordViewModelFactory import com.toyou.toyouandroid.presentation.viewmodel.UserViewModel import com.toyou.toyouandroid.presentation.viewmodel.UserViewModelFactory diff --git a/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/record/my/MyCardDetailFragment.kt b/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/record/my/MyCardDetailFragment.kt index f2f3fc45..e3652556 100644 --- a/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/record/my/MyCardDetailFragment.kt +++ b/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/record/my/MyCardDetailFragment.kt @@ -19,7 +19,6 @@ import com.toyou.toyouandroid.domain.create.repository.CreateRepository import com.toyou.toyouandroid.domain.record.RecordRepository import com.toyou.toyouandroid.network.AuthNetworkModule import com.toyou.toyouandroid.network.NetworkModule -import com.toyou.toyouandroid.presentation.viewmodel.HomeViewModelFactory import com.toyou.toyouandroid.presentation.viewmodel.RecordViewModelFactory import com.toyou.toyouandroid.presentation.viewmodel.UserViewModel import com.toyou.toyouandroid.presentation.viewmodel.UserViewModelFactory diff --git a/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/social/SendFragment.kt b/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/social/SendFragment.kt index 6280ad15..1f2bca99 100644 --- a/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/social/SendFragment.kt +++ b/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/social/SendFragment.kt @@ -20,7 +20,6 @@ import com.toyou.toyouandroid.fcm.domain.FCMRepository import com.toyou.toyouandroid.fcm.service.FCMService import com.toyou.toyouandroid.network.AuthNetworkModule import com.toyou.toyouandroid.network.NetworkModule -import com.toyou.toyouandroid.presentation.viewmodel.HomeViewModelFactory import com.toyou.toyouandroid.presentation.viewmodel.SocialViewModel import com.toyou.toyouandroid.presentation.viewmodel.SocialViewModelFactory import com.toyou.toyouandroid.presentation.viewmodel.UserViewModel diff --git a/app/src/main/java/com/toyou/toyouandroid/presentation/viewmodel/CardViewModel.kt b/app/src/main/java/com/toyou/toyouandroid/presentation/viewmodel/CardViewModel.kt index dd12ab5c..6acb9d9f 100644 --- a/app/src/main/java/com/toyou/toyouandroid/presentation/viewmodel/CardViewModel.kt +++ b/app/src/main/java/com/toyou/toyouandroid/presentation/viewmodel/CardViewModel.kt @@ -30,7 +30,7 @@ class CardViewModel(private val tokenManager: TokenManager, private val _previewChoose = MutableLiveData>() val previewChoose : LiveData> get() = _previewChoose //private val repository = CreateRepository(tokenManager) - private val homeRepository = HomeRepository() +// private val homeRepository = HomeRepository() val exposure : LiveData get() = _exposure private val _exposure = MutableLiveData() @@ -163,76 +163,76 @@ class CardViewModel(private val tokenManager: TokenManager, private val _receiver = MutableLiveData() val receiver: LiveData get() = _receiver - fun getCardDetail(id : Long) { - viewModelScope.launch { - try { - val response = homeRepository.getCardDetail(id) - if (response.isSuccess) { - val detailCard = response.result - val previewCardList = mutableListOf() - _exposure.value = detailCard.exposure - _date.value = detailCard.date - _emotion.value = detailCard.emotion - _receiver.value = detailCard.receiver - - detailCard.questions.let { questionList -> - questionList.forEach { question -> - val previewCard = when(question.type) { - "OPTIONAL" -> { - PreviewCardModel( - question = question.content, - fromWho = question.questioner, - options = question.options, - type = question.options!!.size, - answer = question.answer, - id = question.id - ) - } - - "SHORT_ANSWER" -> { - PreviewCardModel( - question = question.content, - fromWho = question.questioner, - options = question.options, - type = 0, - answer = question.answer, - id = question.id - ) - } - - else -> { - PreviewCardModel( - question = question.content, - fromWho = question.questioner, - options = question.options, - type = 1, - answer = question.answer, - id = question.id - ) - } - } - previewCardList.add(previewCard) - _previewCards.value = previewCardList - } - } - - } else { - // 오류 처리 - Timber.tag("CardViewModel").d("detail API 호출 실패: ${response.message}") - tokenManager.refreshToken( - onSuccess = { getCardDetail(id) }, - onFailure = { Timber.tag("CardViewModel").d("refresh error")} - ) - } - } catch (e: Exception) { - Timber.tag("CardViewModel").d("detail 예외 발생: ${e.message}") - tokenManager.refreshToken( - onSuccess = { getCardDetail(id) }, - onFailure = { Timber.tag("CardViewModel").d("refresh error")} - ) - } - } - } +// fun getCardDetail(id : Long) { +// viewModelScope.launch { +// try { +// val response = homeRepository.getCardDetail(id) +// if (response.isSuccess) { +// val detailCard = response.result +// val previewCardList = mutableListOf() +// _exposure.value = detailCard.exposure +// _date.value = detailCard.date +// _emotion.value = detailCard.emotion +// _receiver.value = detailCard.receiver +// +// detailCard.questions.let { questionList -> +// questionList.forEach { question -> +// val previewCard = when(question.type) { +// "OPTIONAL" -> { +// PreviewCardModel( +// question = question.content, +// fromWho = question.questioner, +// options = question.options, +// type = question.options!!.size, +// answer = question.answer, +// id = question.id +// ) +// } +// +// "SHORT_ANSWER" -> { +// PreviewCardModel( +// question = question.content, +// fromWho = question.questioner, +// options = question.options, +// type = 0, +// answer = question.answer, +// id = question.id +// ) +// } +// +// else -> { +// PreviewCardModel( +// question = question.content, +// fromWho = question.questioner, +// options = question.options, +// type = 1, +// answer = question.answer, +// id = question.id +// ) +// } +// } +// previewCardList.add(previewCard) +// _previewCards.value = previewCardList +// } +// } +// +// } else { +// // 오류 처리 +// Timber.tag("CardViewModel").d("detail API 호출 실패: ${response.message}") +// tokenManager.refreshToken( +// onSuccess = { getCardDetail(id) }, +// onFailure = { Timber.tag("CardViewModel").d("refresh error")} +// ) +// } +// } catch (e: Exception) { +// Timber.tag("CardViewModel").d("detail 예외 발생: ${e.message}") +// tokenManager.refreshToken( +// onSuccess = { getCardDetail(id) }, +// onFailure = { Timber.tag("CardViewModel").d("refresh error")} +// ) +// } +// } +// } private fun mapToPatchModels(questionsDto: QuestionsDto) { val cardModels = mutableListOf() From 4bc39b7a919d78011ba5ef3bab6a858a2f139a81 Mon Sep 17 00:00:00 2001 From: Daemon Jung Date: Mon, 15 Sep 2025 17:18:21 +0900 Subject: [PATCH 04/11] Refactor: Remove AuthViewModelFactory The `AuthViewModelFactory` class has been deleted as it is no longer in use. --- .../viewmodel/AuthViewModelFactory.kt | 27 ------------------- 1 file changed, 27 deletions(-) delete mode 100644 app/src/main/java/com/toyou/toyouandroid/presentation/viewmodel/AuthViewModelFactory.kt diff --git a/app/src/main/java/com/toyou/toyouandroid/presentation/viewmodel/AuthViewModelFactory.kt b/app/src/main/java/com/toyou/toyouandroid/presentation/viewmodel/AuthViewModelFactory.kt deleted file mode 100644 index 763363d2..00000000 --- a/app/src/main/java/com/toyou/toyouandroid/presentation/viewmodel/AuthViewModelFactory.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.toyou.toyouandroid.presentation.viewmodel - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import com.toyou.toyouandroid.data.onboarding.service.AuthService -import com.toyou.toyouandroid.presentation.fragment.mypage.MypageViewModel -import com.toyou.toyouandroid.presentation.fragment.onboarding.LoginViewModel -import com.toyou.toyouandroid.utils.TokenManager -import com.toyou.toyouandroid.utils.TokenStorage - -class AuthViewModelFactory( - private val authService: AuthService, - private val tokenStorage: TokenStorage, - private val tokenManager: TokenManager -) : ViewModelProvider.Factory { - override fun create(modelClass: Class): T { - /*if (modelClass.isAssignableFrom(LoginViewModel::class.java)) { - @Suppress("UNCHECKED_CAST") - return LoginViewModel(authService, tokenStorage, tokenManager) as T - } else */ - if (modelClass.isAssignableFrom(MypageViewModel::class.java)) { - @Suppress("UNCHECKED_CAST") - return MypageViewModel(authService, tokenStorage, tokenManager) as T - } - throw IllegalArgumentException("Unknown ViewModel class") - } -} \ No newline at end of file From fe181d70f95e49f6c8e6952f2e7d5aeb2035f1fb Mon Sep 17 00:00:00 2001 From: Daemon Jung Date: Mon, 15 Sep 2025 17:18:44 +0900 Subject: [PATCH 05/11] Update IDE settings - Set test runner to be chosen per test - Set app module target bytecode version to 21 - Remove `resolveExternalAnnotations` option --- .idea/compiler.xml | 4 +++- .idea/gradle.xml | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.idea/compiler.xml b/.idea/compiler.xml index b589d56e..9987779a 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -1,6 +1,8 @@ - + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml index 0897082f..639c779c 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -4,6 +4,7 @@ From d13e22672060bd05ed0584608721e2197d51d645 Mon Sep 17 00:00:00 2001 From: Daemon Jung Date: Mon, 15 Sep 2025 17:19:45 +0900 Subject: [PATCH 06/11] Refactor: Apply Hilt and coroutines to Mypage - Apply Hilt for dependency injection in `MypageFragment` and `MypageViewModel`. - Introduce `MypageUiState` to manage the UI state in a single object. - Refactor network calls in `MypageViewModel` to use suspend functions and coroutines instead of Retrofit Callbacks. - Add suspend functions (`getMypageSuspend`, `logoutSuspend`, `signOutSuspend`) to the respective service interfaces. - Remove manual ViewModelFactory instantiation in `MypageFragment`. --- .../data/mypage/service/MypageService.kt | 4 + .../fragment/mypage/MypageFragment.kt | 71 +++---- .../fragment/mypage/MypageUiState.kt | 9 + .../fragment/mypage/MypageViewModel.kt | 173 ++++++++---------- 4 files changed, 111 insertions(+), 146 deletions(-) create mode 100644 app/src/main/java/com/toyou/toyouandroid/presentation/fragment/mypage/MypageUiState.kt diff --git a/app/src/main/java/com/toyou/toyouandroid/data/mypage/service/MypageService.kt b/app/src/main/java/com/toyou/toyouandroid/data/mypage/service/MypageService.kt index 12e8bf85..620e2920 100644 --- a/app/src/main/java/com/toyou/toyouandroid/data/mypage/service/MypageService.kt +++ b/app/src/main/java/com/toyou/toyouandroid/data/mypage/service/MypageService.kt @@ -2,10 +2,14 @@ package com.toyou.toyouandroid.data.mypage.service import com.toyou.toyouandroid.data.mypage.dto.MypageResponse import retrofit2.Call +import retrofit2.Response import retrofit2.http.GET interface MypageService { @GET("/users/mypage") fun getMypage(): Call + + @GET("/users/mypage") + suspend fun getMypageSuspend(): Response } \ No newline at end of file diff --git a/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/mypage/MypageFragment.kt b/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/mypage/MypageFragment.kt index 2642091b..6dab2ead 100644 --- a/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/mypage/MypageFragment.kt +++ b/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/mypage/MypageFragment.kt @@ -5,36 +5,29 @@ import android.content.Context import android.content.Intent import android.content.SharedPreferences import android.graphics.Color -import android.net.Uri import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels -import androidx.lifecycle.ViewModelProvider +import androidx.fragment.app.viewModels import androidx.navigation.NavController -import androidx.navigation.Navigation import com.kakao.sdk.user.UserApiClient import com.toyou.toyouandroid.R import com.toyou.toyouandroid.databinding.FragmentMypageBinding import com.toyou.toyouandroid.presentation.base.MainActivity import com.toyou.toyouandroid.presentation.fragment.onboarding.SignupNicknameViewModel -import com.toyou.toyouandroid.data.onboarding.service.AuthService -import com.toyou.toyouandroid.domain.home.repository.HomeRepository -import com.toyou.toyouandroid.network.AuthNetworkModule -import com.toyou.toyouandroid.network.NetworkModule -import com.toyou.toyouandroid.presentation.viewmodel.AuthViewModelFactory import com.toyou.toyouandroid.presentation.fragment.home.HomeViewModel -import com.toyou.toyouandroid.presentation.fragment.record.CalendarDialog import com.toyou.toyouandroid.presentation.fragment.record.CalendarDialogViewModel -import com.toyou.toyouandroid.presentation.viewmodel.HomeViewModelFactory -import com.toyou.toyouandroid.utils.TokenManager import com.toyou.toyouandroid.presentation.viewmodel.ViewModelManager -import com.toyou.toyouandroid.utils.TokenStorage import com.toyou.toyouandroid.utils.TutorialStorage +import dagger.hilt.android.AndroidEntryPoint import timber.log.Timber +import androidx.navigation.findNavController +import androidx.core.net.toUri +@AndroidEntryPoint class MypageFragment : Fragment() { private lateinit var navController: NavController @@ -50,20 +43,12 @@ class MypageFragment : Fragment() { private val calendarDialogViewModel: CalendarDialogViewModel by activityViewModels() private var mypageDialog: MypageDialog? = null private var myPageLogoutDialog: MyPageLogoutDialog? = null - private var calendarDialog: CalendarDialog? = null - private lateinit var homeViewModel: HomeViewModel + private val homeViewModel: HomeViewModel by viewModels() private var sharedPreferences: SharedPreferences? = null - private val mypageViewModel: MypageViewModel by activityViewModels { - val tokenStorage = TokenStorage(requireContext()) - val authService: AuthService = NetworkModule.getClient().create(AuthService::class.java) - val tokenManager = TokenManager(authService, tokenStorage) - - val authService2: AuthService = AuthNetworkModule.getClient().create(AuthService::class.java) - AuthViewModelFactory(authService2, tokenStorage, tokenManager) - } + private val mypageViewModel: MypageViewModel by viewModels() override fun onCreateView( inflater: LayoutInflater, @@ -72,15 +57,7 @@ class MypageFragment : Fragment() { ): View { _binding = FragmentMypageBinding.inflate(inflater, container, false) - val tokenStorage = TokenStorage(requireContext()) - val authService: AuthService = NetworkModule.getClient().create(AuthService::class.java) - val tokenManager = TokenManager(authService, tokenStorage) - val homeRepository = HomeRepository() - - homeViewModel = ViewModelProvider( - this, - HomeViewModelFactory(tokenManager, homeRepository) - )[HomeViewModel::class.java] + // MypageViewModel과 HomeViewModel은 Hilt로 주입됨 sharedPreferences = requireActivity().getSharedPreferences("FCM_PREFERENCES", Context.MODE_PRIVATE) @@ -89,7 +66,7 @@ class MypageFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - navController = Navigation.findNavController(view) + navController = view.findNavController() (requireActivity() as MainActivity).hideBottomNavigation(false) @@ -97,7 +74,7 @@ class MypageFragment : Fragment() { mypageViewModel.updateMypage() binding.mypageProfileBtn.setOnClickListener { - navController.navigate(R.id.action_navigation_mypage_to_profile_fragment) // 프로필 화면으로 이동 + navController.navigate(R.id.action_navigation_mypage_to_profile_fragment) } binding.mypageNoticeSetting.setOnClickListener { @@ -128,27 +105,25 @@ class MypageFragment : Fragment() { redirectLink("https://sumptuous-metacarpal-d3a.notion.site/1437c09ca64e80fb88f6d8ab881ffee3") } - // 사용자 닉네임 설정 - nicknameViewModel.nickname.observe(viewLifecycleOwner) { nickname -> - binding.profileNickname.text = nickname - } + mypageViewModel.uiState.observe(viewLifecycleOwner) { uiState -> + uiState.nickname?.let { nickname -> + binding.profileNickname.text = nickname + } - // 사용자 친구 수 설정 - mypageViewModel.friendNum.observe(viewLifecycleOwner) {friendNum -> - val friendText = if (friendNum != null) { - "친구 ${friendNum}명" + val friendText = if (uiState.friendNum != null) { + "친구 ${uiState.friendNum}명" } else { "친구 0명" } binding.profileFriendCount.text = friendText } - // 닉네임 변경시 프로필에 반영 - mypageViewModel.nickname.observe(viewLifecycleOwner) { nickname -> - binding.profileNickname.text = nickname + nicknameViewModel.nickname.observe(viewLifecycleOwner) { nickname -> + if (mypageViewModel.uiState.value?.nickname == null) { + binding.profileNickname.text = nickname + } } - // 로그아웃 성공시 로그인 화면으로 이동 mypageViewModel.logoutSuccess.observe(viewLifecycleOwner) { isSuccess -> if (isSuccess) { mypageViewModel.setLogoutSuccess(false) @@ -160,7 +135,6 @@ class MypageFragment : Fragment() { } } - // 회원 탈퇴 성공시 로그인 화면으로 이동 mypageViewModel.signOutSuccess.observe(viewLifecycleOwner) { isSuccess -> if (isSuccess) { mypageViewModel.setSignOutSuccess(false) @@ -201,7 +175,6 @@ class MypageFragment : Fragment() { } } - // 회원 탈퇴 private fun handleSignout() { Timber.tag("handleSignout").d("handleSignout") @@ -214,7 +187,6 @@ class MypageFragment : Fragment() { } } - // 회원 탈퇴 후 튜토리얼 다시 보이도록 설정 TutorialStorage(requireContext()).setTutorialNotShown() sharedPreferences?.edit()?.putBoolean("isSubscribed", true)?.apply() @@ -222,7 +194,6 @@ class MypageFragment : Fragment() { mypageDialog?.dismiss() } - // 회원 로그아웃 private fun handleLogout() { Timber.tag("handleLogout").d("handleWithdraw") @@ -251,7 +222,7 @@ class MypageFragment : Fragment() { private fun redirectLink(uri: String) { val i = Intent(Intent.ACTION_VIEW) - i.data = Uri.parse(uri) + i.data = uri.toUri() startActivity(i) } override fun onDestroyView() { diff --git a/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/mypage/MypageUiState.kt b/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/mypage/MypageUiState.kt new file mode 100644 index 00000000..364adc4e --- /dev/null +++ b/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/mypage/MypageUiState.kt @@ -0,0 +1,9 @@ +package com.toyou.toyouandroid.presentation.fragment.mypage + +data class MypageUiState( + val userId: Int? = null, + val nickname: String? = null, + val status: String? = null, + val friendNum: Int? = null, + val isLoading: Boolean = false +) \ No newline at end of file diff --git a/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/mypage/MypageViewModel.kt b/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/mypage/MypageViewModel.kt index df4bfbdb..ad1f4a2f 100644 --- a/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/mypage/MypageViewModel.kt +++ b/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/mypage/MypageViewModel.kt @@ -4,86 +4,85 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.toyou.toyouandroid.network.AuthNetworkModule -import com.toyou.toyouandroid.data.mypage.dto.MypageResponse import com.toyou.toyouandroid.data.mypage.service.MypageService -import com.toyou.toyouandroid.data.onboarding.dto.response.SignUpResponse import com.toyou.toyouandroid.data.onboarding.service.AuthService import com.toyou.toyouandroid.utils.TokenManager import com.toyou.toyouandroid.utils.TokenStorage +import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch -import retrofit2.Call -import retrofit2.Callback -import retrofit2.Response import timber.log.Timber +import javax.inject.Inject -class MypageViewModel( +@HiltViewModel +class MypageViewModel @Inject constructor( private val authService: AuthService, + private val mypageService: MypageService, private val tokenStorage: TokenStorage, private val tokenManager: TokenManager ) : ViewModel() { + private val _uiState = MutableLiveData(MypageUiState()) + val uiState: LiveData get() = _uiState + private val _logoutSuccess = MutableLiveData() val logoutSuccess: LiveData get() = _logoutSuccess + private val _signOutSuccess = MutableLiveData() + val signOutSuccess: LiveData get() = _signOutSuccess + fun setLogoutSuccess(value: Boolean) { _logoutSuccess.value = value } + fun setSignOutSuccess(value: Boolean) { + _signOutSuccess.value = value + } + fun kakaoLogout() { viewModelScope.launch { val refreshToken = tokenStorage.getRefreshToken().toString() val accessToken = tokenStorage.getAccessToken().toString() - Timber.d("Attempting to logout in with refresh token: $refreshToken") + Timber.d("Attempting to logout with refresh token: $refreshToken") Timber.d("accessToken: $accessToken") - authService.logout(refreshToken).enqueue(object : Callback { - override fun onResponse(call: Call, response: Response) { - if (response.isSuccessful) { - Timber.i("Logout successfully") - _logoutSuccess.value = true - tokenStorage.clearTokens() + try { + val response = authService.logoutSuspend(refreshToken) + if (response.isSuccessful) { + Timber.i("Logout successfully") + _logoutSuccess.value = true + tokenStorage.clearTokens() + } else { + val errorMessage = response.errorBody()?.string() ?: "Unknown error: ${response.message()}" + Timber.e("API Error: $errorMessage") + + if (response.code() == 401) { + tokenManager.refreshToken( + onSuccess = { kakaoLogout() }, + onFailure = { + Timber.e("Failed to refresh token and kakao logout") + _logoutSuccess.value = false + } + ) } else { - val errorMessage = response.errorBody()?.string() ?: "Unknown error: ${response.message()}" - Timber.e("API Error: $errorMessage") _logoutSuccess.value = false - - // 토큰 만료 시 토큰 갱신 후 로그아웃 재시도 - if (response.code() == 401) { - tokenManager.refreshToken( - onSuccess = { kakaoLogout() }, // 토큰 갱신 후 로그아웃 재시도 - onFailure = { Timber.e("Failed to refresh token and kakao logout") } - ) - } else { - _logoutSuccess.value = false - } } } - - override fun onFailure(call: Call, t: Throwable) { - val errorMessage = t.message ?: "Unknown error" - Timber.e("Network Failure: $errorMessage") - _logoutSuccess.value = false - } - }) + } catch (e: Exception) { + Timber.e("Network Failure: ${e.message}") + _logoutSuccess.value = false + } } } - private val _signOutSuccess = MutableLiveData() - val signOutSuccess: LiveData get() = _signOutSuccess - - fun setSignOutSuccess(value: Boolean) { - _signOutSuccess.value = value - } - fun kakaoSignOut() { - val refreshToken = tokenStorage.getRefreshToken().toString() - val accessToken = tokenStorage.getAccessToken().toString() - Timber.d("Attempting to signout in with refresh token: $refreshToken") - Timber.d("accessToken: $accessToken") + viewModelScope.launch { + val refreshToken = tokenStorage.getRefreshToken().toString() + val accessToken = tokenStorage.getAccessToken().toString() + Timber.d("Attempting to signout with refresh token: $refreshToken") + Timber.d("accessToken: $accessToken") - authService.signOut(refreshToken).enqueue(object : Callback { - override fun onResponse(call: Call, response: Response) { + try { + val response = authService.signOutSuspend(refreshToken) if (response.isSuccessful) { Timber.i("SignOut successfully") _signOutSuccess.value = true @@ -91,70 +90,52 @@ class MypageViewModel( } else { val errorMessage = response.errorBody()?.string() ?: "Unknown error" Timber.e("API Error: $errorMessage") - _signOutSuccess.value = false tokenManager.refreshToken( onSuccess = { kakaoSignOut() }, - onFailure = { Timber.e("Failed to refresh token and kakao signout") } + onFailure = { + Timber.e("Failed to refresh token and kakao signout") + _signOutSuccess.value = false + } ) } - } - - override fun onFailure(call: Call, t: Throwable) { - val errorMessage = t.message ?: "Unknown error" - Timber.e("Network Failure: $errorMessage") + } catch (e: Exception) { + Timber.e("Network Failure: ${e.message}") _signOutSuccess.value = false } - }) + } } - private val myPageService: MypageService = AuthNetworkModule.getClient().create(MypageService::class.java) - - private val _friendNum = MutableLiveData() - val friendNum: LiveData get() = _friendNum - - private val _userId = MutableLiveData() - val userId: LiveData get() = _userId - - private val _nickname = MutableLiveData() - val nickname: LiveData get() = _nickname - - private val _status = MutableLiveData() - val status: LiveData get() = _status - fun updateMypage() { - val call = myPageService.getMypage() - - call.enqueue(object : Callback { - override fun onResponse( - call: Call, - response: Response - ) { + viewModelScope.launch { + _uiState.value = _uiState.value?.copy(isLoading = true) + try { + val response = mypageService.getMypageSuspend() if (response.isSuccessful) { - val userId = response.body()?.result?.userId - val nickname = response.body()?.result?.nickname - val friendNumber = response.body()?.result?.friendNum - val status = response.body()?.result?.status - - _userId.postValue(userId) - _friendNum.postValue(friendNumber) - _nickname.postValue(nickname) - _status.postValue(status) - - Timber.tag("updateFriendNum").d("FriendNum updated: $friendNumber") - Timber.tag("updateStatus").d("Status updated: $status") + response.body()?.result?.let { result -> + _uiState.value = MypageUiState( + userId = result.userId, + nickname = result.nickname, + status = result.status, + friendNum = result.friendNum, + isLoading = false + ) + Timber.tag("updateMypage").d("Mypage updated: $result") + } } else { - Timber.tag("API Error").e("Failed to update FriendNum. Code: ${response.code()}, Message: ${response.message()}") - Timber.tag("API Error").e("Response Body: ${response.errorBody()?.string()}") + Timber.tag("API Error").e("Failed to update Mypage. Code: ${response.code()}, Message: ${response.message()}") + _uiState.value = _uiState.value?.copy(isLoading = false) tokenManager.refreshToken( - onSuccess = { updateMypage() }, // 토큰 갱신 후 다시 요청 - onFailure = { Timber.e("Failed to refresh token and get notices") } + onSuccess = { updateMypage() }, + onFailure = { + Timber.e("Failed to refresh token and get mypage") + _uiState.value = _uiState.value?.copy(isLoading = false) + } ) } + } catch (e: Exception) { + Timber.tag("API Failure").e(e, "Error occurred during API call") + _uiState.value = _uiState.value?.copy(isLoading = false) } - - override fun onFailure(call: Call, t: Throwable) { - Timber.tag("API Failure").e(t, "Error occurred during API call") - } - }) + } } } \ No newline at end of file From 0733af2d5b1e5e5fa8a4762ac8c46cf94c205a66 Mon Sep 17 00:00:00 2001 From: Daemon Jung Date: Mon, 15 Sep 2025 17:20:29 +0900 Subject: [PATCH 07/11] Refactor: Apply MVVM and DI to profile screen - Add `ProfileRepository` to abstract data sources. - Introduce `ProfileUiState` to manage UI state in the `ProfileViewModel`. - Refactor `ProfileViewModel` to use Hilt for dependency injection, `viewModelScope` for coroutines, and the new `ProfileRepository`. - Convert `ProfileFragment` to use Hilt (`@AndroidEntryPoint`) for ViewModel injection. - Remove manual ViewModel factory instantiation. - Update API calls to be asynchronous using coroutines instead of Retrofit Callbacks. --- .../toyou/toyouandroid/di/RepositoryModule.kt | 22 + .../profile/repository/ProfileRepository.kt | 24 ++ .../fragment/mypage/ProfileFragment.kt | 231 +++++----- .../fragment/mypage/ProfileUiState.kt | 20 + .../fragment/mypage/ProfileViewModel.kt | 408 ++++++++---------- 5 files changed, 353 insertions(+), 352 deletions(-) create mode 100644 app/src/main/java/com/toyou/toyouandroid/di/RepositoryModule.kt create mode 100644 app/src/main/java/com/toyou/toyouandroid/domain/profile/repository/ProfileRepository.kt create mode 100644 app/src/main/java/com/toyou/toyouandroid/presentation/fragment/mypage/ProfileUiState.kt diff --git a/app/src/main/java/com/toyou/toyouandroid/di/RepositoryModule.kt b/app/src/main/java/com/toyou/toyouandroid/di/RepositoryModule.kt new file mode 100644 index 00000000..934e14fe --- /dev/null +++ b/app/src/main/java/com/toyou/toyouandroid/di/RepositoryModule.kt @@ -0,0 +1,22 @@ +package com.toyou.toyouandroid.di + +import com.toyou.toyouandroid.data.onboarding.service.OnboardingService +import com.toyou.toyouandroid.domain.profile.repository.ProfileRepository +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object RepositoryModule { + + @Provides + @Singleton + fun provideProfileRepository( + onboardingService: OnboardingService + ): ProfileRepository { + return ProfileRepository(onboardingService) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/toyou/toyouandroid/domain/profile/repository/ProfileRepository.kt b/app/src/main/java/com/toyou/toyouandroid/domain/profile/repository/ProfileRepository.kt new file mode 100644 index 00000000..e72d9ad5 --- /dev/null +++ b/app/src/main/java/com/toyou/toyouandroid/domain/profile/repository/ProfileRepository.kt @@ -0,0 +1,24 @@ +package com.toyou.toyouandroid.domain.profile.repository + +import com.toyou.toyouandroid.data.onboarding.dto.NicknameCheckResponse +import com.toyou.toyouandroid.data.onboarding.dto.PatchNicknameRequest +import com.toyou.toyouandroid.data.onboarding.dto.PatchNicknameResponse +import com.toyou.toyouandroid.data.onboarding.dto.PatchStatusRequest +import com.toyou.toyouandroid.data.onboarding.service.OnboardingService +import retrofit2.Response + +class ProfileRepository( + private val onboardingService: OnboardingService +) { + suspend fun checkNickname(nickname: String, userId: Int): Response { + return onboardingService.getNicknameCheckSuspend(nickname, userId) + } + + suspend fun updateNickname(nickname: String): Response { + return onboardingService.patchNicknameSuspend(PatchNicknameRequest(nickname)) + } + + suspend fun updateStatus(status: String): Response { + return onboardingService.patchStatusSuspend(PatchStatusRequest(status)) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/mypage/ProfileFragment.kt b/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/mypage/ProfileFragment.kt index bd1934ee..bc93ee03 100644 --- a/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/mypage/ProfileFragment.kt +++ b/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/mypage/ProfileFragment.kt @@ -11,6 +11,7 @@ import android.view.inputmethod.InputMethodManager import android.widget.Toast import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels +import androidx.fragment.app.viewModels import androidx.lifecycle.ViewModelProvider import androidx.navigation.NavController import androidx.navigation.fragment.findNavController @@ -23,14 +24,14 @@ import com.toyou.toyouandroid.domain.create.repository.CreateRepository import com.toyou.toyouandroid.domain.home.repository.HomeRepository import com.toyou.toyouandroid.network.AuthNetworkModule import com.toyou.toyouandroid.network.NetworkModule -import com.toyou.toyouandroid.presentation.viewmodel.AuthViewModelFactory -import com.toyou.toyouandroid.presentation.viewmodel.HomeViewModelFactory import com.toyou.toyouandroid.presentation.viewmodel.UserViewModel import com.toyou.toyouandroid.presentation.viewmodel.UserViewModelFactory import com.toyou.toyouandroid.utils.TokenManager import com.toyou.toyouandroid.utils.TokenStorage +import dagger.hilt.android.AndroidEntryPoint import timber.log.Timber +@AndroidEntryPoint class ProfileFragment : Fragment() { private lateinit var navController: NavController @@ -38,18 +39,11 @@ class ProfileFragment : Fragment() { private val binding: FragmentProfileBinding get() = requireNotNull(_binding){"FragmentProfileBinding -> null"} - private lateinit var viewModel: ProfileViewModel + private val viewModel: ProfileViewModel by viewModels() private lateinit var userViewModel: UserViewModel - private val mypageViewModel: MypageViewModel by activityViewModels { - val tokenStorage = TokenStorage(requireContext()) - val authService: AuthService = NetworkModule.getClient().create(AuthService::class.java) - val tokenManager = TokenManager(authService, tokenStorage) - - val authService2: AuthService = AuthNetworkModule.getClient().create(AuthService::class.java) - AuthViewModelFactory(authService2, tokenStorage, tokenManager) - } + private val mypageViewModel: MypageViewModel by viewModels() override fun onCreateView( inflater: LayoutInflater, @@ -64,15 +58,8 @@ class ProfileFragment : Fragment() { val tokenManager = TokenManager(authService, tokenStorage) val createService = AuthNetworkModule.getClient().create(CreateService::class.java) val createRepository = CreateRepository(createService) - val homeRepository = HomeRepository() - - viewModel = ViewModelProvider( - this, - HomeViewModelFactory( - tokenManager, homeRepository - ) - )[ProfileViewModel::class.java] + // ProfileViewModel은 Hilt로 주입됨 userViewModel = ViewModelProvider( requireActivity(), @@ -95,46 +82,46 @@ class ProfileFragment : Fragment() { // navController 초기화 navController = findNavController() - // 닉네임 변경시 기존 닉네임 불러오기 - mypageViewModel.nickname.observe(viewLifecycleOwner) { nickname -> - binding.signupNicknameInput.setText(nickname) - } - - // 상태에 따른 배경 변경 - mypageViewModel.status.observe(viewLifecycleOwner) { status -> - Timber.d("Status changed: $status") // 상태 변경 시 로그 출력 - - when (status) { - "SCHOOL" -> { - Timber.d("Setting background for SCHOOL status") // SCHOOL 상태에 맞는 배경 설정 전 로그 - binding.signupStatusOption1.setBackgroundResource(R.drawable.signupnickname_doublecheck_activate) - binding.signupStatusOption2.setBackgroundResource(R.drawable.signupnickname_input) - binding.signupStatusOption3.setBackgroundResource(R.drawable.signupnickname_input) - binding.signupStatusOption4.setBackgroundResource(R.drawable.signupnickname_input) - } - "COLLEGE" -> { - Timber.d("Setting background for COLLEGE status") // COLLEGE 상태에 맞는 배경 설정 전 로그 - binding.signupStatusOption1.setBackgroundResource(R.drawable.signupnickname_input) - binding.signupStatusOption2.setBackgroundResource(R.drawable.signupnickname_doublecheck_activate) - binding.signupStatusOption3.setBackgroundResource(R.drawable.signupnickname_input) - binding.signupStatusOption4.setBackgroundResource(R.drawable.signupnickname_input) - } - "OFFICE" -> { - Timber.d("Setting background for OFFICE status") // OFFICE 상태에 맞는 배경 설정 전 로그 - binding.signupStatusOption1.setBackgroundResource(R.drawable.signupnickname_input) - binding.signupStatusOption2.setBackgroundResource(R.drawable.signupnickname_input) - binding.signupStatusOption3.setBackgroundResource(R.drawable.signupnickname_doublecheck_activate) - binding.signupStatusOption4.setBackgroundResource(R.drawable.signupnickname_input) - } - "ETC" -> { - Timber.d("Setting background for ETC status") // ETC 상태에 맞는 배경 설정 전 로그 - binding.signupStatusOption1.setBackgroundResource(R.drawable.signupnickname_input) - binding.signupStatusOption2.setBackgroundResource(R.drawable.signupnickname_input) - binding.signupStatusOption3.setBackgroundResource(R.drawable.signupnickname_input) - binding.signupStatusOption4.setBackgroundResource(R.drawable.signupnickname_doublecheck_activate) - } - } - } +// // 닉네임 변경시 기존 닉네임 불러오기 +// mypageViewModel.nickname.observe(viewLifecycleOwner) { nickname -> +// binding.signupNicknameInput.setText(nickname) +// } +// +// // 상태에 따른 배경 변경 +// mypageViewModel.status.observe(viewLifecycleOwner) { status -> +// Timber.d("Status changed: $status") // 상태 변경 시 로그 출력 +// +// when (status) { +// "SCHOOL" -> { +// Timber.d("Setting background for SCHOOL status") // SCHOOL 상태에 맞는 배경 설정 전 로그 +// binding.signupStatusOption1.setBackgroundResource(R.drawable.signupnickname_doublecheck_activate) +// binding.signupStatusOption2.setBackgroundResource(R.drawable.signupnickname_input) +// binding.signupStatusOption3.setBackgroundResource(R.drawable.signupnickname_input) +// binding.signupStatusOption4.setBackgroundResource(R.drawable.signupnickname_input) +// } +// "COLLEGE" -> { +// Timber.d("Setting background for COLLEGE status") // COLLEGE 상태에 맞는 배경 설정 전 로그 +// binding.signupStatusOption1.setBackgroundResource(R.drawable.signupnickname_input) +// binding.signupStatusOption2.setBackgroundResource(R.drawable.signupnickname_doublecheck_activate) +// binding.signupStatusOption3.setBackgroundResource(R.drawable.signupnickname_input) +// binding.signupStatusOption4.setBackgroundResource(R.drawable.signupnickname_input) +// } +// "OFFICE" -> { +// Timber.d("Setting background for OFFICE status") // OFFICE 상태에 맞는 배경 설정 전 로그 +// binding.signupStatusOption1.setBackgroundResource(R.drawable.signupnickname_input) +// binding.signupStatusOption2.setBackgroundResource(R.drawable.signupnickname_input) +// binding.signupStatusOption3.setBackgroundResource(R.drawable.signupnickname_doublecheck_activate) +// binding.signupStatusOption4.setBackgroundResource(R.drawable.signupnickname_input) +// } +// "ETC" -> { +// Timber.d("Setting background for ETC status") // ETC 상태에 맞는 배경 설정 전 로그 +// binding.signupStatusOption1.setBackgroundResource(R.drawable.signupnickname_input) +// binding.signupStatusOption2.setBackgroundResource(R.drawable.signupnickname_input) +// binding.signupStatusOption3.setBackgroundResource(R.drawable.signupnickname_input) +// binding.signupStatusOption4.setBackgroundResource(R.drawable.signupnickname_doublecheck_activate) +// } +// } +// } // 이전 버튼 binding.signupNicknameBackLayout.setOnClickListener { @@ -167,75 +154,75 @@ class ProfileFragment : Fragment() { Toast.makeText(requireContext(), "프로필 수정이 완료되었습니다", Toast.LENGTH_SHORT).show() } - // 닉네임 중복 확인 - binding.signupAgreeNicknameDoublecheckBtn.setOnClickListener { - mypageViewModel.nickname.observe(viewLifecycleOwner) { nickname -> - mypageViewModel.userId.observe(viewLifecycleOwner) { userId -> - if (nickname != null && userId != null) { - viewModel.checkDuplicate(nickname, userId) - } - } - } - hideKeyboard() - } - - val buttonList = listOf( - binding.signupStatusOption1, - binding.signupStatusOption2, - binding.signupStatusOption3, - binding.signupStatusOption4 - ) - - buttonList.forEach { button -> - button.setOnClickListener { - viewModel.onButtonClicked(button.id) - updateButtonBackgrounds() - } - } - - viewModel.selectedStatusButtonId.observe(viewLifecycleOwner) { id -> - id?.let { - updateButtonBackgrounds() - } - } +// // 닉네임 중복 확인 +// binding.signupAgreeNicknameDoublecheckBtn.setOnClickListener { +// mypageViewModel.nickname.observe(viewLifecycleOwner) { nickname -> +// mypageViewModel.userId.observe(viewLifecycleOwner) { userId -> +// if (nickname != null && userId != null) { +// viewModel.checkDuplicate(nickname, userId) +// } +// } +// } +// hideKeyboard() +// } +// +// val buttonList = listOf( +// binding.signupStatusOption1, +// binding.signupStatusOption2, +// binding.signupStatusOption3, +// binding.signupStatusOption4 +// ) +// +// buttonList.forEach { button -> +// button.setOnClickListener { +// viewModel.onButtonClicked(button.id) +// updateButtonBackgrounds() +// } +// } + +// viewModel.selectedStatusButtonId.observe(viewLifecycleOwner) { id -> +// id?.let { +// updateButtonBackgrounds() +// } +// } // 완료 버튼 활성화 viewModel.isNextButtonEnabled.observe(viewLifecycleOwner) { isEnabled -> binding.signupNicknameBtn.isEnabled = isEnabled } - binding.root.setOnTouchListener { v, event -> - if (event.action == MotionEvent.ACTION_DOWN) { - hideKeyboard() - v.performClick() - } - false - } - binding.root.setOnClickListener { - hideKeyboard() - } +// binding.root.setOnTouchListener { v, event -> +// if (event.action == MotionEvent.ACTION_DOWN) { +// hideKeyboard() +// v.performClick() +// } +// false +// } +// binding.root.setOnClickListener { +// hideKeyboard() +// } } - private fun updateButtonBackgrounds() { - val buttonList = listOf( - binding.signupStatusOption1, - binding.signupStatusOption2, - binding.signupStatusOption3, - binding.signupStatusOption4 - ) - buttonList.forEach { button -> - button.setBackgroundResource(viewModel.getButtonBackground(button.id)) - } - } - - private fun hideKeyboard() { - val imm = requireActivity().getSystemService(InputMethodManager::class.java) - imm.hideSoftInputFromWindow(view?.windowToken, 0) - } - - override fun onDestroyView() { - super.onDestroyView() - viewModel.setSelectedStatusButtonId(null) - _binding = null - } +// private fun updateButtonBackgrounds() { +// val buttonList = listOf( +// binding.signupStatusOption1, +// binding.signupStatusOption2, +// binding.signupStatusOption3, +// binding.signupStatusOption4 +// ) +// buttonList.forEach { button -> +// button.setBackgroundResource(viewModel.getButtonBackground(button.id)) +// } +// } +// +// private fun hideKeyboard() { +// val imm = requireActivity().getSystemService(InputMethodManager::class.java) +// imm.hideSoftInputFromWindow(view?.windowToken, 0) +// } +// +// override fun onDestroyView() { +// super.onDestroyView() +// viewModel.setSelectedStatusButtonId(null) +// _binding = null +// } } \ No newline at end of file diff --git a/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/mypage/ProfileUiState.kt b/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/mypage/ProfileUiState.kt new file mode 100644 index 00000000..8cde6d90 --- /dev/null +++ b/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/mypage/ProfileUiState.kt @@ -0,0 +1,20 @@ +package com.toyou.toyouandroid.presentation.fragment.mypage + +data class ProfileUiState( + val title: String = "회원가입", + val textCount: String = "0/15", + val nickname: String = "", + val status: String = "", + val isDuplicateCheckEnabled: Boolean = false, + val isNextButtonEnabled: Boolean = false, + val duplicateCheckMessage: String = "중복된 닉네임인지 확인해주세요", + val isNicknameValid: Boolean = false, + val selectedStatusType: StatusType? = null +) + +enum class StatusType(val value: String) { + SCHOOL("SCHOOL"), + COLLEGE("COLLEGE"), + OFFICE("OFFICE"), + ETC("ETC") +} \ No newline at end of file diff --git a/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/mypage/ProfileViewModel.kt b/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/mypage/ProfileViewModel.kt index 75695a3e..fa9a5ac3 100644 --- a/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/mypage/ProfileViewModel.kt +++ b/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/mypage/ProfileViewModel.kt @@ -3,274 +3,222 @@ package com.toyou.toyouandroid.presentation.fragment.mypage import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import com.toyou.toyouandroid.R -import com.toyou.toyouandroid.network.AuthNetworkModule -import com.toyou.toyouandroid.data.onboarding.dto.NicknameCheckResponse -import com.toyou.toyouandroid.data.onboarding.service.OnboardingService -import com.toyou.toyouandroid.data.onboarding.dto.PatchNicknameRequest -import com.toyou.toyouandroid.data.onboarding.dto.PatchNicknameResponse -import com.toyou.toyouandroid.data.onboarding.dto.PatchStatusRequest +import androidx.lifecycle.viewModelScope +import com.toyou.toyouandroid.domain.profile.repository.ProfileRepository import com.toyou.toyouandroid.utils.TokenManager -import retrofit2.Call -import retrofit2.Callback -import retrofit2.Response +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch import timber.log.Timber - -class ProfileViewModel(private val tokenManager: TokenManager) : ViewModel() { - private val _title = MutableLiveData() - val title: LiveData get() = _title - - private val _textCount = MutableLiveData("0/15") - val textCount: LiveData get() = _textCount - - fun updateTextCount(count: Int) { - _textCount.value = "($count/15)" - } - - private val inputText = MutableLiveData() - - private val _isDuplicateCheckEnabled = MutableLiveData(false) - val isDuplicateCheckEnabled: LiveData = _isDuplicateCheckEnabled - - private val _duplicateCheckMessage = MutableLiveData().apply { - value = "중복된 닉네임인지 확인해주세요" - } - val duplicateCheckMessage: LiveData = _duplicateCheckMessage - - private val _duplicateCheckMessageColor = MutableLiveData().apply { - value = 0xFF000000.toInt() - } - val duplicateCheckMessageColor: LiveData = _duplicateCheckMessageColor - - private val _duplicateCheckButtonTextColor = MutableLiveData().apply { - value = 0xFFA6A6A6.toInt() - } - val duplicateCheckButtonTextColor: LiveData = _duplicateCheckButtonTextColor - - private val _duplicateCheckButtonBackground = MutableLiveData().apply { - value = R.drawable.next_button - } - val duplicateCheckButtonBackground: LiveData = _duplicateCheckButtonBackground - - - private val _isNextButtonEnabled = MutableLiveData(false) - val isNextButtonEnabled: LiveData = _isNextButtonEnabled - - private val _nextButtonTextColor = MutableLiveData().apply { - value = 0xFFA6A6A6.toInt() +import javax.inject.Inject + +@HiltViewModel +class ProfileViewModel @Inject constructor( + private val profileRepository: ProfileRepository, + private val tokenManager: TokenManager +) : ViewModel() { + + private val _uiState = MutableLiveData(ProfileUiState()) + val uiState: LiveData get() = _uiState + + // 데이터 바인딩 호환성을 위한 프로퍼티 + val textCount: LiveData = MutableLiveData("0/15") + val nickname: LiveData = MutableLiveData() + val duplicateCheckButtonTextColor: LiveData = MutableLiveData(0xFFA6A6A6.toInt()) + val duplicateCheckButtonBackground: LiveData = MutableLiveData(com.toyou.toyouandroid.R.drawable.next_button) + val duplicateCheckMessage: LiveData = MutableLiveData("중복된 닉네임인지 확인해주세요") + val duplicateCheckMessageColor: LiveData = MutableLiveData(0xFF000000.toInt()) + val nextButtonBackground: LiveData = MutableLiveData(com.toyou.toyouandroid.R.drawable.next_button) + val nextButtonTextColor: LiveData = MutableLiveData(0xFFA6A6A6.toInt()) + val isNextButtonEnabled: LiveData = MutableLiveData(false) + + private val _nicknameChangedSuccess = MutableLiveData() + val nicknameChangedSuccess: LiveData get() = _nicknameChangedSuccess + + private val _duplicateCheckMessageType = MutableLiveData() + val duplicateCheckMessageType: LiveData get() = _duplicateCheckMessageType + + init { + _uiState.value = ProfileUiState(title = "회원가입") } - val nextButtonTextColor: LiveData = _nextButtonTextColor - - private val _nextButtonBackground = MutableLiveData().apply { - value = R.drawable.next_button + + fun updateTextCount(count: Int) { + val countText = "($count/15)" + _uiState.value = _uiState.value?.copy( + textCount = countText + ) + (textCount as MutableLiveData).value = countText } - val nextButtonBackground: LiveData = _nextButtonBackground - - private val _nickname = MutableLiveData() - val nickname: LiveData get() = _nickname - - fun duplicateBtnActivate() { - _duplicateCheckButtonTextColor.value = 0xFF000000.toInt() - _duplicateCheckButtonBackground.value = R.drawable.signupnickname_doublecheck_activate + + fun setNickname(newNickname: String) { + _uiState.value = _uiState.value?.copy( + nickname = newNickname, + isDuplicateCheckEnabled = newNickname.isNotEmpty() + ) + (nickname as MutableLiveData).value = newNickname } - + fun updateLength15(length: Int) { - if (length >= 15) { - _duplicateCheckMessage.value = "15자 이내로 입력해주세요." - _duplicateCheckMessageColor.value = 0xFF000000.toInt() + val messageType = if (length >= 15) { + DuplicateCheckMessageType.LENGTH_EXCEEDED } else { - _duplicateCheckMessage.value = "중복된 닉네임인지 확인해주세요" - _duplicateCheckMessageColor.value = 0xFF000000.toInt() + DuplicateCheckMessageType.CHECK_REQUIRED } + _duplicateCheckMessageType.value = messageType + _uiState.value = _uiState.value?.copy( + duplicateCheckMessage = messageType.message + ) } - - fun setNickname(newNickname: String) { - _nickname.value = newNickname + + fun duplicateBtnActivate() { + _uiState.value = _uiState.value?.copy( + isDuplicateCheckEnabled = true + ) + (duplicateCheckButtonTextColor as MutableLiveData).value = 0xFF000000.toInt() + (duplicateCheckButtonBackground as MutableLiveData).value = com.toyou.toyouandroid.R.drawable.signupnickname_doublecheck_activate } - + fun resetNicknameEditState() { - _duplicateCheckMessage.value = "중복된 닉네임인지 확인해주세요" - _duplicateCheckMessageColor.value = 0xFF000000.toInt() - _isNextButtonEnabled.value = false - _nextButtonTextColor.value = 0xFFA6A6A6.toInt() - _nextButtonBackground.value = R.drawable.next_button - _duplicateCheckButtonTextColor.value = 0xFFA6A6A6.toInt() - _duplicateCheckButtonBackground.value = R.drawable.next_button - } - - fun nextButtonDisable() { - _isNextButtonEnabled.value = false - _nextButtonTextColor.value = 0xFFA6A6A6.toInt() - _nextButtonBackground.value = R.drawable.next_button - } - - private fun nextButtonEnableCheck() { - if (_nicknameValidate.value == true) { - _isNextButtonEnabled.value = true - _nextButtonTextColor.value = 0xFF000000.toInt() - _nextButtonBackground.value = R.drawable.next_button_enabled - } - } - - private val _nicknameValidate = MutableLiveData() - private val nicknameValidate: LiveData get() = _nicknameValidate - - init { - inputText.observeForever { text -> - _isDuplicateCheckEnabled.value = !text.isNullOrEmpty() - } - _title.value = "회원가입" - _nicknameValidate.value = true - } - - private val apiService: OnboardingService = AuthNetworkModule.getClient().create( - OnboardingService::class.java) - - // API를 호출하여 닉네임 중복 체크를 수행하는 함수 + _uiState.value = _uiState.value?.copy( + duplicateCheckMessage = DuplicateCheckMessageType.CHECK_REQUIRED.message, + isNextButtonEnabled = false, + isNicknameValid = false + ) + _duplicateCheckMessageType.value = DuplicateCheckMessageType.CHECK_REQUIRED + } + fun checkDuplicate(userNickname: String, userId: Int) { - val nickname = _nickname.value ?: return - - val call = apiService.getNicknameCheck(nickname, userId) - call.enqueue(object : Callback { - override fun onResponse(call: Call, response: Response) { + val nickname = _uiState.value?.nickname ?: return + + viewModelScope.launch { + try { + val response = profileRepository.checkNickname(nickname, userId) if (response.isSuccessful) { val exists = response.body()?.result?.exists ?: false - if (!exists) { - _duplicateCheckMessage.value = "사용 가능한 닉네임입니다." - _duplicateCheckMessageColor.value = 0xFFEA9797.toInt() - _nicknameValidate.value = true - nextButtonEnableCheck() - } else { - if (userNickname == nickname) { - _duplicateCheckMessage.value = "이미 사용 중인 닉네임입니다." - _duplicateCheckMessageColor.value = 0xFFFF0000.toInt() - _nicknameValidate.value = false - nextButtonEnableCheck() - } else { - _duplicateCheckMessage.value = "이미 사용 중인 닉네임입니다." - _duplicateCheckMessageColor.value = 0xFFFF0000.toInt() - _nicknameValidate.value = false - nextButtonDisable() - nextButtonEnableCheck() - } - } + handleNicknameCheckResult(exists, userNickname, nickname) } else { - _duplicateCheckMessage.value = "닉네임 확인에 실패했습니다." - _duplicateCheckMessageColor.value = 0xFFFF0000.toInt() - _nicknameValidate.value = false - nextButtonDisable() - nextButtonEnableCheck() - + handleNicknameCheckError() tokenManager.refreshToken( - onSuccess = { checkDuplicate(userNickname, userId) }, // 토큰 갱신 후 다시 요청 - onFailure = { Timber.e("Failed to refresh token and get notices") } + onSuccess = { checkDuplicate(userNickname, userId) }, + onFailure = { Timber.e("Failed to refresh token and check nickname") } ) } + } catch (e: Exception) { + Timber.tag("API Failure").e(e, "Error checking nickname") + _duplicateCheckMessageType.value = DuplicateCheckMessageType.SERVER_ERROR + _uiState.value = _uiState.value?.copy( + duplicateCheckMessage = DuplicateCheckMessageType.SERVER_ERROR.message, + isNicknameValid = false, + isNextButtonEnabled = false + ) } - - override fun onFailure(call: Call, t: Throwable) { - Timber.tag("API Failure").e(t, "Error checking nickname") - _duplicateCheckMessage.value = "서버에 연결할 수 없습니다." - _duplicateCheckMessageColor.value = 0xFFFF0000.toInt() - _nicknameValidate.value = false - nextButtonDisable() - nextButtonEnableCheck() - } - }) + } } - - private val _nicknameChangedSuccess = MutableLiveData() - val nicknameChangedSuccess: LiveData get() = _nicknameChangedSuccess - + + private fun handleNicknameCheckResult(exists: Boolean, userNickname: String, nickname: String) { + val messageType = when { + !exists -> DuplicateCheckMessageType.AVAILABLE + userNickname == nickname -> DuplicateCheckMessageType.ALREADY_IN_USE_SAME + else -> DuplicateCheckMessageType.ALREADY_IN_USE + } + + _duplicateCheckMessageType.value = messageType + val isValid = !exists || (userNickname == nickname) + + _uiState.value = _uiState.value?.copy( + duplicateCheckMessage = messageType.message, + isNicknameValid = isValid, + isNextButtonEnabled = isValid + ) + + // 호환성 프로퍼티 업데이트 + (duplicateCheckMessage as MutableLiveData).value = messageType.message + (duplicateCheckMessageColor as MutableLiveData).value = when { + !exists -> 0xFFEA9797.toInt() + else -> 0xFFFF0000.toInt() + } + (isNextButtonEnabled as MutableLiveData).value = isValid + if (isValid) { + (nextButtonTextColor as MutableLiveData).value = 0xFF000000.toInt() + (nextButtonBackground as MutableLiveData).value = com.toyou.toyouandroid.R.drawable.next_button_enabled + } + } + + private fun handleNicknameCheckError() { + _duplicateCheckMessageType.value = DuplicateCheckMessageType.CHECK_FAILED + _uiState.value = _uiState.value?.copy( + duplicateCheckMessage = DuplicateCheckMessageType.CHECK_FAILED.message, + isNicknameValid = false, + isNextButtonEnabled = false + ) + } + fun changeNickname() { - val nickname = _nickname.value ?: return - val request = PatchNicknameRequest(nickname) - val call = apiService.patchNickname(request) - call.enqueue(object : Callback { - override fun onResponse( - call: Call, - response: Response - ) { + val nickname = _uiState.value?.nickname ?: return + + viewModelScope.launch { + try { + val response = profileRepository.updateNickname(nickname) if (response.isSuccessful) { - _duplicateCheckMessage.value = response.body()?.message.toString() - _duplicateCheckMessageColor.value = 0xFFFF0000.toInt() _nicknameChangedSuccess.postValue(true) Timber.tag("changeNickname").d("$response") } else { - _duplicateCheckMessage.value = "닉네임 변경에 실패했습니다." - _duplicateCheckMessageColor.value = 0xFFFF0000.toInt() - Timber.tag("changeNickname").d("$response") - + _duplicateCheckMessageType.value = DuplicateCheckMessageType.UPDATE_FAILED + _uiState.value = _uiState.value?.copy( + duplicateCheckMessage = DuplicateCheckMessageType.UPDATE_FAILED.message + ) tokenManager.refreshToken( - onSuccess = { changeNickname() }, // 토큰 갱신 후 다시 요청 - onFailure = { Timber.e("Failed to refresh token and get notices") } + onSuccess = { changeNickname() }, + onFailure = { Timber.e("Failed to refresh token and update nickname") } ) } + } catch (e: Exception) { + Timber.tag("API Failure").e(e, "Error updating nickname") + _duplicateCheckMessageType.value = DuplicateCheckMessageType.SERVER_ERROR + _uiState.value = _uiState.value?.copy( + duplicateCheckMessage = DuplicateCheckMessageType.SERVER_ERROR.message + ) } - - override fun onFailure(call: Call, t: Throwable) { - Timber.tag("API Failure").e(t, "Error updating nickname") - _duplicateCheckMessage.value = "서버에 연결할 수 없습니다." - _duplicateCheckMessageColor.value = 0xFFFF0000.toInt() - } - }) + } } - + fun changeStatus() { - val status = _status.value ?: return - val request = PatchStatusRequest(status) - val call = apiService.patchStatus(request) - call.enqueue(object : Callback { - override fun onResponse( - call: Call, - response: Response - ) { + val status = _uiState.value?.status ?: return + + viewModelScope.launch { + try { + val response = profileRepository.updateStatus(status) if (response.isSuccessful) { Timber.tag("changeStatus").d("${response.body()}") } else { tokenManager.refreshToken( - onSuccess = { changeStatus() }, // 토큰 갱신 후 다시 요청 - onFailure = { Timber.e("Failed to refresh token and get notices") } + onSuccess = { changeStatus() }, + onFailure = { Timber.e("Failed to refresh token and update status") } ) } + } catch (e: Exception) { + Timber.tag("API Failure").e(e, "Error updating status") } - - override fun onFailure(call: Call, t: Throwable) { - // 에러 처리 로직 - Timber.tag("API Failure").e(t, "Error updating nickname") - } - }) - } - - private val _selectedStatusButtonId = MutableLiveData(null) - val selectedStatusButtonId: LiveData get() = _selectedStatusButtonId - - fun setSelectedStatusButtonId(id: Int?) { - _selectedStatusButtonId.value = id - } - - private val _status = MutableLiveData() - val status: LiveData get() = _status - - fun onButtonClicked(buttonId: Int) { - if (_selectedStatusButtonId.value == buttonId) return - _selectedStatusButtonId.value = buttonId - - when (buttonId) { - R.id.signup_status_option_1 -> _status.value = "SCHOOL" - R.id.signup_status_option_2 -> _status.value = "COLLEGE" - R.id.signup_status_option_3 -> _status.value = "OFFICE" - R.id.signup_status_option_4 -> _status.value = "ETC" - } - - nextButtonEnableCheck() - } - - fun getButtonBackground(buttonId: Int): Int { - return if (_selectedStatusButtonId.value == buttonId) { - R.drawable.signupnickname_doublecheck_activate // 선택된 상태의 배경 - } else { - R.drawable.signupnickname_input // 기본 상태의 배경 } } + + fun onStatusButtonClicked(statusType: StatusType) { + if (_uiState.value?.selectedStatusType == statusType) return + + _uiState.value = _uiState.value?.copy( + selectedStatusType = statusType, + status = statusType.value, + isNextButtonEnabled = true + ) + } +} + +enum class DuplicateCheckMessageType(val message: String) { + CHECK_REQUIRED("중복된 닉네임인지 확인해주세요"), + LENGTH_EXCEEDED("15자 이내로 입력해주세요."), + AVAILABLE("사용 가능한 닉네임입니다."), + ALREADY_IN_USE("이미 사용 중인 닉네임입니다."), + ALREADY_IN_USE_SAME("이미 사용 중인 닉네임입니다."), + CHECK_FAILED("닉네임 확인에 실패했습니다."), + UPDATE_FAILED("닉네임 변경에 실패했습니다."), + SERVER_ERROR("서버에 연결할 수 없습니다.") } \ No newline at end of file From 6f76de0b52674af806a8ac3eba960d9c2c665e4b Mon Sep 17 00:00:00 2001 From: Daemon Jung Date: Mon, 15 Sep 2025 17:20:48 +0900 Subject: [PATCH 08/11] feat: Add Hilt qualifiers for Retrofit Add `@AuthRetrofit` and `@NonAuthRetrofit` qualifiers to distinguish between authenticated and non-authenticated Retrofit instances. --- .../main/java/com/toyou/toyouandroid/di/Qualifiers.kt | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 app/src/main/java/com/toyou/toyouandroid/di/Qualifiers.kt diff --git a/app/src/main/java/com/toyou/toyouandroid/di/Qualifiers.kt b/app/src/main/java/com/toyou/toyouandroid/di/Qualifiers.kt new file mode 100644 index 00000000..7346e51d --- /dev/null +++ b/app/src/main/java/com/toyou/toyouandroid/di/Qualifiers.kt @@ -0,0 +1,11 @@ +package com.toyou.toyouandroid.di + +import javax.inject.Qualifier + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class AuthRetrofit + +@Qualifier +@Retention(AnnotationRetention.BINARY) +annotation class NonAuthRetrofit \ No newline at end of file From 8952bf8702ac23ca53397761527a87b50036d437 Mon Sep 17 00:00:00 2001 From: Daemon Jung Date: Mon, 15 Sep 2025 17:21:19 +0900 Subject: [PATCH 09/11] Refactor: Migrate SignupNicknameViewModel to Hilt and Coroutines - Refactored `SignupNicknameViewModel` to use Hilt for dependency injection. - Replaced direct Retrofit calls with a repository pattern (`ProfileRepository`). - Converted asynchronous API calls from `Callback` to `suspend` functions and `viewModelScope` coroutines. - Introduced `ProfileUiState` to manage the view's state in a more structured way. - Updated `OnboardingService` to include `suspend` versions of network methods. - Modified `SignupNicknameFragment` to use the Hilt-injected ViewModel. --- .../onboarding/service/OnboardingService.kt | 17 ++ .../onboarding/SignupNicknameFragment.kt | 21 +- .../onboarding/SignupNicknameViewModel.kt | 245 ++++++++---------- .../onboarding/SignupStatusFragment.kt | 2 +- 4 files changed, 140 insertions(+), 145 deletions(-) diff --git a/app/src/main/java/com/toyou/toyouandroid/data/onboarding/service/OnboardingService.kt b/app/src/main/java/com/toyou/toyouandroid/data/onboarding/service/OnboardingService.kt index b6424daa..7e2c704c 100644 --- a/app/src/main/java/com/toyou/toyouandroid/data/onboarding/service/OnboardingService.kt +++ b/app/src/main/java/com/toyou/toyouandroid/data/onboarding/service/OnboardingService.kt @@ -5,6 +5,7 @@ import com.toyou.toyouandroid.data.onboarding.dto.PatchNicknameRequest import com.toyou.toyouandroid.data.onboarding.dto.PatchNicknameResponse import com.toyou.toyouandroid.data.onboarding.dto.PatchStatusRequest import retrofit2.Call +import retrofit2.Response import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.PATCH @@ -18,13 +19,29 @@ interface OnboardingService { @Query("userId") userId: Int ): Call + @GET("users/nickname/check") + suspend fun getNicknameCheckSuspend( + @Query("nickname") nickname: String, + @Query("userId") userId: Int + ): Response + @PATCH("users/nickname") fun patchNickname( @Body request: PatchNicknameRequest ): Call + @PATCH("users/nickname") + suspend fun patchNicknameSuspend( + @Body request: PatchNicknameRequest + ): Response + @PATCH("users/status") fun patchStatus( @Body request: PatchStatusRequest ): Call + + @PATCH("users/status") + suspend fun patchStatusSuspend( + @Body request: PatchStatusRequest + ): Response } \ No newline at end of file diff --git a/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/onboarding/SignupNicknameFragment.kt b/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/onboarding/SignupNicknameFragment.kt index fcb90b41..98f42b94 100644 --- a/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/onboarding/SignupNicknameFragment.kt +++ b/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/onboarding/SignupNicknameFragment.kt @@ -10,6 +10,7 @@ import android.view.ViewGroup import android.view.inputmethod.InputMethodManager import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels +import androidx.fragment.app.viewModels import androidx.lifecycle.ViewModelProvider import androidx.navigation.NavController import androidx.navigation.fragment.findNavController @@ -20,11 +21,12 @@ import com.toyou.toyouandroid.domain.home.repository.HomeRepository import com.toyou.toyouandroid.network.NetworkModule import com.toyou.toyouandroid.presentation.base.MainActivity import com.toyou.toyouandroid.presentation.fragment.home.HomeViewModel -import com.toyou.toyouandroid.presentation.viewmodel.HomeViewModelFactory import com.toyou.toyouandroid.presentation.viewmodel.ViewModelManager import com.toyou.toyouandroid.utils.TokenManager import com.toyou.toyouandroid.utils.TokenStorage +import dagger.hilt.android.AndroidEntryPoint +@AndroidEntryPoint class SignupNicknameFragment : Fragment() { private lateinit var navController: NavController @@ -32,10 +34,10 @@ class SignupNicknameFragment : Fragment() { private val binding: FragmentSignupnicknameBinding get() = requireNotNull(_binding){"FragmentSignupnicknameBinding -> null"} - private val viewModel: SignupNicknameViewModel by activityViewModels() - private val nicknameViewModel: SignupNicknameViewModel by activityViewModels() + private val viewModel: SignupNicknameViewModel by viewModels() + private val nicknameViewModel: SignupNicknameViewModel by viewModels() - private lateinit var homeViewModel: HomeViewModel + private val homeViewModel: HomeViewModel by viewModels() private lateinit var viewModelManager: ViewModelManager override fun onCreateView( @@ -45,15 +47,8 @@ class SignupNicknameFragment : Fragment() { ): View { _binding = FragmentSignupnicknameBinding.inflate(inflater, container, false) - val tokenStorage = TokenStorage(requireContext()) - val authService: AuthService = NetworkModule.getClient().create(AuthService::class.java) - val tokenManager = TokenManager(authService, tokenStorage) - val homeRepository = HomeRepository() - - homeViewModel = ViewModelProvider( - this, - HomeViewModelFactory(tokenManager, homeRepository) - )[HomeViewModel::class.java] + // SignupNicknameViewModel은 Hilt로 주입됨 + // 다른 ViewModel들은 기존 방식 유지할 필요가 있다면 여기에 초기화 코드 추가 binding.viewModel = viewModel binding.lifecycleOwner = this diff --git a/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/onboarding/SignupNicknameViewModel.kt b/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/onboarding/SignupNicknameViewModel.kt index f827b08d..9ebd9bde 100644 --- a/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/onboarding/SignupNicknameViewModel.kt +++ b/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/onboarding/SignupNicknameViewModel.kt @@ -3,153 +3,136 @@ package com.toyou.toyouandroid.presentation.fragment.onboarding import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import com.toyou.toyouandroid.R -import com.toyou.toyouandroid.network.NetworkModule -import com.toyou.toyouandroid.data.onboarding.dto.NicknameCheckResponse -import com.toyou.toyouandroid.data.onboarding.service.OnboardingService -import retrofit2.Call -import retrofit2.Callback -import retrofit2.Response +import androidx.lifecycle.viewModelScope +import com.toyou.toyouandroid.domain.profile.repository.ProfileRepository +import com.toyou.toyouandroid.presentation.fragment.mypage.DuplicateCheckMessageType +import com.toyou.toyouandroid.presentation.fragment.mypage.ProfileUiState +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch import timber.log.Timber - -class SignupNicknameViewModel : ViewModel() { - - private val _title = MutableLiveData() - val title: LiveData get() = _title - - private val _backButtonAction = MutableLiveData<() -> Unit>() - val backButtonAction: LiveData<() -> Unit> get() = _backButtonAction - - private val _textCount = MutableLiveData("0/15") - val textCount: LiveData get() = _textCount - - fun updateTextCount(count: Int) { - _textCount.value = "($count/15)" - } - - private val inputText = MutableLiveData() - - private val _isDuplicateCheckEnabled = MutableLiveData(false) - val isDuplicateCheckEnabled: LiveData = _isDuplicateCheckEnabled - - private val _duplicateCheckMessage = MutableLiveData().apply { - value = "중복된 닉네임인지 확인해주세요" - } - val duplicateCheckMessage: LiveData = _duplicateCheckMessage - - private val _duplicateCheckMessageColor = MutableLiveData().apply { - value = 0xFF000000.toInt() - } - val duplicateCheckMessageColor: LiveData = _duplicateCheckMessageColor - - private val _duplicateCheckButtonTextColor = MutableLiveData().apply { - value = 0xFFA6A6A6.toInt() - } - val duplicateCheckButtonTextColor: LiveData = _duplicateCheckButtonTextColor - - private val _duplicateCheckButtonBackground = MutableLiveData().apply { - value = R.drawable.next_button - } - val duplicateCheckButtonBackground: LiveData = _duplicateCheckButtonBackground - - - private val _isNextButtonEnabled = MutableLiveData(false) - val isNextButtonEnabled: LiveData = _isNextButtonEnabled - - private val _nextButtonTextColor = MutableLiveData().apply { - value = 0xFFA6A6A6.toInt() - } - val nextButtonTextColor: LiveData = _nextButtonTextColor - - private val _nextButtonBackground = MutableLiveData().apply { - value = R.drawable.next_button - } - val nextButtonBackground: LiveData = _nextButtonBackground - - private val _nickname = MutableLiveData() - val nickname: LiveData get() = _nickname - +import javax.inject.Inject + +@HiltViewModel +class SignupNicknameViewModel @Inject constructor( + private val profileRepository: ProfileRepository +) : ViewModel() { + + private val _uiState = MutableLiveData(ProfileUiState()) + val uiState: LiveData get() = _uiState + + // 데이터 바인딩 호환성을 위한 프로퍼티 + val nickname: LiveData = MutableLiveData() + val textCount: LiveData = MutableLiveData("0/15") + val duplicateCheckButtonTextColor: LiveData = MutableLiveData(0xFFA6A6A6.toInt()) + val duplicateCheckButtonBackground: LiveData = MutableLiveData(com.toyou.toyouandroid.R.drawable.next_button) + val duplicateCheckMessage: LiveData = MutableLiveData("중복된 닉네임인지 확인해주세요") + val duplicateCheckMessageColor: LiveData = MutableLiveData(0xFF000000.toInt()) + val nextButtonBackground: LiveData = MutableLiveData(com.toyou.toyouandroid.R.drawable.next_button) + val nextButtonTextColor: LiveData = MutableLiveData(0xFFA6A6A6.toInt()) + val isNextButtonEnabled: LiveData = MutableLiveData(false) + + private val _duplicateCheckMessageType = MutableLiveData() + val duplicateCheckMessageType: LiveData get() = _duplicateCheckMessageType + init { - inputText.observeForever { text -> - _isDuplicateCheckEnabled.value = !text.isNullOrEmpty() - } - _title.value = "회원가입" -// _backButtonAction.value = { /* 회원가입 화면에서의 back 버튼 로직 */ } + _uiState.value = ProfileUiState(title = "회원가입") } - - fun duplicateBtnActivate() { - _duplicateCheckButtonTextColor.value = 0xFF000000.toInt() - _duplicateCheckButtonBackground.value = R.drawable.signupnickname_doublecheck_activate + + fun updateTextCount(count: Int) { + val countText = "($count/15)" + _uiState.value = _uiState.value?.copy( + textCount = countText + ) + (textCount as MutableLiveData).value = countText } - + + fun setNickname(newNickname: String) { + _uiState.value = _uiState.value?.copy( + nickname = newNickname, + isDuplicateCheckEnabled = newNickname.isNotEmpty() + ) + (nickname as MutableLiveData).value = newNickname + } + fun updateLength15(length: Int) { - if (length >= 15) { - _duplicateCheckMessage.value = "15자 이내로 입력해주세요." - _duplicateCheckMessageColor.value = 0xFF000000.toInt() + val messageType = if (length >= 15) { + DuplicateCheckMessageType.LENGTH_EXCEEDED } else { - _duplicateCheckMessage.value = "중복된 닉네임인지 확인해주세요" - _duplicateCheckMessageColor.value = 0xFF000000.toInt() + DuplicateCheckMessageType.CHECK_REQUIRED } + _duplicateCheckMessageType.value = messageType + _uiState.value = _uiState.value?.copy( + duplicateCheckMessage = messageType.message + ) } - - fun setNickname(newNickname: String) { - _nickname.value = newNickname + + fun duplicateBtnActivate() { + _uiState.value = _uiState.value?.copy( + isDuplicateCheckEnabled = true + ) + (duplicateCheckButtonTextColor as MutableLiveData).value = 0xFF000000.toInt() + (duplicateCheckButtonBackground as MutableLiveData).value = com.toyou.toyouandroid.R.drawable.signupnickname_doublecheck_activate } - + fun resetState() { - _duplicateCheckMessage.value = "중복된 닉네임인지 확인해주세요" - _duplicateCheckMessageColor.value = 0xFF000000.toInt() - _isNextButtonEnabled.value = false - _nextButtonTextColor.value = 0xFFA6A6A6.toInt() - _nextButtonBackground.value = R.drawable.next_button - _nickname.value = "" - _duplicateCheckButtonTextColor.value = 0xFFA6A6A6.toInt() - _duplicateCheckButtonBackground.value = R.drawable.next_button - } - - fun nextButtonDisable() { - _isNextButtonEnabled.value = false - _nextButtonTextColor.value = 0xFFA6A6A6.toInt() - _nextButtonBackground.value = R.drawable.next_button + _uiState.value = ProfileUiState(title = "회원가입") + _duplicateCheckMessageType.value = DuplicateCheckMessageType.CHECK_REQUIRED } - - private val retrofit = NetworkModule.getClient() - private val apiService: OnboardingService = retrofit.create(OnboardingService::class.java) - - // API를 호출하여 닉네임 중복 체크를 수행하는 함수 + fun checkDuplicate(userId: Int) { - val nickname = _nickname.value ?: return - - val call = apiService.getNicknameCheck(nickname, userId) - call.enqueue(object : Callback { - override fun onResponse(call: Call, response: Response) { + val nickname = _uiState.value?.nickname ?: return + + viewModelScope.launch { + try { + val response = profileRepository.checkNickname(nickname, userId) if (response.isSuccessful) { val exists = response.body()?.result?.exists ?: false - if (!exists) { - _duplicateCheckMessage.value = "사용 가능한 닉네임입니다." - _duplicateCheckMessageColor.value = 0xFFEA9797.toInt() - - _isNextButtonEnabled.value = true - _nextButtonTextColor.value = 0xFF000000.toInt() - _nextButtonBackground.value = R.drawable.next_button_enabled - } else { - _duplicateCheckMessage.value = "이미 사용 중인 닉네임입니다." - _duplicateCheckMessageColor.value = 0xFFFF0000.toInt() - nextButtonDisable() - } + handleNicknameCheckResult(exists) } else { - _duplicateCheckMessage.value = "닉네임 확인에 실패했습니다." - _duplicateCheckMessageColor.value = 0xFFFF0000.toInt() - nextButtonDisable() + handleNicknameCheckError() } + } catch (e: Exception) { + Timber.tag("API Failure").e(e, "Error checking nickname") + _duplicateCheckMessageType.value = DuplicateCheckMessageType.SERVER_ERROR + _uiState.value = _uiState.value?.copy( + duplicateCheckMessage = DuplicateCheckMessageType.SERVER_ERROR.message, + isNicknameValid = false, + isNextButtonEnabled = false + ) } - - override fun onFailure(call: Call, t: Throwable) { - Timber.tag("API Failure").e(t, "Error checking nickname") - _duplicateCheckMessage.value = "서버에 연결할 수 없습니다." - _duplicateCheckMessageColor.value = 0xFFFF0000.toInt() - nextButtonDisable() - } - }) + } + } + + private fun handleNicknameCheckResult(exists: Boolean) { + val messageType = if (!exists) { + DuplicateCheckMessageType.AVAILABLE + } else { + DuplicateCheckMessageType.ALREADY_IN_USE + } + + _duplicateCheckMessageType.value = messageType + _uiState.value = _uiState.value?.copy( + duplicateCheckMessage = messageType.message, + isNicknameValid = !exists, + isNextButtonEnabled = !exists + ) + + // 호환성 프로퍼티 업데이트 + (duplicateCheckMessage as MutableLiveData).value = messageType.message + (duplicateCheckMessageColor as MutableLiveData).value = if (!exists) 0xFFEA9797.toInt() else 0xFFFF0000.toInt() + (isNextButtonEnabled as MutableLiveData).value = !exists + if (!exists) { + (nextButtonTextColor as MutableLiveData).value = 0xFF000000.toInt() + (nextButtonBackground as MutableLiveData).value = com.toyou.toyouandroid.R.drawable.next_button_enabled + } + } + + private fun handleNicknameCheckError() { + _duplicateCheckMessageType.value = DuplicateCheckMessageType.CHECK_FAILED + _uiState.value = _uiState.value?.copy( + duplicateCheckMessage = DuplicateCheckMessageType.CHECK_FAILED.message, + isNicknameValid = false, + isNextButtonEnabled = false + ) } } \ No newline at end of file diff --git a/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/onboarding/SignupStatusFragment.kt b/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/onboarding/SignupStatusFragment.kt index 7fea17f9..8e8dec82 100644 --- a/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/onboarding/SignupStatusFragment.kt +++ b/app/src/main/java/com/toyou/toyouandroid/presentation/fragment/onboarding/SignupStatusFragment.kt @@ -18,7 +18,7 @@ import com.toyou.toyouandroid.data.onboarding.service.AuthService import com.toyou.toyouandroid.fcm.domain.FCMRepository import com.toyou.toyouandroid.fcm.service.FCMService import com.toyou.toyouandroid.network.AuthNetworkModule -import com.toyou.toyouandroid.presentation.viewmodel.AuthViewModelFactory +//import com.toyou.toyouandroid.presentation.viewmodel.AuthViewModelFactory import com.toyou.toyouandroid.presentation.viewmodel.LoginViewModelFactory import com.toyou.toyouandroid.utils.TokenManager import com.toyou.toyouandroid.utils.TokenStorage From a8f6cd2e2385a3bdd900ae133e546ea74d439da8 Mon Sep 17 00:00:00 2001 From: Daemon Jung Date: Mon, 15 Sep 2025 17:21:31 +0900 Subject: [PATCH 10/11] =?UTF-8?q?feat:=20Hilt=20=EC=96=B4=EB=85=B8?= =?UTF-8?q?=ED=85=8C=EC=9D=B4=EC=85=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MainActivity에 @AndroidEntryPoint 어노테이션을 추가하여 Hilt 종속성 주입을 활성화합니다. --- .../com/toyou/toyouandroid/presentation/base/MainActivity.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/com/toyou/toyouandroid/presentation/base/MainActivity.kt b/app/src/main/java/com/toyou/toyouandroid/presentation/base/MainActivity.kt index 667ec100..f1af097b 100644 --- a/app/src/main/java/com/toyou/toyouandroid/presentation/base/MainActivity.kt +++ b/app/src/main/java/com/toyou/toyouandroid/presentation/base/MainActivity.kt @@ -16,8 +16,10 @@ import androidx.navigation.fragment.NavHostFragment import androidx.navigation.ui.setupWithNavController import com.toyou.toyouandroid.R import com.toyou.toyouandroid.databinding.ActivityMainBinding +import dagger.hilt.android.AndroidEntryPoint import timber.log.Timber +@AndroidEntryPoint class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding From a7fc58f29ba72f99e3ccbc1e29a9496afef21fae Mon Sep 17 00:00:00 2001 From: Daemon Jung Date: Mon, 15 Sep 2025 17:21:46 +0900 Subject: [PATCH 11/11] =?UTF-8?q?Refactor:=20API=20=EC=84=9C=EB=B9=84?= =?UTF-8?q?=EC=8A=A4=EB=A5=BC=20=EC=BD=94=EB=A3=A8=ED=8B=B4=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=A0=84=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `AuthService`와 `EmotionService`의 API 호출 함수를 `suspend` 함수로 변경하여 코루틴을 지원하도록 수정했습니다. - `AuthService`에 로그아웃 및 회원탈퇴를 위한 `suspend` 함수 추가 - `EmotionService`에 감정 기록을 위한 `suspend` 함수 추가 --- .../data/emotion/service/EmotionService.kt | 6 ++++++ .../data/onboarding/service/AuthService.kt | 11 +++++++++++ 2 files changed, 17 insertions(+) diff --git a/app/src/main/java/com/toyou/toyouandroid/data/emotion/service/EmotionService.kt b/app/src/main/java/com/toyou/toyouandroid/data/emotion/service/EmotionService.kt index 487c5e67..5c4b8d5a 100644 --- a/app/src/main/java/com/toyou/toyouandroid/data/emotion/service/EmotionService.kt +++ b/app/src/main/java/com/toyou/toyouandroid/data/emotion/service/EmotionService.kt @@ -5,6 +5,7 @@ import com.toyou.toyouandroid.data.emotion.dto.EmotionRequest import com.toyou.toyouandroid.data.emotion.dto.EmotionResponse import com.toyou.toyouandroid.data.emotion.dto.YesterdayFriendsResponse import retrofit2.Call +import retrofit2.Response import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.POST @@ -17,6 +18,11 @@ interface EmotionService { @Body emotion: EmotionRequest ): Call + @POST("users/emotions") + suspend fun patchEmotionSuspend( + @Body emotion: EmotionRequest + ): Response + @GET("diarycards/yesterday") fun getYesterdayFriendCard(): Call diff --git a/app/src/main/java/com/toyou/toyouandroid/data/onboarding/service/AuthService.kt b/app/src/main/java/com/toyou/toyouandroid/data/onboarding/service/AuthService.kt index 4a9623b0..3b837f89 100644 --- a/app/src/main/java/com/toyou/toyouandroid/data/onboarding/service/AuthService.kt +++ b/app/src/main/java/com/toyou/toyouandroid/data/onboarding/service/AuthService.kt @@ -3,6 +3,7 @@ package com.toyou.toyouandroid.data.onboarding.service import com.toyou.toyouandroid.data.onboarding.dto.request.SignUpRequest import com.toyou.toyouandroid.data.onboarding.dto.response.SignUpResponse import retrofit2.Call +import retrofit2.Response import retrofit2.http.Body import retrofit2.http.DELETE import retrofit2.http.Header @@ -25,6 +26,11 @@ interface AuthService { @Header("refreshToken") refreshToken: String ): Call + @POST("auth/logout") + suspend fun logoutSuspend( + @Header("refreshToken") refreshToken: String + ): Response + @POST("auth/kakao") fun kakaoLogin( @Header("oauthAccessToken") accessToken: String @@ -34,4 +40,9 @@ interface AuthService { fun signOut( @Header("refreshToken") refreshToken: String ): Call + + @DELETE("auth/unlink") + suspend fun signOutSuspend( + @Header("refreshToken") refreshToken: String + ): Response } \ No newline at end of file