From 0847973ee1ee2ae6ac02e1a338fa9d74bbc307a7 Mon Sep 17 00:00:00 2001 From: KwakEuiJin Date: Sat, 11 Nov 2023 14:24:16 +0900 Subject: [PATCH 1/3] [feat/school_auth]: Add School Auth Screen Route --- .../ui/bottom/BottomNavigation.kt | 3 ++- .../presentation/ui/main/MainScreen.kt | 14 ++++++++--- .../ui/signup/school/SchoolAuthScreen.kt | 20 +++++++++++++-- .../ui/signup/school/SchoolAuthViewModel.kt | 20 ++++++++++++--- .../ui/signup/school/SchoolContract.kt | 8 ++++-- .../school/email/SchoolAuthPostEmailScreen.kt | 25 +++++++++++++++---- 6 files changed, 73 insertions(+), 17 deletions(-) diff --git a/presentation/src/main/java/com/everymeal/presentation/ui/bottom/BottomNavigation.kt b/presentation/src/main/java/com/everymeal/presentation/ui/bottom/BottomNavigation.kt index 9c8ef653..3a54c5ba 100644 --- a/presentation/src/main/java/com/everymeal/presentation/ui/bottom/BottomNavigation.kt +++ b/presentation/src/main/java/com/everymeal/presentation/ui/bottom/BottomNavigation.kt @@ -38,4 +38,5 @@ enum class EveryMealRoute(val route: String) { MY_PAGE("my-page"), DETAIL_LIST("detail-list"), DETAIL_RESTAURANT("detail-restaurant"), -} \ No newline at end of file + SCHOOL_AUTH("school-auth"), +} diff --git a/presentation/src/main/java/com/everymeal/presentation/ui/main/MainScreen.kt b/presentation/src/main/java/com/everymeal/presentation/ui/main/MainScreen.kt index 5b311531..9eb640f3 100644 --- a/presentation/src/main/java/com/everymeal/presentation/ui/main/MainScreen.kt +++ b/presentation/src/main/java/com/everymeal/presentation/ui/main/MainScreen.kt @@ -10,13 +10,10 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.navigation.NavHostController -import androidx.navigation.NavType import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController -import androidx.navigation.navArgument -import com.everymeal.presentation.ui.bottom.BottomNavigation import com.everymeal.presentation.ui.bottom.EveryMealBottomNavigation import com.everymeal.presentation.ui.bottom.EveryMealRoute import com.everymeal.presentation.ui.bottom.navigateBottomNavigationScreen @@ -24,10 +21,12 @@ import com.everymeal.presentation.ui.detail.DetailListScreen import com.everymeal.presentation.ui.home.HomeScreen import com.everymeal.presentation.ui.mypage.MyPageScreen import com.everymeal.presentation.ui.restaurant.DetailRestaurantScreen +import com.everymeal.presentation.ui.signup.school.SchoolAuthScreen import com.everymeal.presentation.ui.univfood.UnivFoodScreen import com.everymeal.presentation.ui.whatfood.WhatFoodScreen const val DETAIL_SCREEN_TYPE = "detailScreenType" + @Composable fun MainScreen( navController: NavHostController = rememberNavController(), @@ -75,7 +74,7 @@ fun MainScreen( composable(route = EveryMealRoute.MY_PAGE.route) { MyPageScreen() } - composable(route = EveryMealRoute.DETAIL_LIST.route.plus("/{$DETAIL_SCREEN_TYPE}"),) { + composable(route = EveryMealRoute.DETAIL_LIST.route.plus("/{$DETAIL_SCREEN_TYPE}")) { val detailScreenType = it.arguments?.getString(DETAIL_SCREEN_TYPE) ?: "" DetailListScreen( title = detailScreenType, @@ -85,6 +84,13 @@ fun MainScreen( composable(route = EveryMealRoute.DETAIL_RESTAURANT.route) { DetailRestaurantScreen() } + composable(route = EveryMealRoute.SCHOOL_AUTH.route) { + SchoolAuthScreen( + onSuccessEmailVerification = { + navController.popBackStack() + } + ) + } } } diff --git a/presentation/src/main/java/com/everymeal/presentation/ui/signup/school/SchoolAuthScreen.kt b/presentation/src/main/java/com/everymeal/presentation/ui/signup/school/SchoolAuthScreen.kt index 4fd1535b..87e860f8 100644 --- a/presentation/src/main/java/com/everymeal/presentation/ui/signup/school/SchoolAuthScreen.kt +++ b/presentation/src/main/java/com/everymeal/presentation/ui/signup/school/SchoolAuthScreen.kt @@ -8,6 +8,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateListOf @@ -21,6 +22,8 @@ import androidx.hilt.navigation.compose.hiltViewModel import com.everymeal.presentation.R import com.everymeal.presentation.components.EveryMealConditionAgreeDialog import com.everymeal.presentation.components.EveryMealConditionAgreeDialogItem +import com.everymeal.presentation.ui.home.HomeContract +import com.everymeal.presentation.ui.home.title import com.everymeal.presentation.ui.signup.school.email.SchoolAuthPostEmailScreen import com.everymeal.presentation.ui.theme.EveryMealTypography @@ -32,9 +35,22 @@ enum class SchoolAuthScreenType { @OptIn(ExperimentalMaterial3Api::class) @Composable fun SchoolAuthScreen( - viewModel: SchoolAuthViewModel = hiltViewModel() + viewModel: SchoolAuthViewModel = hiltViewModel(), + onSuccessEmailVerification: () -> Unit, ) { val viewState by viewModel.viewState.collectAsState() + LaunchedEffect(key1 = viewModel.effect) { + viewModel.effect.collect { effect -> + when (effect) { + is SchoolContract.Effect.Error -> { + // TODO Error 처리 + } + is SchoolContract.Effect.SuccessEmailVerification -> { + // TODO 이메일 인증 성공 + } + } + } + } Scaffold( topBar = { CenterAlignedTopAppBar( @@ -122,5 +138,5 @@ fun SchoolAuthContent( @Preview @Composable fun SchoolAuthScreenPreview() { - SchoolAuthScreen() + } diff --git a/presentation/src/main/java/com/everymeal/presentation/ui/signup/school/SchoolAuthViewModel.kt b/presentation/src/main/java/com/everymeal/presentation/ui/signup/school/SchoolAuthViewModel.kt index fff37636..08599a1c 100644 --- a/presentation/src/main/java/com/everymeal/presentation/ui/signup/school/SchoolAuthViewModel.kt +++ b/presentation/src/main/java/com/everymeal/presentation/ui/signup/school/SchoolAuthViewModel.kt @@ -23,12 +23,19 @@ class SchoolAuthViewModel @Inject constructor( updateState { copy( isEmailError = isValidEmail(event.emailLink), - emailLink = event.emailLink + emailText = event.emailLink + ) + } + } + is SchoolContract.Event.OnTokenTextChanged -> { + updateState { + copy( + tokenText = event.token ) } } - is SchoolContract.Event.OnNextButtonClicked -> { + is SchoolContract.Event.OnEmailNextButtonClicked -> { updateState { copy(isShowConditionBottomSheet = true) } @@ -41,6 +48,13 @@ class SchoolAuthViewModel @Inject constructor( SchoolContract.Event.FailEmailVerification -> { sendEffect() } + + SchoolContract.Event.OnTokenNextButtonClicked -> { + val viewState = viewState.value + if (viewState.emailAuthToken == viewState.tokenText ) { + sendEffect({ SchoolContract.Effect.SuccessEmailVerification }) + } + } } } @@ -51,7 +65,7 @@ class SchoolAuthViewModel @Inject constructor( private fun postEmail() { viewModelScope.launch { - postEmailUseCase(Email(viewState.value.emailLink)).onSuccess { + postEmailUseCase(Email(viewState.value.emailText)).onSuccess { updateState { copy(emailAuthToken = it) } diff --git a/presentation/src/main/java/com/everymeal/presentation/ui/signup/school/SchoolContract.kt b/presentation/src/main/java/com/everymeal/presentation/ui/signup/school/SchoolContract.kt index 9edecde4..0664f817 100644 --- a/presentation/src/main/java/com/everymeal/presentation/ui/signup/school/SchoolContract.kt +++ b/presentation/src/main/java/com/everymeal/presentation/ui/signup/school/SchoolContract.kt @@ -10,18 +10,22 @@ class SchoolContract { val schoolAuthScreenType: SchoolAuthScreenType = SchoolAuthScreenType.POST_EMAIL, val isShowConditionBottomSheet: Boolean = false, val isEmailError: Boolean = false, - val emailLink: String = "", + val emailText: String = "", + val tokenText: String = "", val emailAuthToken: String = "" ) : ViewState sealed class Event : ViewEvent { data class OnEmailTextChanged(val emailLink: String) : Event() - object OnNextButtonClicked : Event() + data class OnTokenTextChanged(val token: String) : Event() + object OnEmailNextButtonClicked : Event() + object OnTokenNextButtonClicked : Event() object OnPostEmail : Event() object FailEmailVerification : Event() } sealed class Effect : ViewSideEffect { data class Error(val code: Int) : Effect() + object SuccessEmailVerification : Effect() } } diff --git a/presentation/src/main/java/com/everymeal/presentation/ui/signup/school/email/SchoolAuthPostEmailScreen.kt b/presentation/src/main/java/com/everymeal/presentation/ui/signup/school/email/SchoolAuthPostEmailScreen.kt index 6a43dc3e..76f234df 100644 --- a/presentation/src/main/java/com/everymeal/presentation/ui/signup/school/email/SchoolAuthPostEmailScreen.kt +++ b/presentation/src/main/java/com/everymeal/presentation/ui/signup/school/email/SchoolAuthPostEmailScreen.kt @@ -40,7 +40,7 @@ fun SchoolAuthPostEmailScreen( ) Spacer(modifier = Modifier.size(40.dp)) Text( - text = when(state.schoolAuthScreenType) { + text = when (state.schoolAuthScreenType) { SchoolAuthScreenType.POST_EMAIL -> stringResource(id = R.string.email) SchoolAuthScreenType.VERIFY_TOKEN -> stringResource(id = R.string.verify_token) }, @@ -50,12 +50,24 @@ fun SchoolAuthPostEmailScreen( Spacer(modifier = Modifier.size(6.dp)) EveryMealTextField( modifier = Modifier.fillMaxWidth(), - value = state.emailLink, + value = state.emailText, onValueChange = { - viewModel.setEvent(SchoolContract.Event.OnEmailTextChanged(it)) + when (state.schoolAuthScreenType) { + SchoolAuthScreenType.POST_EMAIL -> viewModel.setEvent( + SchoolContract.Event.OnEmailTextChanged( + it + ) + ) + + SchoolAuthScreenType.VERIFY_TOKEN -> viewModel.setEvent( + SchoolContract.Event.OnTokenTextChanged( + it + ) + ) + } }, supportingText = { - if (state.isEmailError) { + if (state.isEmailError && state.schoolAuthScreenType == SchoolAuthScreenType.POST_EMAIL) { Text( text = stringResource(id = R.string.email_error), style = EveryMealTypography.Body5, @@ -68,7 +80,10 @@ fun SchoolAuthPostEmailScreen( EveryMealMainButton( text = stringResource(id = R.string.next), onClick = { - viewModel.setEvent(SchoolContract.Event.OnNextButtonClicked) + when (state.schoolAuthScreenType) { + SchoolAuthScreenType.POST_EMAIL -> viewModel.setEvent(SchoolContract.Event.OnEmailNextButtonClicked) + SchoolAuthScreenType.VERIFY_TOKEN -> viewModel.setEvent(SchoolContract.Event.OnTokenNextButtonClicked) + } }, ) } From 458719d40ce2e668740e33661b4c28faaa59d4ee Mon Sep 17 00:00:00 2001 From: KwakEuiJin Date: Wed, 29 Nov 2023 21:58:49 +0900 Subject: [PATCH 2/3] [feat/school_auth]: API TEST --- .../datasource/auth/AuthRemoteDataSource.kt | 3 ++- .../auth/AuthRemoteRemoteDataSourceImpl.kt | 10 +++++--- .../data/model/auth/EmailResponse.kt | 14 +++++++++++ .../data/repository/DefaultAuthRepository.kt | 3 ++- .../everymeal/data/service/auth/AuthApi.kt | 6 ++++- .../domain/model/auth/EmailAuthToken.kt | 5 ++++ .../domain/repository/auth/AuthRepository.kt | 3 ++- .../domain/usecase/auth/PostEmailUseCase.kt | 3 ++- .../presentation/ui/main/MainScreen.kt | 2 +- .../ui/signup/school/SchoolAuthScreen.kt | 19 +++++---------- .../ui/signup/school/SchoolAuthViewModel.kt | 23 ++++++++++++++----- 11 files changed, 63 insertions(+), 28 deletions(-) create mode 100644 data/src/main/java/com/everymeal/data/model/auth/EmailResponse.kt create mode 100644 domain/src/main/java/com/everymeal/domain/model/auth/EmailAuthToken.kt diff --git a/data/src/main/java/com/everymeal/data/datasource/auth/AuthRemoteDataSource.kt b/data/src/main/java/com/everymeal/data/datasource/auth/AuthRemoteDataSource.kt index d7614ba3..b36ee84d 100644 --- a/data/src/main/java/com/everymeal/data/datasource/auth/AuthRemoteDataSource.kt +++ b/data/src/main/java/com/everymeal/data/datasource/auth/AuthRemoteDataSource.kt @@ -1,7 +1,8 @@ package com.everymeal.data.datasource.auth import com.everymeal.domain.model.auth.Email +import com.everymeal.domain.model.auth.EmailAuthToken interface AuthRemoteDataSource { - suspend fun postEmail(email: Email): Result + suspend fun postEmail(email: Email): Result } diff --git a/data/src/main/java/com/everymeal/data/datasource/auth/AuthRemoteRemoteDataSourceImpl.kt b/data/src/main/java/com/everymeal/data/datasource/auth/AuthRemoteRemoteDataSourceImpl.kt index 385992a9..d05549ea 100644 --- a/data/src/main/java/com/everymeal/data/datasource/auth/AuthRemoteRemoteDataSourceImpl.kt +++ b/data/src/main/java/com/everymeal/data/datasource/auth/AuthRemoteRemoteDataSourceImpl.kt @@ -1,16 +1,20 @@ package com.everymeal.data.datasource.auth +import com.everymeal.data.model.auth.EmailResponse +import com.everymeal.data.model.auth.toEmail +import com.everymeal.data.model.auth.toEmailAuthToken import com.everymeal.data.model.auth.toEmailRequest import com.everymeal.data.model.unwrapData import com.everymeal.data.service.auth.AuthApi import com.everymeal.domain.model.auth.Email +import com.everymeal.domain.model.auth.EmailAuthToken import javax.inject.Inject class AuthRemoteRemoteDataSourceImpl @Inject constructor( private val authApi: AuthApi ) : AuthRemoteDataSource { - override suspend fun postEmail(email: Email): Result = runCatching { - authApi.postEmail(email.toEmailRequest()) - }.unwrapData() + override suspend fun postEmail(email: Email): Result = runCatching { + authApi.postEmail(email.toEmailRequest()).data.toEmailAuthToken() + } } diff --git a/data/src/main/java/com/everymeal/data/model/auth/EmailResponse.kt b/data/src/main/java/com/everymeal/data/model/auth/EmailResponse.kt new file mode 100644 index 00000000..ce4daa9e --- /dev/null +++ b/data/src/main/java/com/everymeal/data/model/auth/EmailResponse.kt @@ -0,0 +1,14 @@ +package com.everymeal.data.model.auth + +import com.everymeal.domain.model.auth.EmailAuthToken +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class EmailResponse( + @SerialName("emailAuthToken") val emailAuthToken: String, +) + +fun EmailResponse.toEmailAuthToken(): EmailAuthToken = + EmailAuthToken(emailAuthToken = emailAuthToken) + diff --git a/data/src/main/java/com/everymeal/data/repository/DefaultAuthRepository.kt b/data/src/main/java/com/everymeal/data/repository/DefaultAuthRepository.kt index 5fdbf858..3d63309d 100644 --- a/data/src/main/java/com/everymeal/data/repository/DefaultAuthRepository.kt +++ b/data/src/main/java/com/everymeal/data/repository/DefaultAuthRepository.kt @@ -2,13 +2,14 @@ package com.everymeal.data.repository import com.everymeal.data.datasource.auth.AuthRemoteDataSource import com.everymeal.domain.model.auth.Email +import com.everymeal.domain.model.auth.EmailAuthToken import com.everymeal.domain.repository.auth.AuthRepository import javax.inject.Inject class DefaultAuthRepository @Inject constructor( private val authRemoteDataSource: AuthRemoteDataSource ) : AuthRepository { - override suspend fun postEmail(email: Email): Result { + override suspend fun postEmail(email: Email): Result { return authRemoteDataSource.postEmail(email) } } diff --git a/data/src/main/java/com/everymeal/data/service/auth/AuthApi.kt b/data/src/main/java/com/everymeal/data/service/auth/AuthApi.kt index e0079350..fc685b30 100644 --- a/data/src/main/java/com/everymeal/data/service/auth/AuthApi.kt +++ b/data/src/main/java/com/everymeal/data/service/auth/AuthApi.kt @@ -2,9 +2,13 @@ package com.everymeal.data.service.auth import com.everymeal.data.model.BaseResponse import com.everymeal.data.model.auth.EmailRequest +import com.everymeal.data.model.auth.EmailResponse +import retrofit2.http.Body import retrofit2.http.POST interface AuthApi { @POST("/api/v1/users/email") - suspend fun postEmail(emailRequest: EmailRequest): BaseResponse + suspend fun postEmail( + @Body emailRequest: EmailRequest + ): BaseResponse } diff --git a/domain/src/main/java/com/everymeal/domain/model/auth/EmailAuthToken.kt b/domain/src/main/java/com/everymeal/domain/model/auth/EmailAuthToken.kt new file mode 100644 index 00000000..3592b4ec --- /dev/null +++ b/domain/src/main/java/com/everymeal/domain/model/auth/EmailAuthToken.kt @@ -0,0 +1,5 @@ +package com.everymeal.domain.model.auth + +data class EmailAuthToken( + val emailAuthToken: String +) diff --git a/domain/src/main/java/com/everymeal/domain/repository/auth/AuthRepository.kt b/domain/src/main/java/com/everymeal/domain/repository/auth/AuthRepository.kt index 8d4c53e6..08fd3fad 100644 --- a/domain/src/main/java/com/everymeal/domain/repository/auth/AuthRepository.kt +++ b/domain/src/main/java/com/everymeal/domain/repository/auth/AuthRepository.kt @@ -1,8 +1,9 @@ package com.everymeal.domain.repository.auth import com.everymeal.domain.model.auth.Email +import com.everymeal.domain.model.auth.EmailAuthToken interface AuthRepository { - suspend fun postEmail(email: Email): Result + suspend fun postEmail(email: Email): Result } diff --git a/domain/src/main/java/com/everymeal/domain/usecase/auth/PostEmailUseCase.kt b/domain/src/main/java/com/everymeal/domain/usecase/auth/PostEmailUseCase.kt index e3a44222..5db84d7f 100644 --- a/domain/src/main/java/com/everymeal/domain/usecase/auth/PostEmailUseCase.kt +++ b/domain/src/main/java/com/everymeal/domain/usecase/auth/PostEmailUseCase.kt @@ -1,6 +1,7 @@ package com.everymeal.domain.usecase.auth import com.everymeal.domain.model.auth.Email +import com.everymeal.domain.model.auth.EmailAuthToken import com.everymeal.domain.repository.auth.AuthRepository import javax.inject.Inject @@ -9,7 +10,7 @@ class PostEmailUseCase @Inject constructor( ) { suspend operator fun invoke( email: Email - ): Result { + ): Result { return authRepository.postEmail(email) } } diff --git a/presentation/src/main/java/com/everymeal/presentation/ui/main/MainScreen.kt b/presentation/src/main/java/com/everymeal/presentation/ui/main/MainScreen.kt index 9eb640f3..c7e25d83 100644 --- a/presentation/src/main/java/com/everymeal/presentation/ui/main/MainScreen.kt +++ b/presentation/src/main/java/com/everymeal/presentation/ui/main/MainScreen.kt @@ -61,7 +61,7 @@ fun MainScreen( navController.navigate(EveryMealRoute.DETAIL_LIST.route.plus("/$detailScreenType")) }, onDetailRestaurantClick = { - navController.navigate(EveryMealRoute.DETAIL_RESTAURANT.route) + navController.navigate(EveryMealRoute.SCHOOL_AUTH.route) } ) } diff --git a/presentation/src/main/java/com/everymeal/presentation/ui/signup/school/SchoolAuthScreen.kt b/presentation/src/main/java/com/everymeal/presentation/ui/signup/school/SchoolAuthScreen.kt index 87e860f8..6a86a76f 100644 --- a/presentation/src/main/java/com/everymeal/presentation/ui/signup/school/SchoolAuthScreen.kt +++ b/presentation/src/main/java/com/everymeal/presentation/ui/signup/school/SchoolAuthScreen.kt @@ -1,5 +1,6 @@ package com.everymeal.presentation.ui.signup.school +import android.util.Log import androidx.compose.foundation.Image import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -16,14 +17,11 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import com.everymeal.presentation.R import com.everymeal.presentation.components.EveryMealConditionAgreeDialog import com.everymeal.presentation.components.EveryMealConditionAgreeDialogItem -import com.everymeal.presentation.ui.home.HomeContract -import com.everymeal.presentation.ui.home.title import com.everymeal.presentation.ui.signup.school.email.SchoolAuthPostEmailScreen import com.everymeal.presentation.ui.theme.EveryMealTypography @@ -45,6 +43,7 @@ fun SchoolAuthScreen( is SchoolContract.Effect.Error -> { // TODO Error 처리 } + is SchoolContract.Effect.SuccessEmailVerification -> { // TODO 이메일 인증 성공 } @@ -91,17 +90,17 @@ private fun EmailAuthBottomSheet(viewModel: SchoolAuthViewModel) { mutableStateListOf( EveryMealConditionAgreeDialogItem( title = "[필수] 이용 약관 동의", - isAgreed = true, + isAgreed = false, isEssential = true, ), EveryMealConditionAgreeDialogItem( title = "[필수] 개인정보 수집 및 이용 동의", - isAgreed = true, + isAgreed = false, isEssential = true, ), EveryMealConditionAgreeDialogItem( title = "[선택] 마케팅 정보 수집 동의", - isAgreed = true, + isAgreed = false, ) ) } @@ -113,6 +112,7 @@ private fun EmailAuthBottomSheet(viewModel: SchoolAuthViewModel) { }, onNextButtonClicked = { if (conditionItems.filter { it.isEssential }.any { it.isAgreed }) { + Log.d("TAG", "EmailAuthBottomSheet: ${conditionItems.filter { it.isEssential }.any { it.isAgreed }}") viewModel.setEvent(SchoolContract.Event.OnPostEmail) } }, @@ -133,10 +133,3 @@ fun SchoolAuthContent( state = state, ) } - - -@Preview -@Composable -fun SchoolAuthScreenPreview() { - -} diff --git a/presentation/src/main/java/com/everymeal/presentation/ui/signup/school/SchoolAuthViewModel.kt b/presentation/src/main/java/com/everymeal/presentation/ui/signup/school/SchoolAuthViewModel.kt index 08599a1c..4831b84d 100644 --- a/presentation/src/main/java/com/everymeal/presentation/ui/signup/school/SchoolAuthViewModel.kt +++ b/presentation/src/main/java/com/everymeal/presentation/ui/signup/school/SchoolAuthViewModel.kt @@ -1,20 +1,23 @@ package com.everymeal.presentation.ui.signup.school +import android.util.Log import androidx.lifecycle.viewModelScope import com.everymeal.domain.model.auth.Email import com.everymeal.domain.usecase.auth.PostEmailUseCase import com.everymeal.presentation.base.BaseViewModel +import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch import retrofit2.HttpException import javax.inject.Inject +@HiltViewModel class SchoolAuthViewModel @Inject constructor( private val postEmailUseCase: PostEmailUseCase ) : BaseViewModel(SchoolContract.State()) { companion object { - private val EMAIL_REGEX = "[a-zA-Z0-9._-]+@[a-z]+\\.+[a-z]+".toRegex() + private val EMAIL_REGEX = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}\$".toRegex() } override fun handleEvents(event: SchoolContract.Event) { @@ -27,6 +30,7 @@ class SchoolAuthViewModel @Inject constructor( ) } } + is SchoolContract.Event.OnTokenTextChanged -> { updateState { copy( @@ -36,8 +40,13 @@ class SchoolAuthViewModel @Inject constructor( } is SchoolContract.Event.OnEmailNextButtonClicked -> { - updateState { - copy(isShowConditionBottomSheet = true) + if (viewState.value.isEmailError) { + // TODO 이메일 에러 + sendEffect({ SchoolContract.Effect.Error(400) }) + } else { + updateState { + copy(isShowConditionBottomSheet = true) + } } } @@ -51,7 +60,7 @@ class SchoolAuthViewModel @Inject constructor( SchoolContract.Event.OnTokenNextButtonClicked -> { val viewState = viewState.value - if (viewState.emailAuthToken == viewState.tokenText ) { + if (viewState.emailAuthToken == viewState.tokenText) { sendEffect({ SchoolContract.Effect.SuccessEmailVerification }) } } @@ -60,16 +69,18 @@ class SchoolAuthViewModel @Inject constructor( private fun isValidEmail(email: String): Boolean { - return EMAIL_REGEX.matches(email) + return !EMAIL_REGEX.matches(email) } private fun postEmail() { viewModelScope.launch { postEmailUseCase(Email(viewState.value.emailText)).onSuccess { + Log.d("SchoolAuthViewModel", "postEmail: $it") updateState { - copy(emailAuthToken = it) + copy(emailAuthToken = it.emailAuthToken) } }.onFailure { + Log.d("SchoolAuthViewModel", "postEmail: $it") if (it is HttpException) { sendEffect({ SchoolContract.Effect.Error(it.code()) }) } From 5846566c440782a667aaf56d930a00855ad2b4d0 Mon Sep 17 00:00:00 2001 From: KwakEuiJin Date: Wed, 6 Dec 2023 21:37:16 +0900 Subject: [PATCH 3/3] [feat/school_auth]: Fix EmailTokenVerifyScreen --- .../datasource/auth/AuthRemoteDataSource.kt | 1 + .../auth/AuthRemoteRemoteDataSourceImpl.kt | 4 ++ .../data/repository/DefaultAuthRepository.kt | 7 +++ .../everymeal/data/service/auth/AuthApi.kt | 8 +++ .../domain/repository/auth/AuthRepository.kt | 1 + .../domain/usecase/auth/VerifyTokenUseCase.kt | 15 +++++ .../ui/signup/school/SchoolAuthScreen.kt | 38 +++++++++--- .../ui/signup/school/SchoolAuthViewModel.kt | 48 ++++++++++----- .../ui/signup/school/SchoolContract.kt | 10 +++- .../school/email/EmailTokenVerifyScreen.kt | 59 +++++++++++++++++++ .../school/email/SchoolAuthPostEmailScreen.kt | 32 ++-------- 11 files changed, 170 insertions(+), 53 deletions(-) create mode 100644 domain/src/main/java/com/everymeal/domain/usecase/auth/VerifyTokenUseCase.kt create mode 100644 presentation/src/main/java/com/everymeal/presentation/ui/signup/school/email/EmailTokenVerifyScreen.kt diff --git a/data/src/main/java/com/everymeal/data/datasource/auth/AuthRemoteDataSource.kt b/data/src/main/java/com/everymeal/data/datasource/auth/AuthRemoteDataSource.kt index b36ee84d..16175490 100644 --- a/data/src/main/java/com/everymeal/data/datasource/auth/AuthRemoteDataSource.kt +++ b/data/src/main/java/com/everymeal/data/datasource/auth/AuthRemoteDataSource.kt @@ -5,4 +5,5 @@ import com.everymeal.domain.model.auth.EmailAuthToken interface AuthRemoteDataSource { suspend fun postEmail(email: Email): Result + suspend fun verifyToken(emailAuthToken: String, emailAuthValue: String): Result } diff --git a/data/src/main/java/com/everymeal/data/datasource/auth/AuthRemoteRemoteDataSourceImpl.kt b/data/src/main/java/com/everymeal/data/datasource/auth/AuthRemoteRemoteDataSourceImpl.kt index d05549ea..3be62dab 100644 --- a/data/src/main/java/com/everymeal/data/datasource/auth/AuthRemoteRemoteDataSourceImpl.kt +++ b/data/src/main/java/com/everymeal/data/datasource/auth/AuthRemoteRemoteDataSourceImpl.kt @@ -17,4 +17,8 @@ class AuthRemoteRemoteDataSourceImpl @Inject constructor( override suspend fun postEmail(email: Email): Result = runCatching { authApi.postEmail(email.toEmailRequest()).data.toEmailAuthToken() } + + override suspend fun verifyToken(emailAuthToken: String, emailAuthValue: String) = runCatching { + authApi.verifyToken(emailAuthToken, emailAuthValue).data + } } diff --git a/data/src/main/java/com/everymeal/data/repository/DefaultAuthRepository.kt b/data/src/main/java/com/everymeal/data/repository/DefaultAuthRepository.kt index 3d63309d..ebed66e4 100644 --- a/data/src/main/java/com/everymeal/data/repository/DefaultAuthRepository.kt +++ b/data/src/main/java/com/everymeal/data/repository/DefaultAuthRepository.kt @@ -12,4 +12,11 @@ class DefaultAuthRepository @Inject constructor( override suspend fun postEmail(email: Email): Result { return authRemoteDataSource.postEmail(email) } + + override suspend fun verifyToken( + emailAuthToken: String, + emailAuthValue: String + ): Result { + return authRemoteDataSource.verifyToken(emailAuthToken, emailAuthValue) + } } diff --git a/data/src/main/java/com/everymeal/data/service/auth/AuthApi.kt b/data/src/main/java/com/everymeal/data/service/auth/AuthApi.kt index fc685b30..df35624a 100644 --- a/data/src/main/java/com/everymeal/data/service/auth/AuthApi.kt +++ b/data/src/main/java/com/everymeal/data/service/auth/AuthApi.kt @@ -4,11 +4,19 @@ import com.everymeal.data.model.BaseResponse import com.everymeal.data.model.auth.EmailRequest import com.everymeal.data.model.auth.EmailResponse import retrofit2.http.Body +import retrofit2.http.GET import retrofit2.http.POST +import retrofit2.http.Query interface AuthApi { @POST("/api/v1/users/email") suspend fun postEmail( @Body emailRequest: EmailRequest ): BaseResponse + + @GET("/api/v1/users/email/verify") + suspend fun verifyToken( + @Query("emailAuthToken") emailAuthToken: String, + @Query("emailAuthValue") emailAuthValue: String + ): BaseResponse } diff --git a/domain/src/main/java/com/everymeal/domain/repository/auth/AuthRepository.kt b/domain/src/main/java/com/everymeal/domain/repository/auth/AuthRepository.kt index 08fd3fad..22a84009 100644 --- a/domain/src/main/java/com/everymeal/domain/repository/auth/AuthRepository.kt +++ b/domain/src/main/java/com/everymeal/domain/repository/auth/AuthRepository.kt @@ -5,5 +5,6 @@ import com.everymeal.domain.model.auth.EmailAuthToken interface AuthRepository { suspend fun postEmail(email: Email): Result + suspend fun verifyToken(emailAuthToken: String, emailAuthValue: String): Result } diff --git a/domain/src/main/java/com/everymeal/domain/usecase/auth/VerifyTokenUseCase.kt b/domain/src/main/java/com/everymeal/domain/usecase/auth/VerifyTokenUseCase.kt new file mode 100644 index 00000000..84d3d6b9 --- /dev/null +++ b/domain/src/main/java/com/everymeal/domain/usecase/auth/VerifyTokenUseCase.kt @@ -0,0 +1,15 @@ +package com.everymeal.domain.usecase.auth + +import com.everymeal.domain.repository.auth.AuthRepository +import javax.inject.Inject + +class VerifyTokenUseCase @Inject constructor( + private val authRepository: AuthRepository +) { + suspend operator fun invoke( + emailAuthToken: String, + emailAuthValue: String + ): Result { + return authRepository.verifyToken(emailAuthToken, emailAuthValue) + } +} diff --git a/presentation/src/main/java/com/everymeal/presentation/ui/signup/school/SchoolAuthScreen.kt b/presentation/src/main/java/com/everymeal/presentation/ui/signup/school/SchoolAuthScreen.kt index 6a86a76f..81cd578d 100644 --- a/presentation/src/main/java/com/everymeal/presentation/ui/signup/school/SchoolAuthScreen.kt +++ b/presentation/src/main/java/com/everymeal/presentation/ui/signup/school/SchoolAuthScreen.kt @@ -22,6 +22,7 @@ import androidx.hilt.navigation.compose.hiltViewModel import com.everymeal.presentation.R import com.everymeal.presentation.components.EveryMealConditionAgreeDialog import com.everymeal.presentation.components.EveryMealConditionAgreeDialogItem +import com.everymeal.presentation.ui.signup.school.email.EmailTokenVerifyScreen import com.everymeal.presentation.ui.signup.school.email.SchoolAuthPostEmailScreen import com.everymeal.presentation.ui.theme.EveryMealTypography @@ -41,11 +42,14 @@ fun SchoolAuthScreen( viewModel.effect.collect { effect -> when (effect) { is SchoolContract.Effect.Error -> { - // TODO Error 처리 + Log.e( + "SchoolAuthScreen", + "code: ${effect.code.toString()} message: ${effect.message}" + ) } is SchoolContract.Effect.SuccessEmailVerification -> { - // TODO 이메일 인증 성공 + onSuccessEmailVerification() } } } @@ -112,7 +116,12 @@ private fun EmailAuthBottomSheet(viewModel: SchoolAuthViewModel) { }, onNextButtonClicked = { if (conditionItems.filter { it.isEssential }.any { it.isAgreed }) { - Log.d("TAG", "EmailAuthBottomSheet: ${conditionItems.filter { it.isEssential }.any { it.isAgreed }}") + Log.d( + "TAG", + "EmailAuthBottomSheet: ${ + conditionItems.filter { it.isEssential }.any { it.isAgreed } + }" + ) viewModel.setEvent(SchoolContract.Event.OnPostEmail) } }, @@ -127,9 +136,22 @@ fun SchoolAuthContent( viewModel: SchoolAuthViewModel, state: SchoolContract.State ) { - SchoolAuthPostEmailScreen( - modifier = modifier, - viewModel = viewModel, - state = state, - ) + when (state.schoolAuthScreenType) { + SchoolAuthScreenType.POST_EMAIL -> { + SchoolAuthPostEmailScreen( + modifier = modifier, + viewModel = viewModel, + state = state, + ) + } + + SchoolAuthScreenType.VERIFY_TOKEN -> { + EmailTokenVerifyScreen( + modifier = modifier, + state = state, + viewModel = viewModel + ) + } + } + } diff --git a/presentation/src/main/java/com/everymeal/presentation/ui/signup/school/SchoolAuthViewModel.kt b/presentation/src/main/java/com/everymeal/presentation/ui/signup/school/SchoolAuthViewModel.kt index 4831b84d..164d9d5b 100644 --- a/presentation/src/main/java/com/everymeal/presentation/ui/signup/school/SchoolAuthViewModel.kt +++ b/presentation/src/main/java/com/everymeal/presentation/ui/signup/school/SchoolAuthViewModel.kt @@ -4,6 +4,7 @@ import android.util.Log import androidx.lifecycle.viewModelScope import com.everymeal.domain.model.auth.Email import com.everymeal.domain.usecase.auth.PostEmailUseCase +import com.everymeal.domain.usecase.auth.VerifyTokenUseCase import com.everymeal.presentation.base.BaseViewModel import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch @@ -12,7 +13,8 @@ import javax.inject.Inject @HiltViewModel class SchoolAuthViewModel @Inject constructor( - private val postEmailUseCase: PostEmailUseCase + private val postEmailUseCase: PostEmailUseCase, + private val verifyTokenUseCase: VerifyTokenUseCase ) : BaseViewModel(SchoolContract.State()) { @@ -33,17 +35,12 @@ class SchoolAuthViewModel @Inject constructor( is SchoolContract.Event.OnTokenTextChanged -> { updateState { - copy( - tokenText = event.token - ) + copy(emailAuthValue = event.authValue) } } is SchoolContract.Event.OnEmailNextButtonClicked -> { - if (viewState.value.isEmailError) { - // TODO 이메일 에러 - sendEffect({ SchoolContract.Effect.Error(400) }) - } else { + if (!viewState.value.isEmailError) { updateState { copy(isShowConditionBottomSheet = true) } @@ -59,10 +56,7 @@ class SchoolAuthViewModel @Inject constructor( } SchoolContract.Event.OnTokenNextButtonClicked -> { - val viewState = viewState.value - if (viewState.emailAuthToken == viewState.tokenText) { - sendEffect({ SchoolContract.Effect.SuccessEmailVerification }) - } + verifyToken() } } } @@ -77,12 +71,36 @@ class SchoolAuthViewModel @Inject constructor( postEmailUseCase(Email(viewState.value.emailText)).onSuccess { Log.d("SchoolAuthViewModel", "postEmail: $it") updateState { - copy(emailAuthToken = it.emailAuthToken) + copy( + emailAuthToken = it.emailAuthToken, + schoolAuthScreenType = SchoolAuthScreenType.VERIFY_TOKEN, + isShowConditionBottomSheet = false + ) + } + }.onFailure { + Log.d("SchoolAuthViewModel", "postEmail: $it") + when (it) { + is HttpException -> sendEffect({ SchoolContract.Effect.Error(code = it.code()) }) + else -> sendEffect({ SchoolContract.Effect.Error(message = it.message) }) } + } + } + } + + private fun verifyToken() { + viewModelScope.launch { + val state = viewState.value + verifyTokenUseCase( + emailAuthToken = state.emailAuthToken, + emailAuthValue = state.emailAuthValue + ).onSuccess { + Log.d("SchoolAuthViewModel", "postEmail: $it") + sendEffect({ SchoolContract.Effect.SuccessEmailVerification }) }.onFailure { Log.d("SchoolAuthViewModel", "postEmail: $it") - if (it is HttpException) { - sendEffect({ SchoolContract.Effect.Error(it.code()) }) + when (it) { + is HttpException -> sendEffect({ SchoolContract.Effect.Error(code = it.code()) }) + else -> sendEffect({ SchoolContract.Effect.Error(message = it.message) }) } } } diff --git a/presentation/src/main/java/com/everymeal/presentation/ui/signup/school/SchoolContract.kt b/presentation/src/main/java/com/everymeal/presentation/ui/signup/school/SchoolContract.kt index 0664f817..51ebab9b 100644 --- a/presentation/src/main/java/com/everymeal/presentation/ui/signup/school/SchoolContract.kt +++ b/presentation/src/main/java/com/everymeal/presentation/ui/signup/school/SchoolContract.kt @@ -11,13 +11,13 @@ class SchoolContract { val isShowConditionBottomSheet: Boolean = false, val isEmailError: Boolean = false, val emailText: String = "", - val tokenText: String = "", + val emailAuthValue: String = "", val emailAuthToken: String = "" ) : ViewState sealed class Event : ViewEvent { data class OnEmailTextChanged(val emailLink: String) : Event() - data class OnTokenTextChanged(val token: String) : Event() + data class OnTokenTextChanged(val authValue: String) : Event() object OnEmailNextButtonClicked : Event() object OnTokenNextButtonClicked : Event() object OnPostEmail : Event() @@ -25,7 +25,11 @@ class SchoolContract { } sealed class Effect : ViewSideEffect { - data class Error(val code: Int) : Effect() + data class Error( + val code: Int? = null, + val message: String? = null + ) : Effect() + object SuccessEmailVerification : Effect() } } diff --git a/presentation/src/main/java/com/everymeal/presentation/ui/signup/school/email/EmailTokenVerifyScreen.kt b/presentation/src/main/java/com/everymeal/presentation/ui/signup/school/email/EmailTokenVerifyScreen.kt new file mode 100644 index 00000000..83905651 --- /dev/null +++ b/presentation/src/main/java/com/everymeal/presentation/ui/signup/school/email/EmailTokenVerifyScreen.kt @@ -0,0 +1,59 @@ +package com.everymeal.presentation.ui.signup.school.email + +import android.util.Log +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.everymeal.presentation.R +import com.everymeal.presentation.components.EveryMealMainButton +import com.everymeal.presentation.components.EveryMealTextField +import com.everymeal.presentation.ui.signup.school.SchoolAuthViewModel +import com.everymeal.presentation.ui.signup.school.SchoolContract +import com.everymeal.presentation.ui.theme.EveryMealTypography +import com.everymeal.presentation.ui.theme.Gray100 +import com.everymeal.presentation.ui.theme.Gray900 + +@Composable +fun EmailTokenVerifyScreen( + modifier: Modifier, + state: SchoolContract.State, + viewModel: SchoolAuthViewModel +) { + Column( + modifier = modifier.padding(top = 48.dp) + ) { + Text( + text = stringResource(id = R.string.email_token_verify_title), + style = EveryMealTypography.Heading1, + color = Gray900 + ) + Spacer(modifier = Modifier.size(40.dp)) + Text( + text = stringResource(id = R.string.verify_token), + style = EveryMealTypography.Body5, + color = Gray100 + ) + Spacer(modifier = Modifier.size(6.dp)) + EveryMealTextField( + modifier = Modifier.fillMaxWidth(), + value = state.emailAuthValue, + onValueChange = { + viewModel.setEvent(SchoolContract.Event.OnTokenTextChanged(it)) + }, + ) + Spacer(modifier = Modifier.weight(1f)) + EveryMealMainButton( + text = stringResource(id = R.string.next), + onClick = { + viewModel.setEvent(SchoolContract.Event.OnTokenNextButtonClicked) + }, + ) + } +} diff --git a/presentation/src/main/java/com/everymeal/presentation/ui/signup/school/email/SchoolAuthPostEmailScreen.kt b/presentation/src/main/java/com/everymeal/presentation/ui/signup/school/email/SchoolAuthPostEmailScreen.kt index 76f234df..462daf65 100644 --- a/presentation/src/main/java/com/everymeal/presentation/ui/signup/school/email/SchoolAuthPostEmailScreen.kt +++ b/presentation/src/main/java/com/everymeal/presentation/ui/signup/school/email/SchoolAuthPostEmailScreen.kt @@ -13,7 +13,6 @@ import androidx.compose.ui.unit.dp import com.everymeal.presentation.R import com.everymeal.presentation.components.EveryMealMainButton import com.everymeal.presentation.components.EveryMealTextField -import com.everymeal.presentation.ui.signup.school.SchoolAuthScreenType import com.everymeal.presentation.ui.signup.school.SchoolAuthViewModel import com.everymeal.presentation.ui.signup.school.SchoolContract import com.everymeal.presentation.ui.theme.EveryMealTypography @@ -31,19 +30,13 @@ fun SchoolAuthPostEmailScreen( modifier = modifier.padding(top = 48.dp) ) { Text( - text = when (state.schoolAuthScreenType) { - SchoolAuthScreenType.POST_EMAIL -> stringResource(id = R.string.school_auth_content) - SchoolAuthScreenType.VERIFY_TOKEN -> stringResource(id = R.string.email_token_verify_title) - }, + text = stringResource(id = R.string.school_auth_content), style = EveryMealTypography.Heading1, color = Gray900 ) Spacer(modifier = Modifier.size(40.dp)) Text( - text = when (state.schoolAuthScreenType) { - SchoolAuthScreenType.POST_EMAIL -> stringResource(id = R.string.email) - SchoolAuthScreenType.VERIFY_TOKEN -> stringResource(id = R.string.verify_token) - }, + text = stringResource(id = R.string.email), style = EveryMealTypography.Body5, color = Gray100 ) @@ -52,22 +45,10 @@ fun SchoolAuthPostEmailScreen( modifier = Modifier.fillMaxWidth(), value = state.emailText, onValueChange = { - when (state.schoolAuthScreenType) { - SchoolAuthScreenType.POST_EMAIL -> viewModel.setEvent( - SchoolContract.Event.OnEmailTextChanged( - it - ) - ) - - SchoolAuthScreenType.VERIFY_TOKEN -> viewModel.setEvent( - SchoolContract.Event.OnTokenTextChanged( - it - ) - ) - } + viewModel.setEvent(SchoolContract.Event.OnEmailTextChanged(it)) }, supportingText = { - if (state.isEmailError && state.schoolAuthScreenType == SchoolAuthScreenType.POST_EMAIL) { + if (state.isEmailError) { Text( text = stringResource(id = R.string.email_error), style = EveryMealTypography.Body5, @@ -80,10 +61,7 @@ fun SchoolAuthPostEmailScreen( EveryMealMainButton( text = stringResource(id = R.string.next), onClick = { - when (state.schoolAuthScreenType) { - SchoolAuthScreenType.POST_EMAIL -> viewModel.setEvent(SchoolContract.Event.OnEmailNextButtonClicked) - SchoolAuthScreenType.VERIFY_TOKEN -> viewModel.setEvent(SchoolContract.Event.OnTokenNextButtonClicked) - } + viewModel.setEvent(SchoolContract.Event.OnEmailNextButtonClicked) }, ) }