diff --git a/app/src/main/java/com/threegap/bitnagil/MainNavHost.kt b/app/src/main/java/com/threegap/bitnagil/MainNavHost.kt index 79ac64fd..e06ad28c 100644 --- a/app/src/main/java/com/threegap/bitnagil/MainNavHost.kt +++ b/app/src/main/java/com/threegap/bitnagil/MainNavHost.kt @@ -5,12 +5,16 @@ import androidx.compose.ui.Modifier import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.toRoute -import com.threegap.bitnagil.presentation.home.HomeScreenContainer +import com.threegap.bitnagil.navigation.home.HomeNavHost +import com.threegap.bitnagil.presentation.emotion.EmotionScreenContainer import com.threegap.bitnagil.presentation.intro.IntroScreenContainer import com.threegap.bitnagil.presentation.login.LoginScreenContainer +import com.threegap.bitnagil.presentation.onboarding.OnBoardingScreenContainer +import com.threegap.bitnagil.presentation.setting.SettingScreenContainer import com.threegap.bitnagil.presentation.splash.SplashScreenContainer import com.threegap.bitnagil.presentation.terms.TermsAgreementScreenContainer import com.threegap.bitnagil.presentation.webview.BitnagilWebViewScreen +import com.threegap.bitnagil.presentation.writeroutine.WriteRoutineScreenContainer @Composable fun MainNavHost( @@ -25,7 +29,13 @@ fun MainNavHost( composable { SplashScreenContainer( navigateToIntro = { navigator.navController.navigate(Route.Intro) }, - navigateToHome = { navigator.navController.navigate(Route.Home) }, + navigateToHome = { + navigator.navController.navigate(Route.Home) { + popUpTo(navigator.navController.graph.startDestinationId) { + inclusive = true + } + } + }, ) } @@ -37,7 +47,13 @@ fun MainNavHost( composable { LoginScreenContainer( - navigateToHome = { navigator.navController.navigate(Route.Home) }, + navigateToHome = { + navigator.navController.navigate(Route.Home) { + popUpTo(navigator.navController.graph.startDestinationId) { + inclusive = true + } + } + }, navigateToTermsAgreement = { navigator.navController.navigate(Route.TermsAgreement) }, ) } @@ -60,13 +76,35 @@ fun MainNavHost( ), ) }, - navigateToOnBoarding = { }, + navigateToOnBoarding = { + navigator.navController.navigate(Route.OnBoarding) + }, navigateToBack = { navigator.navController.popBackStack() }, ) } composable { - HomeScreenContainer() + HomeNavHost( + navigateToSetting = { + navigator.navController.navigate(Route.Setting) + }, + navigateToOnBoarding = { + navigator.navController.navigate(Route.OnBoarding) + }, + navigateToNotice = { + }, + navigateToQnA = { + }, + navigateToRegisterRoutine = { routineId -> + navigator.navController.navigate(Route.WriteRoutine(routineId = routineId)) + }, + navigateToEditRoutine = { routineId -> + navigator.navController.navigate(Route.WriteRoutine(routineId = routineId, isRegister = false)) + }, + navigateToEmotion = { + navigator.navController.navigate(Route.Emotion) + }, + ) } composable { @@ -77,5 +115,67 @@ fun MainNavHost( onBackClick = { navigator.navController.popBackStack() }, ) } + + composable { + SettingScreenContainer( + navigateToBack = { + navigator.navController.popBackStack() + }, + navigateToTermsOfService = { + navigator.navController.navigate( + Route.WebView( + title = "약관 동의", + url = "https://complex-wombat-99f.notion.site/2025-7-20-236f4587491d8071833adfaf8115bce2", + ), + ) + }, + navigateToPrivacyPolicy = { + navigator.navController.navigate( + Route.WebView( + title = "약관 동의", + url = "https://complex-wombat-99f.notion.site/2025-07-20-236f4587491d80308016eb810692d18b", + ), + ) + }, + navigateToIntro = { + navigator.navController.navigate(Route.Intro) { + popUpTo(0) { + inclusive = true + } + } + }, + ) + } + + composable { + OnBoardingScreenContainer( + navigateToHome = { + navigator.navController.navigate(Route.Home) { + popUpTo(navigator.navController.graph.startDestinationId) { + inclusive = true + } + } + }, + navigateToBack = { + navigator.navController.popBackStack() + }, + ) + } + + composable { + WriteRoutineScreenContainer( + navigateToBack = { + navigator.navController.popBackStack() + }, + ) + } + + composable { + EmotionScreenContainer( + navigateToBack = { + navigator.navController.popBackStack() + }, + ) + } } } diff --git a/app/src/main/java/com/threegap/bitnagil/Route.kt b/app/src/main/java/com/threegap/bitnagil/Route.kt index 621f026e..c94979a3 100644 --- a/app/src/main/java/com/threegap/bitnagil/Route.kt +++ b/app/src/main/java/com/threegap/bitnagil/Route.kt @@ -24,4 +24,19 @@ sealed interface Route { val title: String, val url: String, ) : Route + + @Serializable + data object Setting : Route + + @Serializable + data object OnBoarding : Route + + @Serializable + data class WriteRoutine( + val routineId: String? = null, + val isRegister: Boolean = true, + ) : Route + + @Serializable + data object Emotion : Route } diff --git a/app/src/main/java/com/threegap/bitnagil/navigation/home/HomeBottomNavigationBar.kt b/app/src/main/java/com/threegap/bitnagil/navigation/home/HomeBottomNavigationBar.kt new file mode 100644 index 00000000..23935ee2 --- /dev/null +++ b/app/src/main/java/com/threegap/bitnagil/navigation/home/HomeBottomNavigationBar.kt @@ -0,0 +1,109 @@ +package com.threegap.bitnagil.navigation.home + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsPressedAsState +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController +import androidx.navigation.compose.currentBackStackEntryAsState +import com.threegap.bitnagil.designsystem.BitnagilTheme + +@Composable +fun HomeBottomNavigationBar( + navController: NavController, +) { + val navBackStackEntry by navController.currentBackStackEntryAsState() + + Row( + modifier = Modifier + .fillMaxWidth() + .background(color = BitnagilTheme.colors.white) + .padding(horizontal = 16.dp, vertical = 7.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + HomeRoute.entries.map { homeRoute -> + HomeBottomNavigationItem( + modifier = Modifier.weight(1f), + selectIconResourceId = homeRoute.selectIconResourceId, + unSelectIconResourceId = homeRoute.unSelectIconResourceId, + title = homeRoute.title, + onClick = { + navController.navigate(homeRoute.route) { + popUpTo(0) { inclusive = true } + } + }, + selected = navBackStackEntry?.destination?.route == homeRoute.route, + ) + } + } +} + +@Composable +private fun HomeBottomNavigationItem( + modifier: Modifier = Modifier, + selectIconResourceId: Int, + unSelectIconResourceId: Int, + title: String, + onClick: () -> Unit, + selected: Boolean, +) { + val interactionSource = remember { MutableInteractionSource() } + val isPressed by interactionSource.collectIsPressedAsState() + + val contentTintColor = when { + isPressed -> BitnagilTheme.colors.navy300 + selected -> BitnagilTheme.colors.navy500 + else -> BitnagilTheme.colors.navy100 + } + val iconResourceId = if (selected) selectIconResourceId else unSelectIconResourceId + + Column( + modifier = modifier.clickable( + onClick = onClick, + interactionSource = interactionSource, + indication = null, + ), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Image( + painter = painterResource(id = iconResourceId), + contentDescription = title, + modifier = Modifier.padding(4.dp).size(24.dp), + colorFilter = ColorFilter.tint(color = contentTintColor), + ) + + Text( + text = title, + style = BitnagilTheme.typography.caption2Medium, + color = contentTintColor, + ) + } +} + +@Composable +@Preview +private fun HomeBottomNavigationBarPreview() { + val navigator = rememberHomeNavigator() + + HomeBottomNavigationBar( + navController = navigator.navController, + + ) +} diff --git a/app/src/main/java/com/threegap/bitnagil/navigation/home/HomeNavHost.kt b/app/src/main/java/com/threegap/bitnagil/navigation/home/HomeNavHost.kt new file mode 100644 index 00000000..d9d7d227 --- /dev/null +++ b/app/src/main/java/com/threegap/bitnagil/navigation/home/HomeNavHost.kt @@ -0,0 +1,90 @@ +package com.threegap.bitnagil.navigation.home + +import android.annotation.SuppressLint +import android.app.Activity +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableLongStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import com.threegap.bitnagil.presentation.home.HomeScreenContainer +import com.threegap.bitnagil.presentation.mypage.MyPageScreenContainer +import com.threegap.bitnagil.presentation.recommendroutine.RecommendRoutineScreenContainer + +@Composable +@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") +fun HomeNavHost( + modifier: Modifier = Modifier, + navigateToSetting: () -> Unit, + navigateToOnBoarding: () -> Unit, + navigateToNotice: () -> Unit, + navigateToQnA: () -> Unit, + navigateToRegisterRoutine: (String?) -> Unit, + navigateToEditRoutine: (String) -> Unit, + navigateToEmotion: () -> Unit, +) { + val navigator = rememberHomeNavigator() + + DoubleBackButtonPressedHandler() + + Scaffold( + modifier = Modifier.fillMaxSize(), + bottomBar = { + HomeBottomNavigationBar(navController = navigator.navController) + }, + content = { _ -> + NavHost( + navController = navigator.navController, + startDestination = navigator.startDestination, + modifier = modifier, + ) { + composable(HomeRoute.Home.route) { + HomeScreenContainer( + navigateToRegisterRoutine = { + navigateToRegisterRoutine(null) + }, + navigateToEditRoutine = navigateToEditRoutine, + navigateToEmotion = navigateToEmotion, + ) + } + + composable(HomeRoute.RecommendRoutine.route) { + RecommendRoutineScreenContainer( + navigateToEmotion = navigateToEmotion, + navigateToRegisterRoutine = navigateToRegisterRoutine, + ) + } + + composable(HomeRoute.MyPage.route) { + MyPageScreenContainer( + navigateToSetting = navigateToSetting, + navigateToOnBoarding = navigateToOnBoarding, + navigateToNotice = navigateToNotice, + navigateToQnA = navigateToQnA, + ) + } + } + }, + ) +} + +@Composable +fun DoubleBackButtonPressedHandler() { + val context = LocalContext.current + var backPressedTimeMillis by remember { mutableLongStateOf(0L) } + BackHandler { + if (System.currentTimeMillis() - backPressedTimeMillis < 2000) { + backPressedTimeMillis = 0 + (context as? Activity)?.finish() + } else { + backPressedTimeMillis = System.currentTimeMillis() + } + } +} diff --git a/app/src/main/java/com/threegap/bitnagil/navigation/home/HomeNavigator.kt b/app/src/main/java/com/threegap/bitnagil/navigation/home/HomeNavigator.kt new file mode 100644 index 00000000..0d6260fe --- /dev/null +++ b/app/src/main/java/com/threegap/bitnagil/navigation/home/HomeNavigator.kt @@ -0,0 +1,18 @@ +package com.threegap.bitnagil.navigation.home + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.navigation.NavHostController +import androidx.navigation.compose.rememberNavController + +class HomeNavigator( + val navController: NavHostController, +) { + val startDestination = HomeRoute.Home.route +} + +@Composable +fun rememberHomeNavigator(navController: NavHostController = rememberNavController()): HomeNavigator = + remember(navController) { + HomeNavigator(navController) + } diff --git a/app/src/main/java/com/threegap/bitnagil/navigation/home/HomeRoute.kt b/app/src/main/java/com/threegap/bitnagil/navigation/home/HomeRoute.kt new file mode 100644 index 00000000..4ac10ce3 --- /dev/null +++ b/app/src/main/java/com/threegap/bitnagil/navigation/home/HomeRoute.kt @@ -0,0 +1,32 @@ +package com.threegap.bitnagil.navigation.home + +import com.threegap.bitnagil.R + +enum class HomeRoute( + val route: String, + val title: String, + val selectIconResourceId: Int, + val unSelectIconResourceId: Int, +) { + Home( + route = "home/home", + title = "홈", + selectIconResourceId = R.drawable.ic_home_fill, + unSelectIconResourceId = R.drawable.ic_home_empty, + ), + + RecommendRoutine( + route = "home/recommend_routine", + title = "추천 루틴", + selectIconResourceId = R.drawable.ic_recommend_fill, + unSelectIconResourceId = R.drawable.ic_recommend_empty, + ), + + MyPage( + route = "home/my_page", + title = "마이페이지", + selectIconResourceId = R.drawable.ic_mypage_fill, + unSelectIconResourceId = R.drawable.ic_mypage_empty, + ), + ; +} diff --git a/app/src/main/res/drawable/ic_home_empty.xml b/app/src/main/res/drawable/ic_home_empty.xml new file mode 100644 index 00000000..e77d8e04 --- /dev/null +++ b/app/src/main/res/drawable/ic_home_empty.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/ic_home_fill.xml b/app/src/main/res/drawable/ic_home_fill.xml new file mode 100644 index 00000000..aa23253e --- /dev/null +++ b/app/src/main/res/drawable/ic_home_fill.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/drawable/ic_mypage_empty.xml b/app/src/main/res/drawable/ic_mypage_empty.xml new file mode 100644 index 00000000..d44821c2 --- /dev/null +++ b/app/src/main/res/drawable/ic_mypage_empty.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_mypage_fill.xml b/app/src/main/res/drawable/ic_mypage_fill.xml new file mode 100644 index 00000000..d338b47d --- /dev/null +++ b/app/src/main/res/drawable/ic_mypage_fill.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/drawable/ic_recommend_empty.xml b/app/src/main/res/drawable/ic_recommend_empty.xml new file mode 100644 index 00000000..4154f12b --- /dev/null +++ b/app/src/main/res/drawable/ic_recommend_empty.xml @@ -0,0 +1,17 @@ + + + + diff --git a/app/src/main/res/drawable/ic_recommend_fill.xml b/app/src/main/res/drawable/ic_recommend_fill.xml new file mode 100644 index 00000000..5d4b84c4 --- /dev/null +++ b/app/src/main/res/drawable/ic_recommend_fill.xml @@ -0,0 +1,30 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_report_empty.xml b/app/src/main/res/drawable/ic_report_empty.xml new file mode 100644 index 00000000..c62ec063 --- /dev/null +++ b/app/src/main/res/drawable/ic_report_empty.xml @@ -0,0 +1,18 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_report_fill.xml b/app/src/main/res/drawable/ic_report_fill.xml new file mode 100644 index 00000000..afa27517 --- /dev/null +++ b/app/src/main/res/drawable/ic_report_fill.xml @@ -0,0 +1,13 @@ + + + + diff --git a/data/src/main/java/com/threegap/bitnagil/data/auth/datasource/AuthLocalDataSource.kt b/data/src/main/java/com/threegap/bitnagil/data/auth/datasource/AuthLocalDataSource.kt index 67dd07c2..4bf7bce4 100644 --- a/data/src/main/java/com/threegap/bitnagil/data/auth/datasource/AuthLocalDataSource.kt +++ b/data/src/main/java/com/threegap/bitnagil/data/auth/datasource/AuthLocalDataSource.kt @@ -4,4 +4,6 @@ interface AuthLocalDataSource { suspend fun hasToken(): Boolean suspend fun updateAuthToken(accessToken: String, refreshToken: String): Result + + suspend fun clearAuthToken(): Result } diff --git a/data/src/main/java/com/threegap/bitnagil/data/auth/datasource/AuthRemoteDataSource.kt b/data/src/main/java/com/threegap/bitnagil/data/auth/datasource/AuthRemoteDataSource.kt index 68470af8..dd4dce44 100644 --- a/data/src/main/java/com/threegap/bitnagil/data/auth/datasource/AuthRemoteDataSource.kt +++ b/data/src/main/java/com/threegap/bitnagil/data/auth/datasource/AuthRemoteDataSource.kt @@ -8,4 +8,8 @@ interface AuthRemoteDataSource { suspend fun login(socialAccessToken: String, loginRequestDto: LoginRequestDto): Result suspend fun submitAgreement(termsAgreementRequestDto: TermsAgreementRequestDto): Result + + suspend fun logout(): Result + + suspend fun withdrawal(): Result } diff --git a/data/src/main/java/com/threegap/bitnagil/data/auth/datasourceimpl/AuthLocalDataSourceImpl.kt b/data/src/main/java/com/threegap/bitnagil/data/auth/datasourceimpl/AuthLocalDataSourceImpl.kt index b0ae1640..cd1fc995 100644 --- a/data/src/main/java/com/threegap/bitnagil/data/auth/datasourceimpl/AuthLocalDataSourceImpl.kt +++ b/data/src/main/java/com/threegap/bitnagil/data/auth/datasourceimpl/AuthLocalDataSourceImpl.kt @@ -17,4 +17,9 @@ class AuthLocalDataSourceImpl @Inject constructor( refreshToken = refreshToken, ) } + + override suspend fun clearAuthToken(): Result = + runCatching { + authTokenDataStore.clearAuthToken() + } } diff --git a/data/src/main/java/com/threegap/bitnagil/data/auth/datasourceimpl/AuthRemoteDataSourceImpl.kt b/data/src/main/java/com/threegap/bitnagil/data/auth/datasourceimpl/AuthRemoteDataSourceImpl.kt index af1fbd5d..d792438b 100644 --- a/data/src/main/java/com/threegap/bitnagil/data/auth/datasourceimpl/AuthRemoteDataSourceImpl.kt +++ b/data/src/main/java/com/threegap/bitnagil/data/auth/datasourceimpl/AuthRemoteDataSourceImpl.kt @@ -6,6 +6,7 @@ import com.threegap.bitnagil.data.auth.model.request.TermsAgreementRequestDto import com.threegap.bitnagil.data.auth.model.response.LoginResponseDto import com.threegap.bitnagil.data.auth.service.AuthService import com.threegap.bitnagil.data.common.safeApiCall +import com.threegap.bitnagil.data.common.safeUnitApiCall import javax.inject.Inject class AuthRemoteDataSourceImpl @Inject constructor( @@ -20,4 +21,14 @@ class AuthRemoteDataSourceImpl @Inject constructor( safeApiCall { authService.submitAgreement(termsAgreementRequestDto) } + + override suspend fun logout(): Result = + safeUnitApiCall { + authService.postLogout() + } + + override suspend fun withdrawal(): Result = + safeUnitApiCall { + authService.postWithdrawal() + } } diff --git a/data/src/main/java/com/threegap/bitnagil/data/auth/repositoryimpl/AuthRepositoryImpl.kt b/data/src/main/java/com/threegap/bitnagil/data/auth/repositoryimpl/AuthRepositoryImpl.kt index bcdb5110..065cc652 100644 --- a/data/src/main/java/com/threegap/bitnagil/data/auth/repositoryimpl/AuthRepositoryImpl.kt +++ b/data/src/main/java/com/threegap/bitnagil/data/auth/repositoryimpl/AuthRepositoryImpl.kt @@ -27,4 +27,16 @@ class AuthRepositoryImpl @Inject constructor( authRemoteDataSource.submitAgreement( termsAgreement.toDto(), ) + + override suspend fun logout(): Result { + return authRemoteDataSource.logout().also { + if (it.isSuccess) authLocalDataSource.clearAuthToken() + } + } + + override suspend fun withdrawal(): Result { + return authRemoteDataSource.withdrawal().also { + if (it.isSuccess) authLocalDataSource.clearAuthToken() + } + } } diff --git a/data/src/main/java/com/threegap/bitnagil/data/auth/service/AuthService.kt b/data/src/main/java/com/threegap/bitnagil/data/auth/service/AuthService.kt index 291dac0f..2548f1c8 100644 --- a/data/src/main/java/com/threegap/bitnagil/data/auth/service/AuthService.kt +++ b/data/src/main/java/com/threegap/bitnagil/data/auth/service/AuthService.kt @@ -21,4 +21,10 @@ interface AuthService { suspend fun submitAgreement( @Body termsAgreementRequestDto: TermsAgreementRequestDto, ): BaseResponse + + @POST("/api/v1/auth/withdrawal") + suspend fun postWithdrawal(): BaseResponse + + @POST("/api/v1/auth/logout") + suspend fun postLogout(): BaseResponse } diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/auth/repository/AuthRepository.kt b/domain/src/main/java/com/threegap/bitnagil/domain/auth/repository/AuthRepository.kt index 8448e86d..f872955f 100644 --- a/domain/src/main/java/com/threegap/bitnagil/domain/auth/repository/AuthRepository.kt +++ b/domain/src/main/java/com/threegap/bitnagil/domain/auth/repository/AuthRepository.kt @@ -11,4 +11,8 @@ interface AuthRepository { suspend fun updateAuthToken(accessToken: String, refreshToken: String): Result suspend fun submitAgreement(termsAgreement: TermsAgreement): Result + + suspend fun logout(): Result + + suspend fun withdrawal(): Result } diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/auth/usecase/LogoutUseCase.kt b/domain/src/main/java/com/threegap/bitnagil/domain/auth/usecase/LogoutUseCase.kt new file mode 100644 index 00000000..f8893314 --- /dev/null +++ b/domain/src/main/java/com/threegap/bitnagil/domain/auth/usecase/LogoutUseCase.kt @@ -0,0 +1,10 @@ +package com.threegap.bitnagil.domain.auth.usecase + +import com.threegap.bitnagil.domain.auth.repository.AuthRepository +import javax.inject.Inject + +class LogoutUseCase @Inject constructor( + private val authRepository: AuthRepository, +) { + suspend operator fun invoke(): Result = authRepository.logout() +} diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/auth/usecase/WithdrawalUseCase.kt b/domain/src/main/java/com/threegap/bitnagil/domain/auth/usecase/WithdrawalUseCase.kt new file mode 100644 index 00000000..7e96ca67 --- /dev/null +++ b/domain/src/main/java/com/threegap/bitnagil/domain/auth/usecase/WithdrawalUseCase.kt @@ -0,0 +1,10 @@ +package com.threegap.bitnagil.domain.auth.usecase + +import com.threegap.bitnagil.domain.auth.repository.AuthRepository +import javax.inject.Inject + +class WithdrawalUseCase @Inject constructor( + private val authRepository: AuthRepository, +) { + suspend operator fun invoke(): Result = authRepository.withdrawal() +} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/HomeScreen.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/HomeScreen.kt index f0854cfe..79bf539f 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/HomeScreen.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/HomeScreen.kt @@ -45,6 +45,9 @@ import java.time.LocalDate @Composable fun HomeScreenContainer( viewModel: HomeViewModel = hiltViewModel(), + navigateToRegisterRoutine: () -> Unit, + navigateToEditRoutine: (String) -> Unit, + navigateToEmotion: () -> Unit, ) { val uiState by viewModel.stateFlow.collectAsStateWithLifecycle() @@ -65,8 +68,7 @@ fun HomeScreenContainer( RoutineDetailsBottomSheet( routine = routine, onDismiss = { viewModel.sendIntent(HomeIntent.HideRoutineDetailsBottomSheet) }, - // TODO: 수정 화면으로 네비게이션 - onEdit = {}, + onEdit = navigateToEditRoutine, onDelete = { if (routine.repeatDay.isEmpty()) { viewModel.deleteRoutine(routine.routineId) @@ -117,6 +119,8 @@ fun HomeScreenContainer( onShowRoutineDetailsBottomSheet = { routine -> viewModel.sendIntent(HomeIntent.ShowRoutineDetailsBottomSheet(routine)) }, + onRegisterRoutineClick = navigateToRegisterRoutine, + onRegisterEmotionClick = navigateToEmotion, ) } @@ -130,6 +134,8 @@ private fun HomeScreen( onSubRoutineCompletionToggle: (String, String, Boolean) -> Unit, onShowRoutineSortBottomSheet: () -> Unit, onShowRoutineDetailsBottomSheet: (RoutineUiModel) -> Unit, + onRegisterRoutineClick: () -> Unit, + onRegisterEmotionClick: () -> Unit, modifier: Modifier = Modifier, ) { val collapsibleHeaderState = rememberCollapsibleHeaderState() @@ -177,8 +183,7 @@ private fun HomeScreen( if (uiState.selectedDateRoutines.isEmpty()) { item { RoutineEmptyView( - // todo: 루린 등록 화면으로 네비게이션 - onRegisterRoutineClick = {}, + onRegisterRoutineClick = onRegisterRoutineClick, modifier = Modifier .fillMaxSize() .padding(top = 62.dp), @@ -242,7 +247,7 @@ private fun HomeScreen( CollapsibleHomeHeader( userName = "대현", collapsibleHeaderState = collapsibleHeaderState, - onEmotionRecordClick = {}, + onEmotionRecordClick = onRegisterEmotionClick, ) } } @@ -259,5 +264,7 @@ private fun HomeScreenPreview() { onSubRoutineCompletionToggle = { _, _, _ -> }, onShowRoutineSortBottomSheet = {}, onShowRoutineDetailsBottomSheet = {}, + onRegisterRoutineClick = {}, + onRegisterEmotionClick = {}, ) } diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/recommendroutine/RecommendRoutineScreen.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/recommendroutine/RecommendRoutineScreen.kt index a5e339b9..ba98326a 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/recommendroutine/RecommendRoutineScreen.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/recommendroutine/RecommendRoutineScreen.kt @@ -42,6 +42,8 @@ import com.threegap.bitnagil.presentation.recommendroutine.type.RecommendRoutine @Composable fun RecommendRoutineScreenContainer( viewmodel: RecommendRoutineViewModel = hiltViewModel(), + navigateToEmotion: () -> Unit, + navigateToRegisterRoutine: (String?) -> Unit, ) { val uiState by viewmodel.container.stateFlow.collectAsStateWithLifecycle() @@ -59,6 +61,8 @@ fun RecommendRoutineScreenContainer( onHideDifficultyBottomSheet = { viewmodel.sendIntent(RecommendRoutineIntent.HideDifficultyBottomSheet) }, + onRecommendRoutineByEmotionClick = navigateToEmotion, + onRegisterRoutineClick = navigateToRegisterRoutine, ) } @@ -69,6 +73,8 @@ private fun RecommendRoutineScreen( onDifficultySelected: (RecommendRoutineDifficulty?) -> Unit, onShowDifficultyBottomSheet: () -> Unit, onHideDifficultyBottomSheet: () -> Unit, + onRecommendRoutineByEmotionClick: () -> Unit, + onRegisterRoutineClick: (String) -> Unit, ) { Column( modifier = Modifier @@ -165,7 +171,7 @@ private fun RecommendRoutineScreen( if (uiState.isDefaultCategory) { item { EmotionRecommendRoutineButton( - onClick = {}, + onClick = onRecommendRoutineByEmotionClick, ) } } @@ -176,7 +182,7 @@ private fun RecommendRoutineScreen( RecommendRoutineItem( routineName = routine.name, routineDescription = routine.description, - onAddRoutineClick = {}, + onAddRoutineClick = { onRegisterRoutineClick(routine.id) }, ) } } @@ -203,5 +209,7 @@ private fun RoutineRecommendScreenPreview() { onDifficultySelected = {}, onShowDifficultyBottomSheet = {}, onHideDifficultyBottomSheet = {}, + onRecommendRoutineByEmotionClick = {}, + onRegisterRoutineClick = {}, ) } diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/recommendroutine/model/RecommendRoutine.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/recommendroutine/model/RecommendRoutine.kt index 5140297b..cc416918 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/recommendroutine/model/RecommendRoutine.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/recommendroutine/model/RecommendRoutine.kt @@ -9,4 +9,5 @@ data class RecommendRoutine( val name: String, val description: String, val difficulty: RecommendRoutineDifficulty, + val id: String = "0", ) : Parcelable diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/setting/SettingScreen.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/setting/SettingScreen.kt index c20ceb5a..23ae4f8e 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/setting/SettingScreen.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/setting/SettingScreen.kt @@ -26,22 +26,39 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import com.threegap.bitnagil.designsystem.BitnagilTheme +import com.threegap.bitnagil.presentation.common.flow.collectAsEffect import com.threegap.bitnagil.presentation.setting.component.atom.settingtitle.SettingTitle import com.threegap.bitnagil.presentation.setting.component.atom.toggleswitch.ToggleSwitch import com.threegap.bitnagil.presentation.setting.component.block.settingrowbutton.SettingRowButton +import com.threegap.bitnagil.presentation.setting.model.mvi.SettingSideEffect import com.threegap.bitnagil.presentation.setting.model.mvi.SettingState @Composable fun SettingScreenContainer( viewModel: SettingViewModel = hiltViewModel(), + navigateToBack: () -> Unit, + navigateToTermsOfService: () -> Unit, + navigateToPrivacyPolicy: () -> Unit, + navigateToIntro: () -> Unit, ) { val state by viewModel.stateFlow.collectAsState() + viewModel.sideEffectFlow.collectAsEffect { sideEffect -> + when (sideEffect) { + SettingSideEffect.NavigateToIntro -> navigateToIntro() + } + } + SettingScreen( state = state, toggleServiceAlarm = viewModel::toggleServiceAlarm, togglePushAlarm = viewModel::togglePushAlarm, onClickUpdate = {}, + onClickBack = navigateToBack, + onClickTermsOfService = navigateToTermsOfService, + onClickPrivacyPolicy = navigateToPrivacyPolicy, + onClickLogout = viewModel::logout, + onClickWithdrawal = viewModel::withdrawal, ) } @@ -51,6 +68,11 @@ private fun SettingScreen( toggleServiceAlarm: () -> Unit, togglePushAlarm: () -> Unit, onClickUpdate: () -> Unit, + onClickBack: () -> Unit, + onClickTermsOfService: () -> Unit, + onClickPrivacyPolicy: () -> Unit, + onClickLogout: () -> Unit, + onClickWithdrawal: () -> Unit, ) { Column( modifier = Modifier @@ -68,8 +90,7 @@ private fun SettingScreen( .size(36.dp) .background(BitnagilTheme.colors.black) .align(Alignment.CenterStart) - .clickable { - }, + .clickable(onClick = onClickBack), ) Text( @@ -159,9 +180,9 @@ private fun SettingScreen( } } - SettingRowButton(text = "서비스 이용약관", onClick = {}) + SettingRowButton(text = "서비스 이용약관", onClick = onClickTermsOfService) - SettingRowButton(text = "개인정보 처리방침", onClick = {}) + SettingRowButton(text = "개인정보 처리방침", onClick = onClickPrivacyPolicy) Spacer(modifier = Modifier.height(6.dp)) @@ -171,9 +192,9 @@ private fun SettingScreen( SettingTitle("계정") - SettingRowButton(text = "로그아웃", onClick = {}) + SettingRowButton(text = "로그아웃", onClick = onClickLogout) - SettingRowButton(text = "탈퇴하기", onClick = {}) + SettingRowButton(text = "탈퇴하기", onClick = onClickWithdrawal) } } } @@ -187,9 +208,15 @@ fun SettingScreenPreview() { usePushAlarm = false, version = "1.0.1", latestVersion = "1.0.0", + loading = false, ), toggleServiceAlarm = {}, togglePushAlarm = {}, onClickUpdate = {}, + onClickBack = {}, + onClickPrivacyPolicy = {}, + onClickTermsOfService = {}, + onClickLogout = {}, + onClickWithdrawal = {}, ) } diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/setting/SettingViewModel.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/setting/SettingViewModel.kt index 6146b59f..baa49ba5 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/setting/SettingViewModel.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/setting/SettingViewModel.kt @@ -2,6 +2,8 @@ package com.threegap.bitnagil.presentation.setting import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope +import com.threegap.bitnagil.domain.auth.usecase.LogoutUseCase +import com.threegap.bitnagil.domain.auth.usecase.WithdrawalUseCase import com.threegap.bitnagil.presentation.common.mviviewmodel.MviViewModel import com.threegap.bitnagil.presentation.setting.model.mvi.SettingIntent import com.threegap.bitnagil.presentation.setting.model.mvi.SettingSideEffect @@ -16,6 +18,8 @@ import javax.inject.Inject @HiltViewModel class SettingViewModel @Inject constructor( savedStateHandle: SavedStateHandle, + private val logoutUseCase: LogoutUseCase, + private val withdrawalUseCase: WithdrawalUseCase, ) : MviViewModel( initState = SettingState.Init, savedStateHandle = savedStateHandle, @@ -26,7 +30,7 @@ class SettingViewModel @Inject constructor( override suspend fun SimpleSyntax.reduceState( intent: SettingIntent, state: SettingState, - ): SettingState { + ): SettingState? { when (intent) { is SettingIntent.LoadSettingSuccess -> { return state.copy( @@ -46,6 +50,26 @@ class SettingViewModel @Inject constructor( useServiceAlarm = !state.useServiceAlarm, ) } + SettingIntent.LogoutSuccess -> { + sendSideEffect(SettingSideEffect.NavigateToIntro) + return null + } + SettingIntent.LogoutLoading -> { + return state.copy(loading = true) + } + SettingIntent.LogoutFailure -> { + return state.copy(loading = false) + } + SettingIntent.WithdrawalSuccess -> { + sendSideEffect(SettingSideEffect.NavigateToIntro) + return null + } + SettingIntent.WithdrawalLoading -> { + return state.copy(loading = true) + } + SettingIntent.WithdrawalFailure -> { + return state.copy(loading = false) + } } } @@ -64,4 +88,26 @@ class SettingViewModel @Inject constructor( delay(1000L) } } + + fun logout() { + viewModelScope.launch { + sendIntent(SettingIntent.LogoutLoading) + logoutUseCase().onSuccess { + sendIntent(SettingIntent.LogoutSuccess) + }.onFailure { + sendIntent(SettingIntent.LogoutFailure) + } + } + } + + fun withdrawal() { + viewModelScope.launch { + sendIntent(SettingIntent.WithdrawalLoading) + withdrawalUseCase().onSuccess { + sendIntent(SettingIntent.WithdrawalSuccess) + }.onFailure { + sendIntent(SettingIntent.WithdrawalFailure) + } + } + } } diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/setting/model/mvi/SettingIntent.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/setting/model/mvi/SettingIntent.kt index b15206d2..066691f3 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/setting/model/mvi/SettingIntent.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/setting/model/mvi/SettingIntent.kt @@ -12,4 +12,10 @@ sealed class SettingIntent : MviIntent { data object ToggleServiceAlarm : SettingIntent() data object TogglePushAlarm : SettingIntent() + data object LogoutLoading : SettingIntent() + data object LogoutSuccess : SettingIntent() + data object LogoutFailure : SettingIntent() + data object WithdrawalLoading : SettingIntent() + data object WithdrawalSuccess : SettingIntent() + data object WithdrawalFailure : SettingIntent() } diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/setting/model/mvi/SettingSideEffect.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/setting/model/mvi/SettingSideEffect.kt index 00f22c19..45dacc0b 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/setting/model/mvi/SettingSideEffect.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/setting/model/mvi/SettingSideEffect.kt @@ -2,4 +2,6 @@ package com.threegap.bitnagil.presentation.setting.model.mvi import com.threegap.bitnagil.presentation.common.mviviewmodel.MviSideEffect -sealed class SettingSideEffect : MviSideEffect +sealed class SettingSideEffect : MviSideEffect { + data object NavigateToIntro : SettingSideEffect() +} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/setting/model/mvi/SettingState.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/setting/model/mvi/SettingState.kt index c1c8ebbd..e2382526 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/setting/model/mvi/SettingState.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/setting/model/mvi/SettingState.kt @@ -9,6 +9,7 @@ data class SettingState( val usePushAlarm: Boolean, val version: String, val latestVersion: String, + val loading: Boolean, ) : MviState { companion object { val Init = SettingState( @@ -16,6 +17,7 @@ data class SettingState( usePushAlarm = false, version = "", latestVersion = "", + loading = false, ) } } diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/writeroutine/WriteRoutineScreen.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/writeroutine/WriteRoutineScreen.kt index 0109bf55..074c1909 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/writeroutine/WriteRoutineScreen.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/writeroutine/WriteRoutineScreen.kt @@ -79,6 +79,7 @@ fun WriteRoutineScreenContainer( showTimePickerBottomSheet = viewModel::showTimePickerBottomSheet, onClickRegister = viewModel::registerRoutine, removeSubRoutine = viewModel::removeSubRoutine, + onClickBack = navigateToBack, ) } @@ -94,6 +95,7 @@ private fun WriteRoutineScreen( showTimePickerBottomSheet: () -> Unit, onClickRegister: () -> Unit, removeSubRoutine: (Int) -> Unit, + onClickBack: () -> Unit, ) { val scrollState = rememberScrollState() @@ -110,7 +112,7 @@ private fun WriteRoutineScreen( .padding(start = 4.dp) .align(alignment = Alignment.CenterStart) .size(36.dp) - .clickable { }, + .clickable(onClick = onClickBack), contentAlignment = Alignment.Center, ) { Image( @@ -415,6 +417,7 @@ fun WriteRoutineScreenPreview() { showTimePickerBottomSheet = {}, onClickRegister = {}, removeSubRoutine = {}, + onClickBack = {}, ) } }