diff --git a/data/src/main/java/org/gdsc/data/datasource/LoginDataSource.kt b/data/src/main/java/org/gdsc/data/datasource/LoginDataSource.kt index eae4a383..d240524f 100644 --- a/data/src/main/java/org/gdsc/data/datasource/LoginDataSource.kt +++ b/data/src/main/java/org/gdsc/data/datasource/LoginDataSource.kt @@ -1,9 +1,10 @@ package org.gdsc.data.datasource +import org.gdsc.domain.model.Response import org.gdsc.domain.model.response.TokenResponse interface LoginDataSource { - suspend fun postSignUpWithGoogleToken(token: String): TokenResponse + suspend fun postSignUpWithGoogleToken(token: String): Result> suspend fun postAppleToken(email: String, clientId: String): TokenResponse diff --git a/data/src/main/java/org/gdsc/data/datasource/LoginDataSourceImpl.kt b/data/src/main/java/org/gdsc/data/datasource/LoginDataSourceImpl.kt index 70340ed9..2a3e4e6a 100644 --- a/data/src/main/java/org/gdsc/data/datasource/LoginDataSourceImpl.kt +++ b/data/src/main/java/org/gdsc/data/datasource/LoginDataSourceImpl.kt @@ -1,6 +1,8 @@ package org.gdsc.data.datasource +import org.gdsc.domain.model.Response import org.gdsc.data.network.LoginAPI +import org.gdsc.domain.exception.JmtException import org.gdsc.domain.model.request.AppleLoginRequest import org.gdsc.domain.model.request.GoogleLoginRequest import org.gdsc.domain.model.response.TokenResponse @@ -10,8 +12,18 @@ import javax.inject.Inject class LoginDataSourceImpl @Inject constructor( private val loginAPI: LoginAPI ) : LoginDataSource { - override suspend fun postSignUpWithGoogleToken(token: String): TokenResponse { - return loginAPI.postUserGoogleToken(GoogleLoginRequest(token)).data + override suspend fun postSignUpWithGoogleToken(token: String): Result> { + return runCatching { + loginAPI.postUserGoogleToken(GoogleLoginRequest(token)).body() + ?: throw JmtException.NoneDataException() + }.onFailure { + throw if (it.message != null) { + JmtException.GeneralException(requireNotNull(it.message)) + } else { + JmtException.UnKnownException() + } + } + } override suspend fun postAppleToken(email: String, clientId: String): TokenResponse { diff --git a/data/src/main/java/org/gdsc/data/network/LoginAPI.kt b/data/src/main/java/org/gdsc/data/network/LoginAPI.kt index 9ea224f5..195392b3 100644 --- a/data/src/main/java/org/gdsc/data/network/LoginAPI.kt +++ b/data/src/main/java/org/gdsc/data/network/LoginAPI.kt @@ -1,20 +1,18 @@ package org.gdsc.data.network -import org.gdsc.data.model.Response +import org.gdsc.domain.model.Response import org.gdsc.domain.model.request.GoogleLoginRequest import org.gdsc.domain.model.request.AppleLoginRequest import org.gdsc.domain.model.response.TokenResponse import retrofit2.http.Body -import retrofit2.http.DELETE -import retrofit2.http.HTTP import retrofit2.http.POST interface LoginAPI { @POST("api/v1/auth/google") suspend fun postUserGoogleToken( @Body request: GoogleLoginRequest - ): Response + ): retrofit2.Response> @POST("api/v1/auth/android/apple") suspend fun postUserAppleToken( diff --git a/data/src/main/java/org/gdsc/data/repository/LoginRepositoryImpl.kt b/data/src/main/java/org/gdsc/data/repository/LoginRepositoryImpl.kt index 4882eaef..a12341bd 100644 --- a/data/src/main/java/org/gdsc/data/repository/LoginRepositoryImpl.kt +++ b/data/src/main/java/org/gdsc/data/repository/LoginRepositoryImpl.kt @@ -8,8 +8,10 @@ import javax.inject.Inject class LoginRepositoryImpl @Inject constructor(private val loginDataSource: LoginDataSource) : LoginRepository { - override suspend fun postSignUpWithGoogleToken(token: String): TokenResponse { - return loginDataSource.postSignUpWithGoogleToken(token) + override suspend fun postSignUpWithGoogleToken(token: String): Result { + return kotlin.runCatching { + loginDataSource.postSignUpWithGoogleToken(token).getOrThrow().data + } } override suspend fun postAppleToken(email: String, clientId: String): TokenResponse { diff --git a/domain/src/main/java/org/gdsc/domain/exception/JmtException.kt b/domain/src/main/java/org/gdsc/domain/exception/JmtException.kt new file mode 100644 index 00000000..496b87fc --- /dev/null +++ b/domain/src/main/java/org/gdsc/domain/exception/JmtException.kt @@ -0,0 +1,15 @@ +package org.gdsc.domain.exception + +import org.gdsc.domain.Empty + +sealed class JmtException( + message: String = String.Empty +) : Exception(message) { + + data class NetworkException(override val message: String = "네트워크 연결을 확인해주세요.") : JmtException() + data class ServerException(override val message: String = "서버에 문제가 발생했습니다.") : JmtException() + data class NoneDataException(override val message: String = "데이터가 없습니다.") : JmtException() + data class UnKnownException(override val message: String = "알 수 없는 에러입니다.") : JmtException() + data class GeneralException(override val message: String) : JmtException() + +} \ No newline at end of file diff --git a/domain/src/main/java/org/gdsc/domain/repository/LoginRepository.kt b/domain/src/main/java/org/gdsc/domain/repository/LoginRepository.kt index aabc2164..2818b138 100644 --- a/domain/src/main/java/org/gdsc/domain/repository/LoginRepository.kt +++ b/domain/src/main/java/org/gdsc/domain/repository/LoginRepository.kt @@ -4,7 +4,7 @@ import org.gdsc.domain.model.response.TokenResponse interface LoginRepository { - suspend fun postSignUpWithGoogleToken(token: String): TokenResponse + suspend fun postSignUpWithGoogleToken(token: String): Result suspend fun postAppleToken(email: String, clientId: String): TokenResponse diff --git a/domain/src/main/java/org/gdsc/domain/usecase/PostSignUpWithGoogleTokenUseCase.kt b/domain/src/main/java/org/gdsc/domain/usecase/PostSignUpWithGoogleTokenUseCase.kt index b65e1206..be4de13a 100644 --- a/domain/src/main/java/org/gdsc/domain/usecase/PostSignUpWithGoogleTokenUseCase.kt +++ b/domain/src/main/java/org/gdsc/domain/usecase/PostSignUpWithGoogleTokenUseCase.kt @@ -9,7 +9,7 @@ import javax.inject.Singleton class PostSignUpWithGoogleTokenUseCase @Inject constructor( private val loginRepository: LoginRepository ) { - suspend operator fun invoke(token: String): TokenResponse { + suspend operator fun invoke(token: String): Result { return loginRepository.postSignUpWithGoogleToken(token) } } diff --git a/presentation/src/main/java/org/gdsc/presentation/login/LoginFragment.kt b/presentation/src/main/java/org/gdsc/presentation/login/LoginFragment.kt index b67316e7..b292d1b8 100644 --- a/presentation/src/main/java/org/gdsc/presentation/login/LoginFragment.kt +++ b/presentation/src/main/java/org/gdsc/presentation/login/LoginFragment.kt @@ -22,6 +22,7 @@ import kotlinx.coroutines.launch import org.gdsc.domain.model.response.UserLoginAction import org.gdsc.presentation.R import org.gdsc.presentation.databinding.FragmentLoginBinding +import org.gdsc.presentation.utils.repeatWhenUiStarted import org.gdsc.presentation.view.LoginManager import org.gdsc.presentation.view.MainActivity @@ -45,6 +46,13 @@ class LoginFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) setLoginButton() + + repeatWhenUiStarted { + // TODO: specific handling + viewModel.eventFlow.collect { + Toast.makeText(requireContext(), it.message, Toast.LENGTH_SHORT).show() + } + } } private fun setLoginButton() { @@ -57,7 +65,7 @@ class LoginFragment : Fragment() { .build() ) } catch (e: ApiException) { - Log.e("Login","ApiException $e") + Log.e("Login", "ApiException $e") showGoogleAccountRegistrationPrompt() } catch (e: Exception) { Log.e("Login", "setLoginButton Exception $e") @@ -96,12 +104,13 @@ class LoginFragment : Fragment() { val credential = loginManager.oneTapClient.getSignInCredentialFromIntent(intent) credential.googleIdToken?.let { viewModel.postSignUpWithGoogleToken(it) { tokenResponse -> - when(tokenResponse.userLoginAction) { + when (tokenResponse.userLoginAction) { UserLoginAction.SIGN_UP.value -> { val action = LoginFragmentDirections.actionLoginFragmentToSignUpNicknameFragment() findNavController().navigate(action) } + UserLoginAction.LOG_IN.value -> { val intent = Intent(requireContext(), MainActivity::class.java) startActivity(intent) diff --git a/presentation/src/main/java/org/gdsc/presentation/login/LoginViewModel.kt b/presentation/src/main/java/org/gdsc/presentation/login/LoginViewModel.kt index b50ed90d..bb688001 100644 --- a/presentation/src/main/java/org/gdsc/presentation/login/LoginViewModel.kt +++ b/presentation/src/main/java/org/gdsc/presentation/login/LoginViewModel.kt @@ -12,6 +12,7 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import okhttp3.MultipartBody import org.gdsc.domain.Empty +import org.gdsc.domain.exception.JmtException import org.gdsc.domain.model.response.TokenResponse import org.gdsc.domain.usecase.CheckDuplicatedNicknameUseCase import org.gdsc.domain.usecase.PostNicknameUseCase @@ -19,6 +20,8 @@ import org.gdsc.domain.usecase.PostSignUpWithGoogleTokenUseCase import org.gdsc.domain.usecase.token.SaveTokenUseCase import org.gdsc.domain.usecase.user.PostDefaultProfileImageUseCase import org.gdsc.domain.usecase.user.PostProfileImageUseCase +import org.gdsc.presentation.utils.MutableEventFlow +import org.gdsc.presentation.utils.asEventFlow import javax.inject.Inject @HiltViewModel @@ -37,6 +40,9 @@ class LoginViewModel @Inject constructor( private var _profileImageState = MutableStateFlow(String.Empty) val profileImageState = _profileImageState.asStateFlow() + private var _eventFlow = MutableEventFlow() + val eventFlow = _eventFlow.asEventFlow() + val isNicknameVerified: StateFlow get() = nicknameState.map { nickname -> nickname.isNotBlank() @@ -56,9 +62,13 @@ class LoginViewModel @Inject constructor( fun postSignUpWithGoogleToken(token: String, afterSuccessSignUp: (TokenResponse) -> Unit) { viewModelScope.launch { - val response = postSignUpWithGoogleTokenUseCase.invoke(token) - saveTokenUseCase.invoke(response) - afterSuccessSignUp(response) + val result = postSignUpWithGoogleTokenUseCase.invoke(token) + result.onSuccess { response -> + saveTokenUseCase.invoke(response) + afterSuccessSignUp(response) + }.onFailure { + _eventFlow.emit(it as JmtException) + } } } diff --git a/presentation/src/main/java/org/gdsc/presentation/login/SplashActivity.kt b/presentation/src/main/java/org/gdsc/presentation/login/SplashActivity.kt index a34e3fb5..289a8212 100644 --- a/presentation/src/main/java/org/gdsc/presentation/login/SplashActivity.kt +++ b/presentation/src/main/java/org/gdsc/presentation/login/SplashActivity.kt @@ -5,7 +5,6 @@ import androidx.appcompat.app.AppCompatActivity import android.os.Bundle import androidx.lifecycle.lifecycleScope import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.gdsc.domain.usecase.token.GetRefreshTokenUseCase @@ -34,7 +33,7 @@ class SplashActivity : AppCompatActivity() { setContentView(R.layout.activity_splash) setToFullPage() - lifecycleScope.launch(Dispatchers.Main) { + lifecycleScope.launch { val accessible = validateToken() diff --git a/presentation/src/main/java/org/gdsc/presentation/utils/EventFlow.kt b/presentation/src/main/java/org/gdsc/presentation/utils/EventFlow.kt new file mode 100644 index 00000000..58342709 --- /dev/null +++ b/presentation/src/main/java/org/gdsc/presentation/utils/EventFlow.kt @@ -0,0 +1,45 @@ +package org.gdsc.presentation.utils + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.FlowCollector +import kotlinx.coroutines.flow.MutableSharedFlow +import org.gdsc.presentation.utils.EventFlow.Companion.DEFAULT_REPLAY +import java.util.concurrent.atomic.AtomicBoolean + +interface EventFlow : Flow { + + companion object { + const val DEFAULT_REPLAY = 2 + } +} + +interface MutableEventFlow : EventFlow, FlowCollector + +private class EventFlowSlot(val value: T) { + + private val consumed = AtomicBoolean(false) + + fun markConsumed() = consumed.getAndSet(true) +} + +private class EventFlowImpl(replay: Int) : MutableEventFlow { + + private val flow: MutableSharedFlow> = MutableSharedFlow(replay) + + override suspend fun collect(collector: FlowCollector) = + flow.collect { slot -> + if (slot.markConsumed().not()) { + collector.emit(slot.value) + } + } + + override suspend fun emit(value: T) { + flow.emit(EventFlowSlot(value)) + } +} + +private class ReadOnlyEventFlow(flow: EventFlow) : EventFlow by flow + +fun MutableEventFlow(replay: Int = DEFAULT_REPLAY): MutableEventFlow = EventFlowImpl(replay) + +fun MutableEventFlow.asEventFlow(): EventFlow = ReadOnlyEventFlow(this)