diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 42d29820a..267920f8a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -16,6 +16,7 @@ android:theme="@style/Theme.Cherrish" > @@ -27,4 +28,4 @@ - \ No newline at end of file + diff --git a/app/src/main/java/com/cherrish/android/core/common/extension/ModifierExt.kt b/app/src/main/java/com/cherrish/android/core/common/extension/ModifierExt.kt index 3ba4c8a87..353ea5bdb 100644 --- a/app/src/main/java/com/cherrish/android/core/common/extension/ModifierExt.kt +++ b/app/src/main/java/com/cherrish/android/core/common/extension/ModifierExt.kt @@ -4,8 +4,14 @@ import android.graphics.BlurMaskFilter import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.consumeWindowInsets +import androidx.compose.foundation.layout.imePadding import androidx.compose.runtime.Composable +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.composed import androidx.compose.ui.draw.drawBehind @@ -17,6 +23,9 @@ import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.drawOutline import androidx.compose.ui.graphics.drawscope.drawIntoCanvas import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.findRootCoordinates +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.layout.positionInWindow import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -86,3 +95,19 @@ fun Modifier.dropShadow( } } } + +fun Modifier.advancedImePadding() = composed { + var consumePadding by remember { mutableStateOf(0) } + onGloballyPositioned { coordinates -> + consumePadding = ( + coordinates.findRootCoordinates().size.height - + (coordinates.positionInWindow().y + coordinates.size.height).toInt() + ).coerceAtLeast( + 0 + ) + } + .consumeWindowInsets( + PaddingValues(bottom = with(LocalDensity.current) { consumePadding.toDp() }) + ) + .imePadding() +} diff --git a/app/src/main/java/com/cherrish/android/data/di/DataSourceModule.kt b/app/src/main/java/com/cherrish/android/data/di/DataSourceModule.kt index 7b7d851d5..e94fd08a5 100644 --- a/app/src/main/java/com/cherrish/android/data/di/DataSourceModule.kt +++ b/app/src/main/java/com/cherrish/android/data/di/DataSourceModule.kt @@ -1,7 +1,9 @@ package com.cherrish.android.data.di import com.cherrish.android.data.remote.datasource.CalendarDataSource +import com.cherrish.android.data.remote.datasource.OnboardingProfileDataSource import com.cherrish.android.data.remote.datasourceimpl.CalendarDataSourceImpl +import com.cherrish.android.data.remote.datasourceimpl.OnboardingProfileDataSourceImpl import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -16,4 +18,10 @@ abstract class DataSourceModule { abstract fun bindCalendarDataSource( calendarDataSourceImpl: CalendarDataSourceImpl ): CalendarDataSource + + @Binds + @Singleton + abstract fun bindOnboardingProfileDataSource( + onboardingProfileDataSourceImpl: OnboardingProfileDataSourceImpl + ): OnboardingProfileDataSource } diff --git a/app/src/main/java/com/cherrish/android/data/di/RepositoryModule.kt b/app/src/main/java/com/cherrish/android/data/di/RepositoryModule.kt index ce7225f16..e249f72dd 100644 --- a/app/src/main/java/com/cherrish/android/data/di/RepositoryModule.kt +++ b/app/src/main/java/com/cherrish/android/data/di/RepositoryModule.kt @@ -1,7 +1,9 @@ package com.cherrish.android.data.di import com.cherrish.android.data.repository.CalendarRepository +import com.cherrish.android.data.repository.OnboardingProfileRepository import com.cherrish.android.data.repositoryimpl.CalendarRepositoryImpl +import com.cherrish.android.data.repositoryimpl.OnboardingProfileRepositoryImpl import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -16,4 +18,10 @@ abstract class RepositoryModule { abstract fun bindCalendarRepository( calendarRepositoryImpl: CalendarRepositoryImpl ): CalendarRepository + + @Binds + @Singleton + abstract fun bindOnboardingProfileRepository( + onboardingProfileRepositoryImpl: OnboardingProfileRepositoryImpl + ): OnboardingProfileRepository } diff --git a/app/src/main/java/com/cherrish/android/data/di/ServiceModule.kt b/app/src/main/java/com/cherrish/android/data/di/ServiceModule.kt index 9bc6bbde6..014d47866 100644 --- a/app/src/main/java/com/cherrish/android/data/di/ServiceModule.kt +++ b/app/src/main/java/com/cherrish/android/data/di/ServiceModule.kt @@ -1,6 +1,7 @@ package com.cherrish.android.data.di import com.cherrish.android.data.remote.service.CalendarService +import com.cherrish.android.data.remote.service.OnboardingProfileService import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -16,4 +17,10 @@ object ServiceModule { fun provideCalendarService( retrofit: Retrofit ): CalendarService = retrofit.create(CalendarService::class.java) + + @Provides + @Singleton + fun provideOnboardingProfileService( + retrofit: Retrofit + ): OnboardingProfileService = retrofit.create(OnboardingProfileService::class.java) } diff --git a/app/src/main/java/com/cherrish/android/data/model/OnboardingProfileRequestModel.kt b/app/src/main/java/com/cherrish/android/data/model/OnboardingProfileRequestModel.kt new file mode 100644 index 000000000..decba6294 --- /dev/null +++ b/app/src/main/java/com/cherrish/android/data/model/OnboardingProfileRequestModel.kt @@ -0,0 +1,13 @@ +package com.cherrish.android.data.model + +import com.cherrish.android.data.remote.dto.request.OnboardingProfileRequestDto + +data class OnboardingProfileRequestModel( + val name: String, + val age: Int +) + +fun OnboardingProfileRequestModel.toDto() = OnboardingProfileRequestDto( + name = this.name, + age = this.age +) diff --git a/app/src/main/java/com/cherrish/android/data/model/OnboardingProfileResponseModel.kt b/app/src/main/java/com/cherrish/android/data/model/OnboardingProfileResponseModel.kt new file mode 100644 index 000000000..2c011a755 --- /dev/null +++ b/app/src/main/java/com/cherrish/android/data/model/OnboardingProfileResponseModel.kt @@ -0,0 +1,15 @@ +package com.cherrish.android.data.model + +import com.cherrish.android.data.remote.dto.response.OnboardingProfileResponseDto + +data class OnboardingProfileResponseModel( + val id: Long, + val name: String, + val date: String +) + +fun OnboardingProfileResponseDto.toModel() = OnboardingProfileResponseModel( + id = this.id, + name = this.name, + date = this.date +) diff --git a/app/src/main/java/com/cherrish/android/data/remote/datasource/OnboardingProfileDataSource.kt b/app/src/main/java/com/cherrish/android/data/remote/datasource/OnboardingProfileDataSource.kt new file mode 100644 index 000000000..89fceef05 --- /dev/null +++ b/app/src/main/java/com/cherrish/android/data/remote/datasource/OnboardingProfileDataSource.kt @@ -0,0 +1,11 @@ +package com.cherrish.android.data.remote.datasource + +import com.cherrish.android.core.network.BaseResponse +import com.cherrish.android.data.remote.dto.request.OnboardingProfileRequestDto +import com.cherrish.android.data.remote.dto.response.OnboardingProfileResponseDto + +interface OnboardingProfileDataSource { + suspend fun postOnboardingProfile( + request: OnboardingProfileRequestDto + ): BaseResponse +} diff --git a/app/src/main/java/com/cherrish/android/data/remote/datasourceimpl/OnboardingProfileDataSourceImpl.kt b/app/src/main/java/com/cherrish/android/data/remote/datasourceimpl/OnboardingProfileDataSourceImpl.kt new file mode 100644 index 000000000..6ec9b140f --- /dev/null +++ b/app/src/main/java/com/cherrish/android/data/remote/datasourceimpl/OnboardingProfileDataSourceImpl.kt @@ -0,0 +1,18 @@ +package com.cherrish.android.data.remote.datasourceimpl + +import com.cherrish.android.core.network.BaseResponse +import com.cherrish.android.data.remote.datasource.OnboardingProfileDataSource +import com.cherrish.android.data.remote.dto.request.OnboardingProfileRequestDto +import com.cherrish.android.data.remote.dto.response.OnboardingProfileResponseDto +import com.cherrish.android.data.remote.service.OnboardingProfileService +import javax.inject.Inject + +class OnboardingProfileDataSourceImpl @Inject constructor( + private val onboardingProfileService: OnboardingProfileService +) : OnboardingProfileDataSource { + override suspend fun postOnboardingProfile( + request: OnboardingProfileRequestDto + ): BaseResponse { + return onboardingProfileService.postOnboardingProfile(request) + } +} diff --git a/app/src/main/java/com/cherrish/android/data/remote/dto/request/OnboardingProfileRequestDto.kt b/app/src/main/java/com/cherrish/android/data/remote/dto/request/OnboardingProfileRequestDto.kt new file mode 100644 index 000000000..133825005 --- /dev/null +++ b/app/src/main/java/com/cherrish/android/data/remote/dto/request/OnboardingProfileRequestDto.kt @@ -0,0 +1,12 @@ +package com.cherrish.android.data.remote.dto.request + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class OnboardingProfileRequestDto( + @SerialName("name") + val name: String, + @SerialName("age") + val age: Int +) diff --git a/app/src/main/java/com/cherrish/android/data/remote/dto/response/OnboardingProfileResponseDto.kt b/app/src/main/java/com/cherrish/android/data/remote/dto/response/OnboardingProfileResponseDto.kt new file mode 100644 index 000000000..d7a18f2f2 --- /dev/null +++ b/app/src/main/java/com/cherrish/android/data/remote/dto/response/OnboardingProfileResponseDto.kt @@ -0,0 +1,14 @@ +package com.cherrish.android.data.remote.dto.response + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class OnboardingProfileResponseDto( + @SerialName("id") + val id: Long, + @SerialName("name") + val name: String, + @SerialName("date") + val date: String +) diff --git a/app/src/main/java/com/cherrish/android/data/remote/service/OnboardingProfileService.kt b/app/src/main/java/com/cherrish/android/data/remote/service/OnboardingProfileService.kt new file mode 100644 index 000000000..ec32a1451 --- /dev/null +++ b/app/src/main/java/com/cherrish/android/data/remote/service/OnboardingProfileService.kt @@ -0,0 +1,14 @@ +package com.cherrish.android.data.remote.service + +import com.cherrish.android.core.network.BaseResponse +import com.cherrish.android.data.remote.dto.request.OnboardingProfileRequestDto +import com.cherrish.android.data.remote.dto.response.OnboardingProfileResponseDto +import retrofit2.http.Body +import retrofit2.http.POST + +interface OnboardingProfileService { + @POST("api/onboarding/profiles") + suspend fun postOnboardingProfile( + @Body request: OnboardingProfileRequestDto + ): BaseResponse +} diff --git a/app/src/main/java/com/cherrish/android/data/repository/OnboardingProfileRepository.kt b/app/src/main/java/com/cherrish/android/data/repository/OnboardingProfileRepository.kt new file mode 100644 index 000000000..dbb14e609 --- /dev/null +++ b/app/src/main/java/com/cherrish/android/data/repository/OnboardingProfileRepository.kt @@ -0,0 +1,10 @@ +package com.cherrish.android.data.repository + +import com.cherrish.android.data.model.OnboardingProfileRequestModel +import com.cherrish.android.data.model.OnboardingProfileResponseModel + +interface OnboardingProfileRepository { + suspend fun postOnboardingProfile( + request: OnboardingProfileRequestModel + ): Result +} diff --git a/app/src/main/java/com/cherrish/android/data/repositoryimpl/OnboardingProfileRepositoryImpl.kt b/app/src/main/java/com/cherrish/android/data/repositoryimpl/OnboardingProfileRepositoryImpl.kt new file mode 100644 index 000000000..64c3e08c8 --- /dev/null +++ b/app/src/main/java/com/cherrish/android/data/repositoryimpl/OnboardingProfileRepositoryImpl.kt @@ -0,0 +1,23 @@ +package com.cherrish.android.data.repositoryimpl + +import com.cherrish.android.core.util.suspendRunCatching +import com.cherrish.android.data.model.OnboardingProfileRequestModel +import com.cherrish.android.data.model.OnboardingProfileResponseModel +import com.cherrish.android.data.model.toDto +import com.cherrish.android.data.model.toModel +import com.cherrish.android.data.remote.datasource.OnboardingProfileDataSource +import com.cherrish.android.data.repository.OnboardingProfileRepository +import javax.inject.Inject + +class OnboardingProfileRepositoryImpl @Inject constructor( + private val onboardingProfileDataSource: OnboardingProfileDataSource +) : OnboardingProfileRepository { + override suspend fun postOnboardingProfile( + request: OnboardingProfileRequestModel + ): Result = + suspendRunCatching { + onboardingProfileDataSource.postOnboardingProfile( + request = request.toDto() + ).data!!.toModel() + } +} diff --git a/app/src/main/java/com/cherrish/android/presentation/main/MainActivity.kt b/app/src/main/java/com/cherrish/android/presentation/main/MainActivity.kt index ecbf6c227..5b0594de7 100644 --- a/app/src/main/java/com/cherrish/android/presentation/main/MainActivity.kt +++ b/app/src/main/java/com/cherrish/android/presentation/main/MainActivity.kt @@ -1,5 +1,6 @@ package com.cherrish.android.presentation.main +import android.content.pm.ActivityInfo import android.graphics.Color import android.os.Bundle import androidx.activity.ComponentActivity @@ -13,11 +14,11 @@ import dagger.hilt.android.AndroidEntryPoint class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - enableEdgeToEdge( statusBarStyle = SystemBarStyle.dark(Color.TRANSPARENT), navigationBarStyle = SystemBarStyle.dark(Color.TRANSPARENT) ) + requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED setContent { CherrishTheme { val appState = rememberMainAppState() diff --git a/app/src/main/java/com/cherrish/android/presentation/main/MainAppState.kt b/app/src/main/java/com/cherrish/android/presentation/main/MainAppState.kt index 549462b95..63d611b5b 100644 --- a/app/src/main/java/com/cherrish/android/presentation/main/MainAppState.kt +++ b/app/src/main/java/com/cherrish/android/presentation/main/MainAppState.kt @@ -6,6 +6,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.navigation.NavDestination.Companion.hasRoute import androidx.navigation.NavHostController +import androidx.navigation.NavOptions import androidx.navigation.compose.rememberNavController import androidx.navigation.navOptions import com.cherrish.android.presentation.calendar.navigation.navigateToCalendar @@ -13,6 +14,8 @@ import com.cherrish.android.presentation.calendar.navigation.navigateToProcedure import com.cherrish.android.presentation.challenge.navigation.navigateToChallenge import com.cherrish.android.presentation.home.navigation.navigateToHome import com.cherrish.android.presentation.mypage.navigation.navigateToMyPage +import com.cherrish.android.presentation.onboarding.navigation.navigateToOnboarding +import com.cherrish.android.presentation.onboarding.navigation.navigateToOnboardingInformation import com.cherrish.android.presentation.splash.navigation.Splash import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.SharingStarted @@ -27,6 +30,17 @@ class MainAppState( ) { val startDestination = Splash + private val clearStackNavOptions = navOptions { + popUpTo(0) { inclusive = true } + launchSingleTop = true + restoreState = false + } + + private val keepStackNavOptions = navOptions { + launchSingleTop = true + restoreState = true + } + private val currentDestination = navController.currentBackStackEntryFlow .map { it.destination } .stateIn( @@ -79,6 +93,18 @@ class MainAppState( } } + fun navigateToOnboarding(navOptions: NavOptions? = clearStackNavOptions) { + navController.navigateToOnboarding(navOptions) + } + + fun navigateToOnboardingInformation(navOptions: NavOptions? = clearStackNavOptions) { + navController.navigateToOnboardingInformation(navOptions) + } + + fun navigateToHome(navOptions: NavOptions? = clearStackNavOptions) { + navController.navigateToHome(navOptions) + } + fun navigateUp() { navController.navigateUp() } diff --git a/app/src/main/java/com/cherrish/android/presentation/main/MainScreen.kt b/app/src/main/java/com/cherrish/android/presentation/main/MainScreen.kt index d0db756f2..5ad6df56e 100644 --- a/app/src/main/java/com/cherrish/android/presentation/main/MainScreen.kt +++ b/app/src/main/java/com/cherrish/android/presentation/main/MainScreen.kt @@ -12,6 +12,8 @@ import com.cherrish.android.presentation.challenge.navigation.challengeNavGraph import com.cherrish.android.presentation.home.navigation.homeNavGraph import com.cherrish.android.presentation.main.component.MainBottomBar import com.cherrish.android.presentation.mypage.navigation.myPageNavGraph +import com.cherrish.android.presentation.onboarding.navigation.onboardingInformationNavGraph +import com.cherrish.android.presentation.onboarding.navigation.onboardingNavGraph import com.cherrish.android.presentation.splash.navigation.splashNavGraph import kotlinx.collections.immutable.toPersistentList @@ -41,10 +43,20 @@ fun MainScreen( startDestination = appState.startDestination ) { splashNavGraph( - navigateToOnboarding = {}, // TODO: 로직 넣어서 바꿀 예정 + navigateToOnboarding = appState::navigateToOnboarding, paddingValues = innerPadding ) + onboardingNavGraph( + paddingValues = innerPadding, + navigateToOnboardingInformation = appState::navigateToOnboardingInformation + ) + + onboardingInformationNavGraph( + paddingValues = innerPadding, + navigateToHome = appState::navigateToHome + ) + homeNavGraph(paddingValues = innerPadding) calendarNavGraph( diff --git a/app/src/main/java/com/cherrish/android/presentation/onboarding/OnboardingScreen.kt b/app/src/main/java/com/cherrish/android/presentation/onboarding/OnboardingScreen.kt index 64b9e8d00..cab2dc74a 100644 --- a/app/src/main/java/com/cherrish/android/presentation/onboarding/OnboardingScreen.kt +++ b/app/src/main/java/com/cherrish/android/presentation/onboarding/OnboardingScreen.kt @@ -5,11 +5,13 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentWidth @@ -50,6 +52,7 @@ import androidx.constraintlayout.compose.ConstraintLayout import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.viewmodel.compose.viewModel import com.cherrish.android.R +import com.cherrish.android.core.common.extension.collectLatestSideEffect import com.cherrish.android.core.common.extension.noRippleClickable import com.cherrish.android.core.designsystem.component.button.CherrishButton import com.cherrish.android.core.designsystem.theme.CherrishTheme @@ -61,16 +64,27 @@ import kotlinx.coroutines.android.awaitFrame @Composable fun OnboardingRoute( + paddingValues: PaddingValues, + navigateToOnboardingInformation: () -> Unit, viewModel: OnboardingViewModel = hiltViewModel() ) { + viewModel.sideEffect.collectLatestSideEffect { sideEffect -> + when (sideEffect) { + OnboardingSideEffect.NavigateToOnboardingInformation -> { + navigateToOnboardingInformation() + } + } + } OnboardingScreen( - onCancelClick = viewModel::onCancelClick, - onNextClick = viewModel::onNextClick + paddingValues = paddingValues, + onCancelClick = viewModel::onClick, + onNextClick = viewModel::onClick ) } @Composable private fun OnboardingScreen( + paddingValues: PaddingValues, onCancelClick: () -> Unit, onNextClick: () -> Unit, modifier: Modifier = Modifier @@ -93,7 +107,9 @@ private fun OnboardingScreen( ) ) { Column( - modifier = Modifier.fillMaxSize() + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) ) { Spacer(modifier = Modifier.height(54.dp)) @@ -140,6 +156,7 @@ private fun OnboardingScreen( modifier = Modifier .fillMaxWidth() .padding(horizontal = 24.dp) + .navigationBarsPadding() .graphicsLayer { alpha = if (showButton) 1f else 0f } @@ -470,6 +487,7 @@ private fun Preview() { modifier = Modifier.fillMaxSize() ) { OnboardingScreen( + paddingValues = PaddingValues(), onCancelClick = {}, onNextClick = {} ) diff --git a/app/src/main/java/com/cherrish/android/presentation/onboarding/OnboardingSideEffect.kt b/app/src/main/java/com/cherrish/android/presentation/onboarding/OnboardingSideEffect.kt new file mode 100644 index 000000000..3f42f4ffc --- /dev/null +++ b/app/src/main/java/com/cherrish/android/presentation/onboarding/OnboardingSideEffect.kt @@ -0,0 +1,5 @@ +package com.cherrish.android.presentation.onboarding + +sealed interface OnboardingSideEffect { + data object NavigateToOnboardingInformation : OnboardingSideEffect +} diff --git a/app/src/main/java/com/cherrish/android/presentation/onboarding/OnboardingViewModel.kt b/app/src/main/java/com/cherrish/android/presentation/onboarding/OnboardingViewModel.kt index d758d826e..36921aa5e 100644 --- a/app/src/main/java/com/cherrish/android/presentation/onboarding/OnboardingViewModel.kt +++ b/app/src/main/java/com/cherrish/android/presentation/onboarding/OnboardingViewModel.kt @@ -1,15 +1,25 @@ package com.cherrish.android.presentation.onboarding import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import javax.inject.Inject +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.launch @HiltViewModel class OnboardingViewModel @Inject constructor() : ViewModel() { - fun onCancelClick() { - } + private val _sideEffect = MutableSharedFlow() + val sideEffect: SharedFlow = _sideEffect.asSharedFlow() - fun onNextClick() { + fun onClick() { + viewModelScope.launch { + _sideEffect.emit( + OnboardingSideEffect.NavigateToOnboardingInformation + ) + } } } diff --git a/app/src/main/java/com/cherrish/android/presentation/onboarding/information/InformationUiState.kt b/app/src/main/java/com/cherrish/android/presentation/onboarding/information/InformationUiState.kt deleted file mode 100644 index e8db292a3..000000000 --- a/app/src/main/java/com/cherrish/android/presentation/onboarding/information/InformationUiState.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.cherrish.android.presentation.onboarding.information - -import androidx.compose.runtime.Immutable - -@Immutable -data class InformationUiState( - val username: String = "", - val age: String = "" -) { - val buttonEnabled: Boolean - get() = username.isNotBlank() && age.toIntOrNull()?.let { it > 0 } == true -} diff --git a/app/src/main/java/com/cherrish/android/presentation/onboarding/information/InformationViewModel.kt b/app/src/main/java/com/cherrish/android/presentation/onboarding/information/InformationViewModel.kt deleted file mode 100644 index be7b2dd7d..000000000 --- a/app/src/main/java/com/cherrish/android/presentation/onboarding/information/InformationViewModel.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.cherrish.android.presentation.onboarding.information - -import androidx.lifecycle.ViewModel -import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update - -@HiltViewModel -class InformationViewModel @Inject constructor() : ViewModel() { - private val _uiState = MutableStateFlow(InformationUiState()) - val uiState: StateFlow = _uiState.asStateFlow() - - fun onNameChanged(name: String) { - _uiState.update { - it.copy( - username = name - ) - } - } - - fun onAgeChanged(input: String) { - val filtered = input.filter { it.isDigit() } - _uiState.update { it.copy(age = filtered) } - } - - fun onNextClicked() { - // TODO: 다음 화면으로 이동 - } -} diff --git a/app/src/main/java/com/cherrish/android/presentation/onboarding/information/InformationScreen.kt b/app/src/main/java/com/cherrish/android/presentation/onboarding/information/OnboardingInformationScreen.kt similarity index 80% rename from app/src/main/java/com/cherrish/android/presentation/onboarding/information/InformationScreen.kt rename to app/src/main/java/com/cherrish/android/presentation/onboarding/information/OnboardingInformationScreen.kt index 8139d0ea4..acd8a7a7c 100644 --- a/app/src/main/java/com/cherrish/android/presentation/onboarding/information/InformationScreen.kt +++ b/app/src/main/java/com/cherrish/android/presentation/onboarding/information/OnboardingInformationScreen.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Text @@ -19,6 +20,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.text.input.ImeAction @@ -29,31 +31,42 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.cherrish.android.core.common.extension.addFocusCleaner +import com.cherrish.android.core.common.extension.advancedImePadding +import com.cherrish.android.core.common.extension.collectLatestSideEffect import com.cherrish.android.core.designsystem.component.button.CherrishButton import com.cherrish.android.core.designsystem.component.textfield.CherrishTextField import com.cherrish.android.core.designsystem.theme.CherrishTheme import com.cherrish.android.presentation.onboarding.information.extension.AgeSuffixTransformation @Composable -fun InformationRoute( +fun OnboardingInformationRoute( paddingValues: PaddingValues, - viewModel: InformationViewModel = hiltViewModel() + navigateToHome: () -> Unit, + viewModel: OnboardingInformationViewModel = hiltViewModel() ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() - InformationScreen( + viewModel.sideEffect.collectLatestSideEffect { sideEffect -> + when (sideEffect) { + InformationSideEffect.NavigateToHome -> navigateToHome() + } + } + + OnboardingInformationScreen( paddingValues = paddingValues, username = uiState.username, onNameChange = viewModel::onNameChanged, age = uiState.age, onAgeChange = viewModel::onAgeChanged, onNextClick = viewModel::onNextClicked, - enabled = uiState.buttonEnabled + enabled = uiState.buttonEnabled, + nameErrorCase = viewModel.onNameErrorCase(uiState.username), + ageErrorCase = viewModel.onAgeErrorCase(uiState.age) ) } @Composable -private fun InformationScreen( +private fun OnboardingInformationScreen( paddingValues: PaddingValues, username: String, onNameChange: (String) -> Unit, @@ -61,6 +74,8 @@ private fun InformationScreen( onAgeChange: (String) -> Unit, onNextClick: () -> Unit, enabled: Boolean, + nameErrorCase: Boolean, + ageErrorCase: Boolean, modifier: Modifier = Modifier ) { val focusManager = LocalFocusManager.current @@ -75,6 +90,7 @@ private fun InformationScreen( .background(color = CherrishTheme.colors.gray0) .addFocusCleaner(focusManager) .padding(paddingValues = paddingValues) + .advancedImePadding() ) { Spacer(modifier = Modifier.weight(135f)) @@ -91,7 +107,9 @@ private fun InformationScreen( onNextAction = { ageFocusRequester.requestFocus() }, - keyboardType = KeyboardType.Text + keyboardType = KeyboardType.Text, + errorText = "이름은 최대 7자까지 입력 가능합니다.", + errorCase = nameErrorCase ) Spacer(modifier = Modifier.weight(30f)) @@ -114,6 +132,8 @@ private fun InformationScreen( " 세" ) }, + errorText = "입력 가능한 최대 나이 100세를 초과했습니다.", + errorCase = ageErrorCase, modifier = Modifier .focusRequester(ageFocusRequester) .onFocusChanged { state -> @@ -164,16 +184,17 @@ private fun UserInfoTextField( placeholder: String, keyboardImeAction: ImeAction, keyboardType: KeyboardType, + errorText: String, modifier: Modifier = Modifier, onNextAction: () -> Unit = {}, onDoneAction: () -> Unit = {}, - visualTransformation: VisualTransformation = VisualTransformation.None + visualTransformation: VisualTransformation = VisualTransformation.None, + errorCase: Boolean = false ) { Column( modifier = modifier .fillMaxWidth() - .padding(horizontal = 26.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) + .padding(horizontal = 26.dp) ) { Text( text = textFieldName, @@ -181,6 +202,8 @@ private fun UserInfoTextField( color = CherrishTheme.colors.gray1000 ) + Spacer(modifier = Modifier.height(8.dp)) + CherrishTextField( value = value, onValueChange = onValueChange, @@ -197,6 +220,14 @@ private fun UserInfoTextField( visualTransformation = visualTransformation, modifier = Modifier.fillMaxWidth() ) + + Spacer(modifier = Modifier.height(4.dp)) + + Text( + text = errorText, + style = CherrishTheme.typography.body1R14, + color = if (errorCase) CherrishTheme.colors.red700 else Color.Transparent + ) } } @@ -204,14 +235,16 @@ private fun UserInfoTextField( @Composable private fun Preview() { CherrishTheme { - InformationScreen( + OnboardingInformationScreen( paddingValues = PaddingValues(), username = "", onNameChange = {}, age = "", onAgeChange = {}, onNextClick = {}, - enabled = true + enabled = true, + nameErrorCase = false, + ageErrorCase = false ) } } diff --git a/app/src/main/java/com/cherrish/android/presentation/onboarding/information/OnboardingInformationUiState.kt b/app/src/main/java/com/cherrish/android/presentation/onboarding/information/OnboardingInformationUiState.kt new file mode 100644 index 000000000..53fc9dbb5 --- /dev/null +++ b/app/src/main/java/com/cherrish/android/presentation/onboarding/information/OnboardingInformationUiState.kt @@ -0,0 +1,20 @@ +package com.cherrish.android.presentation.onboarding.information + +import androidx.compose.runtime.Immutable + +@Immutable +data class InformationUiState( + val username: String = "", + val age: String = "" +) { + val buttonEnabled: Boolean + get() { + val ageInt = age.toIntOrNull() ?: return false + return username.isNotBlank() && age.toIntOrNull()?.let { it > 0 } == true && + ageInt in 1..100 && username.length <= 7 + } +} + +sealed interface InformationSideEffect { + data object NavigateToHome : InformationSideEffect +} diff --git a/app/src/main/java/com/cherrish/android/presentation/onboarding/information/OnboardingInformationViewModel.kt b/app/src/main/java/com/cherrish/android/presentation/onboarding/information/OnboardingInformationViewModel.kt new file mode 100644 index 000000000..a5d517364 --- /dev/null +++ b/app/src/main/java/com/cherrish/android/presentation/onboarding/information/OnboardingInformationViewModel.kt @@ -0,0 +1,63 @@ +package com.cherrish.android.presentation.onboarding.information + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.cherrish.android.core.common.extension.onLogFailure +import com.cherrish.android.core.local.TokenManager +import com.cherrish.android.data.model.OnboardingProfileRequestModel +import com.cherrish.android.data.repository.OnboardingProfileRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +@HiltViewModel +class OnboardingInformationViewModel @Inject constructor( + private val onboardingProfileRepository: OnboardingProfileRepository, + private val tokenManager: TokenManager +) : ViewModel() { + private val _uiState = MutableStateFlow(InformationUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + private val _sideEffect = MutableSharedFlow() + val sideEffect: SharedFlow = _sideEffect.asSharedFlow() + + fun onNameChanged(name: String) { + _uiState.update { + it.copy( + username = name + ) + } + } + + fun onNameErrorCase(name: String): Boolean = name.length > 7 + + fun onAgeChanged(input: String) { + val filtered = input.filter { it.isDigit() } + _uiState.update { it.copy(age = filtered) } + } + + fun onAgeErrorCase(age: String): Boolean = age.toIntOrNull() ?.let { it > 100 } ?: false + + fun onNextClicked() { + val age = uiState.value.age.toIntOrNull() ?: return + + viewModelScope.launch { + onboardingProfileRepository.postOnboardingProfile( + request = OnboardingProfileRequestModel( + name = uiState.value.username, + age = age + ) + ).onSuccess { response -> + tokenManager.saveId(response.id) + _sideEffect.emit(InformationSideEffect.NavigateToHome) + }.onLogFailure {} + } + } +} diff --git a/app/src/main/java/com/cherrish/android/presentation/onboarding/navigation/OnboardingInformationNavigator.kt b/app/src/main/java/com/cherrish/android/presentation/onboarding/navigation/OnboardingInformationNavigator.kt new file mode 100644 index 000000000..1a697414b --- /dev/null +++ b/app/src/main/java/com/cherrish/android/presentation/onboarding/navigation/OnboardingInformationNavigator.kt @@ -0,0 +1,29 @@ +package com.cherrish.android.presentation.onboarding.navigation + +import androidx.compose.foundation.layout.PaddingValues +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import androidx.navigation.compose.composable +import com.cherrish.android.core.common.navigation.Route +import com.cherrish.android.presentation.onboarding.information.OnboardingInformationRoute +import kotlinx.serialization.Serializable + +@Serializable +data object OnboardingInformation : Route + +fun NavController.navigateToOnboardingInformation(navOptions: NavOptions? = null) { + navigate(route = OnboardingInformation, navOptions = navOptions) +} + +fun NavGraphBuilder.onboardingInformationNavGraph( + paddingValues: PaddingValues, + navigateToHome: () -> Unit +) { + composable { + OnboardingInformationRoute( + paddingValues = paddingValues, + navigateToHome = navigateToHome + ) + } +} diff --git a/app/src/main/java/com/cherrish/android/presentation/onboarding/navigation/OnboardingNavigator.kt b/app/src/main/java/com/cherrish/android/presentation/onboarding/navigation/OnboardingNavigator.kt new file mode 100644 index 000000000..7633f8d1b --- /dev/null +++ b/app/src/main/java/com/cherrish/android/presentation/onboarding/navigation/OnboardingNavigator.kt @@ -0,0 +1,29 @@ +package com.cherrish.android.presentation.onboarding.navigation + +import androidx.compose.foundation.layout.PaddingValues +import androidx.navigation.NavController +import androidx.navigation.NavGraphBuilder +import androidx.navigation.NavOptions +import androidx.navigation.compose.composable +import com.cherrish.android.core.common.navigation.Route +import com.cherrish.android.presentation.onboarding.OnboardingRoute +import kotlinx.serialization.Serializable + +@Serializable +data object Onboarding : Route + +fun NavController.navigateToOnboarding(navOptions: NavOptions? = null) { + navigate(route = Onboarding, navOptions = navOptions) +} + +fun NavGraphBuilder.onboardingNavGraph( + paddingValues: PaddingValues, + navigateToOnboardingInformation: () -> Unit +) { + composable { + OnboardingRoute( + paddingValues = paddingValues, + navigateToOnboardingInformation = navigateToOnboardingInformation + ) + } +}