diff --git a/app/build.gradle.kts b/app/build.gradle.kts index da936fee1..ced113c19 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -80,7 +80,7 @@ android { } composeOptions { - kotlinCompilerExtensionVersion = "1.4.4" + kotlinCompilerExtensionVersion = "1.5.0" } splits { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a86b06f77..c6d1c24d2 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -117,13 +117,6 @@ android:scheme="kakao${KAKAO_NATIVE_APP_KEY}" /> - - - - - - - - - diff --git a/app/src/main/java/com/eatssu/android/data/dto/request/CheckValidTokenRequest.kt b/app/src/main/java/com/eatssu/android/data/dto/request/CheckValidTokenRequest.kt new file mode 100644 index 000000000..050892516 --- /dev/null +++ b/app/src/main/java/com/eatssu/android/data/dto/request/CheckValidTokenRequest.kt @@ -0,0 +1,5 @@ +package com.eatssu.android.data.dto.request + +data class CheckValidTokenRequest( + val token: String, +) \ No newline at end of file diff --git a/app/src/main/java/com/eatssu/android/data/repository/OauthRepositoryImpl.kt b/app/src/main/java/com/eatssu/android/data/repository/OauthRepositoryImpl.kt index 262cfe97f..285096b1b 100644 --- a/app/src/main/java/com/eatssu/android/data/repository/OauthRepositoryImpl.kt +++ b/app/src/main/java/com/eatssu/android/data/repository/OauthRepositoryImpl.kt @@ -1,5 +1,6 @@ package com.eatssu.android.data.repository +import com.eatssu.android.data.dto.request.CheckValidTokenRequest import com.eatssu.android.data.dto.request.LoginWithKakaoRequest import com.eatssu.android.data.dto.response.BaseResponse import com.eatssu.android.data.dto.response.TokenResponse @@ -21,4 +22,9 @@ class OauthRepositoryImpl @Inject constructor(private val oauthService: OauthSer flow { emit(oauthService.loginWithKakao(body)) } + + override suspend fun checkValidToken(body: CheckValidTokenRequest): Flow> = + flow { + emit(oauthService.checkValidToken(body)) + } } diff --git a/app/src/main/java/com/eatssu/android/data/service/OauthService.kt b/app/src/main/java/com/eatssu/android/data/service/OauthService.kt index e57e7c1f0..0bc47e3b9 100644 --- a/app/src/main/java/com/eatssu/android/data/service/OauthService.kt +++ b/app/src/main/java/com/eatssu/android/data/service/OauthService.kt @@ -1,5 +1,6 @@ package com.eatssu.android.data.service +import com.eatssu.android.data.dto.request.CheckValidTokenRequest import com.eatssu.android.data.dto.request.LoginWithKakaoRequest import com.eatssu.android.data.dto.response.BaseResponse import com.eatssu.android.data.dto.response.TokenResponse @@ -18,4 +19,9 @@ interface OauthService { //여기는 토큰이 없는 레트로핏을 끼웁니 suspend fun loginWithKakao( @Body request: LoginWithKakaoRequest, ): BaseResponse + + @POST("oauths/valid/token") + suspend fun checkValidToken( + @Body request: CheckValidTokenRequest, + ): BaseResponse } \ No newline at end of file diff --git a/app/src/main/java/com/eatssu/android/di/NetworkModule.kt b/app/src/main/java/com/eatssu/android/di/NetworkModule.kt index 2f5138495..69789b518 100644 --- a/app/src/main/java/com/eatssu/android/di/NetworkModule.kt +++ b/app/src/main/java/com/eatssu/android/di/NetworkModule.kt @@ -46,13 +46,14 @@ object NetworkModule { val loggingInterceptor = HttpLoggingInterceptor() loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY) - OkHttpClient.Builder().addInterceptor(loggingInterceptor).addInterceptor(tokenInterceptor) + OkHttpClient.Builder() + .addInterceptor(loggingInterceptor) + .addInterceptor(tokenInterceptor) .build() } else { - val loggingInterceptor = HttpLoggingInterceptor() - loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY) - - OkHttpClient.Builder().addInterceptor(loggingInterceptor).addInterceptor(tokenInterceptor) + // 프로덕션 환경에서는 로깅 인터셉터를 추가하지 않음 + OkHttpClient.Builder() + .addInterceptor(tokenInterceptor) .build() } diff --git a/app/src/main/java/com/eatssu/android/domain/repository/OauthRepository.kt b/app/src/main/java/com/eatssu/android/domain/repository/OauthRepository.kt index eaa20e386..d3840908e 100644 --- a/app/src/main/java/com/eatssu/android/domain/repository/OauthRepository.kt +++ b/app/src/main/java/com/eatssu/android/domain/repository/OauthRepository.kt @@ -1,5 +1,6 @@ package com.eatssu.android.domain.repository +import com.eatssu.android.data.dto.request.CheckValidTokenRequest import com.eatssu.android.data.dto.request.LoginWithKakaoRequest import com.eatssu.android.data.dto.response.BaseResponse import com.eatssu.android.data.dto.response.TokenResponse @@ -12,5 +13,6 @@ interface OauthRepository { suspend fun login(body: LoginWithKakaoRequest): Flow> + suspend fun checkValidToken(body: CheckValidTokenRequest): Flow> } 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 index b689fea18..29ac52347 100644 --- 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 @@ -1,13 +1,14 @@ package com.eatssu.android.domain.usecase.auth +import com.eatssu.android.data.dto.request.CheckValidTokenRequest import com.eatssu.android.data.dto.response.BaseResponse -import com.eatssu.android.domain.repository.UserRepository +import com.eatssu.android.domain.repository.OauthRepository import kotlinx.coroutines.flow.Flow import javax.inject.Inject class GetIsAccessTokenValidUseCase @Inject constructor( - private val userRepository: UserRepository, + private val oauthRepository: OauthRepository ) { - suspend operator fun invoke(): Flow> = - userRepository.checkUserNameValidation("qkqh") //todo api 만들어지면 수정 + suspend operator fun invoke(userAccessToken: String): Flow> = + oauthRepository.checkValidToken(CheckValidTokenRequest(userAccessToken)) } \ 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 68a0992c7..dcbcdbc8b 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 @@ -33,18 +33,19 @@ class IntroViewModel @Inject constructor( private fun autoLogin() { viewModelScope.launch { - _uiState.value = UiState.Loading + val userAccessToken = getAccessTokenUseCase() + _uiState.value = UiState.Loading try { // 토큰 존재 여부 확인 - if (getAccessTokenUseCase().isEmpty()) { + if (userAccessToken.isEmpty()) { _uiState.value = UiState.Error _uiEvent.emit(UiEvent.ShowToast("로그인이 필요합니다")) return@launch + } else { + checkValid(userAccessToken) } - checkValid() - } catch (e: Exception) { _uiState.value = UiState.Error _uiEvent.emit(UiEvent.ShowToast("오류가 발생했습니다: ${e.message}")) @@ -52,9 +53,9 @@ class IntroViewModel @Inject constructor( } } - private fun checkValid() { + private fun checkValid(userAccessToken: String) { viewModelScope.launch { - getIsAccessTokenValidUseCase() + getIsAccessTokenValidUseCase(userAccessToken) .collect { if (it.result == true) { //토큰이 있고 유효함 _uiState.value = UiState.Success(IntroState.ValidToken) diff --git a/app/src/main/java/com/eatssu/android/presentation/login/LoginActivity.kt b/app/src/main/java/com/eatssu/android/presentation/login/LoginActivity.kt index 758a6dcf7..323649261 100644 --- a/app/src/main/java/com/eatssu/android/presentation/login/LoginActivity.kt +++ b/app/src/main/java/com/eatssu/android/presentation/login/LoginActivity.kt @@ -3,8 +3,13 @@ package com.eatssu.android.presentation.login import android.os.Bundle import android.view.View import androidx.activity.viewModels +import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import com.eatssu.android.R import com.eatssu.android.databinding.ActivityLoginBinding +import com.eatssu.android.presentation.UiEvent +import com.eatssu.android.presentation.UiState import com.eatssu.android.presentation.base.BaseActivity import com.eatssu.android.presentation.main.MainActivity import com.eatssu.android.presentation.util.showToast @@ -13,7 +18,6 @@ import com.kakao.sdk.common.model.ClientError import com.kakao.sdk.common.model.ClientErrorCause import com.kakao.sdk.user.UserApiClient import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import timber.log.Timber @@ -26,73 +30,100 @@ class LoginActivity : override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + initUi() + observeState() + observeEvents() + } - // 툴바 사용하지 않도록 설정 - toolbar.let { - toolbar.visibility = View.GONE - toolbarTitle.visibility = View.GONE - setSupportActionBar(it) - supportActionBar?.setDisplayHomeAsUpEnabled(false) - supportActionBar?.setDisplayShowTitleEnabled(false) + private fun initUi() { + // 툴바 숨기기 + with(toolbar) { + visibility = View.GONE + setSupportActionBar(this) + supportActionBar?.apply { + setDisplayHomeAsUpEnabled(false) + setDisplayShowTitleEnabled(false) + } } - setOnClickListener() + binding.ibKakaoLogin.setOnClickListener { + handleKakaoLogin() + } } - - fun setOnClickListener() { - val context = this - binding.mcvKakaoLogin.setOnClickListener { - - Timber.d("버튼 클릭") - lifecycleScope.launch { - try { - // 서비스 코드에서는 간단하게 로그인 요청하고 oAuthToken 을 받아올 수 있다. - val oAuthToken = UserApiClient.loginWithKakao(context) - Timber.d("beanbean > $oAuthToken") - postUserInfo() - - } catch (error: Throwable) { - if (error is ClientError && error.reason == ClientErrorCause.Cancelled) { - Timber.d("사용자가 명시적으로 취소") - } else { - Timber.e(error, "인증 에러 발생") - } + //kakao login sdk를 통해 유저 정보를 가져와 rest api 호출하는 뷰모델 함수 호출 + private fun handleKakaoLogin() { + lifecycleScope.launch { + try { + loginViewModel.setLoadingState() + val oAuthToken = UserApiClient.loginWithKakao(this@LoginActivity) + Timber.d("Kakao login success: $oAuthToken") + UserApiClient.instance.me { user, error -> + user?.let { + val providerID = user.id.toString() + val email = user.kakaoAccount?.email.toString() + loginViewModel.getKakaoLogin(email, providerID) + } ?: Timber.e(error, "User info fetch failed") } + } catch (error: Throwable) { + handleKakaoLoginError(error) } } } + //kakao login sdk의 error를 다룹니다. + private fun handleKakaoLoginError(error: Throwable) { + when { + error is ClientError && error.reason == ClientErrorCause.Cancelled -> { + Timber.d("User cancelled login") + loginViewModel.setInitState() + } - private fun postUserInfo() { - UserApiClient.instance.me { user, error -> - if (user != null) { - // 유저의 아이디 - Timber.d("invoke: id =" + user.id) - val providerID = user.id.toString() - // 유저의 이메일 - Timber.d("invoke: email =" + user.kakaoAccount!!.email) - val email = user.kakaoAccount!!.email.toString() - - loginViewModel.getLogin(email, providerID) + else -> { + Timber.e(error, "Login failed") + showToast(getString(R.string.login_failed)) + } + } + } - lifecycleScope.launch { - loginViewModel.uiState.collectLatest { - if (!it.error && !it.loading) { - Timber.d(it.toString()) - showToast(it.toastMessage) + private fun observeState() { + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + loginViewModel.uiState.collect { state -> + when (state) { + is UiState.Loading -> showLoading(true) + is UiState.Success -> { startActivity() finishAffinity() } + else -> { + showLoading(false) + } + } + } + } + } + } + + private fun observeEvents() { + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + loginViewModel.uiEvent.collect { event -> + when (event) { + is UiEvent.ShowToast -> showToast(event.message) } } } } } + private fun showLoading(isLoading: Boolean) { + binding.progressBar.visibility = if (isLoading) View.VISIBLE else View.GONE + binding.ibKakaoLogin.visibility = if (isLoading) View.INVISIBLE else View.VISIBLE + } + override fun onBackPressed() { super.onBackPressed() - finishAffinity() - //탈퇴나 로그아웃 하고 로그인 화면으로 오고, 그 뒤에 뒤로 가기를 눌렀을 때에 백스택 방지 + finishAffinity() //로그인 화면에서 뒤로 가기 눌렀을 때에는 백스택 없어야 함 (앱 종료) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/eatssu/android/presentation/login/LoginViewModel.kt b/app/src/main/java/com/eatssu/android/presentation/login/LoginViewModel.kt index ec701a05c..c2937aad0 100644 --- a/app/src/main/java/com/eatssu/android/presentation/login/LoginViewModel.kt +++ b/app/src/main/java/com/eatssu/android/presentation/login/LoginViewModel.kt @@ -9,20 +9,21 @@ import com.eatssu.android.domain.usecase.auth.LoginUseCase import com.eatssu.android.domain.usecase.auth.SetAccessTokenUseCase import com.eatssu.android.domain.usecase.auth.SetRefreshTokenUseCase import com.eatssu.android.domain.usecase.auth.SetUserEmailUseCase +import com.eatssu.android.presentation.UiEvent +import com.eatssu.android.presentation.UiState import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.onStart -import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import timber.log.Timber import javax.inject.Inject + @HiltViewModel class LoginViewModel @Inject constructor( private val loginUseCase: LoginUseCase, @@ -32,45 +33,45 @@ class LoginViewModel @Inject constructor( @ApplicationContext private val context: Context ) : ViewModel() { - private val _uiState: MutableStateFlow = MutableStateFlow(LoginState()) - val uiState: StateFlow = _uiState.asStateFlow() + private val _uiState = MutableStateFlow>(UiState.Init) + val uiState: StateFlow> = _uiState.asStateFlow() + + private val _uiEvent: MutableSharedFlow = MutableSharedFlow() + val uiEvent = _uiEvent.asSharedFlow() - fun getLogin(email: String, providerID: String) { + fun getKakaoLogin(email: String, providerID: String) { viewModelScope.launch { - loginUseCase(LoginWithKakaoRequest(email, providerID)).onStart { - _uiState.update { it.copy(loading = true) } - }.onCompletion { - _uiState.update { it.copy(loading = false, error = true) } - }.catch { e -> - _uiState.update { it.copy(error = true) } - Timber.e(e, "kakaoLogin: ") - }.collectLatest { result -> - _uiState.update { - it.copy( - loading = false, error = false, - toastMessage = context.getString(R.string.login_done) - ) - //Todo 로그인과 회원가입에 따른 토스트 메시지 구분하기 + loginUseCase(LoginWithKakaoRequest(email, providerID)) + .onStart { + _uiState.value = UiState.Loading } + .catch { e -> + _uiState.value = UiState.Error + _uiEvent.emit(UiEvent.ShowToast(context.getString(R.string.login_failed))) + } + .collect { result -> + result.result?.let { + setAccessTokenUseCase(it.accessToken) + setRefreshTokenUseCase(it.refreshToken) + setUserEmailUseCase(email) - /*토큰 저장*/ - result.result?.let { - - Timber.d(it.accessToken) - - //헤더에 토큰 붙이기 - setAccessTokenUseCase(it.accessToken) - setRefreshTokenUseCase(it.refreshToken) - setUserEmailUseCase(email) + _uiState.value = UiState.Success(LoginState.LoginSuccess) + _uiEvent.emit(UiEvent.ShowToast(context.getString(R.string.login_done))) + } } - } } } + fun setInitState() { + _uiState.value = UiState.Init + } + + fun setLoadingState() { + _uiState.value = UiState.Loading + } } -data class LoginState( - var toastMessage: String = "", - var loading: Boolean = true, - var error: Boolean = false, -) \ No newline at end of file +// 상태 및 이벤트 정의 +sealed class LoginState { + object LoginSuccess : LoginState() +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml index b9c19cbf0..8d63a8c5e 100644 --- a/app/src/main/res/layout/activity_login.xml +++ b/app/src/main/res/layout/activity_login.xml @@ -7,19 +7,32 @@ android:padding="16dp" tools:context=".presentation.login.LoginActivity"> + + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/ll_slogan" /> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 03f65dc6b..947e5f0ab 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -169,4 +169,5 @@ http://pf.kakao.com/_ZlVAn 오픈소스 라이브러리 + 로그인이 실패했습니다.\n \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7624a6bf6..80b33f0a3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -40,7 +40,7 @@ firebase-bom = "32.6.0" firebase-crashlytics = "2.9.9" timber = "5.0.1" google-services = "4.4.2" -kotlin-android = "1.8.10" +kotlin-android = "1.9.0" ossLicenses = "17.1.0" ossLicensesPlugin = "0.10.4" uiTestJunit4 = "1.7.8"