Repository: EAT-SSU/Android
Pull request #284: [Modify] 스플래시에서 uiState와 uiEvent를 활용해서 분기처리하기
Author: login: HI-JIN2
Created: 2025-04-09T07:35:43Z | Merged: 2025-04-12T04:22:39Z

## Summary
<!-- 무슨 이유로 코드를 변경했는지 -->
<!-- 테스트 계획 또는 완료 사항 -->
로딩바를 하려다가... 그러면 UiState에서 Loading을 구분해야하는데 ... 생각해보니 IntroActivity에서는 로딩을 할게 없더라구요. 일단 best practice를 하나 만들어 두면 좋을 것 같아서, `IntroActivity` `IntroViewModel`을 선두로 유리님이 언급하신 SharedFlow를 적용해봤습니다.   

분기처리는 크게 
1. 토큰이 없는 경우  -> NoValidToken -> LoginAcitivity
2. 토큰이 있지만 유효하지 않은 경우 -> NoValidToken -> LoginActivity
3. 토큰이 있고 유효한 경우 -> Success -> MainActivity


기존에는 Intro에서 토큰 유효성 검증은 안하고 토큰 유무만 확인해었습니다!

## To reviewers
<!-- 어떤 위험이나 장애가 발견되었는지 -->
<!-- 어떤 부분에 리뷰어가 집중하면 좋을지 -->

- Handler로 2초 지연하는건 불필요할 거 같아서 뺐어요. uiState 변화 감지해서 화면 이동하는게 맞다고 생각해서 지웠습니다! 
- 말씀하신 SharedFlow가 이게 맞나요??
- 그리고 UiState를 공통 구조를 하나 만들어 놓고 상속해서 쓰는게 나을까요?? 아니면 이렇게 각 화면마다 따로 정의하는게 나을까요? (에러를 빼게 되면 공통되는 구조는 딱히 없지 않을까 싶습니다..!)
- 엑세스 토큰이 유효한지 체크하는 api는 아직 없어서 boolean을 반환하는 api로 일단 꼽아뒀습니다!
- 토큰 인터셉터에서 404 500처리를 잘못해서 이상한 오류가 나는건 아닌가 싶어서, 불필요한 처리가 저기 있는 것 같아 일단 주석처리 했습니다!


