Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feat] 온보딩 / 로그아웃 서버통신 구현 #112

Merged
merged 15 commits into from
Aug 15, 2023
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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<ResponseReIssueTokenDto>

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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

함수화 굿!!

} catch (t: Throwable) {
Timber.e(t)
Timber.e("예외발생 ${t.message}")
saveAccessToken("", "")
}
}
Expand All @@ -73,14 +54,44 @@ class AuthInterceptor @Inject constructor(
}

private suspend fun getRefreshToken(): String {
return dataStoreRepository.getAccessToken().first() ?: ""
return dataStoreRepository.getRefreshToken().first() ?: ""
}

private fun saveAccessToken(accessToken: String, refreshToken: String) =
runBlocking {
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<ResponseReIssueTokenDto>
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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
)
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -37,6 +38,11 @@ class AuthRepositoryImpl @Inject constructor(
authDataSource.postReIssueToken(refreshToken).data
}

override suspend fun postLogout(): Result<ResponseLogoutDto> =
runCatching {
authDataSource.postLogout()
}

override suspend fun getNicknameDuplicateCheck(nickname: String): Result<ResponseGetNicknameDuplicateCheckDto?> =
runCatching {
authDataSource.getNicknameDuplicateCheck(nickname).data
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -34,6 +35,9 @@ interface AuthService {
@Header("refreshToken") refreshToken: String
): BaseResponse<ResponseReIssueTokenDto>

@POST("auth/sign-out")
suspend fun postLogout(): ResponseLogoutDto

@GET("user/nickname/is-exist")
suspend fun getNicknameDuplicateCheck(
@Query("nickname") nickname: String
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -30,6 +31,8 @@ class AuthDataSource @Inject constructor(
): BaseResponse<ResponseReIssueTokenDto> =
authService.postReIssueToken(refreshToken)

suspend fun postLogout(): ResponseLogoutDto = authService.postLogout()

suspend fun getNicknameDuplicateCheck(nickname: String): BaseResponse<ResponseGetNicknameDuplicateCheckDto> =
authService.getNicknameDuplicateCheck(nickname)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -20,5 +21,7 @@ interface AuthRepository {

suspend fun postReIssueToken(refreshToken: String): Result<ResponseReIssueTokenDto?>

suspend fun postLogout(): Result<ResponseLogoutDto>

suspend fun getNicknameDuplicateCheck(nickname: String): Result<ResponseGetNicknameDuplicateCheckDto?>
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,27 @@
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
import timber.log.Timber

@AndroidEntryPoint
class MainActivity : BindingActivity<ActivityMainBinding>(R.layout.activity_main) {
Expand All @@ -23,6 +33,8 @@ class MainActivity : BindingActivity<ActivityMainBinding>(R.layout.activity_main
navigateTo<WineyFeedFragment>()
initBnvItemSelectedListener()
syncBottomNavigationSelection()
setupLogoutState()
setupTokenState()
}

private fun initBnvItemSelectedListener() {
Expand Down Expand Up @@ -51,6 +63,52 @@ class MainActivity : BindingActivity<ActivityMainBinding>(R.layout.activity_main
}
}

fun setupLogoutState() {
viewModel.logoutState.flowWithLifecycle(lifecycle).onEach { state ->
when (state) {
is UiState.Loading -> {
}

is UiState.Success -> {
navigateToOnBoardingScreen()
Timber.e("로그인 액티비티로 전환")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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() == 401) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

401이 어떤 코드인지 알 수 있게 CODE_[what] 으로 상수화 시켜주는 게 좋을 거 같아요!

viewModel.postLogout()
}
}
}

else -> {
}
}
}
}

private fun navigateToOnBoardingScreen() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이름을 navigateToLoginScreen() 으로 바꾸는 게 좋을 거 같아요!

Intent(this@MainActivity, LoginActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
startActivity(this)
finish()
}
}

private inline fun <reified T : Fragment> navigateTo() {
supportFragmentManager.commit {
replace<T>(R.id.fcv_main, T::class.simpleName)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -23,9 +24,8 @@ class MainViewModel @Inject constructor(
private val _getUserState = MutableStateFlow<UiState<User?>>(UiState.Loading)
val getUserState: StateFlow<UiState<User?>> = _getUserState.asStateFlow()

init {
getUser()
}
private val _logoutState = MutableStateFlow<UiState<ResponseLogoutDto?>>(UiState.Empty)
val logoutState: StateFlow<UiState<ResponseLogoutDto?>> = _logoutState.asStateFlow()

fun getUser() {
viewModelScope.launch {
Expand All @@ -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}")
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 로직을 MainViewModel 말고, MyPageViewModel 에서 구현하는 건 어떨까요??

보통 activityViewModels 로 선언한 뷰모델은 여러 프래그먼트가 공유해서 사용하게 되는데, 로그아웃은 여러 프래그먼트에서 공통적으로 수행하는 동작이 아니므로, MainViewModel 보다는 MyPageViewModel 에서 로그아웃 로직을 처리하는 게 더 자연스럽다고 느껴져요!

MyPageViewModel 이 아니라, MainViewModel 에서 로그아웃 로직을 처리한 이유가 따로 있을까요??

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

마이페이지 뿐만아니라, 서버통신시 리프레시토큰이 만료될 경우 로그아웃 처리를 해주어야해서 mainviewmodel에서 로직을 처리해 주었습니다. mypageviewmodel에서 처리를 한다면, 위니피드나 추천피드에서 서버통신 시 리프레시 토큰이 만료되었을경우엔 처리를 제대로 해주지 못할 것 같아서 mainviewmodel에서 구현했습니다 !

Copy link
Member

@leeeha leeeha Aug 15, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아하! 리프레시 토큰이 만료되는 경우에도 로그아웃 해줘야 하는군요!! 답변 감사합니당 👍

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ class MyPageFragment : BindingFragment<FragmentMyPageBinding>(R.layout.fragment_
init1On1ButtonClickListener()
initLevelHelpButtonClickListener()
initToMyFeedButtonClickListener()
initLogoutButtonClickListener()
setupGetUserState()
viewModel.getUser()
}
Expand Down Expand Up @@ -66,6 +67,12 @@ class MyPageFragment : BindingFragment<FragmentMyPageBinding>(R.layout.fragment_
}
}

private fun initLogoutButtonClickListener() {
binding.clMypageLogout.setOnClickListener {
viewModel.postLogout()
}
}

private fun setupGetUserState() {
viewModel.getUserState.flowWithLifecycle(lifecycle).onEach { state ->

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -31,8 +30,7 @@ class SplashActivity : BindingActivity<ActivitySplashBinding>(R.layout.activity_

lifecycleScope.launch {
delay(DELAY_TIME)
//checkAutoLogin()
navigateTo<NicknameActivity>()
checkAutoLogin()
}
}

Expand Down