From d81376168255a2f274168fa542b6f750e608d9ab Mon Sep 17 00:00:00 2001 From: soopeach Date: Mon, 11 Sep 2023 18:20:53 +0900 Subject: [PATCH 1/4] =?UTF-8?q?[feat/jmtException=5Fsandbox]:=20JmtExcepti?= =?UTF-8?q?on=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/gdsc/domain/exception/JmtException.kt | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 domain/src/main/java/org/gdsc/domain/exception/JmtException.kt 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 From bbef5820733805c2dcccbd36a7e67d8851b09df6 Mon Sep 17 00:00:00 2001 From: soopeach Date: Mon, 11 Sep 2023 18:32:58 +0900 Subject: [PATCH 2/4] =?UTF-8?q?[feat/jmtException=5Fsandbox]:=20=EB=B6=88?= =?UTF-8?q?=ED=95=84=EC=9A=94=ED=95=9C=20=EB=B6=80=EB=B6=84=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/org/gdsc/presentation/login/SplashActivity.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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() From cb797949d872cd42dc7256662379181346671615 Mon Sep 17 00:00:00 2001 From: soopeach Date: Mon, 11 Sep 2023 18:36:45 +0900 Subject: [PATCH 3/4] =?UTF-8?q?[feat/jmtException=5Fsandbox]:=20=EA=B5=AC?= =?UTF-8?q?=EA=B8=80=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EB=B6=80=EB=B6=84?= =?UTF-8?q?=EC=97=90=20=EC=97=90=EB=9F=AC=20=ED=95=B8=EB=93=A4=EB=A7=81=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../org/gdsc/data/datasource/LoginDataSource.kt | 3 ++- .../gdsc/data/datasource/LoginDataSourceImpl.kt | 16 ++++++++++++++-- .../main/java/org/gdsc/data/network/LoginAPI.kt | 6 ++---- .../gdsc/data/repository/LoginRepositoryImpl.kt | 6 ++++-- .../gdsc/domain/repository/LoginRepository.kt | 2 +- .../usecase/PostSignUpWithGoogleTokenUseCase.kt | 2 +- .../org/gdsc/presentation/login/LoginFragment.kt | 13 +++++++++++-- .../gdsc/presentation/login/LoginViewModel.kt | 16 +++++++++++++--- 8 files changed, 48 insertions(+), 16 deletions(-) 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/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..4a9cb1e8 100644 --- a/presentation/src/main/java/org/gdsc/presentation/login/LoginViewModel.kt +++ b/presentation/src/main/java/org/gdsc/presentation/login/LoginViewModel.kt @@ -3,15 +3,18 @@ package org.gdsc.presentation.login import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.map 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 @@ -37,6 +40,9 @@ class LoginViewModel @Inject constructor( private var _profileImageState = MutableStateFlow(String.Empty) val profileImageState = _profileImageState.asStateFlow() + private var _eventFlow = MutableSharedFlow() + val eventFlow = _eventFlow.asSharedFlow() + 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) + } } } From 5c54ae2cac77e2c388628b47934cc3b0d3f491db Mon Sep 17 00:00:00 2001 From: soopeach Date: Tue, 12 Sep 2023 12:27:18 +0900 Subject: [PATCH 4/4] =?UTF-8?q?[feat/jmtException=5Fsandbox]:=20EventFlow?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gdsc/presentation/login/LoginViewModel.kt | 8 ++-- .../org/gdsc/presentation/utils/EventFlow.kt | 45 +++++++++++++++++++ 2 files changed, 49 insertions(+), 4 deletions(-) create mode 100644 presentation/src/main/java/org/gdsc/presentation/utils/EventFlow.kt 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 4a9cb1e8..bb688001 100644 --- a/presentation/src/main/java/org/gdsc/presentation/login/LoginViewModel.kt +++ b/presentation/src/main/java/org/gdsc/presentation/login/LoginViewModel.kt @@ -3,11 +3,9 @@ package org.gdsc.presentation.login import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn @@ -22,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 @@ -40,8 +40,8 @@ class LoginViewModel @Inject constructor( private var _profileImageState = MutableStateFlow(String.Empty) val profileImageState = _profileImageState.asStateFlow() - private var _eventFlow = MutableSharedFlow() - val eventFlow = _eventFlow.asSharedFlow() + private var _eventFlow = MutableEventFlow() + val eventFlow = _eventFlow.asEventFlow() val isNicknameVerified: StateFlow get() = nicknameState.map { nickname -> 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)