Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
46 changes: 23 additions & 23 deletions app/src/main/java/com/eatssu/android/di/network/TokenInterceptor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -144,29 +144,29 @@ class TokenInterceptor @Inject constructor(
}
}

if (response.code == 404) {
runBlocking { logoutUseCase() }
Timber.e("404 + 다른 유저!")

Handler(Looper.getMainLooper()).post {
Toast.makeText(context, "토큰이 만료되어 로그아웃 됩니다.", Toast.LENGTH_SHORT).show()
val intent = Intent(context, LoginActivity::class.java) // 로그인 화면으로 이동
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
context.startActivity(intent)
}
}

if (response.code == 500) {
runBlocking { logoutUseCase() }
Timber.e("500 + 다른 유저")

Handler(Looper.getMainLooper()).post {
Toast.makeText(context, "토큰이 만료되어 로그아웃 됩니다.", Toast.LENGTH_SHORT).show()
val intent = Intent(context, LoginActivity::class.java) // 로그인 화면으로 이동
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
context.startActivity(intent)
}
}
// if (response.code == 404) {
// runBlocking { logoutUseCase() }
// Timber.e("404 + 다른 유저!")
//
// Handler(Looper.getMainLooper()).post {
// Toast.makeText(context, "토큰이 만료되어 로그아웃 됩니다.", Toast.LENGTH_SHORT).show()
// val intent = Intent(context, LoginActivity::class.java) // 로그인 화면으로 이동
// intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
// context.startActivity(intent)
// }
// }
//
// if (response.code == 500) {
// runBlocking { logoutUseCase() }
// Timber.e("500 + 다른 유저")
//
// Handler(Looper.getMainLooper()).post {
// Toast.makeText(context, "토큰이 만료되어 로그아웃 됩니다.", Toast.LENGTH_SHORT).show()
// val intent = Intent(context, LoginActivity::class.java) // 로그인 화면으로 이동
// intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
// context.startActivity(intent)
// }
// }

return response
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.eatssu.android.domain.usecase.auth

import com.eatssu.android.data.dto.response.BaseResponse
import com.eatssu.android.domain.repository.UserRepository
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject

