diff --git a/app/src/main/java/com/android/go/sopt/winey/data/interceptor/AuthInterceptor.kt b/app/src/main/java/com/android/go/sopt/winey/data/interceptor/AuthInterceptor.kt index 4ff7cea0..f7c642a2 100644 --- a/app/src/main/java/com/android/go/sopt/winey/data/interceptor/AuthInterceptor.kt +++ b/app/src/main/java/com/android/go/sopt/winey/data/interceptor/AuthInterceptor.kt @@ -23,6 +23,7 @@ class AuthInterceptor @Inject constructor( private val dataStoreRepository: DataStoreRepository ) : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { + runBlocking { Timber.e("액세스토큰 : ${getAccessToken()}, 리프레시토큰 : ${getRefreshToken()}") } val originalRequest = chain.request() val headerRequest = originalRequest.newAuthBuilder() @@ -33,31 +34,11 @@ class AuthInterceptor @Inject constructor( when (response.code) { CODE_TOKEN_EXPIRED -> { try { - val refreshTokenRequest = originalRequest.newBuilder().post("".toRequestBody()) - .url("$AUTH_BASE_URL/auth/token") - .addHeader(REFRESH_TOKEN, runBlocking(Dispatchers.IO) { getRefreshToken() }) - .build() - val refreshTokenResponse = chain.proceed(refreshTokenRequest) - Timber.e("리프레시 토큰 : $refreshTokenResponse") - - if (refreshTokenResponse.isSuccessful) { - val responseToken = json.decodeFromString( - refreshTokenResponse.body?.string().toString() - ) as BaseResponse - - if (responseToken.data != null) { - saveAccessToken( - responseToken.data.accessToken, - responseToken.data.refreshToken - ) - } - refreshTokenResponse.close() - val newRequest = originalRequest.newAuthBuilder().build() - return chain.proceed(newRequest) - } - saveAccessToken("", "") + Timber.e("액세스 토큰 만료, 토큰 재발급 합니다.") + response.close() + return handleTokenExpired(chain, originalRequest, headerRequest) } catch (t: Throwable) { - Timber.e(t) + Timber.e("예외발생 ${t.message}") saveAccessToken("", "") } } @@ -73,7 +54,7 @@ class AuthInterceptor @Inject constructor( } private suspend fun getRefreshToken(): String { - return dataStoreRepository.getAccessToken().first() ?: "" + return dataStoreRepository.getRefreshToken().first() ?: "" } private fun saveAccessToken(accessToken: String, refreshToken: String) = @@ -81,6 +62,36 @@ class AuthInterceptor @Inject constructor( dataStoreRepository.saveAccessToken(accessToken, refreshToken) } + private fun handleTokenExpired(chain: Interceptor.Chain, originalRequest: Request, headerRequest: Request): Response { + val refreshTokenRequest = originalRequest.newBuilder().post("".toRequestBody()) + .url("$AUTH_BASE_URL/auth/token") + .addHeader(REFRESH_TOKEN, runBlocking(Dispatchers.IO) { getRefreshToken() }) + .build() + val refreshTokenResponse = chain.proceed(refreshTokenRequest) + Timber.e("리프레시 토큰 : $refreshTokenResponse") + + if (refreshTokenResponse.isSuccessful) { + val responseToken = json.decodeFromString( + refreshTokenResponse.body?.string().toString() + ) as BaseResponse + if (responseToken.data != null) { + Timber.e("리프레시 토큰 : ${responseToken.data.refreshToken}") + saveAccessToken( + responseToken.data.accessToken, + responseToken.data.refreshToken + ) + } + refreshTokenResponse.close() + val newRequest = originalRequest.newAuthBuilder().build() + return chain.proceed(newRequest) + } else { + refreshTokenResponse.close() + Timber.e("리프레시 토큰 : ${refreshTokenResponse.code}") + saveAccessToken("", "") + return chain.proceed(headerRequest) + } + } + companion object { private const val HEADER_TOKEN = "accessToken" private const val CODE_TOKEN_EXPIRED = 401 diff --git a/app/src/main/java/com/android/go/sopt/winey/data/model/remote/response/ResponseLogoutDto.kt b/app/src/main/java/com/android/go/sopt/winey/data/model/remote/response/ResponseLogoutDto.kt new file mode 100644 index 00000000..aee4de8e --- /dev/null +++ b/app/src/main/java/com/android/go/sopt/winey/data/model/remote/response/ResponseLogoutDto.kt @@ -0,0 +1,12 @@ +package com.android.go.sopt.winey.data.model.remote.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ResponseLogoutDto( + @SerialName("code") + val code: Int, + @SerialName("message") + val message: String +) diff --git a/app/src/main/java/com/android/go/sopt/winey/data/repository/AuthRepositoryImpl.kt b/app/src/main/java/com/android/go/sopt/winey/data/repository/AuthRepositoryImpl.kt index 0dfa6e45..2b9c4987 100644 --- a/app/src/main/java/com/android/go/sopt/winey/data/repository/AuthRepositoryImpl.kt +++ b/app/src/main/java/com/android/go/sopt/winey/data/repository/AuthRepositoryImpl.kt @@ -4,6 +4,7 @@ import com.android.go.sopt.winey.data.model.remote.request.RequestCreateGoalDto import com.android.go.sopt.winey.data.model.remote.request.RequestLoginDto import com.android.go.sopt.winey.data.model.remote.response.ResponseGetNicknameDuplicateCheckDto import com.android.go.sopt.winey.data.model.remote.response.ResponseLoginDto +import com.android.go.sopt.winey.data.model.remote.response.ResponseLogoutDto import com.android.go.sopt.winey.data.model.remote.response.ResponseReIssueTokenDto import com.android.go.sopt.winey.data.source.AuthDataSource import com.android.go.sopt.winey.domain.entity.Goal @@ -37,6 +38,11 @@ class AuthRepositoryImpl @Inject constructor( authDataSource.postReIssueToken(refreshToken).data } + override suspend fun postLogout(): Result = + runCatching { + authDataSource.postLogout() + } + override suspend fun getNicknameDuplicateCheck(nickname: String): Result = runCatching { authDataSource.getNicknameDuplicateCheck(nickname).data diff --git a/app/src/main/java/com/android/go/sopt/winey/data/service/AuthService.kt b/app/src/main/java/com/android/go/sopt/winey/data/service/AuthService.kt index ebcc059d..6baa78c2 100644 --- a/app/src/main/java/com/android/go/sopt/winey/data/service/AuthService.kt +++ b/app/src/main/java/com/android/go/sopt/winey/data/service/AuthService.kt @@ -6,6 +6,7 @@ import com.android.go.sopt.winey.data.model.remote.response.ResponseCreateGoalDt import com.android.go.sopt.winey.data.model.remote.response.ResponseGetNicknameDuplicateCheckDto import com.android.go.sopt.winey.data.model.remote.response.ResponseGetUserDto import com.android.go.sopt.winey.data.model.remote.response.ResponseLoginDto +import com.android.go.sopt.winey.data.model.remote.response.ResponseLogoutDto import com.android.go.sopt.winey.data.model.remote.response.ResponseReIssueTokenDto import com.android.go.sopt.winey.data.model.remote.response.base.BaseResponse import retrofit2.http.Body @@ -34,6 +35,9 @@ interface AuthService { @Header("refreshToken") refreshToken: String ): BaseResponse + @POST("auth/sign-out") + suspend fun postLogout(): ResponseLogoutDto + @GET("user/nickname/is-exist") suspend fun getNicknameDuplicateCheck( @Query("nickname") nickname: String diff --git a/app/src/main/java/com/android/go/sopt/winey/data/source/AuthDataSource.kt b/app/src/main/java/com/android/go/sopt/winey/data/source/AuthDataSource.kt index 17c1485c..3806dc1f 100644 --- a/app/src/main/java/com/android/go/sopt/winey/data/source/AuthDataSource.kt +++ b/app/src/main/java/com/android/go/sopt/winey/data/source/AuthDataSource.kt @@ -6,6 +6,7 @@ import com.android.go.sopt.winey.data.model.remote.response.ResponseCreateGoalDt import com.android.go.sopt.winey.data.model.remote.response.ResponseGetNicknameDuplicateCheckDto import com.android.go.sopt.winey.data.model.remote.response.ResponseGetUserDto import com.android.go.sopt.winey.data.model.remote.response.ResponseLoginDto +import com.android.go.sopt.winey.data.model.remote.response.ResponseLogoutDto import com.android.go.sopt.winey.data.model.remote.response.ResponseReIssueTokenDto import com.android.go.sopt.winey.data.model.remote.response.base.BaseResponse import com.android.go.sopt.winey.data.service.AuthService @@ -30,6 +31,8 @@ class AuthDataSource @Inject constructor( ): BaseResponse = authService.postReIssueToken(refreshToken) + suspend fun postLogout(): ResponseLogoutDto = authService.postLogout() + suspend fun getNicknameDuplicateCheck(nickname: String): BaseResponse = authService.getNicknameDuplicateCheck(nickname) } diff --git a/app/src/main/java/com/android/go/sopt/winey/domain/repository/AuthRepository.kt b/app/src/main/java/com/android/go/sopt/winey/domain/repository/AuthRepository.kt index 8223eddc..d778a673 100644 --- a/app/src/main/java/com/android/go/sopt/winey/domain/repository/AuthRepository.kt +++ b/app/src/main/java/com/android/go/sopt/winey/domain/repository/AuthRepository.kt @@ -4,6 +4,7 @@ import com.android.go.sopt.winey.data.model.remote.request.RequestCreateGoalDto import com.android.go.sopt.winey.data.model.remote.request.RequestLoginDto import com.android.go.sopt.winey.data.model.remote.response.ResponseGetNicknameDuplicateCheckDto import com.android.go.sopt.winey.data.model.remote.response.ResponseLoginDto +import com.android.go.sopt.winey.data.model.remote.response.ResponseLogoutDto import com.android.go.sopt.winey.data.model.remote.response.ResponseReIssueTokenDto import com.android.go.sopt.winey.domain.entity.Goal import com.android.go.sopt.winey.domain.entity.User @@ -20,5 +21,7 @@ interface AuthRepository { suspend fun postReIssueToken(refreshToken: String): Result + suspend fun postLogout(): Result + suspend fun getNicknameDuplicateCheck(nickname: String): Result } diff --git a/app/src/main/java/com/android/go/sopt/winey/presentation/main/MainActivity.kt b/app/src/main/java/com/android/go/sopt/winey/presentation/main/MainActivity.kt index 008a9e8b..b4ddf856 100644 --- a/app/src/main/java/com/android/go/sopt/winey/presentation/main/MainActivity.kt +++ b/app/src/main/java/com/android/go/sopt/winey/presentation/main/MainActivity.kt @@ -1,17 +1,26 @@ package com.android.go.sopt.winey.presentation.main +import android.content.Intent import android.os.Bundle import androidx.activity.viewModels import androidx.fragment.app.Fragment import androidx.fragment.app.commit import androidx.fragment.app.replace +import androidx.lifecycle.flowWithLifecycle +import androidx.lifecycle.lifecycleScope import com.android.go.sopt.winey.R import com.android.go.sopt.winey.databinding.ActivityMainBinding import com.android.go.sopt.winey.presentation.main.feed.WineyFeedFragment import com.android.go.sopt.winey.presentation.main.mypage.MyPageFragment import com.android.go.sopt.winey.presentation.main.recommend.RecommendFragment +import com.android.go.sopt.winey.presentation.onboarding.login.LoginActivity import com.android.go.sopt.winey.util.binding.BindingActivity +import com.android.go.sopt.winey.util.context.snackBar +import com.android.go.sopt.winey.util.view.UiState import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import retrofit2.HttpException @AndroidEntryPoint class MainActivity : BindingActivity(R.layout.activity_main) { @@ -23,6 +32,8 @@ class MainActivity : BindingActivity(R.layout.activity_main navigateTo() initBnvItemSelectedListener() syncBottomNavigationSelection() + setupLogoutState() + setupTokenState() } private fun initBnvItemSelectedListener() { @@ -51,9 +62,58 @@ class MainActivity : BindingActivity(R.layout.activity_main } } + fun setupLogoutState() { + viewModel.logoutState.flowWithLifecycle(lifecycle).onEach { state -> + when (state) { + is UiState.Loading -> { + } + + is UiState.Success -> { + navigateToLoginScreen() + } + + is UiState.Failure -> { + snackBar(binding.root) { state.msg } + } + + is UiState.Empty -> { + } + } + }.launchIn(lifecycleScope) + } + + fun setupTokenState() { + viewModel.getUserState.flowWithLifecycle(lifecycle).onEach { state -> + when (state) { + is UiState.Failure -> { + if (state is HttpException) { + if (state.code() == CODE_TOKEN_EXPIRED) { + viewModel.postLogout() + } + } + } + + else -> { + } + } + } + } + + private fun navigateToLoginScreen() { + Intent(this@MainActivity, LoginActivity::class.java).apply { + addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + startActivity(this) + finish() + } + } + private inline fun navigateTo() { supportFragmentManager.commit { replace(R.id.fcv_main, T::class.simpleName) } } + + companion object { + private const val CODE_TOKEN_EXPIRED = 401 + } } diff --git a/app/src/main/java/com/android/go/sopt/winey/presentation/main/MainViewModel.kt b/app/src/main/java/com/android/go/sopt/winey/presentation/main/MainViewModel.kt index 39378ce3..70763089 100644 --- a/app/src/main/java/com/android/go/sopt/winey/presentation/main/MainViewModel.kt +++ b/app/src/main/java/com/android/go/sopt/winey/presentation/main/MainViewModel.kt @@ -2,6 +2,7 @@ package com.android.go.sopt.winey.presentation.main import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.android.go.sopt.winey.data.model.remote.response.ResponseLogoutDto import com.android.go.sopt.winey.domain.entity.User import com.android.go.sopt.winey.domain.repository.AuthRepository import com.android.go.sopt.winey.domain.repository.DataStoreRepository @@ -23,9 +24,8 @@ class MainViewModel @Inject constructor( private val _getUserState = MutableStateFlow>(UiState.Loading) val getUserState: StateFlow> = _getUserState.asStateFlow() - init { - getUser() - } + private val _logoutState = MutableStateFlow>(UiState.Empty) + val logoutState: StateFlow> = _logoutState.asStateFlow() fun getUser() { viewModelScope.launch { @@ -39,11 +39,31 @@ class MainViewModel @Inject constructor( } .onFailure { t -> if (t is HttpException) { - Timber.e("HTTP 실패") + Timber.e("HTTP 실패 ${t.code()}") } Timber.e("${t.message}") _getUserState.value = UiState.Failure("${t.message}") } } } + + fun postLogout() { + viewModelScope.launch { + _logoutState.value = UiState.Loading + + authRepository.postLogout() + .onSuccess { response -> + dataStoreRepository.saveAccessToken("", "") + _logoutState.value = UiState.Success(response) + Timber.e("${response.message}") + } + .onFailure { t -> + if (t is HttpException) { + Timber.e("HTTP 실패") + } + Timber.e("${t.message}") + _logoutState.value = UiState.Failure("${t.message}") + } + } + } } diff --git a/app/src/main/java/com/android/go/sopt/winey/presentation/main/mypage/MyPageFragment.kt b/app/src/main/java/com/android/go/sopt/winey/presentation/main/mypage/MyPageFragment.kt index 9c98c8bf..d925ed19 100644 --- a/app/src/main/java/com/android/go/sopt/winey/presentation/main/mypage/MyPageFragment.kt +++ b/app/src/main/java/com/android/go/sopt/winey/presentation/main/mypage/MyPageFragment.kt @@ -37,6 +37,7 @@ class MyPageFragment : BindingFragment(R.layout.fragment_ init1On1ButtonClickListener() initLevelHelpButtonClickListener() initToMyFeedButtonClickListener() + initLogoutButtonClickListener() setupGetUserState() viewModel.getUser() } @@ -66,6 +67,12 @@ class MyPageFragment : BindingFragment(R.layout.fragment_ } } + private fun initLogoutButtonClickListener() { + binding.clMypageLogout.setOnClickListener { + viewModel.postLogout() + } + } + private fun setupGetUserState() { viewModel.getUserState.flowWithLifecycle(lifecycle).onEach { state -> diff --git a/app/src/main/java/com/android/go/sopt/winey/presentation/onboarding/login/LoginViewModel.kt b/app/src/main/java/com/android/go/sopt/winey/presentation/onboarding/login/LoginViewModel.kt index c626d88d..1d3eb69e 100644 --- a/app/src/main/java/com/android/go/sopt/winey/presentation/onboarding/login/LoginViewModel.kt +++ b/app/src/main/java/com/android/go/sopt/winey/presentation/onboarding/login/LoginViewModel.kt @@ -62,6 +62,7 @@ class LoginViewModel @Inject constructor( if (response != null) { saveAccessToken(response.accessToken, response.refreshToken) saveUserId(response.userId) + Timber.e("액세스 : ${response.accessToken} , 리프레시 : ${response.refreshToken}") _loginState.value = UiState.Success(response) } else { Timber.e("response is null") diff --git a/app/src/main/java/com/android/go/sopt/winey/presentation/splash/SplashActivity.kt b/app/src/main/java/com/android/go/sopt/winey/presentation/splash/SplashActivity.kt index 21f19002..43f0749f 100644 --- a/app/src/main/java/com/android/go/sopt/winey/presentation/splash/SplashActivity.kt +++ b/app/src/main/java/com/android/go/sopt/winey/presentation/splash/SplashActivity.kt @@ -9,7 +9,6 @@ import com.android.go.sopt.winey.databinding.ActivitySplashBinding import com.android.go.sopt.winey.domain.repository.DataStoreRepository import com.android.go.sopt.winey.presentation.main.MainActivity import com.android.go.sopt.winey.presentation.onboarding.login.LoginActivity -import com.android.go.sopt.winey.presentation.onboarding.nickname.NicknameActivity import com.android.go.sopt.winey.util.binding.BindingActivity import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.delay @@ -31,8 +30,7 @@ class SplashActivity : BindingActivity(R.layout.activity_ lifecycleScope.launch { delay(DELAY_TIME) - //checkAutoLogin() - navigateTo() + checkAutoLogin() } }