Diff:
diff --git a/app/src/main/java/com/eatssu/android/di/network/TokenInterceptor.kt b/app/src/main/java/com/eatssu/android/di/network/TokenInterceptor.kt
index 15b0b128a..189071bce 100644
--- a/app/src/main/java/com/eatssu/android/di/network/TokenInterceptor.kt
+++ b/app/src/main/java/com/eatssu/android/di/network/TokenInterceptor.kt
@@ -114,10 +114,6 @@ class TokenInterceptor @Inject constructor(
                     return chain.proceed(newRequest)
                 } else {
                     /**
-                     *
-                     *
-                     * refreshTokenResponse : Response{protocol=http/1.1, code=401, message=, url=https://prod.eat-ssu.shop/oauths/reissue/token}
-                     * 위 상황에서도 로그아웃
                      * 리프레쉬도 상한 상태
                      */
                     runBlocking { logoutUseCase() }
@@ -145,27 +141,27 @@ class TokenInterceptor @Inject constructor(
         }
 
         if (response.code == 404) {
-            runBlocking { logoutUseCase() }
+//            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)
-            }
+//            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() }
+//            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)
-            }
+//            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
diff --git a/app/src/main/java/com/eatssu/android/domain/usecase/auth/GetIsAccessTokenValidUseCase.kt b/app/src/main/java/com/eatssu/android/domain/usecase/auth/GetIsAccessTokenValidUseCase.kt
new file mode 100644
index 000000000..b689fea18
--- /dev/null
+++ b/app/src/main/java/com/eatssu/android/domain/usecase/auth/GetIsAccessTokenValidUseCase.kt
@@ -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 만들어지면 수정
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/eatssu/android/presentation/UiEvent.kt b/app/src/main/java/com/eatssu/android/presentation/UiEvent.kt
new file mode 100644
index 000000000..050b16ee6
--- /dev/null
+++ b/app/src/main/java/com/eatssu/android/presentation/UiEvent.kt
@@ -0,0 +1,10 @@
+package com.eatssu.android.presentation
+
+
+/**
+ * 각 Screen에 공통적인 이벤트 타입입니다.
+ * 이벤트 타입을 추가하고 싶다면 UiEvent를 상속받아 사용하세요.
+ */
+interface UiEvent {
+    data class ShowToast(val message: String) : UiEvent
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/eatssu/android/presentation/UiState.kt b/app/src/main/java/com/eatssu/android/presentation/UiState.kt
new file mode 100644
index 000000000..4a2f528f6
--- /dev/null
+++ b/app/src/main/java/com/eatssu/android/presentation/UiState.kt
@@ -0,0 +1,13 @@
+package com.eatssu.android.presentation
+
+sealed interface UiState<out T> {
+    object Init : UiState<Nothing>
+
+    object Loading : UiState<Nothing>
+
+    data class Success<out T>(
+        val data: T? = null,
+    ) : UiState<T>
+
+    object Error : UiState<Nothing>
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/eatssu/android/presentation/login/IntroActivity.kt b/app/src/main/java/com/eatssu/android/presentation/login/IntroActivity.kt
index 1f2b3fdb7..9ad17a90b 100644
--- a/app/src/main/java/com/eatssu/android/presentation/login/IntroActivity.kt
+++ b/app/src/main/java/com/eatssu/android/presentation/login/IntroActivity.kt
@@ -1,13 +1,14 @@
 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.UiEvent
+import com.eatssu.android.presentation.UiState
 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
@@ -17,36 +18,39 @@ 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)
+        binding = ActivityIntroBinding.inflate(layoutInflater)
+        setContentView(binding.root)
 
-        // 일정 시간 지연 이후 실행하기 위한 코드
-        Handler(Looper.getMainLooper()).postDelayed({
-
-            introViewModel.autoLogin()
-
-            lifecycleScope.launch {
-                introViewModel.uiState.collectLatest {
-                    if (it.isAutoLogined) {
+        lifecycleScope.launch {
+            introViewModel.uiState.collectLatest { state ->
+                when (state) {
+                    is UiState.Success -> {
                         startActivity<MainActivity>()
-
-                        // 이전 키를 눌렀을 때 스플래스 스크린 화면으로 이동을 방지하기 위해
-                        // 이동한 다음 사용안함으로 finish 처리
                         finish()
-                    } else {
-                        startActivity<LoginActivity>()
+                    }
 
-                        // 이전 키를 눌렀을 때 스플래스 스크린 화면으로 이동을 방지하기 위해
-                        // 이동한 다음 사용안함으로 finish 처리
+                    is UiState.Error -> {
+                        // 로그인 액티비티로 이동
+                        startActivity<LoginActivity>()
                         finish()
                     }
 
+                    else -> Unit
                 }
             }
 
-        }, 2000) // 시간 2초 이후 실행
-
+            introViewModel.uiEvent.collectLatest { event ->
+                when (event) {
+                    is UiEvent.ShowToast -> {
+                        // 에러 메시지 표시
+                        showToast(event.message)
+                    }
+                }
+            }
+        }
     }
 }
\ No newline at end of file
diff --git a/app/src/main/java/com/eatssu/android/presentation/login/IntroViewModel.kt b/app/src/main/java/com/eatssu/android/presentation/login/IntroViewModel.kt
index 5dabe8a24..68a0992c7 100644
--- a/app/src/main/java/com/eatssu/android/presentation/login/IntroViewModel.kt
+++ b/app/src/main/java/com/eatssu/android/presentation/login/IntroViewModel.kt
@@ -3,41 +3,70 @@ 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 com.eatssu.android.presentation.UiEvent
+import com.eatssu.android.presentation.UiState
 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<UiState<IntroState>> = MutableStateFlow(UiState.Init)
+    val uiState: StateFlow<UiState<IntroState>> = _uiState.asStateFlow()
+
+    private val _uiEvent = MutableSharedFlow<UiEvent>()
+    val uiEvent: SharedFlow<UiEvent> = _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 = UiState.Loading
+
+            try {
+                // 토큰 존재 여부 확인
+                if (getAccessTokenUseCase().isEmpty()) {
+                    _uiState.value = UiState.Error
+                    _uiEvent.emit(UiEvent.ShowToast("로그인이 필요합니다"))
+                    return@launch
+                }
+
+                checkValid()
+
+            } catch (e: Exception) {
+                _uiState.value = UiState.Error
+                _uiEvent.emit(UiEvent.ShowToast("오류가 발생했습니다: ${e.message}"))
             }
         }
     }
 
