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"