diff --git a/app/src/main/java/com/zucchini/ssuplector/di/DefaultNavigationProvider.kt b/app/src/main/java/com/zucchini/ssuplector/di/DefaultNavigationProvider.kt index ecd21c1..edf89ec 100644 --- a/app/src/main/java/com/zucchini/ssuplector/di/DefaultNavigationProvider.kt +++ b/app/src/main/java/com/zucchini/ssuplector/di/DefaultNavigationProvider.kt @@ -2,6 +2,7 @@ package com.zucchini.ssuplector.di import android.content.Context import android.content.Intent +import com.zucchini.auth.LoginActivity import com.zucchini.common.NavigationProvider import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject @@ -23,6 +24,6 @@ class DefaultNavigationProvider @Inject constructor( } override fun toLogin(): Intent { - TODO("Not yet implemented") + return Intent(context, LoginActivity::class.java) } } diff --git a/core/network/src/main/java/com/sample/network/service/AuthService.kt b/core/network/src/main/java/com/sample/network/service/AuthService.kt index ad0a88b..514adf0 100644 --- a/core/network/src/main/java/com/sample/network/service/AuthService.kt +++ b/core/network/src/main/java/com/sample/network/service/AuthService.kt @@ -4,7 +4,9 @@ import com.sample.network.model.BaseResponse import com.sample.network.reponse.LoginResponse import com.sample.network.reponse.RefreshResponse import retrofit2.http.Body +import retrofit2.http.DELETE import retrofit2.http.GET +import retrofit2.http.Header import retrofit2.http.Query interface AuthService { @@ -18,4 +20,14 @@ interface AuthService { suspend fun refreshToken( @Body refreshToken: String = "", ): BaseResponse + + @DELETE("api/auth/kakao/logout") + suspend fun logout( + @Header("Authorization") accessToken: String, + ): BaseResponse + + @DELETE("api/auth/withdraw") + suspend fun withdrawal( + @Header("Authorization") accessToken: String, + ): BaseResponse } diff --git a/data/src/main/java/com/zucchini/data/AuthRepositoryImpl.kt b/data/src/main/java/com/zucchini/data/AuthRepositoryImpl.kt index 95cd7d6..ab955ba 100644 --- a/data/src/main/java/com/zucchini/data/AuthRepositoryImpl.kt +++ b/data/src/main/java/com/zucchini/data/AuthRepositoryImpl.kt @@ -17,10 +17,16 @@ class AuthRepositoryImpl @Inject constructor( } override suspend fun logout(accessToken: String): Result { - TODO("Not yet implemented") + val bearerToken = "Bearer $accessToken" + return runCatching { + authService.logout(bearerToken) + } } override suspend fun withdrawal(accessToken: String): Result { - TODO("Not yet implemented") + val bearerToken = "Bearer $accessToken" + return runCatching { + authService.withdrawal(bearerToken) + } } } diff --git a/feature/projects/src/main/java/com/zucchini/auth/LoginActivity.kt b/feature/projects/src/main/java/com/zucchini/auth/LoginActivity.kt index 3b1a87c..4944bbc 100644 --- a/feature/projects/src/main/java/com/zucchini/auth/LoginActivity.kt +++ b/feature/projects/src/main/java/com/zucchini/auth/LoginActivity.kt @@ -38,7 +38,6 @@ class LoginActivity @Inject constructor( kakaoLogin() collectKakaoLogin() setLoginViewPager() - collectAutoLoginState() } private fun kakaoLogin() { @@ -54,18 +53,10 @@ class LoginActivity @Inject constructor( navigateToMain() } else { // TODO 회원가입 + navigateToMain() } - else -> Timber.d(getString(R.string.fail_kakao_login)) - } - }.launchIn(lifecycleScope) - } - private fun collectAutoLoginState() { - viewModel.isAutoLoginState.flowWithLifecycle(lifecycle).onEach { isAutoLogin -> - if (!isAutoLogin) { - val intent = Intent(this, LoginActivity::class.java) - startActivity(intent) - finish() + else -> Timber.e("Kakao Login Failed") } }.launchIn(lifecycleScope) } diff --git a/feature/projects/src/main/java/com/zucchini/auth/LoginViewModel.kt b/feature/projects/src/main/java/com/zucchini/auth/LoginViewModel.kt index b7e1cd7..6c14cb3 100644 --- a/feature/projects/src/main/java/com/zucchini/auth/LoginViewModel.kt +++ b/feature/projects/src/main/java/com/zucchini/auth/LoginViewModel.kt @@ -28,12 +28,6 @@ class LoginViewModel @Inject constructor( private val _isLogin = MutableStateFlow(false) val isLogin = _isLogin.asStateFlow() - private val _isAutoLoginState = MutableStateFlow(false) - val isAutoLoginState = _isAutoLoginState.asStateFlow() - - init { - updateAutoLoginState() - } fun loginWithKakaoApp(context: Context) { if (UserApiClient.instance.isKakaoTalkLoginAvailable(context)) { @@ -54,7 +48,7 @@ class LoginViewModel @Inject constructor( } Log.d( "networkPreference", - "accessToken: ${networkPreference.accessToken} refreshToken: ${networkPreference.refreshToken} developerId: ${networkPreference.developerId} autoLoginConfigured: ${networkPreference.autoLoginConfigured}" + "accessToken: ${networkPreference.accessToken} refreshToken: ${networkPreference.refreshToken} developerId: ${networkPreference.developerId} autoLoginConfigured: ${networkPreference.autoLoginConfigured}", ) _isLogin.value = it.isLogin _kakaoLoginSuccess.value = true @@ -97,8 +91,4 @@ class LoginViewModel @Inject constructor( } } } - - private fun updateAutoLoginState() { - _isAutoLoginState.value = networkPreference.autoLoginConfigured - } -} \ No newline at end of file +} diff --git a/feature/projects/src/main/java/com/zucchini/projects/MainActivity.kt b/feature/projects/src/main/java/com/zucchini/projects/MainActivity.kt index f8ea3a1..2cf4758 100644 --- a/feature/projects/src/main/java/com/zucchini/projects/MainActivity.kt +++ b/feature/projects/src/main/java/com/zucchini/projects/MainActivity.kt @@ -24,4 +24,8 @@ class MainActivity : AppCompatActivity() { val navController = navHostFragment.navController binding.bnvMain.setupWithNavController(navController) } + + private fun loadMyLoginInfo() { + + } } diff --git a/feature/projects/src/main/java/com/zucchini/projects/MainViewModel.kt b/feature/projects/src/main/java/com/zucchini/projects/MainViewModel.kt index 84602fd..818ad9b 100644 --- a/feature/projects/src/main/java/com/zucchini/projects/MainViewModel.kt +++ b/feature/projects/src/main/java/com/zucchini/projects/MainViewModel.kt @@ -1,11 +1,39 @@ package com.zucchini.projects import androidx.lifecycle.ViewModel -import com.sample.network.datastore.NetworkPreference +import com.kakao.sdk.user.UserApiClient import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow import javax.inject.Inject @HiltViewModel -class MainViewModel @Inject constructor( - private val netPreference: NetworkPreference, -) : ViewModel() +class MainViewModel @Inject constructor() : ViewModel() { + + private val _userNickname = MutableStateFlow("") + val userNickname = _userNickname.asStateFlow() + + private val _userEmail = MutableStateFlow("") + val userEmail = _userEmail.asStateFlow() + + private val _userProfile = MutableStateFlow("") + val userProfile = _userProfile.asStateFlow() + + init { + getKakaoUserInfo() + } + + private fun getKakaoUserInfo() { + UserApiClient.instance.me { user, error -> + if (error != null) { + _userNickname.value = "" + _userEmail.value = "" + _userProfile.value = "" + } else if (user != null) { + _userNickname.value = user.kakaoAccount?.profile?.nickname ?: "no nickname" + _userEmail.value = user.kakaoAccount?.email ?: "no email" + _userProfile.value = user.kakaoAccount?.profile?.thumbnailImageUrl ?: "" + } + } + } +} diff --git a/feature/projects/src/main/java/com/zucchini/projects/mypage/MypageFragment.kt b/feature/projects/src/main/java/com/zucchini/projects/mypage/MypageFragment.kt index 0060e42..73e713d 100644 --- a/feature/projects/src/main/java/com/zucchini/projects/mypage/MypageFragment.kt +++ b/feature/projects/src/main/java/com/zucchini/projects/mypage/MypageFragment.kt @@ -7,12 +7,32 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.fragment.app.viewModels +import androidx.lifecycle.flowWithLifecycle +import androidx.lifecycle.lifecycleScope +import coil.load +import coil.transform.RoundedCornersTransformation +import com.zucchini.common.NavigationProvider import com.zucchini.feature.projects.R import com.zucchini.feature.projects.databinding.FragmentMypageBinding +import com.zucchini.projects.MainViewModel +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import javax.inject.Inject +@AndroidEntryPoint class MypageFragment : Fragment() { private var _binding: FragmentMypageBinding? = null private val binding: FragmentMypageBinding get() = _binding!! + + private val mainViewModel by activityViewModels() + private val viewModel by viewModels() + + @Inject + lateinit var navigationProvider: NavigationProvider + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -22,7 +42,12 @@ class MypageFragment : Fragment() { navigateToWebDocs() navigateToKakaoOpenChat() - navigateToSubmitForms() + navigateToMyDevInfo() + loadMyKakaoInfo() + collectLogoutState() + collectWithdrawalState() + clickLogout() + clickWithdrawal() return binding.root } @@ -48,16 +73,68 @@ class MypageFragment : Fragment() { } } - private fun navigateToSubmitForms() { - binding.btnApplyDeveloper.setOnClickListener { - val developerFormUri = getString(R.string.developer_form) - val intent = Intent(Intent.ACTION_VIEW, Uri.parse(developerFormUri)) - startActivity(intent) + private fun navigateToMyDevInfo() { + binding.tvNavigateToMyInfo.setOnClickListener { + // TODO 개발자 상세정보로 이동 } - binding.btnApplyProject.setOnClickListener { - val projectFormUri = getString(R.string.project_form) - val intent = Intent(Intent.ACTION_VIEW, Uri.parse(projectFormUri)) - startActivity(intent) + } + + private fun navigateToLoginActivity() { + val intent = navigationProvider.toLogin() + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK) + startActivity(intent) + activity?.finish() + } + + private fun loadMyKakaoInfo() { + mainViewModel.userEmail.flowWithLifecycle(viewLifecycleOwner.lifecycle) + .onEach { email -> + binding.tvUserEmail.text = email + } + .launchIn(viewLifecycleOwner.lifecycleScope) + + mainViewModel.userNickname.flowWithLifecycle(viewLifecycleOwner.lifecycle) + .onEach { nickname -> + binding.tvUserName.text = nickname + } + .launchIn(viewLifecycleOwner.lifecycleScope) + + mainViewModel.userProfile.flowWithLifecycle(viewLifecycleOwner.lifecycle) + .onEach { image -> + binding.ivDeveloperImage.load(image) { + crossfade(true) + placeholder(R.drawable.developer_default_image) + transformations(RoundedCornersTransformation()) + } + } + .launchIn(viewLifecycleOwner.lifecycleScope) + } + + private fun collectLogoutState() { + viewModel.logoutSuccess.flowWithLifecycle(lifecycle).onEach { success -> + if (success) { + navigateToLoginActivity() + } + }.launchIn(viewLifecycleOwner.lifecycleScope) + } + + private fun collectWithdrawalState() { + viewModel.withdrawalSuccess.flowWithLifecycle(lifecycle).onEach { success -> + if (success) { + navigateToLoginActivity() + } + }.launchIn(viewLifecycleOwner.lifecycleScope) + } + + private fun clickLogout() { + binding.tvLogout.setOnClickListener { + viewModel.logout() + } + } + + private fun clickWithdrawal() { + binding.tvWithdrawal.setOnClickListener { + viewModel.withdrawal() } } } diff --git a/feature/projects/src/main/java/com/zucchini/projects/mypage/MypageViewModel.kt b/feature/projects/src/main/java/com/zucchini/projects/mypage/MypageViewModel.kt new file mode 100644 index 0000000..29bb59d --- /dev/null +++ b/feature/projects/src/main/java/com/zucchini/projects/mypage/MypageViewModel.kt @@ -0,0 +1,62 @@ +package com.zucchini.projects.mypage + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.kakao.sdk.user.UserApiClient +import com.sample.network.datastore.NetworkPreference +import com.zucchini.domain.repository.AuthRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject + +@HiltViewModel +class MypageViewModel @Inject constructor( + private val preference: NetworkPreference, + private val authRepository: AuthRepository, + +) : ViewModel() { + + private val _logoutSuccess = MutableStateFlow(false) + val logoutSuccess = _logoutSuccess.asStateFlow() + + private val _withdrawalSuccess = MutableStateFlow(false) + val withdrawalSuccess = _withdrawalSuccess.asStateFlow() + + fun logout() { + viewModelScope.launch { + authRepository.logout(preference.accessToken).onSuccess { + preference.clear() + _logoutSuccess.value = true + }.onFailure { + _logoutSuccess.value = false + Timber.d("failed to logout $it") + } + } + } + + fun withdrawal() { + viewModelScope.launch { + authRepository.withdrawal(preference.accessToken).onSuccess { + preference.clear() + kakaoUnlink() + }.onFailure { + _withdrawalSuccess.value = false + Timber.d("failed to withdrawal") + } + } + } + + private fun kakaoUnlink() { + UserApiClient.instance.unlink { error -> + if (error != null) { + _withdrawalSuccess.value = false + Timber.d("failed to unlink: $error") + } else { + _withdrawalSuccess.value = true + } + } + } +} diff --git a/feature/projects/src/main/res/layout/fragment_mypage.xml b/feature/projects/src/main/res/layout/fragment_mypage.xml index 8cf99a1..5b9a9d0 100644 --- a/feature/projects/src/main/res/layout/fragment_mypage.xml +++ b/feature/projects/src/main/res/layout/fragment_mypage.xml @@ -29,65 +29,56 @@ app:layout_constraintTop_toTopOf="parent" /> - + app:layout_constraintStart_toEndOf="@+id/iv_developer_image" + app:layout_constraintTop_toTopOf="@+id/iv_developer_image" + tools:text="이름" /> + android:fontFamily="@font/pretendardmedium" + android:textColor="@color/dark_olive" + android:textSize="12sp" + app:layout_constraintBottom_toBottomOf="@+id/iv_developer_image" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@+id/iv_developer_image" + app:layout_constraintTop_toBottomOf="@+id/tv_user_name" + tools:text="yuri696@naver.com" /> + android:text="@string/navigateToMyInfo" + android:textColor="@color/main_olive" + android:textSize="15sp" + app:layout_constraintStart_toEndOf="@+id/iv_developer_image" + app:layout_constraintTop_toBottomOf="@+id/tv_user_email" /> + + + + + + + \ No newline at end of file diff --git a/feature/projects/src/main/res/values/strings.xml b/feature/projects/src/main/res/values/strings.xml index 7ab5cc0..664fda0 100644 --- a/feature/projects/src/main/res/values/strings.xml +++ b/feature/projects/src/main/res/values/strings.xml @@ -20,7 +20,7 @@ 관련 링크 개발자 정보 화면 터치 시 슈플렉터를 시작할 수 있어요! - 신청 페이지 + 내 정보 신청하러 가기 카카오톡 오픈 채팅을 통해 프로젝트 업로드 신청을 해보세요!\n슈플렉터에 우리 팀 프로젝트를 공유하고 다양한 숭실대 학우들과 소통할 수 있어요 내 프로젝트 신청하러 가기 @@ -37,5 +37,8 @@ https://github.com/ 카카오 로그인에 실패했습니다. 다시 시도해주세요. 카카오 로그인 성공 + 로그아웃 + 회원탈퇴 + 내 개발자 정보 보러가기 \ No newline at end of file