diff --git a/app/src/main/java/org/sopt/and/core/composition/PreferenceUtilProvider.kt b/app/src/main/java/org/sopt/and/core/composition/PreferenceUtilProvider.kt deleted file mode 100644 index 1db02ba..0000000 --- a/app/src/main/java/org/sopt/and/core/composition/PreferenceUtilProvider.kt +++ /dev/null @@ -1,17 +0,0 @@ -package org.sopt.and.core.composition - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import org.sopt.and.core.utils.PreferenceUtil - -@Composable -fun PreferenceUtilProvider( - preferenceUtil: PreferenceUtil, - content: @Composable () -> Unit -) { - CompositionLocalProvider( - PreferenceUtil.LocalPreferenceUtils provides preferenceUtil - ) { - content() - } -} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/core/utils/PreferenceUtil.kt b/app/src/main/java/org/sopt/and/core/utils/PreferenceUtil.kt index 2899261..23d1ade 100644 --- a/app/src/main/java/org/sopt/and/core/utils/PreferenceUtil.kt +++ b/app/src/main/java/org/sopt/and/core/utils/PreferenceUtil.kt @@ -1,20 +1,15 @@ package org.sopt.and.core.utils -import android.content.SharedPreferences -import androidx.compose.runtime.staticCompositionLocalOf +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject class PreferenceUtil @Inject constructor( - private val sharedPreferences: SharedPreferences + @ApplicationContext private val context: Context ) { - - companion object { - private const val USER_TOKEN = "user_token" - val LocalPreferenceUtils = staticCompositionLocalOf { - error("PreferenceUtils is not initialized") - } - } + private val sharedPreferences = + context.getSharedPreferences("wavve_prefs", Context.MODE_PRIVATE) fun saveUserToken(token: String) { sharedPreferences.edit().putString(USER_TOKEN, token).apply() @@ -31,4 +26,8 @@ class PreferenceUtil @Inject constructor( fun clearAll() { sharedPreferences.edit().clear().apply() } + + companion object { + private const val USER_TOKEN = "user_token" + } } diff --git a/app/src/main/java/org/sopt/and/data/di/PreferenceModule.kt b/app/src/main/java/org/sopt/and/data/di/PreferenceModule.kt index 3158d13..21b2281 100644 --- a/app/src/main/java/org/sopt/and/data/di/PreferenceModule.kt +++ b/app/src/main/java/org/sopt/and/data/di/PreferenceModule.kt @@ -14,10 +14,7 @@ import javax.inject.Singleton object PreferenceModule { @Provides @Singleton - fun providePreferenceUtils( - @ApplicationContext context: Context - ): PreferenceUtil { - val sharedPreferences = context.getSharedPreferences("wavve_prefs", Context.MODE_PRIVATE) - return PreferenceUtil(sharedPreferences) + fun providePreferenceUtil(@ApplicationContext context: Context): PreferenceUtil { + return PreferenceUtil(context) } } \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/data/di/RepositoryModule.kt b/app/src/main/java/org/sopt/and/data/di/RepositoryModule.kt index 3d5784b..0fd0e0a 100644 --- a/app/src/main/java/org/sopt/and/data/di/RepositoryModule.kt +++ b/app/src/main/java/org/sopt/and/data/di/RepositoryModule.kt @@ -5,9 +5,11 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import org.sopt.and.data.api.UserService +import org.sopt.and.data.repository.DummyHomeContentRepositoryImpl import org.sopt.and.data.repository.GetMyHobbyRepositoryImpl import org.sopt.and.data.repository.UserLoginRepositoryImpl import org.sopt.and.data.repository.UserRegisterRepositoryImpl +import org.sopt.and.domain.repository.DummyHomeContentRepository import org.sopt.and.domain.repository.GetMyHobbyRepository import org.sopt.and.domain.repository.UserLoginRepository import org.sopt.and.domain.repository.UserRegisterRepository @@ -34,4 +36,10 @@ object RepositoryModule { fun provideGetMyHobbyRepository(userService: UserService): GetMyHobbyRepository { return GetMyHobbyRepositoryImpl(userService) } + + @Provides + @Singleton + fun provideDummyHomeContentRepository(): DummyHomeContentRepository { + return DummyHomeContentRepositoryImpl() + } } \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/data/repository/DummyHomeContentRepositoryImpl.kt b/app/src/main/java/org/sopt/and/data/repository/DummyHomeContentRepositoryImpl.kt new file mode 100644 index 0000000..721a0d9 --- /dev/null +++ b/app/src/main/java/org/sopt/and/data/repository/DummyHomeContentRepositoryImpl.kt @@ -0,0 +1,181 @@ +package org.sopt.and.data.repository + +import org.sopt.and.R +import org.sopt.and.domain.model.entity.HomeCommonContent +import org.sopt.and.domain.model.entity.HomeContent +import org.sopt.and.domain.repository.DummyHomeContentRepository +import javax.inject.Inject + +class DummyHomeContentRepositoryImpl @Inject constructor() : DummyHomeContentRepository { + override fun getDummyMainContents(): List = listOf( + HomeContent( + id = 1, + title = "이토록 친밀한 배신자", + image = R.drawable.img_banner_poaster_1, + description = "이토록 친밀한 배신자" + ), HomeContent( + id = 1, + title = "어이미남!!2", + image = R.drawable.img_banner_poaster_2, + description = "어이미남!!2" + ), HomeContent( + id = 1, + title = "나의 히어로 아카데미아", + image = R.drawable.img_banner_poaster_3, + description = "나의 히어로 아카데미아" + ) + ) + + override fun getDummyCommonContents(): List = listOf( + HomeCommonContent( + mainTitle = "믿고 보는 웨이브 에디터 추천작", + contentStates = listOf( + HomeContent( + id = 1, + title = "로 앤 오더 : 토론토 - 크리미널 인텐드 시즌1", + image = R.drawable.thumbnail1, + description = "로 앤 오더 : 토론토 - 크리미널 인텐드 시즌1" + ), HomeContent( + id = 1, + title = "원피스", + image = R.drawable.thumbnail2, + description = "원피스" + ), HomeContent( + id = 1, + title = "이토록 친절한 배신자", + image = R.drawable.thumbnail3, + description = "이토록 친절한 배신자" + ), HomeContent( + id = 1, + title = "강철부대", + image = R.drawable.thumbnail4, + description = "강철부대" + ), HomeContent( + id = 1, + title = "지옥에서 온 판사", + image = R.drawable.thumbnail5, + description = "지옥에서 온 판사" + ) + ) + ), HomeCommonContent( + mainTitle = "실시간 인기 콘텐츠", + contentStates = listOf( + HomeContent( + id = 1, + title = "로 앤 오더 : 토론토 - 크리미널 인텐드 시즌1", + image = R.drawable.thumbnail1, + description = "로 앤 오더 : 토론토 - 크리미널 인텐드 시즌1" + ), HomeContent( + id = 1, + title = "원피스", + image = R.drawable.thumbnail2, + description = "원피스" + ), HomeContent( + id = 1, + title = "이토록 친절한 배신자", + image = R.drawable.thumbnail3, + description = "이토록 친절한 배신자" + ), HomeContent( + id = 1, + title = "강철부대", + image = R.drawable.thumbnail4, + description = "강철부대" + ), HomeContent( + id = 1, + title = "지옥에서 온 판사", + image = R.drawable.thumbnail5, + description = "지옥에서 온 판사" + ) + ).reversed() + ), HomeCommonContent( + mainTitle = "오직 웨이브에서", + contentStates = listOf( + HomeContent( + id = 1, + title = "런닝맨", + image = R.drawable.thumbnail6, + description = "런닝맨" + ), HomeContent( + id = 1, + title = "미운 우리 새끼", + image = R.drawable.thumbnail7, + description = "미운 우리 새끼" + ), HomeContent( + id = 1, + title = "심야괴담회", + image = R.drawable.thumbnail8, + description = "심야괴담회" + ), HomeContent( + id = 1, + title = "나 혼자 산다", + image = R.drawable.thumbnail9, + description = "나 혼자 산다" + ), HomeContent( + id = 1, + title = "전지적 참견 시점", + image = R.drawable.thumbnail10, + description = "전지적 참견 시점" + ) + ) + ) + ) + + override fun getDummyRankingContents(): HomeCommonContent = + HomeCommonContent( + mainTitle = "오늘의 TOP 20", + contentStates = listOf( + HomeContent( + id = 1, + title = "로 앤 오더 : 토론토 - 크리미널 인텐드 시즌1", + image = R.drawable.thumbnail1, + description = "로 앤 오더 : 토론토 - 크리미널 인텐드 시즌1" + ), HomeContent( + id = 1, + title = "원피스", + image = R.drawable.thumbnail2, + description = "원피스" + ), HomeContent( + id = 1, + title = "이토록 친절한 배신자", + image = R.drawable.thumbnail3, + description = "이토록 친절한 배신자" + ), HomeContent( + id = 1, + title = "강철부대", + image = R.drawable.thumbnail4, + description = "강철부대" + ), HomeContent( + id = 1, + title = "지옥에서 온 판사", + image = R.drawable.thumbnail5, + description = "지옥에서 온 판사" + ), + HomeContent( + id = 1, + title = "런닝맨", + image = R.drawable.thumbnail6, + description = "런닝맨" + ), HomeContent( + id = 1, + title = "미운 우리 새끼", + image = R.drawable.thumbnail7, + description = "미운 우리 새끼" + ), HomeContent( + id = 1, + title = "심야괴담회", + image = R.drawable.thumbnail8, + description = "심야괴담회" + ), HomeContent( + id = 1, + title = "나 혼자 산다", + image = R.drawable.thumbnail9, + description = "나 혼자 산다" + ), HomeContent( + id = 1, + title = "전지적 참견 시점", + image = R.drawable.thumbnail10, + description = "전지적 참견 시점" + ) + ) + ) +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/domain/model/entity/HomeCommonContent.kt b/app/src/main/java/org/sopt/and/domain/model/entity/HomeCommonContent.kt new file mode 100644 index 0000000..42b0a95 --- /dev/null +++ b/app/src/main/java/org/sopt/and/domain/model/entity/HomeCommonContent.kt @@ -0,0 +1,6 @@ +package org.sopt.and.domain.model.entity + +data class HomeCommonContent( + val mainTitle: String, + val contentStates: List, +) \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/domain/model/entity/HomeContent.kt b/app/src/main/java/org/sopt/and/domain/model/entity/HomeContent.kt new file mode 100644 index 0000000..986be8b --- /dev/null +++ b/app/src/main/java/org/sopt/and/domain/model/entity/HomeContent.kt @@ -0,0 +1,8 @@ +package org.sopt.and.domain.model.entity + +data class HomeContent( + val id: Int, + val title: String, + val image: Int, + val description: String, +) \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/domain/repository/DummyHomeContentRepository.kt b/app/src/main/java/org/sopt/and/domain/repository/DummyHomeContentRepository.kt new file mode 100644 index 0000000..410cdeb --- /dev/null +++ b/app/src/main/java/org/sopt/and/domain/repository/DummyHomeContentRepository.kt @@ -0,0 +1,10 @@ +package org.sopt.and.domain.repository + +import org.sopt.and.domain.model.entity.HomeCommonContent +import org.sopt.and.domain.model.entity.HomeContent + +interface DummyHomeContentRepository { + fun getDummyMainContents(): List + fun getDummyCommonContents(): List + fun getDummyRankingContents(): HomeCommonContent +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/presentation/auth/signin/SignInContract.kt b/app/src/main/java/org/sopt/and/presentation/auth/signin/SignInContract.kt new file mode 100644 index 0000000..b996a5c --- /dev/null +++ b/app/src/main/java/org/sopt/and/presentation/auth/signin/SignInContract.kt @@ -0,0 +1,29 @@ +package org.sopt.and.presentation.auth.signin + +import org.sopt.and.presentation.util.UiEffect +import org.sopt.and.presentation.util.UiEvent +import org.sopt.and.presentation.util.UiState + +class SignInContract { + data class SignInUiState( + val username: String = "", + val password: String = "", + val isLoading: Boolean = false, + val errorMessage: String? = null + ) : UiState + + sealed class SignInUiEvent : UiEvent { + data class UpdateUserName(val username: String) : SignInUiEvent() + data class UpdatePassword(val password: String) : SignInUiEvent() + data object SignInFormSubmit : SignInUiEvent() + data object NavigateUp : SignInUiEvent() + } + + sealed class SignInUiEffect : UiEffect { + data object ShowSuccessSnackBar : SignInUiEffect() + data class ShowErrorSnackBar(val message: String) : SignInUiEffect() + data object NavigateToSignUp : SignInUiEffect() + data object NavigateToMy : SignInUiEffect() + data object NavigateUp : SignInUiEffect() + } +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/presentation/sign/SignInScreen.kt b/app/src/main/java/org/sopt/and/presentation/auth/signin/SignInScreen.kt similarity index 77% rename from app/src/main/java/org/sopt/and/presentation/sign/SignInScreen.kt rename to app/src/main/java/org/sopt/and/presentation/auth/signin/SignInScreen.kt index f6e9eca..bf952be 100644 --- a/app/src/main/java/org/sopt/and/presentation/sign/SignInScreen.kt +++ b/app/src/main/java/org/sopt/and/presentation/auth/signin/SignInScreen.kt @@ -1,4 +1,4 @@ -package org.sopt.and.presentation.sign +package org.sopt.and.presentation.auth.signin import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -14,7 +14,6 @@ import androidx.compose.material3.HorizontalDivider 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.ui.Alignment import androidx.compose.ui.Modifier @@ -23,9 +22,9 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import org.sopt.and.R import org.sopt.and.core.extension.noRippleClickable @@ -33,49 +32,49 @@ import org.sopt.and.core.component.ServiceAccountItemRow import org.sopt.and.core.component.textField.WavveCommonPasswordField import org.sopt.and.core.component.textField.WavveCommonTextField import org.sopt.and.core.component.topBar.BackButtonTopBar -import org.sopt.and.presentation.sign.component.WavveBasicButton -import org.sopt.and.presentation.sign.viewmodel.SignInViewModel +import org.sopt.and.presentation.auth.signin.component.WavveBasicButton +import org.sopt.and.presentation.auth.signin.viewmodel.SignInViewModel import org.sopt.and.core.designsystem.theme.Gray3 import org.sopt.and.core.designsystem.theme.Gray4 import org.sopt.and.core.designsystem.theme.WavveBg -import org.sopt.and.core.utils.PreferenceUtil import org.sopt.and.core.utils.SnackBarUtils @Composable fun SignInScreen( navigateToMy: () -> Unit, navigateToSignUp: () -> Unit, + navigateUp: () -> Unit, modifier: Modifier = Modifier, viewModel: SignInViewModel = hiltViewModel(), ) { - val signInState by viewModel.signInState.collectAsState() - val loginState by viewModel.loginUserResultState.collectAsState() + val signInState by viewModel.uiState.collectAsStateWithLifecycle() val context = LocalContext.current - val preferenceUtil = PreferenceUtil.LocalPreferenceUtils.current LaunchedEffect(Unit) { - viewModel.signInSuccess.collectLatest { success -> - if (success) { - loginState?.let { loginState -> - preferenceUtil.saveUserToken(loginState.token) - } - CoroutineScope(Dispatchers.Main).launch { - SnackBarUtils.showSnackBar( - message = context.getString(R.string.sign_in_snackbar_login_success), - actionLabel = context.getString(R.string.sign_in_snackbar_action_close) - ) + viewModel.effect.collect { effect -> + when (effect) { + is SignInContract.SignInUiEffect.ShowSuccessSnackBar -> { + CoroutineScope(Dispatchers.Main).launch { + SnackBarUtils.showSnackBar( + message = context.getString(R.string.sign_in_snackbar_login_success), + actionLabel = context.getString(R.string.sign_in_snackbar_action_close) + ) + } + navigateToMy() } - navigateToMy() - viewModel.resetSignInSuccess() - } else { - viewModel.errorMessageState.value?.let { message -> + + is SignInContract.SignInUiEffect.ShowErrorSnackBar -> { CoroutineScope(Dispatchers.Main).launch { SnackBarUtils.showSnackBar( - message = message, + message = effect.message, actionLabel = context.getString(R.string.sign_in_snackbar_action_close) ) } } + + is SignInContract.SignInUiEffect.NavigateToSignUp -> navigateToSignUp() + is SignInContract.SignInUiEffect.NavigateToMy -> navigateToMy() + is SignInContract.SignInUiEffect.NavigateUp -> navigateUp() } } } @@ -85,7 +84,9 @@ fun SignInScreen( .fillMaxSize() .background(WavveBg) ) { - BackButtonTopBar({ /*TODO : 뒤로가기처리*/ }) + BackButtonTopBar { + viewModel.sendEvent(SignInContract.SignInUiEvent.NavigateUp) + } Column( modifier = modifier .fillMaxSize() @@ -94,13 +95,21 @@ fun SignInScreen( ) { WavveCommonTextField( value = signInState.username, - onValueChange = viewModel::updateUserName, + onValueChange = { + viewModel.sendEvent( + SignInContract.SignInUiEvent.UpdateUserName(it) + ) + }, hint = stringResource(R.string.sign_in_text_field_id_hint) ) Spacer(modifier = Modifier.height(4.dp)) WavveCommonPasswordField( value = signInState.password, - onValueChange = viewModel::updatePassword, + onValueChange = { + viewModel.sendEvent( + SignInContract.SignInUiEvent.UpdatePassword(it) + ) + }, hint = stringResource(R.string.sign_in_text_field_password_hint) ) diff --git a/app/src/main/java/org/sopt/and/presentation/sign/component/WavveBasicButton.kt b/app/src/main/java/org/sopt/and/presentation/auth/signin/component/WavveBasicButton.kt similarity index 94% rename from app/src/main/java/org/sopt/and/presentation/sign/component/WavveBasicButton.kt rename to app/src/main/java/org/sopt/and/presentation/auth/signin/component/WavveBasicButton.kt index 1f5ce9e..4aeb9a8 100644 --- a/app/src/main/java/org/sopt/and/presentation/sign/component/WavveBasicButton.kt +++ b/app/src/main/java/org/sopt/and/presentation/auth/signin/component/WavveBasicButton.kt @@ -1,4 +1,4 @@ -package org.sopt.and.presentation.sign.component +package org.sopt.and.presentation.auth.signin.component import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding diff --git a/app/src/main/java/org/sopt/and/presentation/auth/signin/viewmodel/SignInViewModel.kt b/app/src/main/java/org/sopt/and/presentation/auth/signin/viewmodel/SignInViewModel.kt new file mode 100644 index 0000000..bfaa7f5 --- /dev/null +++ b/app/src/main/java/org/sopt/and/presentation/auth/signin/viewmodel/SignInViewModel.kt @@ -0,0 +1,82 @@ +package org.sopt.and.presentation.auth.signin.viewmodel + +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import org.sopt.and.core.utils.PreferenceUtil +import org.sopt.and.domain.model.entity.BaseResult +import org.sopt.and.domain.model.entity.UserData +import org.sopt.and.domain.usecase.LoginUseCase +import org.sopt.and.presentation.auth.signin.SignInContract.SignInUiEffect +import org.sopt.and.presentation.auth.signin.SignInContract.SignInUiEvent +import org.sopt.and.presentation.auth.signin.SignInContract.SignInUiState +import org.sopt.and.presentation.util.BaseViewModel +import javax.inject.Inject + +@HiltViewModel +class SignInViewModel @Inject constructor( + private val loginUseCase: LoginUseCase, + private val preferenceUtil: PreferenceUtil +) : BaseViewModel(SignInUiState()) { + override fun reduceState(event: SignInUiEvent) { + when (event) { + is SignInUiEvent.UpdateUserName -> { + updateState( + currentState.copy( + username = event.username + ) + ) + } + + is SignInUiEvent.UpdatePassword -> { + updateState( + currentState.copy( + password = event.password + ) + ) + } + + is SignInUiEvent.SignInFormSubmit -> signIn() + + is SignInUiEvent.NavigateUp -> postEffect(SignInUiEffect.NavigateUp) + } + } + + fun signIn() { + updateState( + currentState.copy( + isLoading = true + ) + ) + viewModelScope.launch { + when ( + val result = loginUseCase( + with(currentState) { + UserData(username, password, "") + } + ) + ) { + is BaseResult.Success -> { + updateState( + currentState.copy( + isLoading = false + ) + ) + preferenceUtil.saveUserToken(result.data.token) + postEffect(SignInUiEffect.ShowSuccessSnackBar) + postEffect(SignInUiEffect.NavigateToMy) + } + + is BaseResult.Error -> { + updateState( + currentState.copy( + isLoading = false, + errorMessage = result.message + ) + ) + postEffect(SignInUiEffect.ShowErrorSnackBar(result.message)) + } + } + } + } +} diff --git a/app/src/main/java/org/sopt/and/presentation/auth/signup/SignUpContract.kt b/app/src/main/java/org/sopt/and/presentation/auth/signup/SignUpContract.kt new file mode 100644 index 0000000..3148e84 --- /dev/null +++ b/app/src/main/java/org/sopt/and/presentation/auth/signup/SignUpContract.kt @@ -0,0 +1,42 @@ +package org.sopt.and.presentation.auth.signup + +import org.sopt.and.presentation.util.UiEffect +import org.sopt.and.presentation.util.UiEvent +import org.sopt.and.presentation.util.UiState + +class SignUpContract { + data class SignUpUiState( + val username: String = "", + val password: String = "", + val hobby: String = "", + val isUserNameValid: Boolean = false, + val isPasswordValid: Boolean = false, + val isHobbyValid: Boolean = false, + val isUserNameFieldFocused: Boolean = false, + val isPasswordFieldFocused: Boolean = false, + val isHobbyFieldFocused: Boolean = false, + val isValid: Boolean = false, + val isLoading: Boolean = false, + val errorMessage: String? = null + ) : UiState + + sealed class SignUpUiEvent : UiEvent { + data class UpdateUserName(val username: String) : SignUpUiEvent() + data class UpdatePassword(val password: String) : SignUpUiEvent() + data class UpdateHobby(val hobby: String) : SignUpUiEvent() + data class UpdateFieldFocus(val field: Field, val isFocused: Boolean) : SignUpUiEvent() + data object SignUpFormSubmit : SignUpUiEvent() + data object Close : SignUpUiEvent() + } + + sealed class SignUpUiEffect : UiEffect { + data object ShowSuccessToast : SignUpUiEffect() + data class ShowErrorToast(val message: String) : SignUpUiEffect() + data object NavigateToSignIn : SignUpUiEffect() + data object NavigateUp : SignUpUiEffect() + } + + enum class Field { + UserName, Password, Hobby + } +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/presentation/sign/SignUpScreen.kt b/app/src/main/java/org/sopt/and/presentation/auth/signup/SignUpScreen.kt similarity index 71% rename from app/src/main/java/org/sopt/and/presentation/sign/SignUpScreen.kt rename to app/src/main/java/org/sopt/and/presentation/auth/signup/SignUpScreen.kt index 86a837e..0cf87ae 100644 --- a/app/src/main/java/org/sopt/and/presentation/sign/SignUpScreen.kt +++ b/app/src/main/java/org/sopt/and/presentation/auth/signup/SignUpScreen.kt @@ -1,4 +1,4 @@ -package org.sopt.and.presentation.sign +package org.sopt.and.presentation.auth.signup import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box @@ -14,7 +14,6 @@ import androidx.compose.material3.HorizontalDivider 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.ui.Alignment import androidx.compose.ui.Modifier @@ -26,13 +25,13 @@ import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel -import kotlinx.coroutines.flow.collectLatest +import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.sopt.and.R import org.sopt.and.core.component.ServiceAccountItemRow -import org.sopt.and.presentation.sign.component.HelperText -import org.sopt.and.presentation.sign.component.SignUpTextField -import org.sopt.and.presentation.sign.component.SignUpPasswordField -import org.sopt.and.presentation.sign.viewmodel.SignUpViewModel +import org.sopt.and.presentation.auth.signup.component.HelperText +import org.sopt.and.presentation.auth.signup.component.SignUpTextField +import org.sopt.and.presentation.auth.signup.component.SignUpPasswordField +import org.sopt.and.presentation.auth.signup.viewmodel.SignUpViewModel import org.sopt.and.core.designsystem.theme.Gray3 import org.sopt.and.core.designsystem.theme.Gray4 import org.sopt.and.core.designsystem.theme.WavveBg @@ -47,22 +46,28 @@ import org.sopt.and.core.designsystem.theme.White @Composable fun SignUpScreen( navigateToSignIn: () -> Unit, + navigateUp: () -> Unit, modifier: Modifier = Modifier, - viewModel: SignUpViewModel = hiltViewModel()) { + viewModel: SignUpViewModel = hiltViewModel() +) { - val signUpState by viewModel.signUpState.collectAsState() + val signUpState by viewModel.uiState.collectAsStateWithLifecycle() val context = LocalContext.current LaunchedEffect(Unit) { - viewModel.signUpSuccess.collectLatest { success -> - if (success) { - context.showToast(context.getString(R.string.sign_up_toast_success)) - navigateToSignIn() - viewModel.resetSignUpSuccess() - } else { - viewModel.errorMessageState.value?.let { message -> - context.showToast(message) + viewModel.effect.collect { effect -> + when (effect) { + is SignUpContract.SignUpUiEffect.ShowSuccessToast -> { + context.showToast(context.getString(R.string.sign_up_toast_success)) + navigateToSignIn() } + + is SignUpContract.SignUpUiEffect.ShowErrorToast -> { + context.showToast(effect.message) + } + + is SignUpContract.SignUpUiEffect.NavigateToSignIn -> navigateToSignIn() + is SignUpContract.SignUpUiEffect.NavigateUp -> navigateUp() } } } @@ -73,7 +78,7 @@ fun SignUpScreen( ) { CloseTopBar( title = stringResource(R.string.sign_up_text_sign_up), - onCloseClicked = {} + onCloseClicked = { viewModel.sendEvent(SignUpContract.SignUpUiEvent.Close) } ) Column(modifier = Modifier.padding(16.dp)) { @@ -105,8 +110,19 @@ fun SignUpScreen( value = signUpState.username, hint = stringResource(R.string.sign_up_text_field_hint_id), isValid = signUpState.isUserNameValid, - onFocusChange = { isFocused -> viewModel.updateUserNameFieldFocused(isFocused)}, - onValueChange = { viewModel.updateUserName(it) } + onFocusChange = { isFocused -> + viewModel.sendEvent( + SignUpContract.SignUpUiEvent.UpdateFieldFocus( + SignUpContract.Field.UserName, + isFocused + ) + ) + }, + onValueChange = { + viewModel.sendEvent( + SignUpContract.SignUpUiEvent.UpdateUserName(it) + ) + } ) Spacer(modifier = Modifier.height(8.dp)) @@ -123,10 +139,21 @@ fun SignUpScreen( SignUpPasswordField( value = signUpState.password, - onValueChange = { viewModel.updatePassword(it) }, hint = stringResource(R.string.sign_up_text_field_hint_password), isValid = signUpState.isPasswordValid, - onFocusChange = { isFocused -> viewModel.updatePasswordFieldFocused(isFocused) } + onFocusChange = { isFocused -> + viewModel.sendEvent( + SignUpContract.SignUpUiEvent.UpdateFieldFocus( + SignUpContract.Field.Password, + isFocused + ) + ) + }, + onValueChange = { + viewModel.sendEvent( + SignUpContract.SignUpUiEvent.UpdatePassword(it) + ) + } ) Spacer(modifier = Modifier.height(8.dp)) @@ -145,8 +172,19 @@ fun SignUpScreen( value = signUpState.hobby, hint = stringResource(R.string.sign_up_text_field_hint_hobby), isValid = signUpState.isHobbyValid, - onFocusChange = { isFocused -> viewModel.updateHobbyFieldFocused(isFocused)}, - onValueChange = { viewModel.updateHobby(it) } + onFocusChange = { isFocused -> + viewModel.sendEvent( + SignUpContract.SignUpUiEvent.UpdateFieldFocus( + SignUpContract.Field.Hobby, + isFocused + ) + ) + }, + onValueChange = { + viewModel.sendEvent( + SignUpContract.SignUpUiEvent.UpdateHobby(it) + ) + } ) Spacer(modifier = Modifier.height(8.dp)) @@ -210,7 +248,7 @@ fun SignUpScreen( color = if (signUpState.isValid) WavvePrimary else WavveDisabled ) .wrapContentHeight() - .noRippleClickable { viewModel.registerUser() } + .noRippleClickable { viewModel.sendEvent(SignUpContract.SignUpUiEvent.SignUpFormSubmit) } .padding(vertical = 14.dp) ) { Text( diff --git a/app/src/main/java/org/sopt/and/presentation/sign/component/HelperText.kt b/app/src/main/java/org/sopt/and/presentation/auth/signup/component/HelperText.kt similarity index 98% rename from app/src/main/java/org/sopt/and/presentation/sign/component/HelperText.kt rename to app/src/main/java/org/sopt/and/presentation/auth/signup/component/HelperText.kt index fa7bcfd..304e2c2 100644 --- a/app/src/main/java/org/sopt/and/presentation/sign/component/HelperText.kt +++ b/app/src/main/java/org/sopt/and/presentation/auth/signup/component/HelperText.kt @@ -1,4 +1,4 @@ -package org.sopt.and.presentation.sign.component +package org.sopt.and.presentation.auth.signup.component import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement diff --git a/app/src/main/java/org/sopt/and/presentation/sign/component/SignUpPasswordField.kt b/app/src/main/java/org/sopt/and/presentation/auth/signup/component/SignUpPasswordField.kt similarity index 98% rename from app/src/main/java/org/sopt/and/presentation/sign/component/SignUpPasswordField.kt rename to app/src/main/java/org/sopt/and/presentation/auth/signup/component/SignUpPasswordField.kt index 5ff411b..14b976a 100644 --- a/app/src/main/java/org/sopt/and/presentation/sign/component/SignUpPasswordField.kt +++ b/app/src/main/java/org/sopt/and/presentation/auth/signup/component/SignUpPasswordField.kt @@ -1,4 +1,4 @@ -package org.sopt.and.presentation.sign.component +package org.sopt.and.presentation.auth.signup.component import androidx.compose.foundation.background import androidx.compose.foundation.border diff --git a/app/src/main/java/org/sopt/and/presentation/sign/component/SignUpTextField.kt b/app/src/main/java/org/sopt/and/presentation/auth/signup/component/SignUpTextField.kt similarity index 98% rename from app/src/main/java/org/sopt/and/presentation/sign/component/SignUpTextField.kt rename to app/src/main/java/org/sopt/and/presentation/auth/signup/component/SignUpTextField.kt index 3adb544..cb9d2d3 100644 --- a/app/src/main/java/org/sopt/and/presentation/sign/component/SignUpTextField.kt +++ b/app/src/main/java/org/sopt/and/presentation/auth/signup/component/SignUpTextField.kt @@ -1,4 +1,4 @@ -package org.sopt.and.presentation.sign.component +package org.sopt.and.presentation.auth.signup.component import androidx.compose.foundation.background import androidx.compose.foundation.border diff --git a/app/src/main/java/org/sopt/and/presentation/auth/signup/viewmodel/SignUpViewModel.kt b/app/src/main/java/org/sopt/and/presentation/auth/signup/viewmodel/SignUpViewModel.kt new file mode 100644 index 0000000..50311d7 --- /dev/null +++ b/app/src/main/java/org/sopt/and/presentation/auth/signup/viewmodel/SignUpViewModel.kt @@ -0,0 +1,134 @@ +package org.sopt.and.presentation.auth.signup.viewmodel + +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import org.sopt.and.domain.model.entity.BaseResult +import org.sopt.and.domain.model.entity.UserData +import org.sopt.and.domain.usecase.RegisterUserUseCase +import org.sopt.and.presentation.auth.signup.SignUpContract +import org.sopt.and.presentation.auth.signup.SignUpContract.SignUpUiEffect +import org.sopt.and.presentation.auth.signup.SignUpContract.SignUpUiState +import org.sopt.and.presentation.auth.signup.SignUpContract.SignUpUiEvent +import org.sopt.and.presentation.util.BaseViewModel +import javax.inject.Inject + +@HiltViewModel +class SignUpViewModel @Inject constructor( + private val registerUserUseCase: RegisterUserUseCase +) : BaseViewModel(SignUpUiState()) { + override fun reduceState(event: SignUpUiEvent) { + when (event) { + is SignUpUiEvent.UpdateUserName -> { + val isValid = validateUserName(event.username) + updateState( + currentState.copy( + username = event.username, + isUserNameValid = isValid, + isValid = isValid && + currentState.isPasswordValid && + currentState.isHobbyValid + ) + ) + } + + is SignUpUiEvent.UpdatePassword -> { + val isValid = validatePassword(event.password) + updateState( + currentState.copy( + password = event.password, + isPasswordValid = isValid, + isValid = isValid && + currentState.username.isNotBlank() && + currentState.hobby.isNotBlank() + ) + ) + } + + is SignUpUiEvent.UpdateHobby -> { + val isValid = validateHobby(event.hobby) + updateState( + currentState.copy( + hobby = event.hobby, + isHobbyValid = isValid, + isValid = isValid && + currentState.username.isNotBlank() && + currentState.password.isNotBlank() + ) + ) + } + + is SignUpUiEvent.UpdateFieldFocus -> { + when (event.field) { + SignUpContract.Field.UserName -> updateState( + currentState.copy( + isUserNameFieldFocused = event.isFocused + ) + ) + + SignUpContract.Field.Password -> updateState( + currentState.copy( + isPasswordFieldFocused = event.isFocused + ) + ) + + SignUpContract.Field.Hobby -> updateState( + currentState.copy( + isHobbyFieldFocused = event.isFocused + ) + ) + } + } + + is SignUpUiEvent.SignUpFormSubmit -> signUp() + + is SignUpUiEvent.Close -> postEffect(SignUpUiEffect.NavigateUp) + } + } + + private fun signUp() { + updateState( + currentState.copy( + isLoading = true + ) + ) + viewModelScope.launch { + when (val result = registerUserUseCase( + UserData( + username = currentState.username, + password = currentState.password, + hobby = currentState.hobby + ) + )) { + is BaseResult.Success -> { + updateState( + currentState.copy( + isLoading = false + ) + ) + postEffect(SignUpUiEffect.ShowSuccessToast) + postEffect(SignUpUiEffect.NavigateToSignIn) + } + + is BaseResult.Error -> { + updateState( + currentState.copy( + isLoading = false, + errorMessage = result.message + ) + ) + postEffect(SignUpUiEffect.ShowErrorToast(result.message)) + } + } + } + } + + private fun validateUserName(username: String) = + username.isNotBlank() && username.length <= 8 + + private fun validatePassword(password: String) = + password.isNotBlank() && password.length <= 8 + + private fun validateHobby(hobby: String) = + hobby.isNotBlank() && hobby.length <= 8 +} diff --git a/app/src/main/java/org/sopt/and/presentation/home/HomeContract.kt b/app/src/main/java/org/sopt/and/presentation/home/HomeContract.kt new file mode 100644 index 0000000..c7d0a10 --- /dev/null +++ b/app/src/main/java/org/sopt/and/presentation/home/HomeContract.kt @@ -0,0 +1,28 @@ +package org.sopt.and.presentation.home + +import org.sopt.and.core.ContentType +import org.sopt.and.domain.model.entity.HomeCommonContent +import org.sopt.and.domain.model.entity.HomeContent +import org.sopt.and.presentation.util.UiEffect +import org.sopt.and.presentation.util.UiEvent +import org.sopt.and.presentation.util.UiState + +class HomeContract { + data class HomeUiState( + val mainContents: List = emptyList(), + + val commonContents: List = emptyList(), + + val rankingContents: HomeCommonContent = HomeCommonContent( + mainTitle = "", + contentStates = emptyList() + ), + val selectedContentType: ContentType? = null + ) : UiState + + sealed class HomeUiEvent : UiEvent { + data class SetContentType(val contentType: ContentType) : HomeUiEvent() + } + + sealed class HomeUiEffect : UiEffect +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/presentation/home/HomeScreen.kt b/app/src/main/java/org/sopt/and/presentation/home/HomeScreen.kt index d5dfa72..fbedfbf 100644 --- a/app/src/main/java/org/sopt/and/presentation/home/HomeScreen.kt +++ b/app/src/main/java/org/sopt/and/presentation/home/HomeScreen.kt @@ -16,11 +16,10 @@ import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.delay import kotlinx.coroutines.withContext @@ -37,18 +36,15 @@ import org.sopt.and.core.designsystem.theme.WavveBg fun HomeScreen( onContentTypeSelected: (ContentType) -> Unit, modifier: Modifier = Modifier, - viewModel: HomeViewModel = HomeViewModel() + viewModel: HomeViewModel = hiltViewModel() ) { - - var selectedContentType by remember { mutableStateOf(null) } + val homeState by viewModel.uiState.collectAsStateWithLifecycle() val mainPagerState = rememberPagerState(initialPage = Int.MAX_VALUE / 2) { Int.MAX_VALUE // 페이지 수가 무한대 } - - val (mainContentState, commonContentState, rankingContentState) = with(viewModel) { - Triple(mainContents, commonContents, rankingContents) + LaunchedEffect(Unit) { + viewModel.getDummyHomeContent() } - LazyColumn( modifier = modifier, verticalArrangement = Arrangement.spacedBy(16.dp) @@ -64,10 +60,12 @@ fun HomeScreen( .background(WavveBg) .padding(horizontal = 16.dp, vertical = 8.dp), onContentTypeSelected = { contentType -> - selectedContentType = contentType onContentTypeSelected(contentType) + viewModel.sendEvent( + HomeContract.HomeUiEvent.SetContentType(contentType) + ) }, - selectedContentType = selectedContentType + selectedContentType = homeState.selectedContentType ) } @@ -77,22 +75,22 @@ fun HomeScreen( .fillMaxWidth() .height(480.dp), state = mainPagerState, - mainContentState = mainContentState, + mainContent = homeState.mainContents, onMainContentClicked = { } ) } - items(commonContentState) { content -> + items(homeState.commonContents) { content -> CommonContentHorizontalColumn( - commonContentState = content, - onContentClicked = { } + commonContent = content, + onContentClicked = { } ) } item { RankingContentHorizontalColumn( modifier = Modifier.fillMaxWidth(), - commonContentState = rankingContentState, + commonContent = homeState.rankingContents, onContentClicked = { } ) } diff --git a/app/src/main/java/org/sopt/and/presentation/home/component/CommonContentHorizontalColumn.kt b/app/src/main/java/org/sopt/and/presentation/home/component/CommonContentHorizontalColumn.kt index 0f5f747..e886021 100644 --- a/app/src/main/java/org/sopt/and/presentation/home/component/CommonContentHorizontalColumn.kt +++ b/app/src/main/java/org/sopt/and/presentation/home/component/CommonContentHorizontalColumn.kt @@ -27,15 +27,15 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import org.sopt.and.R import org.sopt.and.presentation.home.component.item.CommonContentItem -import org.sopt.and.presentation.home.state.HomeCommonContentState -import org.sopt.and.presentation.home.state.HomeContentState import org.sopt.and.core.designsystem.theme.Gray3 import org.sopt.and.core.designsystem.theme.White +import org.sopt.and.domain.model.entity.HomeCommonContent +import org.sopt.and.domain.model.entity.HomeContent @Composable -fun CommonContentHorizontalColumn ( - commonContentState: HomeCommonContentState, - onContentClicked: (HomeContentState) -> Unit, +fun CommonContentHorizontalColumn( + commonContent: HomeCommonContent, + onContentClicked: (HomeContent) -> Unit, modifier: Modifier = Modifier ) { val state = rememberLazyListState() @@ -50,7 +50,7 @@ fun CommonContentHorizontalColumn ( verticalAlignment = Alignment.CenterVertically ) { Text( - text = commonContentState.mainTitle, + text = commonContent.mainTitle, fontSize = 18.sp, color = White, fontWeight = FontWeight.Bold @@ -70,7 +70,7 @@ fun CommonContentHorizontalColumn ( horizontalArrangement = Arrangement.spacedBy(6.dp), flingBehavior = rememberSnapFlingBehavior(lazyListState = state) ) { - items(commonContentState.contentStates) { item -> + items(commonContent.contentStates) { item -> CommonContentItem( modifier = Modifier .width((LocalConfiguration.current.screenWidthDp.dp / 3) - 16.dp) diff --git a/app/src/main/java/org/sopt/and/presentation/home/component/MainContentHorizontalPager.kt b/app/src/main/java/org/sopt/and/presentation/home/component/MainContentHorizontalPager.kt index 9f43337..8573c96 100644 --- a/app/src/main/java/org/sopt/and/presentation/home/component/MainContentHorizontalPager.kt +++ b/app/src/main/java/org/sopt/and/presentation/home/component/MainContentHorizontalPager.kt @@ -7,27 +7,30 @@ import androidx.compose.foundation.pager.PagerState import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import org.sopt.and.presentation.home.state.HomeContentState +import org.sopt.and.domain.model.entity.HomeContent import org.sopt.and.presentation.home.component.item.MainContentItem @Composable fun MainContentHorizontalPager( state: PagerState, - mainContentState: List, - onMainContentClicked: (HomeContentState) -> Unit, + mainContent: List, + onMainContentClicked: (HomeContent) -> Unit, modifier: Modifier = Modifier ) { - HorizontalPager( - modifier = modifier, - state = state, - pageSpacing = 8.dp, - contentPadding = PaddingValues(horizontal = 24.dp) - ) { idx -> - MainContentItem( - modifier = Modifier.fillMaxSize(), - mainContentState = mainContentState[idx % mainContentState.size], onClick = onMainContentClicked, - totalPage = mainContentState.size, - currentPage = idx % mainContentState.size + 1 - ) + if (mainContent.isNotEmpty()) { + HorizontalPager( + modifier = modifier, + state = state, + pageSpacing = 8.dp, + contentPadding = PaddingValues(horizontal = 24.dp) + ) { idx -> + MainContentItem( + modifier = Modifier.fillMaxSize(), + mainContentState = mainContent[idx % mainContent.size], + onClick = onMainContentClicked, + totalPage = mainContent.size, + currentPage = idx % mainContent.size + 1 + ) + } } } \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/presentation/home/component/RankingContentHorizontalColumn.kt b/app/src/main/java/org/sopt/and/presentation/home/component/RankingContentHorizontalColumn.kt index e242841..7713963 100644 --- a/app/src/main/java/org/sopt/and/presentation/home/component/RankingContentHorizontalColumn.kt +++ b/app/src/main/java/org/sopt/and/presentation/home/component/RankingContentHorizontalColumn.kt @@ -1,8 +1,6 @@ package org.sopt.and.presentation.home.component import androidx.compose.runtime.Composable -import org.sopt.and.presentation.home.state.HomeCommonContentState -import org.sopt.and.presentation.home.state.HomeContentState import androidx.compose.foundation.gestures.snapping.SnapPosition import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior import androidx.compose.foundation.layout.Arrangement @@ -23,11 +21,13 @@ import androidx.compose.ui.unit.sp import org.sopt.and.R import org.sopt.and.presentation.home.component.item.RankingContentItem import org.sopt.and.core.designsystem.theme.White +import org.sopt.and.domain.model.entity.HomeCommonContent +import org.sopt.and.domain.model.entity.HomeContent @Composable fun RankingContentHorizontalColumn ( - commonContentState: HomeCommonContentState, - onContentClicked: (HomeContentState) -> Unit, + commonContent: HomeCommonContent, + onContentClicked: (HomeContent) -> Unit, modifier: Modifier = Modifier, ) { val lazyListState = rememberLazyListState() @@ -57,7 +57,7 @@ fun RankingContentHorizontalColumn ( modifier = modifier ) { Text( - text = commonContentState.mainTitle, + text = commonContent.mainTitle, fontSize = 18.sp, color = White, fontWeight = FontWeight.Bold, @@ -75,7 +75,7 @@ fun RankingContentHorizontalColumn ( snapPosition = SnapPosition.Start ) ) { - itemsIndexed(commonContentState.contentStates) { index, item -> + itemsIndexed(commonContent.contentStates) { index, item -> RankingContentItem( modifier = Modifier.width((LocalConfiguration.current.screenWidthDp.dp / 2) - 28.dp), mainContentState = item, diff --git a/app/src/main/java/org/sopt/and/presentation/home/component/item/CommonContentItem.kt b/app/src/main/java/org/sopt/and/presentation/home/component/item/CommonContentItem.kt index a52054d..d877e37 100644 --- a/app/src/main/java/org/sopt/and/presentation/home/component/item/CommonContentItem.kt +++ b/app/src/main/java/org/sopt/and/presentation/home/component/item/CommonContentItem.kt @@ -12,13 +12,13 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import org.sopt.and.core.extension.noRippleClickable -import org.sopt.and.presentation.home.state.HomeContentState +import org.sopt.and.domain.model.entity.HomeContent @Composable fun CommonContentItem( - commonContentState: HomeContentState, + commonContentState: HomeContent, modifier: Modifier = Modifier, - onClick: (HomeContentState) -> Unit = {} + onClick: (HomeContent) -> Unit = {} ) { Box( modifier = modifier diff --git a/app/src/main/java/org/sopt/and/presentation/home/component/item/MainContentItem.kt b/app/src/main/java/org/sopt/and/presentation/home/component/item/MainContentItem.kt index d56c78f..a132757 100644 --- a/app/src/main/java/org/sopt/and/presentation/home/component/item/MainContentItem.kt +++ b/app/src/main/java/org/sopt/and/presentation/home/component/item/MainContentItem.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape @@ -19,18 +20,18 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import org.sopt.and.core.extension.noRippleClickable -import org.sopt.and.presentation.home.state.HomeContentState import org.sopt.and.core.designsystem.theme.WavveBg import org.sopt.and.core.designsystem.theme.WavveDisabled import org.sopt.and.core.designsystem.theme.White +import org.sopt.and.domain.model.entity.HomeContent @Composable fun MainContentItem( - mainContentState: HomeContentState, + mainContentState: HomeContent, totalPage: Int, currentPage: Int, modifier: Modifier = Modifier, - onClick: (HomeContentState) -> Unit = {} + onClick: (HomeContent) -> Unit = {} ) { Box( modifier = modifier @@ -42,7 +43,8 @@ fun MainContentItem( Image( painter = painterResource(mainContentState.image), contentDescription = mainContentState.description, - contentScale = ContentScale.Crop + contentScale = ContentScale.Crop, + modifier = Modifier.fillMaxSize() ) Row( diff --git a/app/src/main/java/org/sopt/and/presentation/home/component/item/RankingContentItem.kt b/app/src/main/java/org/sopt/and/presentation/home/component/item/RankingContentItem.kt index c185f4d..2bef6d0 100644 --- a/app/src/main/java/org/sopt/and/presentation/home/component/item/RankingContentItem.kt +++ b/app/src/main/java/org/sopt/and/presentation/home/component/item/RankingContentItem.kt @@ -14,14 +14,14 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import org.sopt.and.core.extension.noRippleClickable -import org.sopt.and.presentation.home.state.HomeContentState +import org.sopt.and.domain.model.entity.HomeContent @Composable fun RankingContentItem( - mainContentState: HomeContentState, + mainContentState: HomeContent, rank: Int, modifier: Modifier = Modifier, - onClick: (HomeContentState) -> Unit = {} + onClick: (HomeContent) -> Unit = {} ) { Box( modifier = modifier diff --git a/app/src/main/java/org/sopt/and/presentation/home/state/HomeCommonContentState.kt b/app/src/main/java/org/sopt/and/presentation/home/state/HomeCommonContentState.kt deleted file mode 100644 index 1c67ac3..0000000 --- a/app/src/main/java/org/sopt/and/presentation/home/state/HomeCommonContentState.kt +++ /dev/null @@ -1,6 +0,0 @@ -package org.sopt.and.presentation.home.state - -class HomeCommonContentState ( - val mainTitle: String, - val contentStates : List -) \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/presentation/home/state/HomeContentState.kt b/app/src/main/java/org/sopt/and/presentation/home/state/HomeContentState.kt deleted file mode 100644 index dbb20f0..0000000 --- a/app/src/main/java/org/sopt/and/presentation/home/state/HomeContentState.kt +++ /dev/null @@ -1,10 +0,0 @@ -package org.sopt.and.presentation.home.state - -import androidx.annotation.DrawableRes - -data class HomeContentState( - val id: Int, - val title: String, - @DrawableRes val image: Int, - val description: String, -) \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/presentation/home/viewmodel/HomeViewModel.kt b/app/src/main/java/org/sopt/and/presentation/home/viewmodel/HomeViewModel.kt index 3fec2fa..154b65c 100644 --- a/app/src/main/java/org/sopt/and/presentation/home/viewmodel/HomeViewModel.kt +++ b/app/src/main/java/org/sopt/and/presentation/home/viewmodel/HomeViewModel.kt @@ -1,178 +1,35 @@ package org.sopt.and.presentation.home.viewmodel -import androidx.lifecycle.ViewModel -import org.sopt.and.R -import org.sopt.and.presentation.home.state.HomeCommonContentState -import org.sopt.and.presentation.home.state.HomeContentState +import dagger.hilt.android.lifecycle.HiltViewModel +import org.sopt.and.domain.repository.DummyHomeContentRepository +import org.sopt.and.presentation.home.HomeContract.HomeUiEffect +import org.sopt.and.presentation.home.HomeContract.HomeUiEvent +import org.sopt.and.presentation.home.HomeContract.HomeUiState +import org.sopt.and.presentation.util.BaseViewModel +import javax.inject.Inject -class HomeViewModel : ViewModel() { - val mainContents: List = listOf( - HomeContentState( - id = 1, - title = "이토록 친밀한 배신자", - image = R.drawable.img_banner_poaster_1, - description = "이토록 친밀한 배신자" - ), HomeContentState( - id = 1, - title = "어이미남!!2", - image = R.drawable.img_banner_poaster_2, - description = "어이미남!!2" - ), HomeContentState( - id = 1, - title = "나의 히어로 아카데미아", - image = R.drawable.img_banner_poaster_3, - description = "나의 히어로 아카데미아" - ) - ) - val commonContents: List = listOf( - HomeCommonContentState( - mainTitle = "믿고 보는 웨이브 에디터 추천작", - contentStates = listOf( - HomeContentState( - id = 1, - title = "로 앤 오더 : 토론토 - 크리미널 인텐드 시즌1", - image = R.drawable.thumbnail1, - description = "로 앤 오더 : 토론토 - 크리미널 인텐드 시즌1" - ), HomeContentState( - id = 1, - title = "원피스", - image = R.drawable.thumbnail2, - description = "원피스" - ), HomeContentState( - id = 1, - title = "이토록 친절한 배신자", - image = R.drawable.thumbnail3, - description = "이토록 친절한 배신자" - ), HomeContentState( - id = 1, - title = "강철부대", - image = R.drawable.thumbnail4, - description = "강철부대" - ), HomeContentState( - id = 1, - title = "지옥에서 온 판사", - image = R.drawable.thumbnail5, - description = "지옥에서 온 판사" - ) - ) - ), HomeCommonContentState( - mainTitle = "실시간 인기 콘텐츠", - contentStates = listOf( - HomeContentState( - id = 1, - title = "로 앤 오더 : 토론토 - 크리미널 인텐드 시즌1", - image = R.drawable.thumbnail1, - description = "로 앤 오더 : 토론토 - 크리미널 인텐드 시즌1" - ), HomeContentState( - id = 1, - title = "원피스", - image = R.drawable.thumbnail2, - description = "원피스" - ), HomeContentState( - id = 1, - title = "이토록 친절한 배신자", - image = R.drawable.thumbnail3, - description = "이토록 친절한 배신자" - ), HomeContentState( - id = 1, - title = "강철부대", - image = R.drawable.thumbnail4, - description = "강철부대" - ), HomeContentState( - id = 1, - title = "지옥에서 온 판사", - image = R.drawable.thumbnail5, - description = "지옥에서 온 판사" - ) - ).reversed() - ), HomeCommonContentState( - mainTitle = "오직 웨이브에서", - contentStates = listOf( - HomeContentState( - id = 1, - title = "런닝맨", - image = R.drawable.thumbnail6, - description = "런닝맨" - ), HomeContentState( - id = 1, - title = "미운 우리 새끼", - image = R.drawable.thumbnail7, - description = "미운 우리 새끼" - ), HomeContentState( - id = 1, - title = "심야괴담회", - image = R.drawable.thumbnail8, - description = "심야괴담회" - ), HomeContentState( - id = 1, - title = "나 혼자 산다", - image = R.drawable.thumbnail9, - description = "나 혼자 산다" - ), HomeContentState( - id = 1, - title = "전지적 참견 시점", - image = R.drawable.thumbnail10, - description = "전지적 참견 시점" +@HiltViewModel +class HomeViewModel @Inject constructor( + private val dummyHomeContentRepository: DummyHomeContentRepository +) : BaseViewModel(HomeUiState()) { + + override fun reduceState(event: HomeUiEvent) { + when (event) { + is HomeUiEvent.SetContentType -> { + updateState( + currentState.copy( + selectedContentType = event.contentType + ) ) - ) - ) - ) + } + } + } - val rankingContents: HomeCommonContentState = HomeCommonContentState( - mainTitle = "오늘의 TOP 20", - contentStates = listOf( - HomeContentState( - id = 1, - title = "로 앤 오더 : 토론토 - 크리미널 인텐드 시즌1", - image = R.drawable.thumbnail1, - description = "로 앤 오더 : 토론토 - 크리미널 인텐드 시즌1" - ), HomeContentState( - id = 1, - title = "원피스", - image = R.drawable.thumbnail2, - description = "원피스" - ), HomeContentState( - id = 1, - title = "이토록 친절한 배신자", - image = R.drawable.thumbnail3, - description = "이토록 친절한 배신자" - ), HomeContentState( - id = 1, - title = "강철부대", - image = R.drawable.thumbnail4, - description = "강철부대" - ), HomeContentState( - id = 1, - title = "지옥에서 온 판사", - image = R.drawable.thumbnail5, - description = "지옥에서 온 판사" - ), - HomeContentState( - id = 1, - title = "런닝맨", - image = R.drawable.thumbnail6, - description = "런닝맨" - ), HomeContentState( - id = 1, - title = "미운 우리 새끼", - image = R.drawable.thumbnail7, - description = "미운 우리 새끼" - ), HomeContentState( - id = 1, - title = "심야괴담회", - image = R.drawable.thumbnail8, - description = "심야괴담회" - ), HomeContentState( - id = 1, - title = "나 혼자 산다", - image = R.drawable.thumbnail9, - description = "나 혼자 산다" - ), HomeContentState( - id = 1, - title = "전지적 참견 시점", - image = R.drawable.thumbnail10, - description = "전지적 참견 시점" - ) + fun getDummyHomeContent() = updateState( + currentState.copy( + mainContents = dummyHomeContentRepository.getDummyMainContents(), + commonContents = dummyHomeContentRepository.getDummyCommonContents(), + rankingContents = dummyHomeContentRepository.getDummyRankingContents() ) ) } \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/presentation/main/MainActivity.kt b/app/src/main/java/org/sopt/and/presentation/main/MainActivity.kt index e4b5709..c0646ad 100644 --- a/app/src/main/java/org/sopt/and/presentation/main/MainActivity.kt +++ b/app/src/main/java/org/sopt/and/presentation/main/MainActivity.kt @@ -5,23 +5,15 @@ import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import dagger.hilt.android.AndroidEntryPoint -import org.sopt.and.core.composition.PreferenceUtilProvider -import org.sopt.and.core.utils.PreferenceUtil -import javax.inject.Inject //AndroidEntryPoint : 의존성 주입을 사용할 Android 컴포넌트에 부착 -> 즉 DI 활성화 @AndroidEntryPoint class MainActivity : ComponentActivity() { - @Inject - lateinit var preferenceUtils: PreferenceUtil - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() setContent { - PreferenceUtilProvider(preferenceUtil = preferenceUtils) { - MainScreen() - } + MainScreen() } } } \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/presentation/main/MainContract.kt b/app/src/main/java/org/sopt/and/presentation/main/MainContract.kt new file mode 100644 index 0000000..0ca4f19 --- /dev/null +++ b/app/src/main/java/org/sopt/and/presentation/main/MainContract.kt @@ -0,0 +1,18 @@ +package org.sopt.and.presentation.main + +import org.sopt.and.presentation.util.UiEffect +import org.sopt.and.presentation.util.UiEvent +import org.sopt.and.presentation.util.UiState + +class MainContract { + data class MainUiState( + val userToken: String? = null, + val isLoading: Boolean = false + ) : UiState + + sealed class MainUiEvent : UiEvent { + data object LoadUserToken : MainUiEvent() + } + + sealed class MainUiEffect : UiEffect +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/presentation/main/MainNavHost.kt b/app/src/main/java/org/sopt/and/presentation/main/MainNavHost.kt index c917a3d..ccb4d3a 100644 --- a/app/src/main/java/org/sopt/and/presentation/main/MainNavHost.kt +++ b/app/src/main/java/org/sopt/and/presentation/main/MainNavHost.kt @@ -12,8 +12,8 @@ import org.sopt.and.core.navigation.Screen import org.sopt.and.presentation.home.HomeScreen import org.sopt.and.presentation.mypage.MyScreen import org.sopt.and.presentation.search.SearchScreen -import org.sopt.and.presentation.sign.SignInScreen -import org.sopt.and.presentation.sign.SignUpScreen +import org.sopt.and.presentation.auth.signin.SignInScreen +import org.sopt.and.presentation.auth.signup.SignUpScreen import org.sopt.and.core.designsystem.theme.WavveBg @Composable @@ -31,8 +31,15 @@ fun MainNavHost( ) { composable { SignInScreen( - navigateToMy = { navController.navigate(Screen.My) }, - navigateToSignUp = { navController.navigate(Screen.SignUp) } + navigateToMy = { + navController.navigate(Screen.My) + }, + navigateToSignUp = { + navController.navigate(Screen.SignUp) + }, + navigateUp = { + navController.navigateUp() + } ) } composable { @@ -42,6 +49,9 @@ fun MainNavHost( popUpTo { inclusive = true } launchSingleTop = true } + }, + navigateUp = { + navController.navigateUp() } ) } diff --git a/app/src/main/java/org/sopt/and/presentation/main/MainScreen.kt b/app/src/main/java/org/sopt/and/presentation/main/MainScreen.kt index 3c8575b..f75cf84 100644 --- a/app/src/main/java/org/sopt/and/presentation/main/MainScreen.kt +++ b/app/src/main/java/org/sopt/and/presentation/main/MainScreen.kt @@ -14,15 +14,21 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.compose.rememberNavController import org.sopt.and.core.navigation.Screen import org.sopt.and.core.designsystem.theme.BottomNavigationItemUnselected import org.sopt.and.core.designsystem.theme.White -import org.sopt.and.core.utils.PreferenceUtil import org.sopt.and.core.utils.SnackBarUtils +import org.sopt.and.presentation.main.component.MainBottomNavigationBar +import org.sopt.and.presentation.main.viewmodel.MainScreenViewModel @Composable -fun MainScreen() { +fun MainScreen( + viewModel: MainScreenViewModel = hiltViewModel() +) { + val mainState by viewModel.uiState.collectAsStateWithLifecycle() val navController = rememberNavController() val colors = NavigationBarItemDefaults.colors( selectedIconColor = White, @@ -32,11 +38,10 @@ fun MainScreen() { indicatorColor = Color.Transparent ) - val preferenceUtil = PreferenceUtil.LocalPreferenceUtils.current - - val userToken = preferenceUtil.getUserToken().orEmpty() - - val startDestination = if (userToken.isBlank()) Screen.SignIn else Screen.My + LaunchedEffect(Unit) { + viewModel.sendEvent(MainContract.MainUiEvent.LoadUserToken) + } + val startDestination = if (mainState.userToken.isNullOrBlank()) Screen.SignIn else Screen.My var currentRoute by remember { mutableStateOf(null) } LaunchedEffect(navController) { diff --git a/app/src/main/java/org/sopt/and/presentation/main/MainBottomNavigationBar.kt b/app/src/main/java/org/sopt/and/presentation/main/component/MainBottomNavigationBar.kt similarity index 89% rename from app/src/main/java/org/sopt/and/presentation/main/MainBottomNavigationBar.kt rename to app/src/main/java/org/sopt/and/presentation/main/component/MainBottomNavigationBar.kt index d58df49..483a993 100644 --- a/app/src/main/java/org/sopt/and/presentation/main/MainBottomNavigationBar.kt +++ b/app/src/main/java/org/sopt/and/presentation/main/component/MainBottomNavigationBar.kt @@ -1,4 +1,4 @@ -package org.sopt.and.presentation.main +package org.sopt.and.presentation.main.component import androidx.compose.foundation.Image import androidx.compose.foundation.layout.RowScope @@ -20,6 +20,8 @@ import org.sopt.and.R import org.sopt.and.core.navigation.Screen import org.sopt.and.core.designsystem.theme.WavveBg import org.sopt.and.core.designsystem.theme.WavveDisabled +import org.sopt.and.presentation.main.MainBottomTab +import org.sopt.and.presentation.main.MainBottomTabs @Composable fun MainBottomNavigationBar( @@ -75,8 +77,8 @@ fun RowScope.BottomNavigationItem( private fun navigateToScreen(navController: NavController, screen: Screen) { screen.javaClass.canonicalName?.let { navController.navigate(it) { - screen.javaClass.canonicalName?.let { it1 -> popUpTo(it1) { inclusive = false } } - launchSingleTop = true - } + screen.javaClass.canonicalName?.let { it1 -> popUpTo(it1) { inclusive = false } } + launchSingleTop = true + } } } diff --git a/app/src/main/java/org/sopt/and/presentation/main/viewmodel/MainViewModel.kt b/app/src/main/java/org/sopt/and/presentation/main/viewmodel/MainViewModel.kt new file mode 100644 index 0000000..bd6a221 --- /dev/null +++ b/app/src/main/java/org/sopt/and/presentation/main/viewmodel/MainViewModel.kt @@ -0,0 +1,39 @@ +package org.sopt.and.presentation.main.viewmodel + +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import org.sopt.and.core.utils.PreferenceUtil +import org.sopt.and.presentation.util.BaseViewModel +import org.sopt.and.presentation.main.MainContract.MainUiEffect +import org.sopt.and.presentation.main.MainContract.MainUiEvent +import org.sopt.and.presentation.main.MainContract.MainUiState +import javax.inject.Inject + +@HiltViewModel +class MainScreenViewModel @Inject constructor( + private val preferenceUtil: PreferenceUtil +) : BaseViewModel(MainUiState()) { + override fun reduceState(event: MainUiEvent) { + when (event) { + MainUiEvent.LoadUserToken -> loadUserToken() + } + } + + private fun loadUserToken() { + updateState( + currentState.copy( + isLoading = true + ) + ) + viewModelScope.launch { + val token = preferenceUtil.getUserToken() + updateState( + currentState.copy( + isLoading = false, + userToken = token + ) + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/presentation/mypage/MyPageContract.kt b/app/src/main/java/org/sopt/and/presentation/mypage/MyPageContract.kt new file mode 100644 index 0000000..261abc7 --- /dev/null +++ b/app/src/main/java/org/sopt/and/presentation/mypage/MyPageContract.kt @@ -0,0 +1,25 @@ +package org.sopt.and.presentation.mypage + +import org.sopt.and.presentation.util.UiEffect +import org.sopt.and.presentation.util.UiEvent +import org.sopt.and.presentation.util.UiState + +class MyPageContract { + data class MyPageUiState( + val hobby: String = "", + val isLoading: Boolean = false, + val isLoggedOut: Boolean = false, + val errorMessage: String? = null, + val tokenInvalid: Boolean = false + ) : UiState + + sealed class MyPageUiEvent : UiEvent { + data object Logout : MyPageUiEvent() + data object LoadHobby : MyPageUiEvent() + } + + sealed class MyPageUiEffect : UiEffect { + data class ShowErrorSnackBar(val message: String) : MyPageUiEffect() + data object NavigateToSignIn : MyPageUiEffect() + } +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/presentation/mypage/MyScreen.kt b/app/src/main/java/org/sopt/and/presentation/mypage/MyScreen.kt index 01d7847..2dd7dca 100644 --- a/app/src/main/java/org/sopt/and/presentation/mypage/MyScreen.kt +++ b/app/src/main/java/org/sopt/and/presentation/mypage/MyScreen.kt @@ -20,7 +20,6 @@ import androidx.compose.material3.Icon 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.ui.Alignment import androidx.compose.ui.Modifier @@ -29,10 +28,8 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers +import androidx.lifecycle.compose.collectAsStateWithLifecycle import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.launch import org.sopt.and.R import org.sopt.and.core.extension.noRippleClickable import org.sopt.and.presentation.mypage.component.MyPageContents @@ -42,10 +39,7 @@ import org.sopt.and.core.designsystem.theme.Black import org.sopt.and.core.designsystem.theme.WavveBg import org.sopt.and.core.designsystem.theme.WavveDisabled import org.sopt.and.core.designsystem.theme.White -import org.sopt.and.core.utils.PreferenceUtil import org.sopt.and.core.utils.SnackBarUtils -import org.sopt.and.core.utils.showToast - @Composable fun MyScreen( @@ -55,43 +49,25 @@ fun MyScreen( ) { val context = LocalContext.current - val preferenceUtil = PreferenceUtil.LocalPreferenceUtils.current - val userToken = preferenceUtil.getUserToken() - - val myPageState by viewModel.myPageState.collectAsState() - + val myPageState by viewModel.uiState.collectAsStateWithLifecycle() LaunchedEffect(Unit) { - viewModel.tokenInvalid.collectLatest { - viewModel.errorMessageState.value?.let { message -> - CoroutineScope(Dispatchers.Main).launch { + viewModel.sendEvent(MyPageContract.MyPageUiEvent.LoadHobby) + viewModel.effect.collectLatest { effect -> + when (effect) { + is MyPageContract.MyPageUiEffect.ShowErrorSnackBar -> { SnackBarUtils.showSnackBar( - message = message, + message = effect.message, actionLabel = context.getString(R.string.sign_in_snackbar_action_close) ) } - } - preferenceUtil.clearUserToken() - navigateToSignIn() - } - } - LaunchedEffect(Unit) { - viewModel.isLogout.collectLatest { logout -> - if(logout) { - context.showToast( - context.getString(R.string.my_page_toast_success_logout) - ) - preferenceUtil.clearUserToken() - navigateToSignIn() + MyPageContract.MyPageUiEffect.NavigateToSignIn -> { + navigateToSignIn() + } } } } - - LaunchedEffect(true) { - viewModel.getMyHobby(userToken.orEmpty()) - } - Column( modifier = modifier .fillMaxSize() @@ -172,7 +148,7 @@ fun MyScreen( .background(WavveDisabled) .wrapContentHeight() .noRippleClickable { - viewModel.logout() + viewModel.sendEvent(MyPageContract.MyPageUiEvent.Logout) } .padding(vertical = 14.dp) ) { diff --git a/app/src/main/java/org/sopt/and/presentation/mypage/viewmodel/MyViewModel.kt b/app/src/main/java/org/sopt/and/presentation/mypage/viewmodel/MyViewModel.kt index 379721e..18da3d7 100644 --- a/app/src/main/java/org/sopt/and/presentation/mypage/viewmodel/MyViewModel.kt +++ b/app/src/main/java/org/sopt/and/presentation/mypage/viewmodel/MyViewModel.kt @@ -1,54 +1,88 @@ package org.sopt.and.presentation.mypage.viewmodel -import androidx.lifecycle.ViewModel +import android.util.Log import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharedFlow -import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch +import org.sopt.and.core.utils.PreferenceUtil import org.sopt.and.data.common.ErrorTypeWithMessage import org.sopt.and.domain.model.entity.BaseResult -import org.sopt.and.domain.model.entity.GetMyHobbyResult import org.sopt.and.domain.usecase.GetMyHobbyUseCase +import org.sopt.and.presentation.util.BaseViewModel +import org.sopt.and.presentation.mypage.MyPageContract.MyPageUiEffect +import org.sopt.and.presentation.mypage.MyPageContract.MyPageUiEvent +import org.sopt.and.presentation.mypage.MyPageContract.MyPageUiState import javax.inject.Inject @HiltViewModel class MyViewModel @Inject constructor( - private val getMyHobbyUseCase: GetMyHobbyUseCase -) : ViewModel() { - private val _tokenInvalid = MutableSharedFlow() - val tokenInvalid: SharedFlow = _tokenInvalid - - private val _myPageState = MutableStateFlow(GetMyHobbyResult(hobby = "")) - val myPageState: StateFlow = _myPageState - - private val _errorMessageState = MutableStateFlow(null) - val errorMessageState: StateFlow = _errorMessageState - - private val _isLogout = MutableSharedFlow() - val isLogout: SharedFlow = _isLogout + private val getMyHobbyUseCase: GetMyHobbyUseCase, + private val preferenceUtil: PreferenceUtil +) : BaseViewModel(MyPageUiState()) { + override fun reduceState(event: MyPageUiEvent) { + when (event) { + is MyPageUiEvent.LoadHobby -> { + loadHobby() + } - fun logout() { - viewModelScope.launch { - _isLogout.emit(true) + MyPageUiEvent.Logout -> { + logout() + } } } - fun getMyHobby(token: String) { + + private fun loadHobby() { + val token = preferenceUtil.getUserToken() + Log.d("my**", token.toString()) + if (token.isNullOrEmpty()) { + updateState( + currentState.copy( + tokenInvalid = true + ) + ) + postEffect(MyPageUiEffect.NavigateToSignIn) + return + } + updateState(currentState.copy(isLoading = true)) viewModelScope.launch { when (val result = getMyHobbyUseCase(token)) { is BaseResult.Success -> { - _myPageState.value = result.data - _errorMessageState.value = null + updateState( + currentState.copy( + hobby = result.data.hobby, + isLoading = false, + errorMessage = null, + tokenInvalid = false + ) + ) } + is BaseResult.Error -> { - _errorMessageState.value = result.message + updateState( + currentState.copy( + isLoading = false, + errorMessage = result.message + ) + ) if (result.errorCode == ErrorTypeWithMessage.INVALID_TOKEN) { - _tokenInvalid.emit(true) + preferenceUtil.clearUserToken() + updateState( + currentState.copy( + tokenInvalid = true + ) + ) + postEffect(MyPageUiEffect.NavigateToSignIn) + } else { + postEffect(MyPageUiEffect.ShowErrorSnackBar(result.message)) } } } } } + + private fun logout() { + preferenceUtil.clearUserToken() + updateState(currentState.copy(isLoggedOut = true)) + postEffect(MyPageUiEffect.NavigateToSignIn) + } } \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/presentation/sign/state/SignInState.kt b/app/src/main/java/org/sopt/and/presentation/sign/state/SignInState.kt deleted file mode 100644 index 26033c0..0000000 --- a/app/src/main/java/org/sopt/and/presentation/sign/state/SignInState.kt +++ /dev/null @@ -1,6 +0,0 @@ -package org.sopt.and.presentation.sign.state - -data class SignInState ( - val username: String = "", - val password: String = "" -) \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/presentation/sign/state/SignUpState.kt b/app/src/main/java/org/sopt/and/presentation/sign/state/SignUpState.kt deleted file mode 100644 index 9f39cea..0000000 --- a/app/src/main/java/org/sopt/and/presentation/sign/state/SignUpState.kt +++ /dev/null @@ -1,14 +0,0 @@ -package org.sopt.and.presentation.sign.state - -data class SignUpState ( - val username: String = "", - val password: String = "", - val hobby: String = "", - val isUserNameValid : Boolean = false, - val isPasswordValid : Boolean = false, - val isHobbyValid : Boolean = false, - val isUserNameFieldFocused : Boolean = false, - val isPasswordFieldFocused : Boolean = false, - val isHobbyFieldFocused : Boolean = false, - val isValid : Boolean = false -) \ No newline at end of file diff --git a/app/src/main/java/org/sopt/and/presentation/sign/viewmodel/SignInViewModel.kt b/app/src/main/java/org/sopt/and/presentation/sign/viewmodel/SignInViewModel.kt deleted file mode 100644 index 8882046..0000000 --- a/app/src/main/java/org/sopt/and/presentation/sign/viewmodel/SignInViewModel.kt +++ /dev/null @@ -1,86 +0,0 @@ -package org.sopt.and.presentation.sign.viewmodel - -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.SharedFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import org.sopt.and.presentation.sign.state.SignInState -import org.sopt.and.domain.model.entity.BaseResult -import org.sopt.and.domain.model.entity.UserData -import org.sopt.and.domain.model.entity.UserLoginResult -import org.sopt.and.domain.usecase.LoginUseCase -import javax.inject.Inject - -@HiltViewModel -class SignInViewModel @Inject constructor( - private val loginUseCase: LoginUseCase -) : ViewModel() { - private val _signInState = MutableStateFlow(SignInState()) - val signInState = _signInState.asStateFlow() - - private val _loginUserResultState = MutableStateFlow(null) - val loginUserResultState: StateFlow = _loginUserResultState - - private val _errorMessageState = MutableStateFlow(null) - val errorMessageState: StateFlow = _errorMessageState - - private val _signInSuccess = MutableSharedFlow() - val signInSuccess: SharedFlow = _signInSuccess - - fun updateUserName(newUserName: String) { - _signInState.update { currentState -> - currentState.copy( - username = newUserName - ) - } - } - - fun updatePassword(newPassword: String) { - _signInState.update { currentState -> - currentState.copy( - password = newPassword - ) - } - } - - private fun setSignInSuccess(value: Boolean) { - viewModelScope.launch { - _signInSuccess.emit(value) - } - } - - fun signIn() { - viewModelScope.launch { - when (val result = loginUseCase( - with(_signInState.value) { - UserData(username, password, "") - } - ) - ) { - is BaseResult.Success -> { - _loginUserResultState.value = result.data - _errorMessageState.value = null - setSignInSuccess(true) - } - - is BaseResult.Error -> { - _loginUserResultState.value = null - _errorMessageState.value = result.message - setSignInSuccess(false) - } - } - } - } - - fun resetSignInSuccess() { - viewModelScope.launch { - setSignInSuccess(false) - } - } -} diff --git a/app/src/main/java/org/sopt/and/presentation/sign/viewmodel/SignUpViewModel.kt b/app/src/main/java/org/sopt/and/presentation/sign/viewmodel/SignUpViewModel.kt deleted file mode 100644 index 3050ec1..0000000 --- a/app/src/main/java/org/sopt/and/presentation/sign/viewmodel/SignUpViewModel.kt +++ /dev/null @@ -1,155 +0,0 @@ -package org.sopt.and.presentation.sign.viewmodel - -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.SharedFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import org.sopt.and.domain.model.entity.BaseResult -import org.sopt.and.domain.model.entity.UserData -import org.sopt.and.domain.model.entity.UserRegisterResult -import org.sopt.and.domain.usecase.RegisterUserUseCase -import org.sopt.and.presentation.sign.state.SignUpState -import javax.inject.Inject - -@HiltViewModel -class SignUpViewModel @Inject constructor( - private val registerUserUseCase: RegisterUserUseCase -) : ViewModel() { - private val _signUpState = MutableStateFlow(SignUpState()) - val signUpState = _signUpState.asStateFlow() - - private val _registerUserResultState = MutableStateFlow(null) - val registerUserResultState: StateFlow = _registerUserResultState - - private val _errorMessageState = MutableStateFlow(null) - val errorMessageState: StateFlow = _errorMessageState - - - private val _signUpSuccess = MutableSharedFlow() - val signUpSuccess: SharedFlow = _signUpSuccess - - fun updateUserName(newUserName: String) { - _signUpState.update { currentState -> - val isUserNameValid = validateUserName(newUserName) - currentState.copy( - username = newUserName, - isUserNameValid = isUserNameValid - ) - } - updateIsValid() - } - - fun updatePassword(newPassword: String) { - _signUpState.update { currentState -> - val isPasswordValid = validatePassword(newPassword) - currentState.copy( - password = newPassword, - isPasswordValid = isPasswordValid - ) - } - updateIsValid() - } - - fun updateHobby(newHobby: String) { - _signUpState.update { currentState -> - val isHobbyValid = validateHobby(newHobby) - currentState.copy( - hobby = newHobby, - isHobbyValid = isHobbyValid - ) - } - updateIsValid() - } - - fun updateUserNameFieldFocused(isFocused: Boolean) { - _signUpState.update { currentState -> - currentState.copy(isUserNameFieldFocused = isFocused) - } - } - - fun updatePasswordFieldFocused(isFocused: Boolean) { - _signUpState.update { currentState -> - currentState.copy(isPasswordFieldFocused = isFocused) - } - } - - fun updateHobbyFieldFocused(isFocused: Boolean) { - _signUpState.update { currentState -> - currentState.copy(isHobbyFieldFocused = isFocused) - } - } - - private fun updateIsValid() { - _signUpState.update { currentState -> - currentState.copy( - isValid = _signUpState.value.isUserNameValid && - _signUpState.value.isPasswordValid && - _signUpState.value.isHobbyValid - ) - } - } - - /* 기존 wavve 제약사항 - private fun validateEmail(email: String): Boolean { - return Patterns.EMAIL_ADDRESS.matcher(email).matches() - } - private fun validatePassword(password: String): Boolean { - val hasUpperCase = password.any { it.isUpperCase() } - val hasLowerCase = password.any { it.isLowerCase() } - val hasDigit = password.any { it.isDigit() } - val hasSpecialChar = password.any { !it.isLetterOrDigit() } - val lengthValid = password.length in 8..20 - val complexityValid = listOf(hasUpperCase, hasLowerCase, hasDigit, hasSpecialChar).count { it } >= 3 - return lengthValid && complexityValid - } - */ - - //과제 기능 명세에 따른 제약사항, 공통 기능이지만 제약사항 변경 시를 대비해 각각 함수 분리 - private fun validateUserName(email: String): Boolean { - return email.isNotBlank() && email.length <= 8 - } - private fun validatePassword(password: String): Boolean { - return password.isNotBlank() && password.length <= 8 - } - - private fun validateHobby(hobby: String): Boolean { - return hobby.isNotBlank() && hobby.length <= 8 - } - - private suspend fun setSignUpSuccess(value: Boolean) { - _signUpSuccess.emit(value) - } - fun registerUser() { - viewModelScope.launch { - when (val result = registerUserUseCase( - UserData( - _signUpState.value.username, - _signUpState.value.password, - _signUpState.value.hobby - ) - ) - ) { - is BaseResult.Success -> { - _registerUserResultState.value = result.data - _errorMessageState.value = null - setSignUpSuccess(true) - } - is BaseResult.Error -> { - _registerUserResultState.value = null - _errorMessageState.value = result.message - setSignUpSuccess(false) - } - } - } - } - - suspend fun resetSignUpSuccess() { - setSignUpSuccess(false) - } -} diff --git a/app/src/main/java/org/sopt/and/presentation/util/BaseViewModel.kt b/app/src/main/java/org/sopt/and/presentation/util/BaseViewModel.kt new file mode 100644 index 0000000..59381e8 --- /dev/null +++ b/app/src/main/java/org/sopt/and/presentation/util/BaseViewModel.kt @@ -0,0 +1,62 @@ +package org.sopt.and.presentation.util + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +interface UiState +interface UiEvent +interface UiEffect + +abstract class BaseViewModel( + initialState: State +) : ViewModel() { + private val _uiState: MutableStateFlow = MutableStateFlow(initialState) + val currentState: State + get() = _uiState.value + + val uiState = _uiState.asStateFlow() + + private val _event: MutableSharedFlow = MutableSharedFlow() + val event = _event.asSharedFlow() + + private val _effect: Channel = Channel() + val effect = _effect.receiveAsFlow() + + init { + subscribeEvents() + } + + protected abstract fun reduceState(event: Event) + protected fun postEffect(effect: Effect) { + viewModelScope.launch { + _effect.send(effect) + } + } + + private fun subscribeEvents() { + viewModelScope.launch { + event.collect { + reduceState(it) + } + } + } + + protected fun updateState(currentState: State) { + _uiState.update { + currentState + } + } + + fun sendEvent(event: Event) { + viewModelScope.launch { _event.emit(event) } + } + +} \ No newline at end of file