diff --git a/app/src/main/java/com/example/hangsha_android/ui/navigation/HangshaNavHost.kt b/app/src/main/java/com/example/hangsha_android/ui/navigation/HangshaNavHost.kt index 486e28a..b3ed8a2 100644 --- a/app/src/main/java/com/example/hangsha_android/ui/navigation/HangshaNavHost.kt +++ b/app/src/main/java/com/example/hangsha_android/ui/navigation/HangshaNavHost.kt @@ -60,6 +60,9 @@ import com.example.hangsha_android.ui.view.onboarding.OnboardingViewModel import com.example.hangsha_android.ui.view.onboarding.OnboardingWelcomeScreen import com.example.hangsha_android.ui.view.signup.SignUpScreen import com.example.hangsha_android.ui.view.signup.SignUpViewModel +import com.example.hangsha_android.ui.view.splash.SplashNavigationTarget +import com.example.hangsha_android.ui.view.splash.SplashScreen +import com.example.hangsha_android.ui.view.splash.SplashViewModel import com.example.hangsha_android.ui.view.timetable.TimetableScreen import com.google.android.gms.auth.api.signin.GoogleSignIn import com.google.android.gms.auth.api.signin.GoogleSignInOptions @@ -70,6 +73,7 @@ import com.kakao.sdk.common.model.ClientErrorCause import com.kakao.sdk.user.UserApiClient sealed class HangshaDestinations(val route: String) { + data object Splash : HangshaDestinations("splash") data object Login : HangshaDestinations("login") data object CredentialLogin : HangshaDestinations("credential_login") data object SignUp : HangshaDestinations("sign_up") @@ -116,9 +120,10 @@ fun HangshaNavHost( ) { NavHost( navController = navController, - startDestination = HangshaDestinations.Login.route, + startDestination = HangshaDestinations.Splash.route, modifier = Modifier.padding(innerPadding) ) { + splashGraph(navController = navController) loginGraph(navController = navController) signUpGraph(navController = navController) onboardingGraph(navController = navController) @@ -126,6 +131,33 @@ fun HangshaNavHost( } } +fun NavGraphBuilder.splashGraph(navController: NavHostController) { + composable(HangshaDestinations.Splash.route) { + val splashViewModel: SplashViewModel = hiltViewModel() + val splashUiState by splashViewModel.uiState.collectAsState() + + LaunchedEffect(splashUiState.navigationTarget) { + when (splashUiState.navigationTarget) { + SplashNavigationTarget.Calendar -> { + navController.navigate(HangshaDestinations.Main.route) { + popUpTo(HangshaDestinations.Splash.route) { inclusive = true } + launchSingleTop = true + } + } + SplashNavigationTarget.Login -> { + navController.navigate(HangshaDestinations.Login.route) { + popUpTo(HangshaDestinations.Splash.route) { inclusive = true } + launchSingleTop = true + } + } + null -> Unit + } + } + + SplashScreen() + } +} + fun NavGraphBuilder.loginGraph(navController: NavHostController) { composable(HangshaDestinations.Login.route) { val loginViewModel: LoginViewModel = hiltViewModel() @@ -180,10 +212,6 @@ fun NavGraphBuilder.loginGraph(navController: NavHostController) { } } - LaunchedEffect(Unit) { - loginViewModel.tryAutoLogin() - } - LaunchedEffect(loginUiState.isLoginSuccessful) { if (!loginUiState.isLoginSuccessful) { return@LaunchedEffect diff --git a/app/src/main/java/com/example/hangsha_android/ui/view/login/LoginUiState.kt b/app/src/main/java/com/example/hangsha_android/ui/view/login/LoginUiState.kt index a7549f3..7abdc72 100644 --- a/app/src/main/java/com/example/hangsha_android/ui/view/login/LoginUiState.kt +++ b/app/src/main/java/com/example/hangsha_android/ui/view/login/LoginUiState.kt @@ -3,7 +3,6 @@ package com.example.hangsha_android.ui.view.login data class LoginUiState( val username: String = "", val password: String = "", - val isAutoLoginLoading: Boolean = false, val isCredentialLoginLoading: Boolean = false, val isGoogleLoginLoading: Boolean = false, val isKakaoLoginLoading: Boolean = false, @@ -13,8 +12,7 @@ data class LoginUiState( val loginMessage: String? = null ) { val isAnyLoginLoading: Boolean - get() = isAutoLoginLoading || - isCredentialLoginLoading || + get() = isCredentialLoginLoading || isGoogleLoginLoading || isKakaoLoginLoading || isNaverLoginLoading diff --git a/app/src/main/java/com/example/hangsha_android/ui/view/login/LoginViewModel.kt b/app/src/main/java/com/example/hangsha_android/ui/view/login/LoginViewModel.kt index c90eded..69c00d0 100644 --- a/app/src/main/java/com/example/hangsha_android/ui/view/login/LoginViewModel.kt +++ b/app/src/main/java/com/example/hangsha_android/ui/view/login/LoginViewModel.kt @@ -32,8 +32,6 @@ class LoginViewModel @Inject constructor( private val _uiState = MutableStateFlow(LoginUiState()) val uiState: StateFlow = _uiState.asStateFlow() - private var hasAttemptedAutoLogin = false - private var isGuestModeRequested = false fun onUsernameChanged(username: String) { _uiState.update { @@ -53,52 +51,6 @@ class LoginViewModel @Inject constructor( } } - fun tryAutoLogin() { - if (isGuestModeRequested) { - return - } - if (hasAttemptedAutoLogin) { - return - } - hasAttemptedAutoLogin = true - - val refreshToken = authTokenStorage.getRefreshToken() - if (refreshToken.isNullOrBlank()) { - return - } - - viewModelScope.launch { - _uiState.update { - it.copy( - isAutoLoginLoading = true, - loginMessage = null - ) - } - - val result = runCatching { - val response = authRepository.refresh(refreshToken) - if (!response.isSuccessful) { - throw HttpException(response) - } - if (isGuestModeRequested) { - return@launch - } - saveTokensFromResponse(response) - loadOrganizationNames() - loadExcludedKeywords() - } - - result.fold( - onSuccess = { - onAuthSuccess("Logged in automatically.") - }, - onFailure = { error -> - onAutoLoginFailure(error) - } - ) - } - } - fun loginWithCredentials() { val currentState = _uiState.value val email = currentState.username.trim() @@ -235,11 +187,9 @@ class LoginViewModel @Inject constructor( } fun continueAsGuest() { - isGuestModeRequested = true authTokenStorage.clearTokens() _uiState.update { it.copy( - isAutoLoginLoading = false, isCredentialLoginLoading = false, isGoogleLoginLoading = false, isKakaoLoginLoading = false, @@ -384,7 +334,6 @@ class LoginViewModel @Inject constructor( private fun onAuthSuccess(message: String) { _uiState.update { it.copy( - isAutoLoginLoading = false, isCredentialLoginLoading = false, isGoogleLoginLoading = false, isKakaoLoginLoading = false, @@ -398,7 +347,6 @@ class LoginViewModel @Inject constructor( private fun onAuthFailure(message: String) { _uiState.update { it.copy( - isAutoLoginLoading = false, isCredentialLoginLoading = false, isGoogleLoginLoading = false, isKakaoLoginLoading = false, @@ -434,35 +382,4 @@ class LoginViewModel @Inject constructor( onAuthFailure(message) } - private fun onAutoLoginFailure(error: Throwable) { - when (error) { - is HttpException -> { - if (error.code() == 404) { - authTokenStorage.clearTokens() - _uiState.update { - it.copy( - isAutoLoginLoading = false, - isLoginSuccessful = false, - loginMessage = null - ) - } - return - } - - if (error.code() == 401) { - authTokenStorage.clearTokens() - _uiState.update { - it.copy( - isAutoLoginLoading = false, - isLoginSuccessful = false, - loginMessage = "Your session has expired. Please log in again." - ) - } - return - } - } - } - - onAuthFailure(error, "auto login") - } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/example/hangsha_android/ui/view/splash/SplashScreen.kt b/app/src/main/java/com/example/hangsha_android/ui/view/splash/SplashScreen.kt new file mode 100644 index 0000000..69ca529 --- /dev/null +++ b/app/src/main/java/com/example/hangsha_android/ui/view/splash/SplashScreen.kt @@ -0,0 +1,54 @@ +package com.example.hangsha_android.ui.view.splash + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.example.hangsha_android.R + +@Composable +fun SplashScreen() { + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.White), + contentAlignment = Alignment.Center + ) { + Column( + modifier = Modifier.offset(y = (-18).dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image( + painter = painterResource(id = R.mipmap.ic_launcher_foreground), + contentDescription = "\uD589\uC0E4 \uB85C\uACE0", + modifier = Modifier.size(86.dp), + contentScale = ContentScale.Fit + ) + Spacer(modifier = Modifier.height(18.dp)) + Text( + text = "\uC11C\uC6B8\uB300 \uD589\uC0AC\uB294 \uD589\uC0E4", + color = Color.Black, + fontSize = 15.sp, + fontWeight = FontWeight.SemiBold, + letterSpacing = 0.sp, + textAlign = TextAlign.Center + ) + } + } +} diff --git a/app/src/main/java/com/example/hangsha_android/ui/view/splash/SplashUiState.kt b/app/src/main/java/com/example/hangsha_android/ui/view/splash/SplashUiState.kt new file mode 100644 index 0000000..8b3717b --- /dev/null +++ b/app/src/main/java/com/example/hangsha_android/ui/view/splash/SplashUiState.kt @@ -0,0 +1,10 @@ +package com.example.hangsha_android.ui.view.splash + +data class SplashUiState( + val navigationTarget: SplashNavigationTarget? = null +) + +enum class SplashNavigationTarget { + Calendar, + Login +} diff --git a/app/src/main/java/com/example/hangsha_android/ui/view/splash/SplashViewModel.kt b/app/src/main/java/com/example/hangsha_android/ui/view/splash/SplashViewModel.kt new file mode 100644 index 0000000..35faa72 --- /dev/null +++ b/app/src/main/java/com/example/hangsha_android/ui/view/splash/SplashViewModel.kt @@ -0,0 +1,101 @@ +package com.example.hangsha_android.ui.view.splash + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.example.hangsha_android.data.local.AuthTokenStorage +import com.example.hangsha_android.data.network.model.LoginResponse +import com.example.hangsha_android.data.repository.AuthRepository +import com.example.hangsha_android.data.repository.ExcludedKeywordsRepository +import com.example.hangsha_android.data.repository.UserRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import retrofit2.HttpException +import retrofit2.Response + +@HiltViewModel +class SplashViewModel @Inject constructor( + private val authRepository: AuthRepository, + private val authTokenStorage: AuthTokenStorage, + private val userRepository: UserRepository, + private val excludedKeywordsRepository: ExcludedKeywordsRepository +) : ViewModel() { + + private val _uiState = MutableStateFlow(SplashUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + init { + verifyRefreshToken() + } + + private fun verifyRefreshToken() { + val refreshToken = authTokenStorage.getRefreshToken() + if (refreshToken.isNullOrBlank()) { + navigateTo(SplashNavigationTarget.Login) + return + } + + viewModelScope.launch { + val target = runCatching { + refreshSession(refreshToken) + }.fold( + onSuccess = { SplashNavigationTarget.Calendar }, + onFailure = { error -> + if (error is HttpException && (error.code() == 401 || error.code() == 404)) { + authTokenStorage.clearTokens() + } + SplashNavigationTarget.Login + } + ) + + navigateTo(target) + } + } + + private suspend fun refreshSession(refreshToken: String) { + val response = authRepository.refresh(refreshToken) + saveTokensFromResponse(response) + loadOrganizationNames() + loadExcludedKeywords() + } + + private fun saveTokensFromResponse(response: Response) { + if (!response.isSuccessful) { + throw HttpException(response) + } + + val authTokens = response.body() + val accessToken = authTokens?.accessToken + val refreshToken = authTokens?.refreshToken + if (accessToken.isNullOrBlank() || refreshToken.isNullOrBlank()) { + throw IllegalStateException("Authentication response did not include auth tokens.") + } + + authTokenStorage.saveTokens( + accessToken = accessToken, + refreshToken = refreshToken + ) + } + + private suspend fun loadOrganizationNames() { + runCatching { + userRepository.ensureOrganizationNamesLoaded() + } + } + + private suspend fun loadExcludedKeywords() { + runCatching { + excludedKeywordsRepository.refreshExcludedKeywords() + } + } + + private fun navigateTo(target: SplashNavigationTarget) { + _uiState.update { + it.copy(navigationTarget = target) + } + } +} diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml index 07d5da9..c20bcbe 100644 --- a/app/src/main/res/drawable/ic_launcher_background.xml +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -5,166 +5,6 @@ android:viewportWidth="108" android:viewportHeight="108"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index 6f3b755..3ba4e35 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -1,6 +1,5 @@ - - - \ No newline at end of file + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml index 6f3b755..3ba4e35 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -1,6 +1,5 @@ - - - \ No newline at end of file + + diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..f56bc51 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp deleted file mode 100644 index c209e78..0000000 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..fc4769e Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000..f56bc51 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp deleted file mode 100644 index b2dfe3d..0000000 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..58dc457 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp deleted file mode 100644 index 4f0f1d6..0000000 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..7566f49 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000..58dc457 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp deleted file mode 100644 index 62b611d..0000000 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..3423184 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp deleted file mode 100644 index 948a307..0000000 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..4853237 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000..3423184 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp deleted file mode 100644 index 1b9a695..0000000 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..c51b292 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp deleted file mode 100644 index 28d4b77..0000000 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..bb1fbc7 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..c51b292 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp deleted file mode 100644 index 9287f50..0000000 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..86fbd58 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp deleted file mode 100644 index aa7d642..0000000 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..44d3438 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..86fbd58 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp deleted file mode 100644 index 9126ae3..0000000 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 76a185c..67dc12e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,3 @@ - Hangsha + 행샤