+    private fun checkValid() {
+        viewModelScope.launch {
+            getIsAccessTokenValidUseCase()
+                .collect {
+                    if (it.result == true) { //토큰이 있고 유효함
+                        _uiState.value = UiState.Success(IntroState.ValidToken)
+                    } else { //토큰이 있어도 유효하지 않음
+                        _uiState.value = UiState.Error
+                        _uiEvent.emit(UiEvent.ShowToast("로그인이 필요합니다"))
+                    }
+                }
+        }
+    }
 }
 
-data class IntroState(
-    var toastMessage: String = "",
-    var loading: Boolean = true,
-    var error: Boolean = false,
-    var isAutoLogined: Boolean = false,
-)
\ No newline at end of file
+sealed class IntroState {
+    object ValidToken : IntroState()
+}


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: 

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.




state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-09T07:36:13Z
user: login: kangyuri1114
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-09T14:34:42Z
user: login: HI-JIN2
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-10T05:08:22Z
user: login: HI-JIN2
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-10T05:52:36Z
user: login: HI-JIN2
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-10T06:03:45Z
user: login: kangyuri1114
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-11T05:06:02Z
user: login: kangyuri1114
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-11T05:08:47Z

Review comments:
diff_hunk: @@ -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 만들어지면 수정
path: app/src/main/java/com/eatssu/android/domain/usecase/auth/GetIsAccessTokenValidUseCase.kt
user: login: Copilot
body: 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.
created_at: 2025-04-09T07:36:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -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()
path: app/src/main/java/com/eatssu/android/presentation/login/IntroViewModel.kt
user: login: kangyuri1114
body: 이렇게 모든 화면에 대해서 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 금방 적용하지 않을까 싶은데.. 로딩이라도 일단?

created_at: 2025-04-09T14:31:47Z
author_association: MEMBER
diff_hunk: @@ -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>()
path: app/src/main/java/com/eatssu/android/presentation/login/IntroActivity.kt
user: login: kangyuri1114
body: ```
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
}

```

아래 처럼 적용하면 여기도 이런 식으루?
created_at: 2025-04-09T14:34:16Z
author_association: MEMBER
diff_hunk: @@ -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()
 }
 
-data class IntroState(
-    var toastMessage: String = "",
-    var loading: Boolean = true,
-    var error: Boolean = false,
-    var isAutoLogined: Boolean = false,
-)
\ No newline at end of file
+sealed class IntroUiEvent {
+    data class ShowToast(val error: String) : IntroUiEvent()
+}
path: app/src/main/java/com/eatssu/android/presentation/login/IntroViewModel.kt
user: login: kangyuri1114
body: 그리고 이벤트 분리하자는건 이런 식 맞습니다!! 굿
created_at: 2025-04-09T14:34:38Z
author_association: MEMBER
diff_hunk: @@ -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()
path: app/src/main/java/com/eatssu/android/presentation/login/IntroViewModel.kt
user: login: HI-JIN2
body: 오오 좋습니다. UiState는 저렇게 제네릭으로 쓰고 UiEvent는 일단 Toast만 해서 이것도 돌려 쓸 수 있게 바꿔볼게요! 

