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

[REFACTOR/#213] 에러대응을 강화 합니다 #216

Open
wants to merge 3 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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 @@ -47,9 +47,9 @@ class AuthInterceptor @Inject constructor(
}

private fun shouldAddAuthorization(url: String): Boolean {
return !url.contains("api/v1/auth/signin") &&
!url.contains("api/v1/auth/signup") &&
!url.contains("api/v1/auth/reissue")
return !url.contains(AUTH_SIGNIN_URL) &&
!url.contains(AUTH_SIGNUP_URL) &&
!url.contains(AUTH_REISSUE_URL)
}

private fun addAuthorizationHeader(request: Request): Request {
Expand Down Expand Up @@ -95,6 +95,10 @@ class AuthInterceptor @Inject constructor(
runBlocking {
reissueTokenRepository.getReissueToken(tokenDataStore.refreshToken).onSuccess { data ->
updateTokens(data.accessToken, data.refreshToken)
}.onFailure { error ->
if (error.message?.contains(REFRESH_TOKEN_EXPIRED.toString()) == true) {
clearUserInfoAndNavigateToLogin()
}
}
}
true
Expand Down Expand Up @@ -126,7 +130,11 @@ class AuthInterceptor @Inject constructor(

companion object {
private const val TOKEN_EXPIRED = 401
private const val REFRESH_TOKEN_EXPIRED = 500
private const val BEARER = "Bearer"
private const val AUTHORIZATION = "Authorization"
private const val AUTH_SIGNIN_URL = "api/v1/auth/signin"
private const val AUTH_SIGNUP_URL = "api/v1/auth/signup"
private const val AUTH_REISSUE_URL = "api/v1/auth/reissue"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
Expand All @@ -30,9 +31,11 @@ class MainActivity : ComponentActivity() {
CLODYTheme {
val navController = rememberNavController()

if (intent.getBooleanExtra("NAVIGATE_TO_LOGIN", false)) {
navController.navigate("register_graph") {
popUpTo(0) { inclusive = true }
LaunchedEffect(key1 = intent.getBooleanExtra("NAVIGATE_TO_LOGIN", false)) {
if (intent.getBooleanExtra("NAVIGATE_TO_LOGIN", false)) {
navController.navigate("register_graph") {
popUpTo(0) { inclusive = true }
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,21 @@ package com.sopt.clody.presentation.ui.replydiary

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.sopt.clody.data.remote.dto.ResponseReplyDiaryDto
import com.sopt.clody.data.repository.ReplyDiaryRepository
import com.sopt.clody.presentation.utils.extension.throttleFirst
import com.sopt.clody.presentation.utils.network.ErrorMessages
import com.sopt.clody.presentation.utils.network.ErrorMessages.FAILURE_NETWORK_MESSAGE
import com.sopt.clody.presentation.utils.network.ErrorMessages.FAILURE_TEMPORARY_MESSAGE
import com.sopt.clody.presentation.utils.network.ErrorMessages.UNKNOWN_ERROR
import com.sopt.clody.presentation.utils.network.NetworkUtil
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject

@HiltViewModel
Expand All @@ -26,52 +32,70 @@ class ReplyDiaryViewModel @Inject constructor(
private var lastMonth: Int = 0
private var lastDate: Int = 0

private var retryCount = 0
private val maxRetryCount = 3
private val _retryFlow = MutableSharedFlow<Unit>() //연속 클릭을 제어하기 위해 선언.

fun getReplyDiary(year: Int, month: Int, date: Int) {
if (retryCount >= maxRetryCount) {
return
}
init {
setupRetryFlow()
}

private fun setupRetryFlow() {
_retryFlow
.throttleFirst(2000L) // 2초 동안 첫 번째 이벤트만 발행.
.onEach { // Flow에서 발생한 이벤트를 받아서 getReplyDiaryInternal 호출.
getReplyDiaryInternal(lastYear, lastMonth, lastDate)
}
.launchIn(viewModelScope) //Flow를 viewModelScope에서 실행하고 구독을 유지, 즉 viewmodel이 살아있는 동안 flow가 실행됨
}


fun getReplyDiary(year: Int, month: Int, date: Int) {
lastYear = year
lastMonth = month
lastDate = date
getReplyDiaryInternal(year, month, date)
}

private fun getReplyDiaryInternal(year: Int, month: Int, date: Int) {
viewModelScope.launch {
if (!networkUtil.isNetworkAvailable()) {
_replyDiaryState.value = ReplyDiaryState.Failure(FAILURE_NETWORK_MESSAGE)
updateState(ReplyDiaryState.Failure(FAILURE_NETWORK_MESSAGE))
return@launch
}

_replyDiaryState.value = ReplyDiaryState.Loading
updateState(ReplyDiaryState.Loading)

val result = replyDiaryRepository.getReplyDiary(year, month, date)
_replyDiaryState.value = result.fold(
onSuccess = { data ->
retryCount = 0
handleResult(result)
}
}

private fun handleResult(result: Result<ResponseReplyDiaryDto>) {
result.fold(
onSuccess = { data ->
updateState(
ReplyDiaryState.Success(
content = data.content ?: "", //content가 널이 아님을 보장함, 널이면 onFailure로 감
content = data.content ?: "",
nickname = data.nickname,
month = data.month,
date = data.date
)
},
onFailure = {
retryCount++
if (retryCount >= maxRetryCount) {
ReplyDiaryState.Failure(FAILURE_TEMPORARY_MESSAGE)
} else {
val message = it.localizedMessage ?: UNKNOWN_ERROR
ReplyDiaryState.Failure(message)
}
}
)
}
)
},
onFailure = { throwable ->
updateState(ReplyDiaryState.Failure(ErrorMessages.FAILURE_TEMPORARY_MESSAGE))
val errorMessage = throwable.localizedMessage ?: UNKNOWN_ERROR
Timber.tag("ReplyDiaryViewModel").e("API 요청 실패: %s", errorMessage)
}
)
}

private fun updateState(newState: ReplyDiaryState) {
_replyDiaryState.value = newState
}

fun retryLastRequest() {
if (retryCount < maxRetryCount) {
getReplyDiary(lastYear, lastMonth, lastDate)
viewModelScope.launch {
_retryFlow.emit(Unit)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,21 @@ package com.sopt.clody.presentation.ui.replyloading.screen

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.sopt.clody.data.remote.dto.response.ResponseDiaryTimeDto
import com.sopt.clody.data.repository.DiaryTimeRepository
import com.sopt.clody.presentation.utils.extension.throttleFirst
import com.sopt.clody.presentation.utils.network.ErrorMessages.FAILURE_NETWORK_MESSAGE
import com.sopt.clody.presentation.utils.network.ErrorMessages.FAILURE_TEMPORARY_MESSAGE
import com.sopt.clody.presentation.utils.network.ErrorMessages.UNKNOWN_ERROR
import com.sopt.clody.presentation.utils.network.NetworkUtil
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import timber.log.Timber
import java.time.LocalDateTime
import javax.inject.Inject

Expand All @@ -23,22 +29,33 @@ class ReplyLoadingViewModel @Inject constructor(
private val _replyLoadingState = MutableStateFlow<ReplyLoadingState>(ReplyLoadingState.Idle)
val replyLoadingState: StateFlow<ReplyLoadingState> = _replyLoadingState

private var retryCount = 0
private val maxRetryCount = 3

private var lastYear: Int = 0
private var lastMonth: Int = 0
private var lastDate: Int = 0

fun getDiaryTime(year: Int, month: Int, date: Int) {
if (retryCount >= maxRetryCount) {
return
}
private val _retryFlow = MutableSharedFlow<Unit>()

init {
setupRetryFlow()
}

private fun setupRetryFlow() {
_retryFlow
.throttleFirst(2000L)
.onEach {
getDiaryTimeInternal(lastYear, lastMonth, lastDate)
}
.launchIn(viewModelScope)
}

fun getDiaryTime(year: Int, month: Int, date: Int) {
lastYear = year
lastMonth = month
lastDate = date
getDiaryTimeInternal(year, month, date)
}

private fun getDiaryTimeInternal(year: Int, month: Int, date: Int) {
_replyLoadingState.value = ReplyLoadingState.Loading

viewModelScope.launch {
Expand All @@ -48,37 +65,36 @@ class ReplyLoadingViewModel @Inject constructor(
}

val result = diaryTimeRepository.getDiaryTime(year, month, date)
_replyLoadingState.value = result.fold(
onSuccess = { data ->
retryCount = 0
val targetDateTime = LocalDateTime.of(
year, month, date,
data.HH, data.mm, data.ss
).plusMinutes(if (data.isFirst) 1 else 12 * 60)

ReplyLoadingState.Success(targetDateTime)
},
onFailure = {
retryCount++
if (retryCount >= maxRetryCount) {
ReplyLoadingState.Failure(FAILURE_TEMPORARY_MESSAGE)
} else {
val message = if (it.message?.contains("200") == false) {
FAILURE_TEMPORARY_MESSAGE
} else {
it.localizedMessage ?: UNKNOWN_ERROR
}
ReplyLoadingState.Failure(message)
}
}
)
handleResult(result)
}
}

private fun handleResult(result: Result<ResponseDiaryTimeDto>) {
result.fold(
onSuccess = { data ->
val targetDateTime = LocalDateTime.of(
lastYear, lastMonth, lastDate,
data.HH, data.mm, data.ss
).plusMinutes(if (data.isFirst) INITIAL_REMINDER_MINUTES else REGULAR_REMINDER_HOURS * 60)

_replyLoadingState.value = ReplyLoadingState.Success(targetDateTime)
},
onFailure = { throwable ->
_replyLoadingState.value = ReplyLoadingState.Failure(FAILURE_TEMPORARY_MESSAGE)
val errorMessage = throwable.localizedMessage ?: UNKNOWN_ERROR
Timber.tag("ReplyLoadingViewModel").e("API 요청 실패: %s", errorMessage)
}
)
}

fun retryLastRequest() {
if (retryCount < maxRetryCount) {
_replyLoadingState.value = ReplyLoadingState.Loading
getDiaryTime(lastYear, lastMonth, lastDate)
viewModelScope.launch {
_retryFlow.emit(Unit)
}
}

companion object {
private const val INITIAL_REMINDER_MINUTES = 1L
private const val REGULAR_REMINDER_HOURS = 12L
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.sopt.clody.presentation.utils.extension

import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filter

fun <T> Flow<T>.throttleFirst(throttleTimeMillis: Long): Flow<T> {
var lastEmissionTime = 0L
return this.filter { // .filter을 통해 조건에 맞는 이벤트만 통과시킨다.
val currentTime = System.currentTimeMillis()
if (currentTime - lastEmissionTime >= throttleTimeMillis) {
lastEmissionTime = currentTime
true
} else {
false
}
}
}