class GetIsAccessTokenValidUseCase @Inject constructor(
private val userRepository: UserRepository,
) {
suspend operator fun invoke(): Flow<BaseResponse<Boolean>> =
userRepository.checkUserNameValidation("qkqh") //todo api 만들어지면 수정
Copy link

Copilot AI Apr 9, 2025

Choose a reason for hiding this comment

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

The use-case currently calls 'checkUserNameValidation', which does not align with token validation. Update the repository method to reflect token validation once the proper API is available.

Copilot uses AI. Check for mistakes.
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
package com.eatssu.android.presentation.login

import android.os.Bundle
import android.os.Handler
import android.os.Looper
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.eatssu.android.R
import com.eatssu.android.databinding.ActivityIntroBinding
import com.eatssu.android.presentation.main.MainActivity
import com.eatssu.android.presentation.util.showToast
import com.eatssu.android.presentation.util.startActivity
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.collectLatest
Expand All @@ -17,36 +16,42 @@ import kotlinx.coroutines.launch
class IntroActivity : AppCompatActivity() {

private val introViewModel: IntroViewModel by viewModels()
private lateinit var binding: ActivityIntroBinding

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_intro)

// 일정 시간 지연 이후 실행하기 위한 코드
Handler(Looper.getMainLooper()).postDelayed({

introViewModel.autoLogin()
binding = ActivityIntroBinding.inflate(layoutInflater)
setContentView(binding.root)

lifecycleScope.launch {
introViewModel.uiState.collectLatest { state ->
when (state) {
is IntroUiState.Loading -> {
// 할게 없는뎅? 그냥 뷰 보여주기
}

lifecycleScope.launch {
introViewModel.uiState.collectLatest {
if (it.isAutoLogined) {
is IntroUiState.Success -> {
// 메인 액티비티로 이동
startActivity<MainActivity>()

// 이전 키를 눌렀을 때 스플래스 스크린 화면으로 이동을 방지하기 위해
// 이동한 다음 사용안함으로 finish 처리
finish()
} else {
startActivity<LoginActivity>()
}

// 이전 키를 눌렀을 때 스플래스 스크린 화면으로 이동을 방지하기 위해
// 이동한 다음 사용안함으로 finish 처리
is IntroUiState.NoValidToken -> {
// 로그인 액티비티로 이동
startActivity<LoginActivity>()
Copy link
Member

Choose a reason for hiding this comment

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

when (val state = uiState) {
    is UiState.Loading -> 그냥 뷰 보여주기
    is UiState.Failure -> showToast(state.errorMessage)
    is UiState.Success -> {
        when (state.data) {
            IntroState.ValidToken -> startActivity<MainActivity>()
            IntroState.NoValidToken -> startActivity<LoginActivity>()
        }
    }
    UiState.Initial -> Unit
}

아래 처럼 적용하면 여기도 이런 식으루?

Copy link
Member Author

@HI-JIN2 HI-JIN2 Apr 10, 2025

Choose a reason for hiding this comment

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

음... 근데 NoValidToken는 성공은 아니라서.. 모델링 이거 어렵네요ㅜㅜ

finish()
}

}
}

}, 2000) // 시간 2초 이후 실행

introViewModel.uiEvent.collectLatest { event ->
when (event) {
is IntroUiEvent.ShowToast -> {
// 에러 메시지 표시
showToast(event.error)
}
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,41 +3,74 @@ package com.eatssu.android.presentation.login
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.eatssu.android.domain.usecase.auth.GetAccessTokenUseCase
import com.eatssu.android.domain.usecase.auth.GetIsAccessTokenValidUseCase
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject

@HiltViewModel
class IntroViewModel @Inject constructor(
private val getAccessTokenUseCase: GetAccessTokenUseCase
private val getAccessTokenUseCase: GetAccessTokenUseCase,
private val getIsAccessTokenValidUseCase: GetIsAccessTokenValidUseCase
) : ViewModel() {

private val _uiState: MutableStateFlow<IntroState> = MutableStateFlow(IntroState())
val uiState: StateFlow<IntroState> = _uiState.asStateFlow()
private val _uiState: MutableStateFlow<IntroUiState> = MutableStateFlow(IntroUiState.Loading)
val uiState: StateFlow<IntroUiState> = _uiState.asStateFlow()

private val _uiEvent = MutableSharedFlow<IntroUiEvent>()
val uiEvent: SharedFlow<IntroUiEvent> = _uiEvent

init {
autoLogin()
}

fun autoLogin() {
private fun autoLogin() {
viewModelScope.launch {
if (getAccessTokenUseCase().isEmpty()) {
_uiState.update { it.copy(isAutoLogined = false) }
} else {
_uiState.update { it.copy(isAutoLogined = true) }
_uiState.value = IntroUiState.Loading

try {
// 토큰 존재 여부 확인
if (getAccessTokenUseCase().isEmpty()) {
_uiState.value = IntroUiState.NoValidToken
_uiEvent.emit(IntroUiEvent.ShowToast("로그인이 필요합니다"))
return@launch
}

checkValid()

} catch (e: Exception) {
_uiState.value = IntroUiState.NoValidToken
_uiEvent.emit(IntroUiEvent.ShowToast("오류가 발생했습니다: ${e.message}"))
}
}
}

private fun checkValid() {
viewModelScope.launch {
getIsAccessTokenValidUseCase()
.collect {
if (it.result == true) { //토큰이 있고 유효함
_uiState.value = IntroUiState.Success
} else { //토큰이 있어도 유효하지 않음
_uiState.value = IntroUiState.NoValidToken
_uiEvent.emit(IntroUiEvent.ShowToast("로그인이 필요합니다"))
}
}
}
}
}

sealed class IntroUiState {
object Loading : IntroUiState()
object Success : IntroUiState()
object NoValidToken : IntroUiState()
Copy link
Member

Choose a reason for hiding this comment

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

이렇게 모든 화면에 대해서 UIState를 만들면 모든 ViewModel에서 같은 구조의 클래스(로딩, 성공, 실패, 초기 등)을 만드는 게 귀찮을 거 같긴한데..

최상위 uistate를 만드는 건 어떨까요..?

sealed interface UiState<out T> {
    object Initial : UiState<Nothing>

    object Loading : UiState<Nothing>

    data class Success<out T>(
        val data: T? = null,
    ) : UiState<T>

    data class Failure(
        val errorMessage: String,
    ) : UiState<Nothing>
}

이 인터페이스를 사용해서 제네릭 부분에 각 화면의 state를 넣어주는 거에요
전 이렇게 한번 해봤던 적이 있어서,,!

NoValidToken 같은 각 화면에 해당하는 state는 IntroState로 따로 만들어서

sealed class IntroState {
    object NoValidToken : IntroState()
    object ValidToken : IntroState()
}

이런식으로 사용하면 어떨까요??

private val _uiState = MutableStateFlow<UiState<IntroState>>(UiState.Loading)
val uiState: StateFlow<UiState<IntroState>> = _uiState.asStateFlow()

그럼 메뉴화면에서도 uistate 금방 적용하지 않을까 싶은데.. 로딩이라도 일단?

Copy link
Member Author

Choose a reason for hiding this comment

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

오오 좋습니다. UiState는 저렇게 제네릭으로 쓰고 UiEvent는 일단 Toast만 해서 이것도 돌려 쓸 수 있게 바꿔볼게요!

근데 한가지 고민은, UiState의 Failure(=Error)와 UiEvent의 Toast가 결국은 같은 에러 상황에서 쓰이는건데 UiState랑 UiEvent를 같은 상황에 대해서 둘다 써야할까요?

Copy link
Member Author

Choose a reason for hiding this comment

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

UiEvent에 Toast를 넣은 이유는 성공의 경우에도 토스트를 띄우는 액션이 있어서 그렇게 했습니다. 딱히 성공/실패랑 연관이 있는 액션은 아니라고 생각해서요!

Copy link
Member

Choose a reason for hiding this comment

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

넵 저도 그렇게 생각해요! 모든 에러 경우에서 토스트를 띄우는 상황이 아니라면 uistate 와는 별개로 쓰는 게 좋을 것 같습니다!
지금 반영해주신 구조가 좋을 것 같습니다 👍

}

data class IntroState(
var toastMessage: String = "",
var loading: Boolean = true,
var error: Boolean = false,
var isAutoLogined: Boolean = false,
)
sealed class IntroUiEvent {
data class ShowToast(val error: String) : IntroUiEvent()
}
Copy link
Member

Choose a reason for hiding this comment

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

그리고 이벤트 분리하자는건 이런 식 맞습니다!! 굿