근데 한가지 고민은, UiState의 Failure(=Error)와 UiEvent의 Toast가 결국은 같은 에러 상황에서 쓰이는건데 UiState랑 UiEvent를 같은 상황에 대해서 둘다 써야할까요?
created_at: 2025-04-10T05:08:22Z
author_association: MEMBER
diff_hunk: @@ -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()
path: app/src/main/java/com/eatssu/android/presentation/login/IntroViewModel.kt
user: login: HI-JIN2
body: UiEvent에 Toast를 넣은 이유는 성공의 경우에도 토스트를 띄우는 액션이 있어서 그렇게 했습니다. 딱히 성공/실패랑 연관이 있는 액션은 아니라고 생각해서요!
created_at: 2025-04-10T05:52:36Z
author_association: MEMBER
diff_hunk: @@ -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>()
path: app/src/main/java/com/eatssu/android/presentation/login/IntroActivity.kt
user: login: HI-JIN2
body: 음... 근데 NoValidToken는 성공은 아니라서.. 모델링 이거 어렵네요ㅜㅜ
created_at: 2025-04-10T06:03:45Z
author_association: MEMBER
diff_hunk: @@ -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()
path: app/src/main/java/com/eatssu/android/presentation/login/IntroViewModel.kt
user: login: kangyuri1114
body: 넵 저도 그렇게 생각해요! 모든 에러 경우에서 토스트를 띄우는 상황이 아니라면 uistate 와는 별개로 쓰는 게 좋을 것 같습니다!
지금 반영해주신 구조가 좋을 것 같습니다 👍 
created_at: 2025-04-11T05:06:01Z
author_association: MEMBER
diff_hunk: @@ -3,41 +3,70 @@ 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 com.eatssu.android.presentation.UiEvent
+import com.eatssu.android.presentation.UiState
 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<UiState<IntroState>> = MutableStateFlow(UiState.Init)
+    val uiState: StateFlow<UiState<IntroState>> = _uiState.asStateFlow()
+
+    private val _uiEvent = MutableSharedFlow<UiEvent>()
+    val uiEvent: SharedFlow<UiEvent> = _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 = UiState.Loading
+
+            try {
+                // 토큰 존재 여부 확인
+                if (getAccessTokenUseCase().isEmpty()) {
+                    _uiState.value = UiState.Error
+                    _uiEvent.emit(UiEvent.ShowToast("로그인이 필요합니다"))
+                    return@launch
+                }
+
+                checkValid()
+
+            } catch (e: Exception) {
+                _uiState.value = UiState.Error
+                _uiEvent.emit(UiEvent.ShowToast("오류가 발생했습니다: ${e.message}"))
             }
         }
     }
 
+    private fun checkValid() {
+        viewModelScope.launch {
+            getIsAccessTokenValidUseCase()
+                .collect {
+                    if (it.result == true) { //토큰이 있고 유효함
+                        _uiState.value = UiState.Success(IntroState.ValidToken)
+                    } else { //토큰이 있어도 유효하지 않음
+                        _uiState.value = UiState.Error
path: app/src/main/java/com/eatssu/android/presentation/login/IntroViewModel.kt
user: login: kangyuri1114
body: 저도 여기 Error 로 두는 것 좋은 것 같아요~
created_at: 2025-04-11T05:08:39Z
author_association: MEMBER

PR comments:
user: login: kangyuri1114
created_at: 2025-04-09T14:38:59Z
body:  404 500처리 혹시 모르니 완전 주석처리 보다는 조건문 안에 로그라도 두는 거 어떨까요
author_association: MEMBER
user: login: HI-JIN2
created_at: 2025-04-10T06:23:27Z
body: > 404 500처리 혹시 모르니 완전 주석처리 보다는 조건문 안에 로그라도 두는 거 어떨까요

404랑 500은 토큰 이슈랑은 정말 상관이 없는 문제 같긴한데.. 로그는 남겨 두겠습니다. 
author_association: MEMBER
user: login: HI-JIN2
created_at: 2025-04-10T06:29:47Z
body: @kangyuri1114 최종 코드 두었습니다! 
UiState<T> 안에 Success를 제네릭으로 끼우다보니 뷰에 성공일 때 넘겨줄 데이터가 없어도 해당 화면의 State를 정의해놔야하더라구요... 그래서 IntroState에는 ValidToken 하나 두었습니다.

그리고 NoValideToken은 토큰이 있는데 유효하지 않은 경우인데요, 이 경우나 아예 토큰이 없는 경우이나 차피 로그인 화면으로 가야하는건 똑같고, 또 NoValideToken은 Success로 정의하는건 아닌 것 같아서 그냥 공통으로 Error로 두었습니다.

그리고 공통 구조에서 Error시 메시지 전달은 뺐는데, 그 message의 역할이 없다고 생각해서 뺐어요. 에러 메시지는 차피 UiEvent의 토스트를 통해서 보여줄거라서 필요 없을 것 같은데, 유리님은 어떻게 생각하시낭용??!?
author_association: MEMBER