diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 03625ecd..f643fa4b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -17,6 +17,7 @@ tools:targetApi="31"> diff --git a/app/src/main/java/com/threegap/bitnagil/MainNavHost.kt b/app/src/main/java/com/threegap/bitnagil/MainNavHost.kt index 5716453b..b344ac1e 100644 --- a/app/src/main/java/com/threegap/bitnagil/MainNavHost.kt +++ b/app/src/main/java/com/threegap/bitnagil/MainNavHost.kt @@ -12,10 +12,12 @@ import com.threegap.bitnagil.presentation.login.LoginScreenContainer import com.threegap.bitnagil.presentation.onboarding.OnBoardingScreenContainer import com.threegap.bitnagil.presentation.onboarding.OnBoardingViewModel import com.threegap.bitnagil.presentation.onboarding.model.navarg.OnBoardingScreenArg +import com.threegap.bitnagil.presentation.routinelist.RoutineListScreenContainer 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.withdrawal.WithdrawalScreenContainer import com.threegap.bitnagil.presentation.writeroutine.WriteRoutineScreenContainer import com.threegap.bitnagil.presentation.writeroutine.WriteRoutineViewModel import com.threegap.bitnagil.presentation.writeroutine.model.navarg.WriteRoutineScreenArg @@ -114,9 +116,6 @@ fun MainNavHost( 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) }, @@ -166,6 +165,9 @@ fun MainNavHost( } } }, + navigateToWithdrawal = { + navigator.navController.navigate(Route.Withdrawal) + }, ) } @@ -223,5 +225,32 @@ fun MainNavHost( }, ) } + + composable { + WithdrawalScreenContainer( + navigateToBack = { + if (navigator.navController.previousBackStackEntry != null) { + navigator.navController.popBackStack() + } + }, + navigateToLogin = { + navigator.navController.navigate(Route.Login) { + popUpTo(0) { + inclusive = true + } + } + }, + ) + } + + composable { + RoutineListScreenContainer( + navigateToBack = { + if (navigator.navController.previousBackStackEntry != null) { + navigator.navController.popBackStack() + } + }, + ) + } } } diff --git a/app/src/main/java/com/threegap/bitnagil/MainScreen.kt b/app/src/main/java/com/threegap/bitnagil/MainScreen.kt index 6800fe7a..dc2dad43 100644 --- a/app/src/main/java/com/threegap/bitnagil/MainScreen.kt +++ b/app/src/main/java/com/threegap/bitnagil/MainScreen.kt @@ -1,11 +1,11 @@ package com.threegap.bitnagil import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.exclude import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.ime +import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.systemBars import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -18,7 +18,7 @@ fun MainScreen( ) { Scaffold( modifier = modifier.fillMaxSize(), - contentWindowInsets = WindowInsets.systemBars.only(WindowInsetsSides.Bottom), + contentWindowInsets = WindowInsets.navigationBars.exclude(WindowInsets.ime), containerColor = BitnagilTheme.colors.white, ) { innerPadding -> MainNavHost( diff --git a/app/src/main/java/com/threegap/bitnagil/Route.kt b/app/src/main/java/com/threegap/bitnagil/Route.kt index ced10832..49ddc465 100644 --- a/app/src/main/java/com/threegap/bitnagil/Route.kt +++ b/app/src/main/java/com/threegap/bitnagil/Route.kt @@ -38,4 +38,10 @@ sealed interface Route { @Serializable data object Emotion : Route + + @Serializable + data object Withdrawal : Route + + @Serializable + data object RoutineList : 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 index 086cfb8f..7c480e6c 100644 --- a/app/src/main/java/com/threegap/bitnagil/navigation/home/HomeBottomNavigationBar.kt +++ b/app/src/main/java/com/threegap/bitnagil/navigation/home/HomeBottomNavigationBar.kt @@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -30,27 +31,34 @@ fun HomeBottomNavigationBar( ) { val navBackStackEntry by navController.currentBackStackEntryAsState() - Row( - modifier = Modifier - .fillMaxWidth() - .background(color = BitnagilTheme.colors.white) - .height(62.dp) - .padding(horizontal = 16.dp, vertical = 7.dp), - horizontalArrangement = Arrangement.spacedBy(10.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - HomeRoute.entries.map { homeRoute -> - HomeBottomNavigationItem( - modifier = Modifier.weight(1f), - icon = homeRoute.icon, - title = homeRoute.title, - onClick = { - navController.navigate(homeRoute.route) { - popUpTo(0) { inclusive = true } - } - }, - selected = navBackStackEntry?.destination?.route == homeRoute.route, - ) + Column { + HorizontalDivider( + modifier = Modifier.fillMaxWidth(), + thickness = 1.dp, + color = BitnagilTheme.colors.coolGray98, + ) + Row( + modifier = Modifier + .fillMaxWidth() + .background(color = BitnagilTheme.colors.white) + .height(62.dp) + .padding(horizontal = 16.dp, vertical = 7.dp), + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + HomeRoute.entries.map { homeRoute -> + HomeBottomNavigationItem( + modifier = Modifier.weight(1f), + icon = homeRoute.icon, + title = homeRoute.title, + onClick = { + navController.navigate(homeRoute.route) { + popUpTo(0) { inclusive = true } + } + }, + selected = navBackStackEntry?.destination?.route == homeRoute.route, + ) + } } } } 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 index 6b933f10..41633372 100644 --- a/app/src/main/java/com/threegap/bitnagil/navigation/home/HomeNavHost.kt +++ b/app/src/main/java/com/threegap/bitnagil/navigation/home/HomeNavHost.kt @@ -40,7 +40,6 @@ fun HomeNavHost( navigateToNotice: () -> Unit, navigateToQnA: () -> Unit, navigateToRegisterRoutine: (String?) -> Unit, - navigateToEditRoutine: (String) -> Unit, navigateToEmotion: () -> Unit, ) { val navigator = rememberHomeNavigator() @@ -66,7 +65,6 @@ fun HomeNavHost( navigateToRegisterRoutine = { navigateToRegisterRoutine(null) }, - navigateToEditRoutine = navigateToEditRoutine, navigateToEmotion = navigateToEmotion, ) } @@ -102,11 +100,6 @@ fun HomeNavHost( BitnagilFloatingActionMenu( actions = listOf( - FloatingActionItem( - icon = R.drawable.ic_report, - text = "제보하기", - onClick = { GlobalBitnagilToast.showWarning("제보하기 기능은 추후 제공될 예정입니다.") }, - ), FloatingActionItem( icon = R.drawable.ic_routine_add, text = "루틴 등록", diff --git a/core/designsystem/src/main/java/com/threegap/bitnagil/designsystem/color/BitnagilColors.kt b/core/designsystem/src/main/java/com/threegap/bitnagil/designsystem/color/BitnagilColors.kt index 0148a48b..c295efca 100644 --- a/core/designsystem/src/main/java/com/threegap/bitnagil/designsystem/color/BitnagilColors.kt +++ b/core/designsystem/src/main/java/com/threegap/bitnagil/designsystem/color/BitnagilColors.kt @@ -15,6 +15,7 @@ data class BitnagilColors( val purple10: Color = Purple10, val green10: Color = Green10, val pink10: Color = Pink10, + val yellow10: Color = Yellow10, val progressBarGradientStartColor: Color = ProgressBarGradientStartColor, val progressBarGradientEndColor: Color = ProgressBarGradientEndColor, val homeGradientStartColor: Color = HomeGradientStartColor, diff --git a/core/designsystem/src/main/java/com/threegap/bitnagil/designsystem/color/Color.kt b/core/designsystem/src/main/java/com/threegap/bitnagil/designsystem/color/Color.kt index 22dde055..851d91b3 100644 --- a/core/designsystem/src/main/java/com/threegap/bitnagil/designsystem/color/Color.kt +++ b/core/designsystem/src/main/java/com/threegap/bitnagil/designsystem/color/Color.kt @@ -11,6 +11,7 @@ val SkyBlue10 = Color(0xFFDBF1FF) val Purple10 = Color(0xFFE6E2FF) val Green10 = Color(0xFFE6F5C6) val Pink10 = Color(0xFFFEE3E9) +val Yellow10 = Color(0xFFFFF5C7) val ProgressBarGradientStartColor = Color(0xFFA9CFFF) val ProgressBarGradientEndColor = Color(0xFFFFCDB3) val HomeGradientStartColor = Color(0xFFFFEADF) diff --git a/core/designsystem/src/main/java/com/threegap/bitnagil/designsystem/component/atom/BitnagilFloatingButton.kt b/core/designsystem/src/main/java/com/threegap/bitnagil/designsystem/component/atom/BitnagilFloatingButton.kt index 98e94cd1..ed883b23 100644 --- a/core/designsystem/src/main/java/com/threegap/bitnagil/designsystem/component/atom/BitnagilFloatingButton.kt +++ b/core/designsystem/src/main/java/com/threegap/bitnagil/designsystem/component/atom/BitnagilFloatingButton.kt @@ -10,7 +10,6 @@ import androidx.compose.animation.scaleIn import androidx.compose.animation.scaleOut import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutVertically -import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -22,13 +21,12 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.scale -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.threegap.bitnagil.designsystem.BitnagilTheme @@ -40,22 +38,22 @@ fun BitnagilFloatingButton( @DrawableRes id: Int, onClick: () -> Unit, modifier: Modifier = Modifier, + isActive: Boolean = false, + colors: BitnagilFloatingButtonColor = BitnagilFloatingButtonColor.default(), ) { Box( contentAlignment = Alignment.Center, modifier = modifier .background( - color = BitnagilTheme.colors.navy500, + color = if (isActive) colors.activeIconBackgroundColor else colors.defaultIconBackgroundColor, shape = CircleShape, ) .size(52.dp) .clickableWithoutRipple { onClick() }, ) { - Image( - imageVector = ImageVector.vectorResource(id), - contentDescription = null, - colorFilter = ColorFilter.tint(BitnagilTheme.colors.white), - modifier = Modifier.size(24.dp), + BitnagilIcon( + id = id, + tint = if (isActive) colors.activeIconColor else colors.defaultIconColor, ) } } @@ -66,8 +64,9 @@ fun BitnagilFloatingActionMenu( isExpanded: Boolean, onToggle: (Boolean) -> Unit, modifier: Modifier = Modifier, - @DrawableRes defaultIcon: Int = R.drawable.ic_plus, + @DrawableRes defaultIcon: Int = R.drawable.ic_add, @DrawableRes activeIcon: Int = R.drawable.ic_close, + colors: BitnagilFloatingButtonColor = BitnagilFloatingButtonColor.default(), ) { Box(modifier = modifier) { AnimatedVisibility( @@ -102,7 +101,7 @@ fun BitnagilFloatingActionMenu( ), ) { Column( - modifier = Modifier.padding(vertical = 16.dp, horizontal = 22.dp), + modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(24.dp), ) { actions.forEach { action -> @@ -125,6 +124,8 @@ fun BitnagilFloatingActionMenu( BitnagilFloatingButton( id = if (isExpanded) activeIcon else defaultIcon, onClick = { onToggle(!isExpanded) }, + isActive = isExpanded, + colors = colors, ) } } @@ -136,6 +137,24 @@ data class FloatingActionItem( val onClick: () -> Unit, ) +@Immutable +data class BitnagilFloatingButtonColor( + val defaultIconColor: Color, + val defaultIconBackgroundColor: Color, + val activeIconColor: Color, + val activeIconBackgroundColor: Color, +) { + companion object { + @Composable + fun default() = BitnagilFloatingButtonColor( + defaultIconColor = BitnagilTheme.colors.white, + defaultIconBackgroundColor = BitnagilTheme.colors.orange500, + activeIconColor = BitnagilTheme.colors.coolGray30, + activeIconBackgroundColor = BitnagilTheme.colors.white, + ) + } +} + @Composable private fun FloatingActionMenuItem( @DrawableRes icon: Int, @@ -159,12 +178,13 @@ private fun FloatingActionMenuItem( BitnagilIcon( id = icon, tint = null, + modifier = Modifier.size(24.dp), ) Text( text = text, - style = BitnagilTheme.typography.subtitle1Medium, - color = BitnagilTheme.colors.navy500, + style = BitnagilTheme.typography.body2Medium, + color = BitnagilTheme.colors.coolGray30, ) } } @@ -174,17 +194,12 @@ private fun FloatingActionMenuItem( private fun BitnagilFloatingButtonPreview() { Column { BitnagilFloatingButton( - id = R.drawable.ic_plus, + id = R.drawable.ic_add, onClick = {}, ) BitnagilFloatingActionMenu( actions = listOf( - FloatingActionItem( - icon = R.drawable.ic_report, - text = "제보하기", - onClick = {}, - ), FloatingActionItem( icon = R.drawable.ic_routine_add, text = "루틴 등록", diff --git a/core/designsystem/src/main/java/com/threegap/bitnagil/designsystem/component/atom/BitnagilSelectButton.kt b/core/designsystem/src/main/java/com/threegap/bitnagil/designsystem/component/atom/BitnagilSelectButton.kt index dabf3fad..8d607125 100644 --- a/core/designsystem/src/main/java/com/threegap/bitnagil/designsystem/component/atom/BitnagilSelectButton.kt +++ b/core/designsystem/src/main/java/com/threegap/bitnagil/designsystem/component/atom/BitnagilSelectButton.kt @@ -1,44 +1,57 @@ package com.threegap.bitnagil.designsystem.component.atom +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween 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.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.role import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.threegap.bitnagil.designsystem.BitnagilTheme +import com.threegap.bitnagil.designsystem.R @Composable fun BitnagilSelectButton( title: String, onClick: (() -> Unit)?, modifier: Modifier = Modifier, + titleTextStyle: TextStyle = BitnagilTheme.typography.subtitle1SemiBold, description: String? = null, selected: Boolean = false, colors: BitnagilSelectButtonColor = BitnagilSelectButtonColor.default(), shape: Shape = RoundedCornerShape(12.dp), ) { - val interactionSource = remember { MutableInteractionSource() } + val interactionSource = remember(onClick) { MutableInteractionSource() } val isPressed by interactionSource.collectIsPressedAsState() + val iconAlpha by animateFloatAsState( + targetValue = if (selected) 1f else 0f, + animationSpec = tween(200), + label = "iconAlpha", + ) val backgroundColor = when { isPressed -> colors.pressedBackgroundColor @@ -52,7 +65,7 @@ fun BitnagilSelectButton( else -> colors.defaultContentColor } - Column( + Row( modifier = modifier .fillMaxWidth() .clip(shape) @@ -68,30 +81,38 @@ fun BitnagilSelectButton( it } } - .padding(horizontal = 20.dp, vertical = 16.dp) + .padding(horizontal = 20.dp, vertical = 14.dp) .semantics { role = Role.Button }, - verticalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, ) { - Text( - text = title, - color = contentColor, - style = if (description == null) { - BitnagilTheme.typography.body1Regular - } else { - BitnagilTheme.typography.subtitle1SemiBold - }, - ) - - description?.let { - Spacer(modifier = Modifier.height(4.dp)) + Column( + modifier = Modifier.weight(1f), + ) { Text( - text = description, + text = title, color = contentColor, - style = BitnagilTheme.typography.body2Regular, + style = titleTextStyle, ) + + description?.let { + Spacer(modifier = Modifier.height(2.dp)) + Text( + text = description, + color = contentColor, + style = BitnagilTheme.typography.body2Medium, + ) + } } + + BitnagilIcon( + id = R.drawable.ic_check_circle_orange, + tint = null, + modifier = Modifier + .size(28.dp) + .alpha(iconAlpha), + ) } } @@ -107,12 +128,22 @@ data class BitnagilSelectButtonColor( companion object { @Composable fun default(): BitnagilSelectButtonColor = BitnagilSelectButtonColor( - defaultBackgroundColor = BitnagilTheme.colors.white, - selectedBackgroundColor = BitnagilTheme.colors.lightBlue200, - pressedBackgroundColor = BitnagilTheme.colors.lightBlue200, + defaultBackgroundColor = BitnagilTheme.colors.coolGray99, + selectedBackgroundColor = BitnagilTheme.colors.orange50, + pressedBackgroundColor = BitnagilTheme.colors.orange50, defaultContentColor = BitnagilTheme.colors.coolGray50, - selectedContentColor = BitnagilTheme.colors.navy500, - pressedContentColor = BitnagilTheme.colors.navy500, + selectedContentColor = BitnagilTheme.colors.orange500, + pressedContentColor = BitnagilTheme.colors.orange500, + ) + + @Composable + fun withdrawal(): BitnagilSelectButtonColor = BitnagilSelectButtonColor( + defaultBackgroundColor = BitnagilTheme.colors.coolGray99, + selectedBackgroundColor = BitnagilTheme.colors.orange50, + pressedBackgroundColor = BitnagilTheme.colors.orange50, + defaultContentColor = BitnagilTheme.colors.coolGray80, + selectedContentColor = BitnagilTheme.colors.orange500, + pressedContentColor = BitnagilTheme.colors.orange500, ) } } @@ -128,9 +159,26 @@ private fun Preview() { Spacer(modifier = Modifier.height(12.dp)) + BitnagilSelectButton( + title = "루틴명", + selected = true, + onClick = {}, + ) + + Spacer(modifier = Modifier.height(12.dp)) + + BitnagilSelectButton( + title = "루틴명", + description = "세부 루틴 한 줄 설명", + onClick = {}, + ) + + Spacer(modifier = Modifier.height(12.dp)) + BitnagilSelectButton( title = "루틴명", description = "세부 루틴 한 줄 설명", + selected = true, onClick = {}, ) } diff --git a/core/designsystem/src/main/java/com/threegap/bitnagil/designsystem/component/atom/BitnagilTextButton.kt b/core/designsystem/src/main/java/com/threegap/bitnagil/designsystem/component/atom/BitnagilTextButton.kt index f4096627..cfcd4bc2 100644 --- a/core/designsystem/src/main/java/com/threegap/bitnagil/designsystem/component/atom/BitnagilTextButton.kt +++ b/core/designsystem/src/main/java/com/threegap/bitnagil/designsystem/component/atom/BitnagilTextButton.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsPressedAsState import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -40,6 +41,7 @@ fun BitnagilTextButton( shape: Shape = RoundedCornerShape(12.dp), textStyle: TextStyle = BitnagilTheme.typography.body1SemiBold, textDecoration: TextDecoration? = null, + textPadding: PaddingValues = PaddingValues(0.dp), ) { val interactionSource = remember { MutableInteractionSource() } val isPressed by interactionSource.collectIsPressedAsState() @@ -77,6 +79,7 @@ fun BitnagilTextButton( color = textColor, style = textStyle, textDecoration = textDecoration, + modifier = Modifier.padding(textPadding), ) } } @@ -110,6 +113,26 @@ data class BitnagilTextButtonColor( pressedTextColor = BitnagilTheme.colors.navy500, disabledTextColor = BitnagilTheme.colors.navy500, ) + + @Composable + fun delete(): BitnagilTextButtonColor = BitnagilTextButtonColor( + defaultBackgroundColor = BitnagilTheme.colors.error10, + pressedBackgroundColor = BitnagilTheme.colors.error10, + disabledBackgroundColor = BitnagilTheme.colors.error10, + defaultTextColor = BitnagilTheme.colors.white, + pressedTextColor = BitnagilTheme.colors.white, + disabledTextColor = BitnagilTheme.colors.white, + ) + + @Composable + fun cancel(): BitnagilTextButtonColor = BitnagilTextButtonColor( + defaultBackgroundColor = BitnagilTheme.colors.coolGray97, + pressedBackgroundColor = BitnagilTheme.colors.coolGray97, + disabledBackgroundColor = BitnagilTheme.colors.coolGray97, + defaultTextColor = BitnagilTheme.colors.coolGray40, + pressedTextColor = BitnagilTheme.colors.coolGray40, + disabledTextColor = BitnagilTheme.colors.coolGray40, + ) } } diff --git a/core/designsystem/src/main/java/com/threegap/bitnagil/designsystem/component/block/BitnagilOptionButton.kt b/core/designsystem/src/main/java/com/threegap/bitnagil/designsystem/component/block/BitnagilOptionButton.kt index 5c8aed22..c21409ed 100644 --- a/core/designsystem/src/main/java/com/threegap/bitnagil/designsystem/component/block/BitnagilOptionButton.kt +++ b/core/designsystem/src/main/java/com/threegap/bitnagil/designsystem/component/block/BitnagilOptionButton.kt @@ -31,14 +31,14 @@ fun BitnagilOptionButton( ) { Text( text = title, - color = BitnagilTheme.colors.black, - style = BitnagilTheme.typography.body1Regular, + color = BitnagilTheme.colors.coolGray10, + style = BitnagilTheme.typography.body1Medium, modifier = Modifier.weight(1f), ) BitnagilIcon( - id = R.drawable.ic_right_arrow_20, - tint = BitnagilTheme.colors.black, + id = R.drawable.ic_chevron_right_md, + tint = BitnagilTheme.colors.coolGray10, modifier = Modifier.padding(10.dp), ) } diff --git a/core/designsystem/src/main/java/com/threegap/bitnagil/designsystem/typography/Type.kt b/core/designsystem/src/main/java/com/threegap/bitnagil/designsystem/typography/Type.kt index 54b1b58a..fb304e13 100644 --- a/core/designsystem/src/main/java/com/threegap/bitnagil/designsystem/typography/Type.kt +++ b/core/designsystem/src/main/java/com/threegap/bitnagil/designsystem/typography/Type.kt @@ -97,8 +97,8 @@ class BitnagilTypography internal constructor( private val _cafe24SsurroundAir: BitnagilTextStyle = BitnagilTextStyle( fontFamily = cafe24SsurroundAir, fontWeight = FontWeight.Light, - fontSize = 24, - lineHeight = 36, + fontSize = 20, + lineHeight = 30, letterSpacing = (-0.5f), ), ) { diff --git a/core/designsystem/src/main/res/drawable-hdpi/default_emotion.png b/core/designsystem/src/main/res/drawable-hdpi/default_emotion.png new file mode 100644 index 00000000..113a4534 Binary files /dev/null and b/core/designsystem/src/main/res/drawable-hdpi/default_emotion.png differ diff --git a/core/designsystem/src/main/res/drawable-mdpi/default_emotion.png b/core/designsystem/src/main/res/drawable-mdpi/default_emotion.png new file mode 100644 index 00000000..2d18fc03 Binary files /dev/null and b/core/designsystem/src/main/res/drawable-mdpi/default_emotion.png differ diff --git a/core/designsystem/src/main/res/drawable-xhdpi/default_emotion.png b/core/designsystem/src/main/res/drawable-xhdpi/default_emotion.png new file mode 100644 index 00000000..92bb2867 Binary files /dev/null and b/core/designsystem/src/main/res/drawable-xhdpi/default_emotion.png differ diff --git a/core/designsystem/src/main/res/drawable-xxhdpi/default_emotion.png b/core/designsystem/src/main/res/drawable-xxhdpi/default_emotion.png new file mode 100644 index 00000000..608dddc8 Binary files /dev/null and b/core/designsystem/src/main/res/drawable-xxhdpi/default_emotion.png differ diff --git a/core/designsystem/src/main/res/drawable-xxxhdpi/default_emotion.png b/core/designsystem/src/main/res/drawable-xxxhdpi/default_emotion.png new file mode 100644 index 00000000..6715cd73 Binary files /dev/null and b/core/designsystem/src/main/res/drawable-xxxhdpi/default_emotion.png differ diff --git a/core/designsystem/src/main/res/drawable/ic_add.xml b/core/designsystem/src/main/res/drawable/ic_add.xml new file mode 100644 index 00000000..52852298 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_add.xml @@ -0,0 +1,13 @@ + + + diff --git a/core/designsystem/src/main/res/drawable/ic_arrow_down_up.xml b/core/designsystem/src/main/res/drawable/ic_arrow_down_up.xml deleted file mode 100644 index ee960d07..00000000 --- a/core/designsystem/src/main/res/drawable/ic_arrow_down_up.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - diff --git a/core/designsystem/src/main/res/drawable/ic_check_circle_orange.xml b/core/designsystem/src/main/res/drawable/ic_check_circle_orange.xml new file mode 100644 index 00000000..93997a5f --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_check_circle_orange.xml @@ -0,0 +1,23 @@ + + + + + diff --git a/core/designsystem/src/main/res/drawable/ic_close.xml b/core/designsystem/src/main/res/drawable/ic_close.xml index 1daad874..c086c467 100644 --- a/core/designsystem/src/main/res/drawable/ic_close.xml +++ b/core/designsystem/src/main/res/drawable/ic_close.xml @@ -1,18 +1,16 @@ - - + android:width="20dp" + android:height="20dp" + android:viewportWidth="20" + android:viewportHeight="20"> + + + + diff --git a/core/designsystem/src/main/res/drawable/ic_connect.xml b/core/designsystem/src/main/res/drawable/ic_connect.xml new file mode 100644 index 00000000..a2f79946 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_connect.xml @@ -0,0 +1,50 @@ + + + + + + + + + + diff --git a/core/designsystem/src/main/res/drawable/ic_edit.xml b/core/designsystem/src/main/res/drawable/ic_edit.xml index eefe8caf..d5fa64ee 100644 --- a/core/designsystem/src/main/res/drawable/ic_edit.xml +++ b/core/designsystem/src/main/res/drawable/ic_edit.xml @@ -1,13 +1,23 @@ + android:width="20dp" + android:height="20dp" + android:viewportWidth="20" + android:viewportHeight="20"> + + diff --git a/core/designsystem/src/main/res/drawable/ic_grow.xml b/core/designsystem/src/main/res/drawable/ic_grow.xml new file mode 100644 index 00000000..b4883ceb --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_grow.xml @@ -0,0 +1,23 @@ + + + + + diff --git a/core/designsystem/src/main/res/drawable/ic_modal_warning.xml b/core/designsystem/src/main/res/drawable/ic_modal_warning.xml deleted file mode 100644 index 38d03a9a..00000000 --- a/core/designsystem/src/main/res/drawable/ic_modal_warning.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - diff --git a/core/designsystem/src/main/res/drawable/ic_outside.xml b/core/designsystem/src/main/res/drawable/ic_outside.xml new file mode 100644 index 00000000..8ca93e5d --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_outside.xml @@ -0,0 +1,33 @@ + + + + + + + diff --git a/core/designsystem/src/main/res/drawable/ic_rest.xml b/core/designsystem/src/main/res/drawable/ic_rest.xml new file mode 100644 index 00000000..0c0b67a8 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_rest.xml @@ -0,0 +1,30 @@ + + + + + + diff --git a/core/designsystem/src/main/res/drawable/ic_setting.xml b/core/designsystem/src/main/res/drawable/ic_setting.xml index 4eaef82b..295bd7c1 100644 --- a/core/designsystem/src/main/res/drawable/ic_setting.xml +++ b/core/designsystem/src/main/res/drawable/ic_setting.xml @@ -3,18 +3,24 @@ android:height="24dp" android:viewportWidth="24" android:viewportHeight="24"> + + diff --git a/core/designsystem/src/main/res/drawable/ic_shine.xml b/core/designsystem/src/main/res/drawable/ic_shine.xml new file mode 100644 index 00000000..285ed0e0 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_shine.xml @@ -0,0 +1,15 @@ + + + + diff --git a/core/designsystem/src/main/res/drawable/ic_trash.xml b/core/designsystem/src/main/res/drawable/ic_trash.xml index 096bb18f..c9343c00 100644 --- a/core/designsystem/src/main/res/drawable/ic_trash.xml +++ b/core/designsystem/src/main/res/drawable/ic_trash.xml @@ -1,21 +1,51 @@ + android:width="20dp" + android:height="20dp" + android:viewportWidth="20" + android:viewportHeight="20"> + + + + + diff --git a/core/designsystem/src/main/res/drawable/ic_wakeup.xml b/core/designsystem/src/main/res/drawable/ic_wakeup.xml new file mode 100644 index 00000000..225d36e0 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/ic_wakeup.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + diff --git a/data/src/main/java/com/threegap/bitnagil/data/emotion/datasource/EmotionDataSource.kt b/data/src/main/java/com/threegap/bitnagil/data/emotion/datasource/EmotionDataSource.kt index 5e21227d..634b984c 100644 --- a/data/src/main/java/com/threegap/bitnagil/data/emotion/datasource/EmotionDataSource.kt +++ b/data/src/main/java/com/threegap/bitnagil/data/emotion/datasource/EmotionDataSource.kt @@ -1,11 +1,11 @@ package com.threegap.bitnagil.data.emotion.datasource import com.threegap.bitnagil.data.emotion.model.dto.EmotionDto -import com.threegap.bitnagil.data.emotion.model.response.GetEmotionResponse import com.threegap.bitnagil.data.emotion.model.response.RegisterEmotionResponse +import com.threegap.bitnagil.data.emotion.model.response.TodayEmotionResponseDto interface EmotionDataSource { suspend fun getEmotions(): Result> suspend fun registerEmotion(emotion: String): Result - suspend fun getEmotionMarble(currentDate: String): Result + suspend fun fetchTodayEmotion(currentDate: String): Result } diff --git a/data/src/main/java/com/threegap/bitnagil/data/emotion/datasourceImpl/EmotionDataSourceImpl.kt b/data/src/main/java/com/threegap/bitnagil/data/emotion/datasourceImpl/EmotionDataSourceImpl.kt index b8893258..cb49e964 100644 --- a/data/src/main/java/com/threegap/bitnagil/data/emotion/datasourceImpl/EmotionDataSourceImpl.kt +++ b/data/src/main/java/com/threegap/bitnagil/data/emotion/datasourceImpl/EmotionDataSourceImpl.kt @@ -4,8 +4,8 @@ import com.threegap.bitnagil.data.common.safeApiCall import com.threegap.bitnagil.data.emotion.datasource.EmotionDataSource import com.threegap.bitnagil.data.emotion.model.dto.EmotionDto import com.threegap.bitnagil.data.emotion.model.request.RegisterEmotionRequest -import com.threegap.bitnagil.data.emotion.model.response.GetEmotionResponse import com.threegap.bitnagil.data.emotion.model.response.RegisterEmotionResponse +import com.threegap.bitnagil.data.emotion.model.response.TodayEmotionResponseDto import com.threegap.bitnagil.data.emotion.service.EmotionService import javax.inject.Inject @@ -25,8 +25,8 @@ class EmotionDataSourceImpl @Inject constructor( } } - override suspend fun getEmotionMarble(currentDate: String): Result = + override suspend fun fetchTodayEmotion(currentDate: String): Result = safeApiCall { - emotionService.getEmotionMarble(currentDate) + emotionService.fetchTodayEmotion(currentDate) } } diff --git a/data/src/main/java/com/threegap/bitnagil/data/emotion/model/response/GetEmotionResponse.kt b/data/src/main/java/com/threegap/bitnagil/data/emotion/model/response/TodayEmotionResponseDto.kt similarity index 52% rename from data/src/main/java/com/threegap/bitnagil/data/emotion/model/response/GetEmotionResponse.kt rename to data/src/main/java/com/threegap/bitnagil/data/emotion/model/response/TodayEmotionResponseDto.kt index 01e75c86..07d33a83 100644 --- a/data/src/main/java/com/threegap/bitnagil/data/emotion/model/response/GetEmotionResponse.kt +++ b/data/src/main/java/com/threegap/bitnagil/data/emotion/model/response/TodayEmotionResponseDto.kt @@ -1,25 +1,28 @@ package com.threegap.bitnagil.data.emotion.model.response -import com.threegap.bitnagil.domain.emotion.model.Emotion +import com.threegap.bitnagil.domain.emotion.model.TodayEmotion import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable -data class GetEmotionResponse( +data class TodayEmotionResponseDto( @SerialName("emotionMarbleType") val emotionMarbleType: String?, @SerialName("emotionMarbleName") val emotionMarbleName: String?, @SerialName("imageUrl") val imageUrl: String?, + @SerialName("emotionMarbleHomeMessage") + val emotionMarbleHomeMessage: String?, ) -fun GetEmotionResponse.toDomain(): Emotion? { - return if (emotionMarbleType != null && emotionMarbleName != null && imageUrl != null) { - Emotion( - emotionType = emotionMarbleType, - emotionMarbleName = emotionMarbleName, +fun TodayEmotionResponseDto.toDomain(): TodayEmotion? { + return if (emotionMarbleType != null && emotionMarbleName != null && imageUrl != null && emotionMarbleHomeMessage != null) { + TodayEmotion( + type = emotionMarbleType, + name = emotionMarbleName, imageUrl = imageUrl, + homeMessage = emotionMarbleHomeMessage, ) } else { null diff --git a/data/src/main/java/com/threegap/bitnagil/data/emotion/repositoryImpl/EmotionRepositoryImpl.kt b/data/src/main/java/com/threegap/bitnagil/data/emotion/repositoryImpl/EmotionRepositoryImpl.kt index b5748332..f2f69abd 100644 --- a/data/src/main/java/com/threegap/bitnagil/data/emotion/repositoryImpl/EmotionRepositoryImpl.kt +++ b/data/src/main/java/com/threegap/bitnagil/data/emotion/repositoryImpl/EmotionRepositoryImpl.kt @@ -5,6 +5,7 @@ import com.threegap.bitnagil.data.emotion.model.response.toDomain import com.threegap.bitnagil.domain.emotion.model.Emotion import com.threegap.bitnagil.domain.emotion.model.EmotionChangeEvent import com.threegap.bitnagil.domain.emotion.model.EmotionRecommendRoutine +import com.threegap.bitnagil.domain.emotion.model.TodayEmotion import com.threegap.bitnagil.domain.emotion.repository.EmotionRepository import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow @@ -33,8 +34,8 @@ class EmotionRepositoryImpl @Inject constructor( } } - override suspend fun getEmotionMarble(currentDate: String): Result = - emotionDataSource.getEmotionMarble(currentDate).map { it.toDomain() } + override suspend fun fetchTodayEmotion(currentDate: String): Result = + emotionDataSource.fetchTodayEmotion(currentDate).map { it.toDomain() } private val _emotionChangeEventFlow = MutableSharedFlow() override suspend fun getEmotionChangeEventFlow(): Flow = _emotionChangeEventFlow.asSharedFlow() diff --git a/data/src/main/java/com/threegap/bitnagil/data/emotion/service/EmotionService.kt b/data/src/main/java/com/threegap/bitnagil/data/emotion/service/EmotionService.kt index 0752f695..15abfeac 100644 --- a/data/src/main/java/com/threegap/bitnagil/data/emotion/service/EmotionService.kt +++ b/data/src/main/java/com/threegap/bitnagil/data/emotion/service/EmotionService.kt @@ -2,8 +2,8 @@ package com.threegap.bitnagil.data.emotion.service import com.threegap.bitnagil.data.emotion.model.dto.EmotionDto import com.threegap.bitnagil.data.emotion.model.request.RegisterEmotionRequest -import com.threegap.bitnagil.data.emotion.model.response.GetEmotionResponse import com.threegap.bitnagil.data.emotion.model.response.RegisterEmotionResponse +import com.threegap.bitnagil.data.emotion.model.response.TodayEmotionResponseDto import com.threegap.bitnagil.network.model.BaseResponse import retrofit2.http.Body import retrofit2.http.GET @@ -19,8 +19,8 @@ interface EmotionService { @Body request: RegisterEmotionRequest, ): BaseResponse - @GET("/api/v1/emotion-marbles/{searchDate}") - suspend fun getEmotionMarble( + @GET("/api/v2/emotion-marbles/{searchDate}") + suspend fun fetchTodayEmotion( @Path("searchDate") date: String, - ): BaseResponse + ): BaseResponse } diff --git a/data/src/main/java/com/threegap/bitnagil/data/routine/mapper/RoutineMapper.kt b/data/src/main/java/com/threegap/bitnagil/data/routine/mapper/RoutineMapper.kt deleted file mode 100644 index ee109951..00000000 --- a/data/src/main/java/com/threegap/bitnagil/data/routine/mapper/RoutineMapper.kt +++ /dev/null @@ -1,63 +0,0 @@ -package com.threegap.bitnagil.data.routine.mapper - -import com.threegap.bitnagil.data.routine.model.request.RoutineCompletionInfoDto -import com.threegap.bitnagil.data.routine.model.request.RoutineCompletionRequestDto -import com.threegap.bitnagil.data.routine.model.response.RoutineDto -import com.threegap.bitnagil.data.routine.model.response.RoutinesResponseDto -import com.threegap.bitnagil.data.routine.model.response.SubRoutineDto -import com.threegap.bitnagil.domain.routine.model.DayOfWeek -import com.threegap.bitnagil.domain.routine.model.Routine -import com.threegap.bitnagil.domain.routine.model.RoutineCompletion -import com.threegap.bitnagil.domain.routine.model.RoutineCompletionInfo -import com.threegap.bitnagil.domain.routine.model.RoutineType -import com.threegap.bitnagil.domain.routine.model.Routines -import com.threegap.bitnagil.domain.routine.model.SubRoutine - -// toDomain -internal fun RoutinesResponseDto.toDomain() = - Routines( - routinesByDate = this.routines.mapValues { (_, routineDto) -> - routineDto.map { it.toDomain() } - }, - ) - -internal fun RoutineDto.toDomain() = - Routine( - routineId = this.routineId, - historySeq = this.historySeq, - routineName = this.routineName, - executionTime = this.executionTime, - subRoutines = this.subRoutines.sortedBy { it.sortOrder }.map { it.toDomain() }, - isModified = this.isModified, - routineCompletionId = this.routineCompletionId, - isCompleted = this.isCompleted, - repeatDay = this.repeatDay.map { DayOfWeek.fromString(it) }, - routineType = RoutineType.fromString(this.routineType), - ) - -internal fun SubRoutineDto.toDomain() = - SubRoutine( - subRoutineId = this.subRoutineId, - historySeq = this.historySeq, - subRoutineName = this.subRoutineName, - isModified = this.isModified, - sortOrder = this.sortOrder, - routineCompletionId = this.routineCompletionId, - isCompleted = this.isCompleted, - routineType = RoutineType.fromString(this.routineType), - ) - -// toDto -internal fun RoutineCompletion.toDto() = - RoutineCompletionRequestDto( - performedDate = this.performedDate, - routineCompletions = this.routineCompletions.map { it.toDto() }, - ) - -internal fun RoutineCompletionInfo.toDto() = - RoutineCompletionInfoDto( - routineType = this.routineType.name, - routineId = this.routineId, - historySeq = this.historySeq, - isCompleted = this.isCompleted, - ) diff --git a/data/src/main/java/com/threegap/bitnagil/data/routine/model/request/RoutineCompletionInfoDto.kt b/data/src/main/java/com/threegap/bitnagil/data/routine/model/request/RoutineCompletionInfoDto.kt index 93f7c608..06427e87 100644 --- a/data/src/main/java/com/threegap/bitnagil/data/routine/model/request/RoutineCompletionInfoDto.kt +++ b/data/src/main/java/com/threegap/bitnagil/data/routine/model/request/RoutineCompletionInfoDto.kt @@ -1,16 +1,22 @@ package com.threegap.bitnagil.data.routine.model.request +import com.threegap.bitnagil.domain.routine.model.RoutineCompletionInfo import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable data class RoutineCompletionInfoDto( - @SerialName("routineType") - val routineType: String, @SerialName("routineId") val routineId: String, - @SerialName("historySeq") - val historySeq: Int, - @SerialName("completeYn") - val isCompleted: Boolean, + @SerialName("routineCompleteYn") + val routineCompleteYn: Boolean, + @SerialName("subRoutineCompleteYn") + val subRoutineCompleteYn: List, ) + +internal fun RoutineCompletionInfo.toDto() = + RoutineCompletionInfoDto( + routineId = this.routineId, + routineCompleteYn = this.routineCompleteYn, + subRoutineCompleteYn = this.subRoutineCompleteYn, + ) diff --git a/data/src/main/java/com/threegap/bitnagil/data/routine/model/request/RoutineCompletionRequestDto.kt b/data/src/main/java/com/threegap/bitnagil/data/routine/model/request/RoutineCompletionRequestDto.kt index bbdabf2f..464e0d93 100644 --- a/data/src/main/java/com/threegap/bitnagil/data/routine/model/request/RoutineCompletionRequestDto.kt +++ b/data/src/main/java/com/threegap/bitnagil/data/routine/model/request/RoutineCompletionRequestDto.kt @@ -1,12 +1,16 @@ package com.threegap.bitnagil.data.routine.model.request +import com.threegap.bitnagil.domain.routine.model.RoutineCompletionInfos import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable data class RoutineCompletionRequestDto( - @SerialName("performedDate") - val performedDate: String, @SerialName("routineCompletionInfos") - val routineCompletions: List, + val routineCompletionInfos: List, ) + +internal fun RoutineCompletionInfos.toDto() = + RoutineCompletionRequestDto( + routineCompletionInfos = this.routineCompletionInfos.map { it.toDto() }, + ) diff --git a/data/src/main/java/com/threegap/bitnagil/data/routine/model/response/DayRoutinesDto.kt b/data/src/main/java/com/threegap/bitnagil/data/routine/model/response/DayRoutinesDto.kt new file mode 100644 index 00000000..6ceff985 --- /dev/null +++ b/data/src/main/java/com/threegap/bitnagil/data/routine/model/response/DayRoutinesDto.kt @@ -0,0 +1,19 @@ +package com.threegap.bitnagil.data.routine.model.response + +import com.threegap.bitnagil.domain.routine.model.DayRoutines +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class DayRoutinesDto( + @SerialName("routineList") + val routineList: List, + @SerialName("allCompleted") + val allCompleted: Boolean, +) + +fun DayRoutinesDto.toDomain(): DayRoutines = + DayRoutines( + routineList = routineList.map { it.toDomain() }, + allCompleted = allCompleted, + ) diff --git a/data/src/main/java/com/threegap/bitnagil/data/routine/model/response/RoutineDto.kt b/data/src/main/java/com/threegap/bitnagil/data/routine/model/response/RoutineDto.kt index 5b93cfff..c7deb42b 100644 --- a/data/src/main/java/com/threegap/bitnagil/data/routine/model/response/RoutineDto.kt +++ b/data/src/main/java/com/threegap/bitnagil/data/routine/model/response/RoutineDto.kt @@ -1,5 +1,8 @@ package com.threegap.bitnagil.data.routine.model.response +import com.threegap.bitnagil.domain.routine.model.DayOfWeek +import com.threegap.bitnagil.domain.routine.model.RecommendedRoutineType +import com.threegap.bitnagil.domain.routine.model.Routine import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -7,22 +10,33 @@ import kotlinx.serialization.Serializable data class RoutineDto( @SerialName("routineId") val routineId: String, - @SerialName("historySeq") - val historySeq: Int, @SerialName("routineName") val routineName: String, @SerialName("repeatDay") val repeatDay: List, @SerialName("executionTime") val executionTime: String, - @SerialName("subRoutineSearchResultDto") - val subRoutines: List, - @SerialName("modifiedYn") - val isModified: Boolean, - @SerialName("routineCompletionId") - val routineCompletionId: Int?, - @SerialName("completeYn") - val isCompleted: Boolean, - @SerialName("routineType") - val routineType: String, + @SerialName("routineDate") + val routineDate: String, + @SerialName("routineCompleteYn") + val routineCompleteYn: Boolean, + @SerialName("subRoutineNames") + val subRoutineNames: List, + @SerialName("subRoutineCompleteYn") + val subRoutineCompleteYn: List, + @SerialName("recommendedRoutineType") + val recommendedRoutineType: String?, ) + +fun RoutineDto.toDomain(): Routine = + Routine( + routineId = this.routineId, + routineName = this.routineName, + repeatDay = this.repeatDay.map { DayOfWeek.fromString(it) }, + executionTime = this.executionTime, + routineDate = this.routineDate, + routineCompleteYn = this.routineCompleteYn, + subRoutineNames = this.subRoutineNames, + subRoutineCompleteYn = this.subRoutineCompleteYn, + recommendedRoutineType = RecommendedRoutineType.fromString(this.recommendedRoutineType), + ) diff --git a/data/src/main/java/com/threegap/bitnagil/data/routine/model/response/RoutinesResponseDto.kt b/data/src/main/java/com/threegap/bitnagil/data/routine/model/response/RoutinesResponseDto.kt index ed354c2a..bbf5f92f 100644 --- a/data/src/main/java/com/threegap/bitnagil/data/routine/model/response/RoutinesResponseDto.kt +++ b/data/src/main/java/com/threegap/bitnagil/data/routine/model/response/RoutinesResponseDto.kt @@ -1,10 +1,18 @@ package com.threegap.bitnagil.data.routine.model.response +import com.threegap.bitnagil.domain.routine.model.Routines import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable data class RoutinesResponseDto( @SerialName("routines") - val routines: Map>, + val routines: Map, ) + +fun RoutinesResponseDto.toDomain() = + Routines( + routines = this.routines.mapValues { (_, dayRoutinesDto) -> + dayRoutinesDto.toDomain() + }, + ) diff --git a/data/src/main/java/com/threegap/bitnagil/data/routine/model/response/SubRoutineDto.kt b/data/src/main/java/com/threegap/bitnagil/data/routine/model/response/SubRoutineDto.kt deleted file mode 100644 index ba573a9b..00000000 --- a/data/src/main/java/com/threegap/bitnagil/data/routine/model/response/SubRoutineDto.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.threegap.bitnagil.data.routine.model.response - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -data class SubRoutineDto( - @SerialName("subRoutineId") - val subRoutineId: String, - @SerialName("historySeq") - val historySeq: Int, - @SerialName("subRoutineName") - val subRoutineName: String, - @SerialName("modifiedYn") - val isModified: Boolean, - @SerialName("sortOrder") - val sortOrder: Int, - @SerialName("routineCompletionId") - val routineCompletionId: Int?, - @SerialName("completeYn") - val isCompleted: Boolean, - @SerialName("routineType") - val routineType: String, -) diff --git a/data/src/main/java/com/threegap/bitnagil/data/routine/repositoryImpl/RoutineRepositoryImpl.kt b/data/src/main/java/com/threegap/bitnagil/data/routine/repositoryImpl/RoutineRepositoryImpl.kt index 0601a78c..480aeb70 100644 --- a/data/src/main/java/com/threegap/bitnagil/data/routine/repositoryImpl/RoutineRepositoryImpl.kt +++ b/data/src/main/java/com/threegap/bitnagil/data/routine/repositoryImpl/RoutineRepositoryImpl.kt @@ -1,12 +1,11 @@ package com.threegap.bitnagil.data.routine.repositoryImpl import com.threegap.bitnagil.data.routine.datasource.RoutineRemoteDataSource -import com.threegap.bitnagil.data.routine.mapper.toDomain -import com.threegap.bitnagil.data.routine.mapper.toDto import com.threegap.bitnagil.data.routine.model.request.toDto +import com.threegap.bitnagil.data.routine.model.response.toDomain import com.threegap.bitnagil.domain.routine.model.Routine import com.threegap.bitnagil.domain.routine.model.RoutineByDayDeletion -import com.threegap.bitnagil.domain.routine.model.RoutineCompletion +import com.threegap.bitnagil.domain.routine.model.RoutineCompletionInfos import com.threegap.bitnagil.domain.routine.model.Routines import com.threegap.bitnagil.domain.routine.repository.RoutineRepository import javax.inject.Inject @@ -18,8 +17,8 @@ class RoutineRepositoryImpl @Inject constructor( routineRemoteDataSource.fetchWeeklyRoutines(startDate, endDate) .map { it.toDomain() } - override suspend fun syncRoutineCompletion(routineCompletion: RoutineCompletion): Result = - routineRemoteDataSource.syncRoutineCompletion(routineCompletion.toDto()) + override suspend fun syncRoutineCompletion(routineCompletionInfos: RoutineCompletionInfos): Result = + routineRemoteDataSource.syncRoutineCompletion(routineCompletionInfos.toDto()) override suspend fun deleteRoutine(routineId: String): Result = routineRemoteDataSource.deleteRoutine(routineId) diff --git a/data/src/main/java/com/threegap/bitnagil/data/routine/service/RoutineService.kt b/data/src/main/java/com/threegap/bitnagil/data/routine/service/RoutineService.kt index 403789eb..322596d1 100644 --- a/data/src/main/java/com/threegap/bitnagil/data/routine/service/RoutineService.kt +++ b/data/src/main/java/com/threegap/bitnagil/data/routine/service/RoutineService.kt @@ -9,18 +9,18 @@ import retrofit2.http.Body import retrofit2.http.DELETE import retrofit2.http.GET import retrofit2.http.HTTP -import retrofit2.http.POST +import retrofit2.http.PUT import retrofit2.http.Path import retrofit2.http.Query interface RoutineService { - @GET("/api/v1/routines") + @GET("/api/v2/routines") suspend fun fetchRoutines( @Query("startDate") startDate: String, @Query("endDate") endDate: String, ): BaseResponse - @POST("/api/v1/routines/completions") + @PUT("/api/v2/routines") suspend fun routineCompletion( @Body request: RoutineCompletionRequestDto, ): BaseResponse diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/emotion/model/TodayEmotion.kt b/domain/src/main/java/com/threegap/bitnagil/domain/emotion/model/TodayEmotion.kt new file mode 100644 index 00000000..bbcddc64 --- /dev/null +++ b/domain/src/main/java/com/threegap/bitnagil/domain/emotion/model/TodayEmotion.kt @@ -0,0 +1,8 @@ +package com.threegap.bitnagil.domain.emotion.model + +data class TodayEmotion( + val type: String, + val name: String, + val imageUrl: String, + val homeMessage: String, +) diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/emotion/repository/EmotionRepository.kt b/domain/src/main/java/com/threegap/bitnagil/domain/emotion/repository/EmotionRepository.kt index 56367614..526e1ec1 100644 --- a/domain/src/main/java/com/threegap/bitnagil/domain/emotion/repository/EmotionRepository.kt +++ b/domain/src/main/java/com/threegap/bitnagil/domain/emotion/repository/EmotionRepository.kt @@ -3,11 +3,12 @@ package com.threegap.bitnagil.domain.emotion.repository import com.threegap.bitnagil.domain.emotion.model.Emotion import com.threegap.bitnagil.domain.emotion.model.EmotionChangeEvent import com.threegap.bitnagil.domain.emotion.model.EmotionRecommendRoutine +import com.threegap.bitnagil.domain.emotion.model.TodayEmotion import kotlinx.coroutines.flow.Flow interface EmotionRepository { suspend fun getEmotions(): Result> suspend fun registerEmotion(emotionMarbleType: String): Result> - suspend fun getEmotionMarble(currentDate: String): Result + suspend fun fetchTodayEmotion(currentDate: String): Result suspend fun getEmotionChangeEventFlow(): Flow } diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/emotion/usecase/FetchTodayEmotionUseCase.kt b/domain/src/main/java/com/threegap/bitnagil/domain/emotion/usecase/FetchTodayEmotionUseCase.kt new file mode 100644 index 00000000..35750da6 --- /dev/null +++ b/domain/src/main/java/com/threegap/bitnagil/domain/emotion/usecase/FetchTodayEmotionUseCase.kt @@ -0,0 +1,12 @@ +package com.threegap.bitnagil.domain.emotion.usecase + +import com.threegap.bitnagil.domain.emotion.model.TodayEmotion +import com.threegap.bitnagil.domain.emotion.repository.EmotionRepository +import javax.inject.Inject + +class FetchTodayEmotionUseCase @Inject constructor( + private val emotionRepository: EmotionRepository, +) { + suspend operator fun invoke(currentDate: String): Result = + emotionRepository.fetchTodayEmotion(currentDate) +} diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/emotion/usecase/GetEmotionUseCase.kt b/domain/src/main/java/com/threegap/bitnagil/domain/emotion/usecase/GetEmotionUseCase.kt deleted file mode 100644 index 16d886d6..00000000 --- a/domain/src/main/java/com/threegap/bitnagil/domain/emotion/usecase/GetEmotionUseCase.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.threegap.bitnagil.domain.emotion.usecase - -import com.threegap.bitnagil.domain.emotion.model.Emotion -import com.threegap.bitnagil.domain.emotion.repository.EmotionRepository -import javax.inject.Inject - -class GetEmotionUseCase @Inject constructor( - private val emotionRepository: EmotionRepository, -) { - suspend operator fun invoke(currentDate: String): Result = - emotionRepository.getEmotionMarble(currentDate) -} diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/routine/model/DayRoutines.kt b/domain/src/main/java/com/threegap/bitnagil/domain/routine/model/DayRoutines.kt new file mode 100644 index 00000000..2fb1abdd --- /dev/null +++ b/domain/src/main/java/com/threegap/bitnagil/domain/routine/model/DayRoutines.kt @@ -0,0 +1,6 @@ +package com.threegap.bitnagil.domain.routine.model + +data class DayRoutines( + val routineList: List, + val allCompleted: Boolean, +) diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/routine/model/RecommendedRoutineType.kt b/domain/src/main/java/com/threegap/bitnagil/domain/routine/model/RecommendedRoutineType.kt new file mode 100644 index 00000000..6672863a --- /dev/null +++ b/domain/src/main/java/com/threegap/bitnagil/domain/routine/model/RecommendedRoutineType.kt @@ -0,0 +1,18 @@ +package com.threegap.bitnagil.domain.routine.model + +enum class RecommendedRoutineType { + PERSONALIZED, + OUTING, + WAKE_UP, + CONNECT, + REST, + GROW, + OUTING_REPORT, + UNKNOWN, + ; + + companion object { + fun fromString(categoryName: String?): RecommendedRoutineType = + RecommendedRoutineType.entries.find { it.name == categoryName } ?: UNKNOWN + } +} diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/routine/model/Routine.kt b/domain/src/main/java/com/threegap/bitnagil/domain/routine/model/Routine.kt index 808ba180..4cc43d29 100644 --- a/domain/src/main/java/com/threegap/bitnagil/domain/routine/model/Routine.kt +++ b/domain/src/main/java/com/threegap/bitnagil/domain/routine/model/Routine.kt @@ -2,16 +2,12 @@ package com.threegap.bitnagil.domain.routine.model data class Routine( val routineId: String, - val historySeq: Int, val routineName: String, val repeatDay: List, val executionTime: String, - val subRoutines: List, - val isModified: Boolean, - val routineCompletionId: Int?, - val isCompleted: Boolean, - val routineType: RoutineType, -) { - fun withSortedSubRoutines(): Routine = - copy(subRoutines = subRoutines.sortedBy { it.sortOrder }) -} + val routineDate: String, + val routineCompleteYn: Boolean, + val subRoutineNames: List, + val subRoutineCompleteYn: List, + val recommendedRoutineType: RecommendedRoutineType?, +) diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/routine/model/RoutineCompletion.kt b/domain/src/main/java/com/threegap/bitnagil/domain/routine/model/RoutineCompletion.kt deleted file mode 100644 index f388aaf9..00000000 --- a/domain/src/main/java/com/threegap/bitnagil/domain/routine/model/RoutineCompletion.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.threegap.bitnagil.domain.routine.model - -data class RoutineCompletion( - val performedDate: String, - val routineCompletions: List, -) diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/routine/model/RoutineCompletionInfo.kt b/domain/src/main/java/com/threegap/bitnagil/domain/routine/model/RoutineCompletionInfo.kt index ca7dfe7c..7280802c 100644 --- a/domain/src/main/java/com/threegap/bitnagil/domain/routine/model/RoutineCompletionInfo.kt +++ b/domain/src/main/java/com/threegap/bitnagil/domain/routine/model/RoutineCompletionInfo.kt @@ -1,8 +1,7 @@ package com.threegap.bitnagil.domain.routine.model data class RoutineCompletionInfo( - val routineType: RoutineType, val routineId: String, - val historySeq: Int, - val isCompleted: Boolean, + val routineCompleteYn: Boolean, + val subRoutineCompleteYn: List, ) diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/routine/model/RoutineCompletionInfos.kt b/domain/src/main/java/com/threegap/bitnagil/domain/routine/model/RoutineCompletionInfos.kt new file mode 100644 index 00000000..3e2697a0 --- /dev/null +++ b/domain/src/main/java/com/threegap/bitnagil/domain/routine/model/RoutineCompletionInfos.kt @@ -0,0 +1,5 @@ +package com.threegap.bitnagil.domain.routine.model + +data class RoutineCompletionInfos( + val routineCompletionInfos: List, +) diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/routine/model/Routines.kt b/domain/src/main/java/com/threegap/bitnagil/domain/routine/model/Routines.kt index 59b86f18..93a895b8 100644 --- a/domain/src/main/java/com/threegap/bitnagil/domain/routine/model/Routines.kt +++ b/domain/src/main/java/com/threegap/bitnagil/domain/routine/model/Routines.kt @@ -1,12 +1,5 @@ package com.threegap.bitnagil.domain.routine.model data class Routines( - val routinesByDate: Map>, -) { - fun withSortedSubRoutines(): Routines = - copy( - routinesByDate = routinesByDate.mapValues { (_, routinesList) -> - routinesList.map { it.withSortedSubRoutines() } - }, - ) -} + val routines: Map, +) diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/routine/model/SubRoutine.kt b/domain/src/main/java/com/threegap/bitnagil/domain/routine/model/SubRoutine.kt deleted file mode 100644 index 3101b38c..00000000 --- a/domain/src/main/java/com/threegap/bitnagil/domain/routine/model/SubRoutine.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.threegap.bitnagil.domain.routine.model - -data class SubRoutine( - val subRoutineId: String, - val historySeq: Int, - val subRoutineName: String, - val isModified: Boolean, - val sortOrder: Int, - val routineCompletionId: Int?, - val isCompleted: Boolean, - val routineType: RoutineType, -) diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/routine/repository/RoutineRepository.kt b/domain/src/main/java/com/threegap/bitnagil/domain/routine/repository/RoutineRepository.kt index bededfb7..8b49174f 100644 --- a/domain/src/main/java/com/threegap/bitnagil/domain/routine/repository/RoutineRepository.kt +++ b/domain/src/main/java/com/threegap/bitnagil/domain/routine/repository/RoutineRepository.kt @@ -2,12 +2,12 @@ package com.threegap.bitnagil.domain.routine.repository import com.threegap.bitnagil.domain.routine.model.Routine import com.threegap.bitnagil.domain.routine.model.RoutineByDayDeletion -import com.threegap.bitnagil.domain.routine.model.RoutineCompletion +import com.threegap.bitnagil.domain.routine.model.RoutineCompletionInfos import com.threegap.bitnagil.domain.routine.model.Routines interface RoutineRepository { suspend fun fetchWeeklyRoutines(startDate: String, endDate: String): Result - suspend fun syncRoutineCompletion(routineCompletion: RoutineCompletion): Result + suspend fun syncRoutineCompletion(routineCompletionInfos: RoutineCompletionInfos): Result suspend fun deleteRoutine(routineId: String): Result suspend fun getRoutine(routineId: String): Result suspend fun deleteRoutineByDay(routineByDayDeletion: RoutineByDayDeletion): Result diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/routine/usecase/FetchWeeklyRoutinesUseCase.kt b/domain/src/main/java/com/threegap/bitnagil/domain/routine/usecase/FetchWeeklyRoutinesUseCase.kt index 8e886f13..0876e3fe 100644 --- a/domain/src/main/java/com/threegap/bitnagil/domain/routine/usecase/FetchWeeklyRoutinesUseCase.kt +++ b/domain/src/main/java/com/threegap/bitnagil/domain/routine/usecase/FetchWeeklyRoutinesUseCase.kt @@ -9,5 +9,4 @@ class FetchWeeklyRoutinesUseCase @Inject constructor( ) { suspend operator fun invoke(startDate: String, endDate: String): Result = routineRepository.fetchWeeklyRoutines(startDate, endDate) - .map { it.withSortedSubRoutines() } } diff --git a/domain/src/main/java/com/threegap/bitnagil/domain/routine/usecase/RoutineCompletionUseCase.kt b/domain/src/main/java/com/threegap/bitnagil/domain/routine/usecase/RoutineCompletionUseCase.kt index 6d213aba..0acc1e02 100644 --- a/domain/src/main/java/com/threegap/bitnagil/domain/routine/usecase/RoutineCompletionUseCase.kt +++ b/domain/src/main/java/com/threegap/bitnagil/domain/routine/usecase/RoutineCompletionUseCase.kt @@ -1,12 +1,12 @@ package com.threegap.bitnagil.domain.routine.usecase -import com.threegap.bitnagil.domain.routine.model.RoutineCompletion +import com.threegap.bitnagil.domain.routine.model.RoutineCompletionInfos import com.threegap.bitnagil.domain.routine.repository.RoutineRepository import javax.inject.Inject class RoutineCompletionUseCase @Inject constructor( private val routineRepository: RoutineRepository, ) { - suspend operator fun invoke(routineCompletion: RoutineCompletion): Result = - routineRepository.syncRoutineCompletion(routineCompletion) + suspend operator fun invoke(routineCompletionInfos: RoutineCompletionInfos): Result = + routineRepository.syncRoutineCompletion(routineCompletionInfos) } 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 dbbb7c1a..6be6a6cb 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 @@ -1,44 +1,39 @@ package com.threegap.bitnagil.presentation.home 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.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.padding import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.Brush import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.compose.ui.zIndex import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.threegap.bitnagil.designsystem.BitnagilTheme -import com.threegap.bitnagil.designsystem.R -import com.threegap.bitnagil.designsystem.component.atom.BitnagilIcon import com.threegap.bitnagil.designsystem.modifier.clickableWithoutRipple import com.threegap.bitnagil.presentation.common.flow.collectAsEffect import com.threegap.bitnagil.presentation.common.toast.GlobalBitnagilToast import com.threegap.bitnagil.presentation.home.component.template.CollapsibleHomeHeader -import com.threegap.bitnagil.presentation.home.component.template.DeleteConfirmDialog -import com.threegap.bitnagil.presentation.home.component.template.RoutineDetailsBottomSheet -import com.threegap.bitnagil.presentation.home.component.template.RoutineEmptyView +import com.threegap.bitnagil.presentation.home.component.template.EmptyRoutineView import com.threegap.bitnagil.presentation.home.component.template.RoutineSection -import com.threegap.bitnagil.presentation.home.component.template.RoutineSortBottomSheet import com.threegap.bitnagil.presentation.home.component.template.WeeklyDatePicker import com.threegap.bitnagil.presentation.home.model.HomeIntent import com.threegap.bitnagil.presentation.home.model.HomeSideEffect import com.threegap.bitnagil.presentation.home.model.HomeState -import com.threegap.bitnagil.presentation.home.model.RoutineUiModel import com.threegap.bitnagil.presentation.home.util.rememberCollapsibleHeaderState import java.time.LocalDate @@ -46,7 +41,6 @@ import java.time.LocalDate fun HomeScreenContainer( viewModel: HomeViewModel = hiltViewModel(), navigateToRegisterRoutine: () -> Unit, - navigateToEditRoutine: (String) -> Unit, navigateToEmotion: () -> Unit, ) { val uiState by viewModel.stateFlow.collectAsStateWithLifecycle() @@ -61,10 +55,6 @@ fun HomeScreenContainer( navigateToEmotion() } - is HomeSideEffect.NavigateToEditRoutine -> { - navigateToEditRoutine(sideEffect.routineId) - } - is HomeSideEffect.ShowToastWithIcon -> { GlobalBitnagilToast.showCheck(sideEffect.message) } @@ -75,52 +65,6 @@ fun HomeScreenContainer( } } - if (uiState.routineSortBottomSheetVisible) { - RoutineSortBottomSheet( - currentSortType = uiState.currentSortType, - onSortTypeChange = { sortType -> - viewModel.sendIntent(HomeIntent.OnSortTypeChange(sortType)) - }, - onDismiss = { - viewModel.sendIntent(HomeIntent.HideRoutineSortBottomSheet) - }, - ) - } - - uiState.selectedRoutine?.let { routine -> - if (uiState.routineDetailsBottomSheetVisible) { - RoutineDetailsBottomSheet( - routine = routine, - onDismiss = { viewModel.sendIntent(HomeIntent.HideRoutineDetailsBottomSheet) }, - onEdit = { viewModel.sendIntent(HomeIntent.NavigateToEditRoutine(routine.routineId)) }, - onDelete = { - if (routine.repeatDay.isEmpty()) { - viewModel.deleteRoutineByDay(routine) - } else { - viewModel.sendIntent(HomeIntent.ShowDeleteConfirmDialog(routine)) - } - }, - ) - } - } - - uiState.deletingRoutine?.let { routine -> - if (uiState.showDeleteConfirmDialog) { - DeleteConfirmDialog( - onDeleteToday = { - viewModel.deleteRoutineByDay(routine) - viewModel.sendIntent(HomeIntent.HideDeleteConfirmDialog) - }, - onDeleteAll = { - viewModel.deleteRoutine(routine.routineId) - }, - onDismiss = { - viewModel.sendIntent(HomeIntent.HideDeleteConfirmDialog) - }, - ) - } - } - HomeScreen( uiState = uiState, onDateSelect = { date -> @@ -135,14 +79,8 @@ fun HomeScreenContainer( onRoutineCompletionToggle = { routineId, isCompleted -> viewModel.toggleRoutineCompletion(routineId, isCompleted) }, - onSubRoutineCompletionToggle = { routineId, subRoutineId, isCompleted -> - viewModel.toggleSubRoutineCompletion(routineId, subRoutineId, isCompleted) - }, - onShowRoutineSortBottomSheet = { - viewModel.sendIntent(HomeIntent.ShowRoutineSortBottomSheet) - }, - onShowRoutineDetailsBottomSheet = { routine -> - viewModel.sendIntent(HomeIntent.ShowRoutineDetailsBottomSheet(routine)) + onSubRoutineCompletionToggle = { routineId, subRoutineIndex, isCompleted -> + viewModel.toggleSubRoutineCompletion(routineId, subRoutineIndex, isCompleted) }, onRegisterRoutineClick = { viewModel.sendIntent(HomeIntent.OnRegisterRoutineClick) @@ -150,6 +88,9 @@ fun HomeScreenContainer( onRegisterEmotionClick = { viewModel.sendIntent(HomeIntent.OnRegisterEmotionClick) }, + onShowMoreRoutinesClick = { + // TODO: 루틴 리스트 화면으로 이동 + }, ) } @@ -160,11 +101,10 @@ private fun HomeScreen( onPreviousWeekClick: () -> Unit, onNextWeekClick: () -> Unit, onRoutineCompletionToggle: (String, Boolean) -> Unit, - onSubRoutineCompletionToggle: (String, String, Boolean) -> Unit, - onShowRoutineSortBottomSheet: () -> Unit, - onShowRoutineDetailsBottomSheet: (RoutineUiModel) -> Unit, + onSubRoutineCompletionToggle: (String, Int, Boolean) -> Unit, onRegisterRoutineClick: () -> Unit, onRegisterEmotionClick: () -> Unit, + onShowMoreRoutinesClick: () -> Unit, modifier: Modifier = Modifier, ) { val collapsibleHeaderState = rememberCollapsibleHeaderState() @@ -172,16 +112,7 @@ private fun HomeScreen( Box( modifier = modifier .fillMaxSize() - .background( - brush = Brush.linearGradient( - colors = listOf( - BitnagilTheme.colors.homeGradientStartColor, - BitnagilTheme.colors.homeGradientEndColor, - ), - start = Offset(0f, 0f), - end = Offset(collapsibleHeaderState.screenHeight.value, collapsibleHeaderState.screenWidth.value * 2), - ), - ), + .background(BitnagilTheme.colors.coolGray10), ) { Column { Spacer(modifier = Modifier.height(collapsibleHeaderState.currentHeaderHeight)) @@ -189,12 +120,13 @@ private fun HomeScreen( WeeklyDatePicker( selectedDate = uiState.selectedDate, weeklyDates = uiState.currentWeeks, + routines = uiState.routines, onDateSelect = onDateSelect, onPreviousWeekClick = onPreviousWeekClick, onNextWeekClick = onNextWeekClick, modifier = Modifier .background( - color = BitnagilTheme.colors.white, + color = BitnagilTheme.colors.coolGray99, shape = RoundedCornerShape( topStart = 20.dp, topEnd = 20.dp, @@ -202,79 +134,71 @@ private fun HomeScreen( ), ) - LazyColumn( - state = collapsibleHeaderState.lazyListState, - modifier = Modifier - .fillMaxSize() - .background(BitnagilTheme.colors.white) - .nestedScroll(collapsibleHeaderState.nestedScrollConnection), - ) { - if (uiState.selectedDateRoutines.isEmpty()) { - item { - RoutineEmptyView( - onRegisterRoutineClick = onRegisterRoutineClick, - modifier = Modifier - .fillMaxSize() - .padding(top = 62.dp), - ) - } - } else { - uiState.selectedDateRoutines.forEachIndexed { index, routine -> - item( - key = "${routine.routineId}_${uiState.selectedDate}", - ) { - Box( - modifier = Modifier.fillMaxWidth(), - ) { - RoutineSection( - routine = routine, - onRoutineToggle = { isCompleted -> - onRoutineCompletionToggle( - routine.routineId, - isCompleted, - ) - }, - onSubRoutineToggle = { subRoutineId, isCompleted -> - onSubRoutineCompletionToggle( - routine.routineId, - subRoutineId, - isCompleted, - ) - }, - onMoreClick = { - onShowRoutineDetailsBottomSheet(routine) - }, - modifier = Modifier - .padding(top = 23.dp, bottom = 10.dp) - .padding(horizontal = 16.dp), - ) + if (uiState.selectedDateRoutines.isEmpty()) { + EmptyRoutineView( + onRegisterRoutineClick = onRegisterRoutineClick, + modifier = Modifier + .fillMaxSize() + .background(BitnagilTheme.colors.coolGray99) + .padding(top = 62.dp), + ) + } else { + Row( + modifier = Modifier + .fillMaxWidth() + .background(BitnagilTheme.colors.coolGray99) + .padding(start = 16.dp, end = 4.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Top, + ) { + Text( + text = "루틴 리스트", + color = BitnagilTheme.colors.coolGray60, + style = BitnagilTheme.typography.body2SemiBold, + modifier = Modifier.padding(top = 6.dp), + ) + Text( + text = "더보기", + color = BitnagilTheme.colors.coolGray10, + style = BitnagilTheme.typography.body2SemiBold, + modifier = Modifier + .clickableWithoutRipple { onShowMoreRoutinesClick() } + .padding(vertical = 10.dp, horizontal = 12.dp), + ) + } - if (index == 0) { - BitnagilIcon( - id = R.drawable.ic_arrow_down_up, - tint = BitnagilTheme.colors.navy200, - modifier = Modifier - .align(Alignment.TopEnd) - .padding(end = 4.dp) - .clickableWithoutRipple { onShowRoutineSortBottomSheet() } - .zIndex(1f), - ) - } - } - } + LazyColumn( + modifier = Modifier + .fillMaxSize() + .background(BitnagilTheme.colors.coolGray99) + .nestedScroll(collapsibleHeaderState.nestedScrollConnection) + .padding(horizontal = 16.dp), + state = collapsibleHeaderState.lazyListState, + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + items( + items = uiState.selectedDateRoutines, + key = { routine -> "${routine.routineId}_${uiState.selectedDate}" }, + ) { routine -> + RoutineSection( + routine = routine, + onRoutineToggle = { isCompleted -> + onRoutineCompletionToggle(routine.routineId, isCompleted) + }, + onSubRoutineToggle = { subRoutineIndex, isCompleted -> + onSubRoutineCompletionToggle(routine.routineId, subRoutineIndex, isCompleted) + }, + ) } } - item { - Spacer(modifier = Modifier.height(110.dp)) - } } } CollapsibleHomeHeader( userName = uiState.userNickname, - emotionBallType = uiState.myEmotion, + todayEmotion = uiState.todayEmotion, collapsibleHeaderState = collapsibleHeaderState, - onEmotionRecordClick = onRegisterEmotionClick, + onRegisterEmotion = onRegisterEmotionClick, ) } } @@ -289,9 +213,8 @@ private fun HomeScreenPreview() { onNextWeekClick = {}, onRoutineCompletionToggle = { _, _ -> }, onSubRoutineCompletionToggle = { _, _, _ -> }, - onShowRoutineSortBottomSheet = {}, - onShowRoutineDetailsBottomSheet = {}, onRegisterRoutineClick = {}, onRegisterEmotionClick = {}, + onShowMoreRoutinesClick = {}, ) } diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/HomeViewModel.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/HomeViewModel.kt index f7c856c9..cf6a796c 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/HomeViewModel.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/HomeViewModel.kt @@ -3,26 +3,21 @@ package com.threegap.bitnagil.presentation.home import android.util.Log import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope +import com.threegap.bitnagil.domain.emotion.usecase.FetchTodayEmotionUseCase import com.threegap.bitnagil.domain.emotion.usecase.GetEmotionChangeEventFlowUseCase -import com.threegap.bitnagil.domain.emotion.usecase.GetEmotionUseCase import com.threegap.bitnagil.domain.onboarding.usecase.GetOnBoardingRecommendRoutineEventFlowUseCase -import com.threegap.bitnagil.domain.routine.model.RoutineCompletion import com.threegap.bitnagil.domain.routine.model.RoutineCompletionInfo -import com.threegap.bitnagil.domain.routine.usecase.DeleteRoutineByDayUseCase -import com.threegap.bitnagil.domain.routine.usecase.DeleteRoutineUseCase +import com.threegap.bitnagil.domain.routine.model.RoutineCompletionInfos import com.threegap.bitnagil.domain.routine.usecase.FetchWeeklyRoutinesUseCase import com.threegap.bitnagil.domain.routine.usecase.RoutineCompletionUseCase import com.threegap.bitnagil.domain.user.usecase.FetchUserProfileUseCase import com.threegap.bitnagil.domain.writeroutine.usecase.GetWriteRoutineEventFlowUseCase import com.threegap.bitnagil.presentation.common.mviviewmodel.MviViewModel -import com.threegap.bitnagil.presentation.home.model.EmotionBallType import com.threegap.bitnagil.presentation.home.model.HomeIntent import com.threegap.bitnagil.presentation.home.model.HomeSideEffect import com.threegap.bitnagil.presentation.home.model.HomeState -import com.threegap.bitnagil.presentation.home.model.RoutineSortType import com.threegap.bitnagil.presentation.home.model.RoutineUiModel import com.threegap.bitnagil.presentation.home.model.RoutinesUiModel -import com.threegap.bitnagil.presentation.home.model.toRoutineByDayDeletion import com.threegap.bitnagil.presentation.home.model.toUiModel import com.threegap.bitnagil.presentation.home.util.getCurrentWeekDays import dagger.hilt.android.lifecycle.HiltViewModel @@ -42,10 +37,8 @@ class HomeViewModel @Inject constructor( savedStateHandle: SavedStateHandle, private val fetchWeeklyRoutinesUseCase: FetchWeeklyRoutinesUseCase, private val fetchUserProfileUseCase: FetchUserProfileUseCase, - private val getEmotionUseCase: GetEmotionUseCase, + private val fetchTodayEmotionUseCase: FetchTodayEmotionUseCase, private val routineCompletionUseCase: RoutineCompletionUseCase, - private val deleteRoutineUseCase: DeleteRoutineUseCase, - private val deleteRoutineByDayUseCase: DeleteRoutineByDayUseCase, private val getWriteRoutineEventFlowUseCase: GetWriteRoutineEventFlowUseCase, private val getEmotionChangeEventFlowUseCase: GetEmotionChangeEventFlowUseCase, private val getOnBoardingRecommendRoutineEventFlowUseCase: GetOnBoardingRecommendRoutineEventFlowUseCase, @@ -65,7 +58,7 @@ class HomeViewModel @Inject constructor( observeRoutineUpdates() fetchWeeklyRoutines(container.stateFlow.value.currentWeeks) fetchUserProfile() - getMyEmotion(container.stateFlow.value.selectedDate) + fetchTodayEmotion(LocalDate.now()) } override suspend fun SimpleSyntax.reduceState( @@ -110,112 +103,15 @@ class HomeViewModel @Inject constructor( } is HomeIntent.OnSubRoutineCompletionToggle -> { - updateSubRoutine(state, intent.routineId, intent.subRoutineId, intent.isCompleted) + updateSubRoutine(state, intent.routineId, intent.subRoutineIndex, intent.isCompleted) } - is HomeIntent.OnSortTypeChange -> { - val newSortType = if (state.currentSortType == intent.sortType) { - RoutineSortType.TIME_ORDER - } else { - intent.sortType - } - state.copy( - currentSortType = newSortType, - ) - } - - is HomeIntent.DeleteRoutineOptimistically -> { - val updatedRoutinesByDate = state.routines.routinesByDate.mapValues { (_, routineList) -> - routineList.filterNot { it.routineId == intent.routineId } - } - - state.copy( - routines = RoutinesUiModel(routinesByDate = updatedRoutinesByDate), - showDeleteConfirmDialog = false, - deletingRoutine = null, - routineDetailsBottomSheetVisible = false, - selectedRoutine = null, - ) - } - - is HomeIntent.RestoreRoutinesAfterDeleteFailure -> { - state.copy(routines = intent.backupRoutines) - } - - is HomeIntent.ConfirmRoutineDeletion -> null - - is HomeIntent.ShowRoutineSortBottomSheet -> { - state.copy(routineSortBottomSheetVisible = true) - } - - is HomeIntent.HideRoutineSortBottomSheet -> { - state.copy(routineSortBottomSheetVisible = false) - } - - is HomeIntent.ShowRoutineDetailsBottomSheet -> { - state.copy( - routineDetailsBottomSheetVisible = true, - selectedRoutine = intent.routine, - ) - } - - is HomeIntent.HideRoutineDetailsBottomSheet -> { - state.copy( - routineDetailsBottomSheetVisible = false, - selectedRoutine = null, - ) - } - - is HomeIntent.ShowDeleteConfirmDialog -> { - state.copy( - showDeleteConfirmDialog = true, - deletingRoutine = intent.routine, - ) - } - - is HomeIntent.HideDeleteConfirmDialog -> { - state.copy( - showDeleteConfirmDialog = false, - deletingRoutine = null, - ) - } - - is HomeIntent.DeleteRoutineByDayOptimistically -> { - val dateKey = intent.performedDate - val updatedRoutinesByDate = state.routines.routinesByDate.toMutableMap() - val routinesForDate = updatedRoutinesByDate[dateKey]?.toMutableList() - - if (routinesForDate != null) { - updatedRoutinesByDate[dateKey] = routinesForDate.filterNot { - it.routineId == intent.routineId - } - } - - state.copy( - routines = RoutinesUiModel(routinesByDate = updatedRoutinesByDate), - showDeleteConfirmDialog = false, - deletingRoutine = null, - routineDetailsBottomSheetVisible = false, - selectedRoutine = null, - ) - } - - is HomeIntent.RestoreRoutinesAfterDeleteByDayFailure -> { - state.copy(routines = intent.backupRoutines) - } - - is HomeIntent.ConfirmRoutineByDayDeletion -> null - - is HomeIntent.LoadMyEmotion -> { - state.copy(myEmotion = intent.emotion) + is HomeIntent.LoadTodayEmotion -> { + state.copy(todayEmotion = intent.emotion) } is HomeIntent.OnRegisterEmotionClick -> { - if (state.myEmotion == null) { - sendSideEffect(HomeSideEffect.NavigateToEmotion) - } else { - sendSideEffect(HomeSideEffect.ShowToastWithIcon("선택한 감정 구슬이 이미 반영되었어요.")) - } + sendSideEffect(HomeSideEffect.NavigateToEmotion) null } @@ -228,11 +124,6 @@ class HomeViewModel @Inject constructor( sendSideEffect(HomeSideEffect.ShowToast("루틴 완료 상태 저장에 실패했어요.\n다시 시도해 주세요.")) null } - - is HomeIntent.NavigateToEditRoutine -> { - sendSideEffect(HomeSideEffect.NavigateToEditRoutine(intent.routineId)) - null - } } return newState } @@ -249,7 +140,7 @@ class HomeViewModel @Inject constructor( viewModelScope.launch { getEmotionChangeEventFlowUseCase().collect { val currentDate = LocalDate.now() - getMyEmotion(currentDate) + fetchTodayEmotion(currentDate) } } } @@ -310,8 +201,7 @@ class HomeViewModel @Inject constructor( viewModelScope.launch { fetchWeeklyRoutinesUseCase(startDate, endDate).fold( onSuccess = { routines -> - val routinesUiModel = routines.toUiModel() - sendIntent(HomeIntent.LoadWeeklyRoutines(routinesUiModel)) + sendIntent(HomeIntent.LoadWeeklyRoutines(routines.toUiModel())) sendIntent(HomeIntent.UpdateLoading(false)) }, onFailure = { error -> @@ -322,13 +212,12 @@ class HomeViewModel @Inject constructor( } } - private fun getMyEmotion(currentDate: LocalDate) { + private fun fetchTodayEmotion(currentDate: LocalDate) { sendIntent(HomeIntent.UpdateLoading(true)) viewModelScope.launch { - getEmotionUseCase(currentDate.toString()).fold( - onSuccess = { emotion -> - val ballType = EmotionBallType.fromDomainEmotion(emotion?.emotionType) - sendIntent(HomeIntent.LoadMyEmotion(ballType)) + fetchTodayEmotionUseCase(currentDate.toString()).fold( + onSuccess = { todayEmotion -> + sendIntent(HomeIntent.LoadTodayEmotion(todayEmotion?.toUiModel())) sendIntent(HomeIntent.UpdateLoading(false)) }, onFailure = { error -> @@ -347,11 +236,11 @@ class HomeViewModel @Inject constructor( processRoutineToggleChanges(originalState, predictedUpdatedState) } - fun toggleSubRoutineCompletion(routineId: String, subRoutineId: String, isCompleted: Boolean) { + fun toggleSubRoutineCompletion(routineId: String, subRoutineIndex: Int, isCompleted: Boolean) { val originalState = container.stateFlow.value - sendIntent(HomeIntent.OnSubRoutineCompletionToggle(routineId, subRoutineId, isCompleted)) + sendIntent(HomeIntent.OnSubRoutineCompletionToggle(routineId, subRoutineIndex, isCompleted)) - val predictedUpdatedState = updateSubRoutine(originalState, routineId, subRoutineId, isCompleted) + val predictedUpdatedState = updateSubRoutine(originalState, routineId, subRoutineIndex, isCompleted) processRoutineToggleChanges(originalState, predictedUpdatedState) } @@ -388,11 +277,10 @@ class HomeViewModel @Inject constructor( val routineIndex = routinesForDate.indexOfFirst { it.routineId == routineId } if (routineIndex == -1) return@updateRoutinesForDate false - val updatedRoutine = routinesForDate[routineIndex].copy( - isCompleted = isCompleted, - subRoutines = routinesForDate[routineIndex].subRoutines.map { subRoutine -> - subRoutine.copy(isCompleted = isCompleted) - }, + val routine = routinesForDate[routineIndex] + val updatedRoutine = routine.copy( + routineCompleteYn = isCompleted, + subRoutineCompleteYn = routine.subRoutineCompleteYn.map { isCompleted }, ) routinesForDate[routineIndex] = updatedRoutine @@ -403,7 +291,7 @@ class HomeViewModel @Inject constructor( private fun updateSubRoutine( state: HomeState, routineId: String, - subRoutineId: String, + subRoutineIndex: Int, isCompleted: Boolean, ): HomeState { return updateRoutinesForDate(state) { routinesForDate -> @@ -411,19 +299,20 @@ class HomeViewModel @Inject constructor( if (routineIndex == -1) return@updateRoutinesForDate false val routine = routinesForDate[routineIndex] - val updatedSubRoutines = routine.subRoutines.map { subRoutine -> - if (subRoutine.subRoutineId == subRoutineId) { - subRoutine.copy(isCompleted = isCompleted) - } else { - subRoutine - } + + if (subRoutineIndex !in routine.subRoutineCompleteYn.indices) { + return@updateRoutinesForDate false } - val routineCompleted = if (isCompleted) updatedSubRoutines.all { it.isCompleted } else false + val updatedSubRoutineCompleteYn = routine.subRoutineCompleteYn.toMutableList().also { + it[subRoutineIndex] = isCompleted + } + + val routineCompleted = updatedSubRoutineCompleteYn.all { it } val updatedRoutine = routine.copy( - subRoutines = updatedSubRoutines, - isCompleted = routineCompleted, + subRoutineCompleteYn = updatedSubRoutineCompleteYn, + routineCompleteYn = routineCompleted, ) routinesForDate[routineIndex] = updatedRoutine @@ -436,12 +325,18 @@ class HomeViewModel @Inject constructor( updateLogic: (MutableList) -> Boolean, ): HomeState { val dateKey = state.selectedDate.toString() - val routinesForDate = state.routines.routinesByDate[dateKey]?.toMutableList() ?: return state + val dayRoutines = state.routines.routines[dateKey] ?: return state + val routinesForDate = dayRoutines.routineList.toMutableList() if (!updateLogic(routinesForDate)) return state - val updatedRoutinesByDate = state.routines.routinesByDate.toMutableMap() - updatedRoutinesByDate[dateKey] = routinesForDate + val allCompleted = routinesForDate.all { it.routineCompleteYn } + + val updatedRoutinesByDate = state.routines.routines.toMutableMap() + updatedRoutinesByDate[dateKey] = dayRoutines.copy( + routineList = routinesForDate, + allCompleted = allCompleted, + ) return state.copy(routines = RoutinesUiModel(updatedRoutinesByDate)) } @@ -452,39 +347,25 @@ class HomeViewModel @Inject constructor( date: LocalDate, ): List { val dateKey = date.toString() - val originalRoutineList = originalRoutines.routinesByDate[dateKey] ?: emptyList() - val updatedRoutineList = updatedRoutines.routinesByDate[dateKey] ?: emptyList() + val originalRoutineList = originalRoutines.routines[dateKey]?.routineList ?: emptyList() + val updatedRoutineList = updatedRoutines.routines[dateKey]?.routineList ?: emptyList() return buildList { updatedRoutineList.forEach { updatedRoutine -> val originalRoutine = originalRoutineList.find { it.routineId == updatedRoutine.routineId } - if (originalRoutine?.isCompleted != updatedRoutine.isCompleted) { + val hasMainRoutineChanged = originalRoutine?.routineCompleteYn != updatedRoutine.routineCompleteYn + val hasSubRoutinesChanged = originalRoutine?.subRoutineCompleteYn != updatedRoutine.subRoutineCompleteYn + + if (hasMainRoutineChanged || hasSubRoutinesChanged) { add( RoutineCompletionInfo( - routineType = updatedRoutine.routineType, routineId = updatedRoutine.routineId, - historySeq = updatedRoutine.historySeq, - isCompleted = updatedRoutine.isCompleted, + routineCompleteYn = updatedRoutine.routineCompleteYn, + subRoutineCompleteYn = updatedRoutine.subRoutineCompleteYn, ), ) } - - updatedRoutine.subRoutines.forEach { updatedSubRoutine -> - val originalSubRoutine = originalRoutine?.subRoutines - ?.find { it.subRoutineId == updatedSubRoutine.subRoutineId } - - if (originalSubRoutine?.isCompleted != updatedSubRoutine.isCompleted) { - add( - RoutineCompletionInfo( - routineType = updatedSubRoutine.routineType, - routineId = updatedSubRoutine.subRoutineId, - historySeq = updatedSubRoutine.historySeq, - isCompleted = updatedSubRoutine.isCompleted, - ), - ) - } - } } } } @@ -495,9 +376,8 @@ class HomeViewModel @Inject constructor( if (unsyncedChanges.isEmpty()) return - val syncRequest = RoutineCompletion( - performedDate = dateKey, - routineCompletions = unsyncedChanges.toList(), + val syncRequest = RoutineCompletionInfos( + routineCompletionInfos = unsyncedChanges.toList(), ) routineCompletionUseCase(syncRequest).fold( @@ -516,52 +396,4 @@ class HomeViewModel @Inject constructor( }, ) } - - fun deleteRoutine(routineId: String) { - val currentRoutines = container.stateFlow.value.routines - sendIntent(HomeIntent.DeleteRoutineOptimistically(routineId)) - - viewModelScope.launch { - deleteRoutineUseCase(routineId).fold( - onSuccess = { - sendIntent(HomeIntent.ConfirmRoutineDeletion(routineId)) - }, - onFailure = { error -> - Log.e("HomeViewModel", "루틴 삭제 실패: ${error.message}") - sendIntent(HomeIntent.RestoreRoutinesAfterDeleteFailure(currentRoutines)) - }, - ) - } - } - - fun deleteRoutineByDay(routineUiModel: RoutineUiModel) { - val currentRoutines = container.stateFlow.value.routines - val performedDate = container.stateFlow.value.selectedDate.toString() - - sendIntent( - HomeIntent.DeleteRoutineByDayOptimistically( - routineId = routineUiModel.routineId, - performedDate = performedDate, - ), - ) - - viewModelScope.launch { - val routineByDayDeletion = routineUiModel.toRoutineByDayDeletion(performedDate) - - deleteRoutineByDayUseCase(routineByDayDeletion).fold( - onSuccess = { - sendIntent( - HomeIntent.ConfirmRoutineByDayDeletion( - routineId = routineUiModel.routineId, - performedDate = performedDate, - ), - ) - }, - onFailure = { - Log.e("HomeViewModel", "루틴 삭제 실패: ${it.message}") - sendIntent(HomeIntent.RestoreRoutinesAfterDeleteByDayFailure(currentRoutines)) - }, - ) - } - } } diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/atom/EmotionBall.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/atom/EmotionBall.kt deleted file mode 100644 index 0e71603f..00000000 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/atom/EmotionBall.kt +++ /dev/null @@ -1,151 +0,0 @@ -package com.threegap.bitnagil.presentation.home.component.atom - -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.shadow -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.threegap.bitnagil.designsystem.R -import com.threegap.bitnagil.designsystem.modifier.clickableWithoutRipple -import com.threegap.bitnagil.presentation.home.model.EmotionBallType - -@Composable -fun EmotionBall( - emotionType: EmotionBallType?, - onClick: () -> Unit, - modifier: Modifier = Modifier, -) { - if (emotionType != null) { - Image( - painter = painterResource(emotionType.drawableId), - contentDescription = null, - modifier = modifier - .size(172.dp) - .clickableWithoutRipple { onClick() } - .padding(10.dp) - .fillMaxSize() - .shadow( - elevation = 24.dp, - shape = CircleShape, - ambientColor = emotionType.ambientColor, - spotColor = emotionType.spotColor, - ), - ) - } else { - Image( - painter = painterResource(R.drawable.default_ball), - contentDescription = null, - modifier = modifier - .size(172.dp) - .clickableWithoutRipple { onClick() } - .fillMaxSize(), - ) - } -} - -@Preview(showBackground = true, name = "Default (Null)") -@Composable -private fun EmotionBallDefaultPreview() { - Column( - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - EmotionBall( - emotionType = null, - onClick = {}, - ) - } -} - -@Preview(showBackground = true, name = "Calm") -@Composable -private fun EmotionBallCalmPreview() { - Column( - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - EmotionBall( - emotionType = EmotionBallType.CALM, - onClick = {}, - ) - } -} - -@Preview(showBackground = true, name = "Vitality") -@Composable -private fun EmotionBallVitalityPreview() { - Column( - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - EmotionBall( - emotionType = EmotionBallType.VITALITY, - onClick = {}, - ) - } -} - -@Preview(showBackground = true, name = "Lethargy") -@Composable -private fun EmotionBallLethargyPreview() { - Column( - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - EmotionBall( - emotionType = EmotionBallType.LETHARGY, - onClick = {}, - ) - } -} - -@Preview(showBackground = true, name = "Anxiety") -@Composable -private fun EmotionBallAnxietyPreview() { - Column( - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - EmotionBall( - emotionType = EmotionBallType.ANXIETY, - onClick = {}, - ) - } -} - -@Preview(showBackground = true, name = "Satisfaction") -@Composable -private fun EmotionBallSatisfactionPreview() { - Column( - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - EmotionBall( - emotionType = EmotionBallType.SATISFACTION, - onClick = {}, - ) - } -} - -@Preview(showBackground = true, name = "Fatigue") -@Composable -private fun EmotionBallFatiguePreview() { - Column( - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - EmotionBall( - emotionType = EmotionBallType.FATIGUE, - onClick = {}, - ) - } -} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/atom/EmotionRegisterButton.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/atom/EmotionRegisterButton.kt new file mode 100644 index 00000000..fdceb3b1 --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/atom/EmotionRegisterButton.kt @@ -0,0 +1,46 @@ +package com.threegap.bitnagil.presentation.home.component.atom + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.threegap.bitnagil.designsystem.BitnagilTheme +import com.threegap.bitnagil.designsystem.component.atom.BitnagilTextButton +import com.threegap.bitnagil.designsystem.component.atom.BitnagilTextButtonColor + +@Composable +fun EmotionRegisterButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, +) { + BitnagilTextButton( + text = "오늘 감정 등록하기", + onClick = onClick, + enabled = enabled, + shape = RoundedCornerShape(8.dp), + colors = BitnagilTextButtonColor( + defaultBackgroundColor = BitnagilTheme.colors.orange500, + pressedBackgroundColor = BitnagilTheme.colors.orange600, + disabledBackgroundColor = BitnagilTheme.colors.coolGray30, + defaultTextColor = BitnagilTheme.colors.white, + pressedTextColor = BitnagilTheme.colors.white, + disabledTextColor = BitnagilTheme.colors.coolGray10, + ), + textStyle = BitnagilTheme.typography.body2SemiBold, + textPadding = PaddingValues(vertical = 8.dp, horizontal = 16.dp), + modifier = modifier + .height(36.dp), + ) +} + +@Preview +@Composable +private fun EmotionRegisterButtonPreview() { + EmotionRegisterButton( + onClick = {}, + ) +} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/atom/RoundTriangleShape.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/atom/RoundTriangleShape.kt deleted file mode 100644 index d98332cb..00000000 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/atom/RoundTriangleShape.kt +++ /dev/null @@ -1,52 +0,0 @@ -package com.threegap.bitnagil.presentation.home.component.atom - -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.size -import androidx.compose.foundation.shape.GenericShape -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp - -fun roundedTriangleShape(cornerRadius: Float) = - GenericShape { size, _ -> - val width = size.width - val height = size.height - val radius = cornerRadius.coerceAtMost(minOf(width, height) * 0.3f) - - moveTo(0f, 0f) - lineTo(width, 0f) - - lineTo(width / 2 + radius, height - radius) - quadraticTo( - width / 2, - height, - width / 2 - radius, - height - radius, - ) - - lineTo(0f, 0f) - close() - } - -@Preview -@Composable -private fun RoundedTriangleShapePreview() { - Column( - verticalArrangement = Arrangement.spacedBy(16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Box( - modifier = Modifier - .size(120.dp, 100.dp) - .clip(roundedTriangleShape(20f)) - .background(Color.Green), - ) - } -} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/block/RoutineItem.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/block/RoutineItem.kt index 7fa94129..acadbc38 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/block/RoutineItem.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/block/RoutineItem.kt @@ -2,11 +2,13 @@ package com.threegap.bitnagil.presentation.home.component.block import androidx.compose.foundation.background 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.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -15,56 +17,67 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.threegap.bitnagil.designsystem.BitnagilTheme import com.threegap.bitnagil.designsystem.R -import com.threegap.bitnagil.designsystem.component.atom.BitnagilCheckBox import com.threegap.bitnagil.designsystem.component.atom.BitnagilIcon import com.threegap.bitnagil.designsystem.modifier.clickableWithoutRipple -import com.threegap.bitnagil.domain.routine.model.RoutineType import com.threegap.bitnagil.presentation.home.model.RoutineUiModel -import com.threegap.bitnagil.presentation.home.model.SubRoutineUiModel @Composable fun RoutineItem( routine: RoutineUiModel, onRoutineToggle: (Boolean) -> Unit, - onMoreClick: () -> Unit, + onSubRoutineToggle: (Int, Boolean) -> Unit, modifier: Modifier = Modifier, ) { - Row( - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, + Column( modifier = modifier .fillMaxWidth() - .clickableWithoutRipple( - onClick = { onRoutineToggle(!routine.isCompleted) }, - ) - .height(61.dp) .background( - color = BitnagilTheme.colors.lightBlue75, - shape = RoundedCornerShape(8.dp), + color = BitnagilTheme.colors.white, + shape = RoundedCornerShape(12.dp), ) .padding( - start = 16.dp, + vertical = 14.dp, + horizontal = 16.dp, ), ) { Row( - horizontalArrangement = Arrangement.spacedBy(12.dp), + modifier = Modifier + .fillMaxWidth() + .clickableWithoutRipple { onRoutineToggle(!routine.routineCompleteYn) }, + horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { - BitnagilCheckBox( - checked = routine.isCompleted, - ) - Text( text = routine.routineName, - style = BitnagilTheme.typography.subtitle1SemiBold, - color = BitnagilTheme.colors.navy500, + style = BitnagilTheme.typography.body1SemiBold, + color = BitnagilTheme.colors.coolGray10, + modifier = Modifier.weight(1f), + ) + + BitnagilIcon( + id = if (routine.routineCompleteYn) R.drawable.ic_check_circle else R.drawable.ic_check_default, + tint = null, + modifier = Modifier + .padding(start = 10.dp) + .size(28.dp), ) } - BitnagilIcon( - id = R.drawable.ic_see_more, - modifier = Modifier.clickableWithoutRipple(onClick = onMoreClick), - ) + if (routine.subRoutineNames.isNotEmpty()) { + HorizontalDivider( + thickness = 1.dp, + color = BitnagilTheme.colors.coolGray97, + modifier = Modifier.padding(vertical = 10.dp), + ) + + SubRoutinesItem( + subRoutineNames = routine.subRoutineNames, + subRoutineCompleteYn = routine.subRoutineCompleteYn, + onSubRoutineToggle = { index, isCompleted -> + onSubRoutineToggle(index, isCompleted) + }, + ) + } } } @@ -74,48 +87,36 @@ private fun RoutineItemPreview() { RoutineItem( routine = RoutineUiModel( routineId = "uuid1", - historySeq = 1, routineName = "개운하게 일어나기", - executionTime = "20:30:00", - isCompleted = false, - routineCompletionId = 1, - isModified = false, - subRoutines = listOf( - SubRoutineUiModel( - subRoutineId = "uuid1", - historySeq = 1, - subRoutineName = "물 마시기", - sortOrder = 1, - routineCompletionId = 1, - isCompleted = false, - isModified = false, - routineType = RoutineType.SUB_ROUTINE, - ), - SubRoutineUiModel( - subRoutineId = "uuid2", - historySeq = 1, - subRoutineName = "스트레칭하기", - sortOrder = 1, - routineCompletionId = 1, - isCompleted = false, - isModified = false, - routineType = RoutineType.SUB_ROUTINE, - ), - SubRoutineUiModel( - subRoutineId = "uuid3", - historySeq = 1, - subRoutineName = "심호흡하기", - sortOrder = 1, - routineCompletionId = 1, - isCompleted = false, - isModified = false, - routineType = RoutineType.SUB_ROUTINE, - ), - ), repeatDay = emptyList(), - routineType = RoutineType.ROUTINE, + executionTime = "20:30:00", + routineDate = "2025-08-15", + routineCompleteYn = false, + subRoutineNames = listOf("물 마시기", "스트레칭하기", "심호흡하기"), + subRoutineCompleteYn = listOf(true, false, true), + recommendedRoutineType = null, ), onRoutineToggle = { }, - onMoreClick = { }, + onSubRoutineToggle = { _, _ -> }, + ) +} + +@Preview +@Composable +private fun NoneSubRoutineRoutineItemPreview() { + RoutineItem( + routine = RoutineUiModel( + routineId = "uuid1", + routineName = "개운하게 일어나기", + repeatDay = emptyList(), + executionTime = "20:30:00", + routineDate = "2025-08-15", + routineCompleteYn = false, + subRoutineNames = emptyList(), + subRoutineCompleteYn = emptyList(), + recommendedRoutineType = null, + ), + onRoutineToggle = {}, + onSubRoutineToggle = { _, _ -> }, ) } diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/block/SpeechBubbleTooltip.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/block/SpeechBubbleTooltip.kt deleted file mode 100644 index 3aaa2d38..00000000 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/block/SpeechBubbleTooltip.kt +++ /dev/null @@ -1,61 +0,0 @@ -package com.threegap.bitnagil.presentation.home.component.block - -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.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.threegap.bitnagil.designsystem.BitnagilTheme -import com.threegap.bitnagil.presentation.home.component.atom.roundedTriangleShape - -@Composable -fun SpeechBubbleTooltip( - text: String, - modifier: Modifier = Modifier, -) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - modifier = modifier, - ) { - Box( - modifier = Modifier - .background( - color = BitnagilTheme.colors.navy400, - shape = RoundedCornerShape(8.dp), - ) - .padding(10.dp), - ) { - Text( - text = text, - color = BitnagilTheme.colors.white, - style = BitnagilTheme.typography.caption1Medium, - ) - } - - Box( - modifier = Modifier - .size(18.dp, 10.dp) - .background( - color = BitnagilTheme.colors.navy400, - shape = roundedTriangleShape(6f), - ), - ) - } -} - -@Preview -@Composable -private fun SpeechBubbleTooltipPreview() { - SpeechBubbleTooltip( - text = "감정 기록 시, 루틴을 추천 받아요!", - ) -} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/block/SubRoutinesItem.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/block/SubRoutinesItem.kt index 5f787836..01651f28 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/block/SubRoutinesItem.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/block/SubRoutinesItem.kt @@ -3,10 +3,7 @@ package com.threegap.bitnagil.presentation.home.component.block import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -18,58 +15,42 @@ import com.threegap.bitnagil.designsystem.BitnagilTheme import com.threegap.bitnagil.designsystem.R import com.threegap.bitnagil.designsystem.component.atom.BitnagilIcon import com.threegap.bitnagil.designsystem.modifier.clickableWithoutRipple -import com.threegap.bitnagil.domain.routine.model.RoutineType -import com.threegap.bitnagil.presentation.home.model.SubRoutineUiModel @Composable fun SubRoutinesItem( - subRoutines: List, - onSubRoutineToggle: (String, Boolean) -> Unit, + subRoutineNames: List, + subRoutineCompleteYn: List, + onSubRoutineToggle: (Int, Boolean) -> Unit, modifier: Modifier = Modifier, ) { + val minSize = minOf(subRoutineNames.size, subRoutineCompleteYn.size) + Column( modifier = modifier, + verticalArrangement = Arrangement.spacedBy(10.dp), ) { - Text( - text = "세부 루틴", - style = BitnagilTheme.typography.caption1Medium, - color = BitnagilTheme.colors.coolGray60, - modifier = Modifier - .fillMaxWidth() - .padding(start = 16.dp), - ) - - Spacer(modifier = Modifier.height(8.dp)) + repeat(minSize) { index -> + val subRoutineName = subRoutineNames[index] + val isCompleted = subRoutineCompleteYn.getOrElse(index) { false } - subRoutines.forEach { subRoutine -> Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp), modifier = Modifier .fillMaxWidth() - .height(34.dp) - .clickableWithoutRipple { - onSubRoutineToggle(subRoutine.subRoutineId, !subRoutine.isCompleted) - } - .padding( - vertical = 7.dp, - horizontal = 14.dp, - ), + .clickableWithoutRipple { onSubRoutineToggle(index, !isCompleted) }, ) { BitnagilIcon( - id = R.drawable.ic_check, - tint = if (subRoutine.isCompleted) { - BitnagilTheme.colors.orange500 - } else { - BitnagilTheme.colors.coolGray96 - }, - modifier = Modifier.size(20.dp), + id = if (isCompleted) R.drawable.ic_check_circle else R.drawable.ic_check_default, + tint = null, + modifier = Modifier.size(24.dp), ) Text( - text = subRoutine.subRoutineName, + text = subRoutineName, style = BitnagilTheme.typography.body2Medium, - color = BitnagilTheme.colors.coolGray20, + color = BitnagilTheme.colors.coolGray40, + modifier = Modifier.weight(1f), ) } } @@ -80,38 +61,8 @@ fun SubRoutinesItem( @Composable private fun SubRoutinesItemPreview() { SubRoutinesItem( - subRoutines = listOf( - SubRoutineUiModel( - subRoutineId = "uuid1", - historySeq = 1, - subRoutineName = "물 마시기", - sortOrder = 1, - routineCompletionId = 1, - isCompleted = false, - isModified = false, - routineType = RoutineType.SUB_ROUTINE, - ), - SubRoutineUiModel( - subRoutineId = "uuid2", - historySeq = 1, - subRoutineName = "스트레칭하기", - sortOrder = 1, - routineCompletionId = 1, - isCompleted = false, - isModified = false, - routineType = RoutineType.SUB_ROUTINE, - ), - SubRoutineUiModel( - subRoutineId = "uuid3", - historySeq = 1, - subRoutineName = "심호흡하기", - sortOrder = 1, - routineCompletionId = 1, - isCompleted = false, - isModified = false, - routineType = RoutineType.SUB_ROUTINE, - ), - ), + subRoutineNames = listOf("물 마시기", "스트레칭하기", "심호흡하기"), + subRoutineCompleteYn = listOf(true, false, true), onSubRoutineToggle = { _, _ -> }, ) } diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/template/CollapsibleHomeHeader.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/template/CollapsibleHomeHeader.kt index e0e741ab..1be0dd03 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/template/CollapsibleHomeHeader.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/template/CollapsibleHomeHeader.kt @@ -1,146 +1,122 @@ package com.threegap.bitnagil.presentation.home.component.template +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.material3.Text 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.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Popup +import coil3.compose.AsyncImage +import coil3.request.ImageRequest +import coil3.request.crossfade import com.threegap.bitnagil.designsystem.BitnagilTheme +import com.threegap.bitnagil.designsystem.R import com.threegap.bitnagil.designsystem.component.atom.BitnagilIcon -import com.threegap.bitnagil.designsystem.modifier.clickableWithoutRipple -import com.threegap.bitnagil.presentation.home.component.atom.EmotionBall -import com.threegap.bitnagil.presentation.home.component.block.SpeechBubbleTooltip -import com.threegap.bitnagil.presentation.home.model.EmotionBallType +import com.threegap.bitnagil.presentation.home.component.atom.EmotionRegisterButton +import com.threegap.bitnagil.presentation.home.model.TodayEmotionUiModel import com.threegap.bitnagil.presentation.home.util.CollapsibleHeaderState import com.threegap.bitnagil.presentation.home.util.rememberCollapsibleHeaderState @Composable fun CollapsibleHomeHeader( userName: String, - emotionBallType: EmotionBallType?, + todayEmotion: TodayEmotionUiModel?, collapsibleHeaderState: CollapsibleHeaderState, - onEmotionRecordClick: () -> Unit, + onRegisterEmotion: () -> Unit, modifier: Modifier = Modifier, ) { - var showTooltip by remember { mutableStateOf(false) } + val context = LocalContext.current + val alpha by animateFloatAsState( + targetValue = 1f - collapsibleHeaderState.collapseProgress, + animationSpec = tween(durationMillis = 300), + label = "header_alpha", + ) + val hasEmotion = todayEmotion != null + val welcomeMessage = if (hasEmotion) { + "${userName}님,\n${todayEmotion?.homeMessage}" + } else { + "${userName}님, 오셨군요!\n오늘 기분은 어떤가요?" + } Column( - modifier = modifier.height(collapsibleHeaderState.currentHeaderHeight), + modifier = modifier + .height(collapsibleHeaderState.currentHeaderHeight), ) { - Box( + Row( modifier = Modifier .fillMaxWidth() .height(collapsibleHeaderState.collapsedHeaderHeight) - .padding(horizontal = 16.dp), + .statusBarsPadding(), + verticalAlignment = Alignment.CenterVertically, ) { - // TODO: 알림 아이콘 추가예정 + BitnagilIcon( + id = R.drawable.ic_logo, + tint = BitnagilTheme.colors.coolGray50, + modifier = Modifier.padding(start = 16.dp), + ) } if (collapsibleHeaderState.currentHeaderHeight > collapsibleHeaderState.collapsedHeaderHeight) { - Box { + Box( + modifier = Modifier + .padding(top = 18.dp) + .height(collapsibleHeaderState.currentHeaderHeight - collapsibleHeaderState.collapsedHeaderHeight) + .alpha(alpha), + ) { Column( - verticalArrangement = Arrangement.Top, modifier = Modifier .fillMaxWidth() - .height(collapsibleHeaderState.currentHeaderHeight - collapsibleHeaderState.collapsedHeaderHeight) .padding(horizontal = 16.dp) - .alpha(1f - collapsibleHeaderState.collapseProgress) .align(Alignment.TopStart), + verticalArrangement = Arrangement.spacedBy(20.dp), ) { - GreetingMessage( - userName = userName, - showTooltip = showTooltip, - onInfoClick = { showTooltip = true }, - onTooltipDismiss = { showTooltip = false }, + Text( + text = welcomeMessage, + style = BitnagilTheme.typography.cafe24SsurroundAir, + color = BitnagilTheme.colors.white, + fontWeight = FontWeight.SemiBold, ) - Spacer(modifier = Modifier.height(8.dp)) - - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(2.dp), - modifier = Modifier - .clickableWithoutRipple { onEmotionRecordClick() } - .padding(vertical = 11.dp), - ) { - Text( - text = "감정구슬 기록하기", - style = BitnagilTheme.typography.caption1Medium, - color = BitnagilTheme.colors.navy300, - ) - - BitnagilIcon( - id = com.threegap.bitnagil.designsystem.R.drawable.ic_right_arrow_20, - tint = BitnagilTheme.colors.navy300, - ) - } - } - - Box( - modifier = modifier - .padding(10.dp) - .align(Alignment.BottomEnd), - ) { - EmotionBall( - emotionType = emotionBallType, - onClick = {}, + EmotionRegisterButton( + onClick = onRegisterEmotion, + enabled = !hasEmotion, ) } - } - } - } -} -@Composable -private fun GreetingMessage( - userName: String, - showTooltip: Boolean, - onInfoClick: () -> Unit, - onTooltipDismiss: () -> Unit, -) { - Row( - verticalAlignment = Alignment.Bottom, - horizontalArrangement = Arrangement.spacedBy(2.dp), - ) { - Text( - text = "${userName}님,\n오늘의 기분 어때요?", - style = BitnagilTheme.typography.title1SemiBold, - color = BitnagilTheme.colors.coolGray10, - ) - - Box { - BitnagilIcon( - id = com.threegap.bitnagil.designsystem.R.drawable.ic_tooltip, - tint = null, - modifier = Modifier.clickableWithoutRipple { onInfoClick() }, - ) - - if (showTooltip) { - Popup( - onDismissRequest = onTooltipDismiss, - alignment = Alignment.TopCenter, - offset = IntOffset(x = 0, y = -110), - ) { - SpeechBubbleTooltip( - text = "감정 기록 시, 루틴을 추천 받아요!", - ) - } + AsyncImage( + model = remember(todayEmotion?.imageUrl) { + ImageRequest.Builder(context) + .data(todayEmotion?.imageUrl) + .crossfade(true) + .build() + }, + contentDescription = null, + placeholder = painterResource(R.drawable.default_emotion), + error = painterResource(R.drawable.default_emotion), + contentScale = ContentScale.Fit, + modifier = Modifier + .align(Alignment.BottomEnd) + .padding(end = 18.dp) + .aspectRatio(108f / 148f), + ) } } } @@ -148,11 +124,11 @@ private fun GreetingMessage( @Preview @Composable -private fun HomeTopBarPreview() { +private fun CollapsibleHomeHeaderPreview() { CollapsibleHomeHeader( userName = "대현", - emotionBallType = null, + todayEmotion = null, collapsibleHeaderState = rememberCollapsibleHeaderState(), - onEmotionRecordClick = {}, + onRegisterEmotion = {}, ) } diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/template/DeleteConfirmDialog.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/template/DeleteConfirmDialog.kt deleted file mode 100644 index b7202981..00000000 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/template/DeleteConfirmDialog.kt +++ /dev/null @@ -1,126 +0,0 @@ -package com.threegap.bitnagil.presentation.home.component.template - -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -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.BasicAlertDialog -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.DialogProperties -import com.threegap.bitnagil.designsystem.BitnagilTheme -import com.threegap.bitnagil.designsystem.modifier.clickableWithoutRipple - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun DeleteConfirmDialog( - onDeleteToday: () -> Unit, - onDeleteAll: () -> Unit, - onDismiss: () -> Unit, - modifier: Modifier = Modifier, -) { - BasicAlertDialog( - onDismissRequest = onDismiss, - properties = DialogProperties( - dismissOnBackPress = true, - dismissOnClickOutside = true, - ), - modifier = modifier, - ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier - .background( - color = BitnagilTheme.colors.white, - shape = RoundedCornerShape(16.dp), - ) - .padding(24.dp), - ) { - Text( - text = "해당 루틴은\n반복 루틴으로 설정되어 있어요", - color = BitnagilTheme.colors.coolGray10, - style = BitnagilTheme.typography.body1SemiBold, - textAlign = TextAlign.Center, - ) - - Spacer(modifier = Modifier.height(24.dp)) - - Column( - verticalArrangement = Arrangement.spacedBy(12.dp), - modifier = Modifier.fillMaxWidth(), - ) { - Box( - contentAlignment = Alignment.Center, - modifier = Modifier - .fillMaxWidth() - .clickableWithoutRipple { onDeleteToday() } - .height(44.dp) - .background( - color = BitnagilTheme.colors.white, - shape = RoundedCornerShape(8.dp), - ) - .border( - width = 1.dp, - color = BitnagilTheme.colors.navy500, - shape = RoundedCornerShape(8.dp), - ), - ) { - Text( - text = "선택한 요일만 삭제", - style = BitnagilTheme.typography.body2Medium, - color = BitnagilTheme.colors.navy500, - modifier = Modifier.padding(vertical = 8.dp), - ) - } - - Box( - contentAlignment = Alignment.Center, - modifier = Modifier - .fillMaxWidth() - .clickableWithoutRipple { onDeleteAll() } - .height(44.dp) - .background( - color = BitnagilTheme.colors.white, - shape = RoundedCornerShape(8.dp), - ) - .border( - width = 1.dp, - color = BitnagilTheme.colors.navy500, - shape = RoundedCornerShape(8.dp), - ), - ) { - Text( - text = "전체 루틴 삭제", - style = BitnagilTheme.typography.body2Medium, - color = BitnagilTheme.colors.navy500, - modifier = Modifier.padding(vertical = 8.dp), - ) - } - } - } - } -} - -@Preview -@Composable -private fun DeleteConfirmDialogPreview() { - BitnagilTheme { - DeleteConfirmDialog( - onDeleteToday = {}, - onDeleteAll = {}, - onDismiss = {}, - ) - } -} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/template/RoutineEmptyView.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/template/EmptyRoutineView.kt similarity index 74% rename from presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/template/RoutineEmptyView.kt rename to presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/template/EmptyRoutineView.kt index 11a62c20..e0df124c 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/template/RoutineEmptyView.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/template/EmptyRoutineView.kt @@ -18,12 +18,12 @@ import com.threegap.bitnagil.designsystem.BitnagilTheme import com.threegap.bitnagil.designsystem.modifier.clickableWithoutRipple @Composable -fun RoutineEmptyView( +fun EmptyRoutineView( onRegisterRoutineClick: () -> Unit, modifier: Modifier = Modifier, ) { Column( - verticalArrangement = Arrangement.Center, + verticalArrangement = Arrangement.Top, horizontalAlignment = Alignment.CenterHorizontally, modifier = modifier, ) { @@ -31,10 +31,11 @@ fun RoutineEmptyView( text = "등록한 루틴이 없어요", style = BitnagilTheme.typography.subtitle1SemiBold, color = BitnagilTheme.colors.coolGray30, + modifier = Modifier.height(28.dp), ) Text( - text = "루틴을 등록해서 빛나길을 시작해보세요", - style = BitnagilTheme.typography.body2Medium, + text = "루틴을 등록하고, 작은 변화부터 시작해보세요!", + style = BitnagilTheme.typography.body2Regular, color = BitnagilTheme.colors.coolGray70, ) @@ -43,28 +44,28 @@ fun RoutineEmptyView( Box( modifier = Modifier .background( - color = BitnagilTheme.colors.navy50, - shape = RoundedCornerShape(100.dp), + color = BitnagilTheme.colors.coolGray96, + shape = RoundedCornerShape(8.dp), ) .clickableWithoutRipple { onRegisterRoutineClick() } .padding( - vertical = 8.dp, - horizontal = 10.dp, + vertical = 10.dp, + horizontal = 14.dp, ), ) { Text( text = "루틴 등록하기", - style = BitnagilTheme.typography.caption1Medium, + style = BitnagilTheme.typography.caption1SemiBold, color = BitnagilTheme.colors.coolGray30, ) } } } -@Preview +@Preview(showBackground = true) @Composable -private fun RoutineEmptyViewPreview() { - RoutineEmptyView( +private fun EmptyRoutineViewPreview() { + EmptyRoutineView( onRegisterRoutineClick = {}, ) } diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/template/RoutineDetailsBottomSheet.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/template/RoutineDetailsBottomSheet.kt deleted file mode 100644 index c841ce1d..00000000 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/template/RoutineDetailsBottomSheet.kt +++ /dev/null @@ -1,329 +0,0 @@ -package com.threegap.bitnagil.presentation.home.component.template - -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -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.padding -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ModalBottomSheet -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.threegap.bitnagil.designsystem.BitnagilTheme -import com.threegap.bitnagil.designsystem.R -import com.threegap.bitnagil.designsystem.component.atom.BitnagilIcon -import com.threegap.bitnagil.designsystem.modifier.clickableWithoutRipple -import com.threegap.bitnagil.domain.routine.model.DayOfWeek -import com.threegap.bitnagil.domain.routine.model.DayOfWeek.Companion.formatRepeatDays -import com.threegap.bitnagil.domain.routine.model.RoutineType -import com.threegap.bitnagil.presentation.home.model.RoutineUiModel -import com.threegap.bitnagil.presentation.home.model.SubRoutineUiModel -import com.threegap.bitnagil.presentation.home.util.formatExecutionTime - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun RoutineDetailsBottomSheet( - routine: RoutineUiModel, - onDismiss: () -> Unit, - onEdit: (String) -> Unit, - onDelete: (String) -> Unit, - modifier: Modifier = Modifier, -) { - ModalBottomSheet( - onDismissRequest = onDismiss, - containerColor = BitnagilTheme.colors.white, - contentColor = BitnagilTheme.colors.white, - modifier = modifier, - ) { - RoutineInfoContent( - routine = routine, - onEdit = { onEdit(routine.routineId) }, - onDelete = { onDelete(routine.routineId) }, - modifier = Modifier.padding(horizontal = 20.dp), - ) - } -} - -@Composable -private fun RoutineInfoContent( - routine: RoutineUiModel, - onEdit: () -> Unit, - onDelete: () -> Unit, - modifier: Modifier = Modifier, -) { - Column( - modifier = modifier, - ) { - Column( - verticalArrangement = Arrangement.spacedBy(24.dp), - ) { - Row( - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .height(28.dp), - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(10.dp), - ) { - BitnagilIcon( - id = R.drawable.ic_name_routine, - tint = BitnagilTheme.colors.coolGray50, - ) - - Text( - text = "루틴 이름", - color = BitnagilTheme.colors.coolGray50, - style = BitnagilTheme.typography.body2Medium, - ) - } - Text( - text = routine.routineName, - color = BitnagilTheme.colors.coolGray10, - style = BitnagilTheme.typography.body2SemiBold, - ) - } - - Box( - modifier = Modifier.fillMaxWidth(), - ) { - Row( - modifier = Modifier.align(Alignment.TopStart), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(10.dp), - ) { - BitnagilIcon( - id = R.drawable.ic_detail_routine, - tint = BitnagilTheme.colors.coolGray50, - ) - - Text( - text = "세부 루틴", - color = BitnagilTheme.colors.coolGray50, - style = BitnagilTheme.typography.body2Medium, - ) - } - - if (routine.subRoutines.isEmpty()) { - Text( - text = "세부루틴 없음", - color = BitnagilTheme.colors.coolGray10, - style = BitnagilTheme.typography.body2SemiBold, - modifier = Modifier.align(Alignment.CenterEnd), - ) - } else { - Column( - modifier = Modifier.align(Alignment.TopEnd), - ) { - routine.subRoutines.forEach { subRoutine -> - Text( - text = subRoutine.subRoutineName, - color = BitnagilTheme.colors.coolGray10, - style = BitnagilTheme.typography.body2SemiBold, - textAlign = TextAlign.End, - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 4.dp), - ) - } - } - } - } - - Column { - Row( - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .height(28.dp), - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(10.dp), - ) { - BitnagilIcon( - id = R.drawable.ic_rotate, - tint = BitnagilTheme.colors.coolGray50, - ) - - Text( - text = "루틴 반복", - color = BitnagilTheme.colors.coolGray50, - style = BitnagilTheme.typography.body2Medium, - ) - } - - Text( - text = routine.repeatDay.formatRepeatDays(), - color = BitnagilTheme.colors.coolGray10, - style = BitnagilTheme.typography.body2SemiBold, - ) - } - Text( - text = "${routine.executionTime.formatExecutionTime()} 시작", - color = BitnagilTheme.colors.coolGray40, - style = BitnagilTheme.typography.caption1Medium, - textAlign = TextAlign.End, - modifier = Modifier.fillMaxWidth(), - ) - } - } - - Spacer(modifier = Modifier.height(33.dp)) - - Row( - horizontalArrangement = Arrangement.spacedBy(13.dp), - modifier = Modifier - .padding(vertical = 14.dp) - .height(54.dp) - .fillMaxWidth(), - ) { - Box( - contentAlignment = Alignment.Center, - modifier = Modifier - .weight(1f) - .fillMaxSize() - .background( - color = BitnagilTheme.colors.navy500, - shape = RoundedCornerShape(12.dp), - ) - .clickableWithoutRipple { onEdit() }, - ) { - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - BitnagilIcon( - id = com.threegap.bitnagil.designsystem.R.drawable.ic_edit, - tint = BitnagilTheme.colors.white, - ) - Text( - text = "수정하기", - color = BitnagilTheme.colors.white, - style = BitnagilTheme.typography.subtitle1SemiBold, - ) - } - } - - Box( - contentAlignment = Alignment.Center, - modifier = Modifier - .weight(1f) - .fillMaxSize() - .border( - width = 1.dp, - color = BitnagilTheme.colors.navy500, - shape = RoundedCornerShape(12.dp), - ) - .background( - color = BitnagilTheme.colors.white, - shape = RoundedCornerShape(12.dp), - ) - .clickableWithoutRipple { onDelete() }, - ) { - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - BitnagilIcon( - id = com.threegap.bitnagil.designsystem.R.drawable.ic_trash, - tint = BitnagilTheme.colors.navy500, - ) - Text( - text = "삭제하기", - color = BitnagilTheme.colors.navy500, - style = BitnagilTheme.typography.subtitle1SemiBold, - ) - } - } - } - } -} - -@Preview(showBackground = true) -@Composable -private fun RoutineInfoContentPreview() { - RoutineInfoContent( - routine = RoutineUiModel( - routineId = "uuid1", - historySeq = 1, - repeatDay = listOf(DayOfWeek.MONDAY, DayOfWeek.WEDNESDAY, DayOfWeek.FRIDAY), - routineName = "개운하게 일어나기", - executionTime = "20:30:00", - isCompleted = false, - routineCompletionId = 1, - isModified = false, - subRoutines = listOf( - SubRoutineUiModel( - subRoutineId = "uuid1", - historySeq = 1, - subRoutineName = "물 마시기", - sortOrder = 1, - routineCompletionId = 1, - isCompleted = false, - isModified = false, - routineType = RoutineType.SUB_ROUTINE, - ), - SubRoutineUiModel( - subRoutineId = "uuid2", - historySeq = 1, - subRoutineName = "스트레칭하기", - sortOrder = 1, - routineCompletionId = 1, - isCompleted = false, - isModified = false, - routineType = RoutineType.SUB_ROUTINE, - ), - SubRoutineUiModel( - subRoutineId = "uuid3", - historySeq = 1, - subRoutineName = "심호흡하기", - sortOrder = 1, - routineCompletionId = 1, - isCompleted = false, - isModified = false, - routineType = RoutineType.SUB_ROUTINE, - ), - ), - routineType = RoutineType.ROUTINE, - ), - onEdit = {}, - onDelete = {}, - ) -} - -@Preview(showBackground = true) -@Composable -private fun RoutineInfoContentSinglePreview() { - RoutineInfoContent( - routine = RoutineUiModel( - routineId = "uuid1", - historySeq = 1, - routineName = "개운하게 일어나기", - executionTime = "20:30:00", - isCompleted = false, - routineCompletionId = 1, - isModified = false, - subRoutines = emptyList(), - repeatDay = emptyList(), - routineType = RoutineType.ROUTINE, - ), - onEdit = {}, - onDelete = {}, - ) -} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/template/RoutineSection.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/template/RoutineSection.kt index 7e35b987..da36b646 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/template/RoutineSection.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/template/RoutineSection.kt @@ -1,153 +1,94 @@ package com.threegap.bitnagil.presentation.home.component.template -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material3.Text -import androidx.compose.material3.VerticalDivider import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.threegap.bitnagil.designsystem.BitnagilTheme -import com.threegap.bitnagil.domain.routine.model.RoutineType import com.threegap.bitnagil.presentation.home.component.block.RoutineItem -import com.threegap.bitnagil.presentation.home.component.block.SubRoutinesItem import com.threegap.bitnagil.presentation.home.model.RoutineUiModel -import com.threegap.bitnagil.presentation.home.model.SubRoutineUiModel import com.threegap.bitnagil.presentation.home.util.formatExecutionTime @Composable fun RoutineSection( routine: RoutineUiModel, onRoutineToggle: (Boolean) -> Unit, - onSubRoutineToggle: (String, Boolean) -> Unit, - onMoreClick: () -> Unit, + onSubRoutineToggle: (Int, Boolean) -> Unit, modifier: Modifier = Modifier, ) { - Column( - modifier = modifier, + Row( + modifier = modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), ) { Text( - text = "${routine.executionTime.formatExecutionTime()}부터 시작", - style = BitnagilTheme.typography.caption1Regular, - color = BitnagilTheme.colors.navy300, + text = routine.executionTime.formatExecutionTime(), + style = BitnagilTheme.typography.body2Medium, + color = BitnagilTheme.colors.coolGray10, + modifier = Modifier.defaultMinSize(minWidth = 42.dp), ) - Spacer(modifier = Modifier.height(10.dp)) - - Row( - modifier = Modifier.height(IntrinsicSize.Min), - ) { - Box( - modifier = Modifier - .fillMaxHeight() - .padding( - start = 9.dp, - end = 10.dp, - ), - ) { - VerticalDivider( - modifier = Modifier - .fillMaxHeight() - .border( - width = 1.dp, - color = BitnagilTheme.colors.lightBlue300, - ) - .align(Alignment.Center), - ) - - Box( - modifier = Modifier - .padding(top = 26.5.dp) - .size(8.dp) - .background( - color = BitnagilTheme.colors.navy500, - shape = CircleShape, - ), - ) - } - - Column { - RoutineItem( - routine = routine, - onRoutineToggle = onRoutineToggle, - onMoreClick = onMoreClick, - ) - - if (routine.subRoutines.isNotEmpty()) { - SubRoutinesItem( - subRoutines = routine.subRoutines, - onSubRoutineToggle = { subRoutineId, isCompleted -> - onSubRoutineToggle(subRoutineId, isCompleted) - }, - modifier = Modifier.padding(top = 10.dp), - ) - } - } - } + RoutineItem( + routine = routine, + onRoutineToggle = onRoutineToggle, + onSubRoutineToggle = onSubRoutineToggle, + modifier = Modifier.fillMaxWidth(), + ) } } @Preview(showBackground = true) @Composable -private fun RoutineTemplatePreview() { - RoutineSection( - routine = RoutineUiModel( - routineId = "uuid1", - routineName = "개운하게 일어나기", - executionTime = "20:30:00", - routineCompletionId = 1, - isCompleted = false, - isModified = false, - subRoutines = listOf( - SubRoutineUiModel( - subRoutineId = "uuid1", - historySeq = 1, - subRoutineName = "물 마시기", - sortOrder = 1, - routineCompletionId = 1, - isCompleted = false, - isModified = false, - routineType = RoutineType.SUB_ROUTINE, - ), - SubRoutineUiModel( - subRoutineId = "uuid2", - historySeq = 1, - subRoutineName = "스트레칭하기", - sortOrder = 1, - routineCompletionId = 1, - isCompleted = false, - isModified = false, - routineType = RoutineType.SUB_ROUTINE, - ), - SubRoutineUiModel( - subRoutineId = "uuid3", - historySeq = 1, - subRoutineName = "심호흡하기", - sortOrder = 1, - routineCompletionId = 1, - isCompleted = false, - isModified = false, - routineType = RoutineType.SUB_ROUTINE, - ), - ), - historySeq = 1, - repeatDay = listOf(), - routineType = RoutineType.ROUTINE, - ), - onRoutineToggle = {}, - onSubRoutineToggle = { _, _ -> }, - onMoreClick = {}, - ) +private fun RoutineSectionPreview() { +// RoutineSection( +// routine = RoutineUiModel( +// routineId = "uuid1", +// routineName = "개운하게 일어나기", +// executionTime = "20:30:00", +// routineCompletionId = 1, +// isCompleted = false, +// isModified = false, +// subRoutines = listOf( +// SubRoutineUiModel( +// subRoutineId = "uuid1", +// historySeq = 1, +// subRoutineName = "물 마시기", +// sortOrder = 1, +// routineCompletionId = 1, +// isCompleted = false, +// isModified = false, +// routineType = RoutineType.SUB_ROUTINE, +// ), +// SubRoutineUiModel( +// subRoutineId = "uuid2", +// historySeq = 1, +// subRoutineName = "스트레칭하기", +// sortOrder = 1, +// routineCompletionId = 1, +// isCompleted = false, +// isModified = false, +// routineType = RoutineType.SUB_ROUTINE, +// ), +// SubRoutineUiModel( +// subRoutineId = "uuid3", +// historySeq = 1, +// subRoutineName = "심호흡하기", +// sortOrder = 1, +// routineCompletionId = 1, +// isCompleted = false, +// isModified = false, +// routineType = RoutineType.SUB_ROUTINE, +// ), +// ), +// historySeq = 1, +// repeatDay = listOf(), +// routineType = RoutineType.ROUTINE, +// ), +// onRoutineToggle = {}, +// onSubRoutineToggle = { _, _ -> }, +// ) } diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/template/RoutineSortBottomSheet.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/template/RoutineSortBottomSheet.kt deleted file mode 100644 index 63b754c7..00000000 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/template/RoutineSortBottomSheet.kt +++ /dev/null @@ -1,134 +0,0 @@ -package com.threegap.bitnagil.presentation.home.component.template - -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.height -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.ModalBottomSheet -import androidx.compose.material3.Text -import androidx.compose.material3.rememberModalBottomSheetState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.threegap.bitnagil.designsystem.BitnagilTheme -import com.threegap.bitnagil.designsystem.R -import com.threegap.bitnagil.designsystem.component.atom.BitnagilIcon -import com.threegap.bitnagil.designsystem.modifier.clickableWithoutRipple -import com.threegap.bitnagil.presentation.home.model.RoutineSortType -import kotlinx.coroutines.launch - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun RoutineSortBottomSheet( - currentSortType: RoutineSortType, - onSortTypeChange: (RoutineSortType) -> Unit, - onDismiss: () -> Unit, - modifier: Modifier = Modifier, -) { - val sheetState = rememberModalBottomSheetState() - val coroutineScope = rememberCoroutineScope() - - ModalBottomSheet( - sheetState = sheetState, - onDismissRequest = onDismiss, - containerColor = BitnagilTheme.colors.white, - contentColor = BitnagilTheme.colors.white, - modifier = modifier, - ) { - SortOption( - currentSortType = currentSortType, - onClick = { sortType -> - onSortTypeChange(sortType) - coroutineScope - .launch { sheetState.hide() } - .invokeOnCompletion { - if (!sheetState.isVisible) { - onDismiss() - } - } - }, - modifier = Modifier - .padding(horizontal = 16.dp) - .padding(bottom = 18.dp), - ) - } -} - -@Composable -private fun SortOption( - currentSortType: RoutineSortType, - onClick: (RoutineSortType) -> Unit, - modifier: Modifier = Modifier, -) { - val sortOptions = listOf( - "완료한 루틴 순" to RoutineSortType.COMPLETED_FIRST, - "미완료한 루틴 순" to RoutineSortType.INCOMPLETE_FIRST, - ) - - Column(modifier = modifier) { - sortOptions.forEachIndexed { index, (text, sortType) -> - SortOptionItem( - text = text, - sortType = sortType, - isSelected = currentSortType == sortType, - onClick = onClick, - ) - - if (index < sortOptions.lastIndex) { - HorizontalDivider( - thickness = 1.dp, - color = BitnagilTheme.colors.coolGray97, - ) - } - } - } -} - -@Composable -private fun SortOptionItem( - text: String, - sortType: RoutineSortType, - isSelected: Boolean, - onClick: (RoutineSortType) -> Unit, - modifier: Modifier = Modifier, -) { - Row( - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - modifier = modifier - .fillMaxWidth() - .padding(vertical = 8.dp) - .height(36.dp) - .clickableWithoutRipple { onClick(sortType) }, - ) { - Text( - text = text, - color = BitnagilTheme.colors.black, - style = BitnagilTheme.typography.body1Regular, - modifier = Modifier.weight(1f), - ) - - if (isSelected) { - BitnagilIcon( - id = R.drawable.ic_check, - tint = BitnagilTheme.colors.orange500, - ) - } - } -} - -@Preview(showBackground = true) -@Composable -private fun SortOptionsPreview() { - SortOption( - currentSortType = RoutineSortType.COMPLETED_FIRST, - onClick = {}, - ) -} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/template/WeeklyDatePicker.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/template/WeeklyDatePicker.kt index 9da34285..45eed46a 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/template/WeeklyDatePicker.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/template/WeeklyDatePicker.kt @@ -1,9 +1,21 @@ package com.threegap.bitnagil.presentation.home.component.template +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.EaseInOutBack +import androidx.compose.animation.core.EaseOut +import androidx.compose.animation.core.EaseOutBounce +import androidx.compose.animation.core.EaseOutQuart +import androidx.compose.animation.core.keyframes +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.scaleIn +import androidx.compose.animation.scaleOut 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.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -23,7 +35,9 @@ import androidx.compose.ui.unit.dp import com.threegap.bitnagil.designsystem.BitnagilTheme import com.threegap.bitnagil.designsystem.R import com.threegap.bitnagil.designsystem.component.atom.BitnagilIcon +import com.threegap.bitnagil.designsystem.component.atom.BitnagilIconButton import com.threegap.bitnagil.designsystem.modifier.clickableWithoutRipple +import com.threegap.bitnagil.presentation.home.model.RoutinesUiModel import com.threegap.bitnagil.presentation.home.util.formatDayOfMonth import com.threegap.bitnagil.presentation.home.util.formatDayOfWeekShort import com.threegap.bitnagil.presentation.home.util.formatMonthYear @@ -34,11 +48,19 @@ import java.time.LocalDate fun WeeklyDatePicker( selectedDate: LocalDate, weeklyDates: List, + routines: RoutinesUiModel, onDateSelect: (LocalDate) -> Unit, onPreviousWeekClick: () -> Unit, onNextWeekClick: () -> Unit, modifier: Modifier = Modifier, ) { + val today = remember { LocalDate.now() } + val completionStates = remember(weeklyDates, routines) { + weeklyDates.associateWith { date -> + routines.routines[date.toString()]?.allCompleted ?: false + } + } + Column( modifier = modifier, ) { @@ -54,36 +76,26 @@ fun WeeklyDatePicker( ) { Text( text = selectedDate.formatMonthYear(), - style = BitnagilTheme.typography.body1SemiBold, + style = BitnagilTheme.typography.title3SemiBold, color = BitnagilTheme.colors.coolGray10, ) Row( verticalAlignment = Alignment.CenterVertically, ) { - Box( - modifier = Modifier - .clickableWithoutRipple(onClick = onPreviousWeekClick) - .padding(14.dp), - contentAlignment = Alignment.Center, - ) { - BitnagilIcon( - id = R.drawable.ic_back_arrow_20, - tint = BitnagilTheme.colors.black, - ) - } + BitnagilIconButton( + id = R.drawable.ic_chevron_left_md, + onClick = onPreviousWeekClick, + paddingValues = PaddingValues(12.dp), + tint = BitnagilTheme.colors.coolGray10, + ) - Box( - modifier = Modifier - .clickableWithoutRipple(onClick = onNextWeekClick) - .padding(14.dp), - contentAlignment = Alignment.Center, - ) { - BitnagilIcon( - id = R.drawable.ic_right_arrow_20, - tint = BitnagilTheme.colors.black, - ) - } + BitnagilIconButton( + id = R.drawable.ic_chevron_right_md, + onClick = onNextWeekClick, + paddingValues = PaddingValues(12.dp), + tint = BitnagilTheme.colors.coolGray10, + ) } } @@ -100,7 +112,8 @@ fun WeeklyDatePicker( DateItem( date = date, isSelected = selectedDate == date, - isToday = date == LocalDate.now(), + isToday = date == today, + isCompleted = completionStates[date] ?: false, onDateClick = { onDateSelect(date) }, ) } @@ -113,6 +126,7 @@ private fun DateItem( date: LocalDate, isSelected: Boolean, isToday: Boolean, + isCompleted: Boolean, onDateClick: () -> Unit, modifier: Modifier = Modifier, ) { @@ -123,8 +137,8 @@ private fun DateItem( ) { Text( text = if (!isToday) date.formatDayOfWeekShort() else "오늘", - style = BitnagilTheme.typography.caption1Medium, - color = if (!isSelected) BitnagilTheme.colors.coolGray70 else BitnagilTheme.colors.navy500, + style = BitnagilTheme.typography.caption1SemiBold, + color = if (!isSelected) BitnagilTheme.colors.coolGray70 else BitnagilTheme.colors.coolGray10, ) Box( @@ -132,16 +146,46 @@ private fun DateItem( modifier = modifier .size(30.dp) .background( - color = if (!isSelected) Color.Transparent else BitnagilTheme.colors.lightBlue100, + color = if (!isSelected) Color.Transparent else BitnagilTheme.colors.coolGray10, shape = RoundedCornerShape(8.dp), ), ) { Text( text = date.formatDayOfMonth(), - style = BitnagilTheme.typography.body2Medium, - color = if (!isSelected) BitnagilTheme.colors.coolGray70 else BitnagilTheme.colors.navy500, + style = if (!isSelected) BitnagilTheme.typography.body2Medium else BitnagilTheme.typography.body2SemiBold, + color = if (!isSelected) BitnagilTheme.colors.coolGray70 else BitnagilTheme.colors.white, ) } + + Column( + modifier = Modifier.size(12.dp), + ) { + AnimatedVisibility( + visible = isCompleted, + enter = scaleIn( + initialScale = 0f, + animationSpec = keyframes { + durationMillis = 600 + 0f at 0 using EaseOutQuart + 1.3f at 300 using EaseInOutBack + 1f at 600 using EaseOutBounce + }, + ) + fadeIn( + animationSpec = tween(300, easing = EaseOut), + ), + exit = scaleOut( + targetScale = 0.8f, + animationSpec = tween(200), + ) + fadeOut( + animationSpec = tween(200), + ), + ) { + BitnagilIcon( + id = R.drawable.ic_routine_success, + tint = null, + ) + } + } } } @@ -152,8 +196,9 @@ private fun WeeklyDatePickerPreview() { WeeklyDatePicker( selectedDate = selectedDate, - onDateSelect = { selectedDate = it }, weeklyDates = selectedDate.getCurrentWeekDays(), + routines = RoutinesUiModel(), + onDateSelect = { selectedDate = it }, onPreviousWeekClick = { selectedDate = selectedDate.minusWeeks(1) }, onNextWeekClick = { selectedDate = selectedDate.plusWeeks(1) }, ) diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/DayRoutinesUiModel.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/DayRoutinesUiModel.kt new file mode 100644 index 00000000..a17726fa --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/DayRoutinesUiModel.kt @@ -0,0 +1,17 @@ +package com.threegap.bitnagil.presentation.home.model + +import android.os.Parcelable +import com.threegap.bitnagil.domain.routine.model.DayRoutines +import kotlinx.parcelize.Parcelize + +@Parcelize +data class DayRoutinesUiModel( + val routineList: List = emptyList(), + val allCompleted: Boolean = false, +) : Parcelable + +fun DayRoutines.toUiModel(): DayRoutinesUiModel = + DayRoutinesUiModel( + routineList = routineList.map { it.toUiModel() }, + allCompleted = allCompleted, + ) diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/EmotionBallType.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/EmotionBallType.kt deleted file mode 100644 index a829f0c0..00000000 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/EmotionBallType.kt +++ /dev/null @@ -1,48 +0,0 @@ -package com.threegap.bitnagil.presentation.home.model - -import androidx.annotation.DrawableRes -import androidx.compose.ui.graphics.Color -import com.threegap.bitnagil.designsystem.R - -enum class EmotionBallType( - @DrawableRes val drawableId: Int, - val ambientColor: Color, - val spotColor: Color, -) { - CALM( - drawableId = R.drawable.calm, - ambientColor = Color(0xFFB987FF).copy(alpha = 0.57f), - spotColor = Color(0xFFB987FF), - ), - VITALITY( - R.drawable.vitality, - ambientColor = Color(0xFF55840F).copy(alpha = 0.25f), - spotColor = Color(0xFF55840F), - ), - LETHARGY( - R.drawable.lethargy, - ambientColor = Color(0xFF000000).copy(alpha = 0.19f), - spotColor = Color(0xFF000000), - ), - ANXIETY( - R.drawable.anxiety, - ambientColor = Color(0xFFDE4C17).copy(alpha = 0.33f), - spotColor = Color(0xFFDE4C17), - ), - SATISFACTION( - R.drawable.satisfaction, - ambientColor = Color(0xFF24846B).copy(alpha = 0.28f), - spotColor = Color(0xFF24846B), - ), - FATIGUE( - R.drawable.fatigue, - ambientColor = Color(0xFFC71A1A).copy(alpha = 0.28f), - spotColor = Color(0xFFC71A1A), - ), - ; - - companion object { - fun fromDomainEmotion(emotionMarbleType: String?): EmotionBallType? = - emotionMarbleType?.let { valueOf(it) } - } -} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/HomeIntent.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/HomeIntent.kt index 883b1533..a15b8f77 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/HomeIntent.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/HomeIntent.kt @@ -6,28 +6,14 @@ import java.time.LocalDate sealed class HomeIntent : MviIntent { data class UpdateLoading(val isLoading: Boolean) : HomeIntent() data class LoadUserProfile(val nickname: String) : HomeIntent() - data class LoadMyEmotion(val emotion: EmotionBallType?) : HomeIntent() + data class LoadTodayEmotion(val emotion: TodayEmotionUiModel?) : HomeIntent() data class LoadWeeklyRoutines(val routines: RoutinesUiModel) : HomeIntent() data class OnDateSelect(val date: LocalDate) : HomeIntent() data class OnRoutineCompletionToggle(val routineId: String, val isCompleted: Boolean) : HomeIntent() - data class OnSubRoutineCompletionToggle(val routineId: String, val subRoutineId: String, val isCompleted: Boolean) : HomeIntent() - data class OnSortTypeChange(val sortType: RoutineSortType) : HomeIntent() - data class DeleteRoutineOptimistically(val routineId: String) : HomeIntent() - data class ConfirmRoutineDeletion(val routineId: String) : HomeIntent() - data class RestoreRoutinesAfterDeleteFailure(val backupRoutines: RoutinesUiModel) : HomeIntent() - data class DeleteRoutineByDayOptimistically(val routineId: String, val performedDate: String) : HomeIntent() - data class ConfirmRoutineByDayDeletion(val routineId: String, val performedDate: String) : HomeIntent() - data class RestoreRoutinesAfterDeleteByDayFailure(val backupRoutines: RoutinesUiModel) : HomeIntent() - data class ShowRoutineDetailsBottomSheet(val routine: RoutineUiModel) : HomeIntent() - data class ShowDeleteConfirmDialog(val routine: RoutineUiModel) : HomeIntent() - data class NavigateToEditRoutine(val routineId: String) : HomeIntent() + data class OnSubRoutineCompletionToggle(val routineId: String, val subRoutineIndex: Int, val isCompleted: Boolean) : HomeIntent() data object RoutineToggleCompletionFailure : HomeIntent() data object OnRegisterEmotionClick : HomeIntent() data object OnRegisterRoutineClick : HomeIntent() data object OnPreviousWeekClick : HomeIntent() data object OnNextWeekClick : HomeIntent() - data object ShowRoutineSortBottomSheet : HomeIntent() - data object HideRoutineSortBottomSheet : HomeIntent() - data object HideRoutineDetailsBottomSheet : HomeIntent() - data object HideDeleteConfirmDialog : HomeIntent() } diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/HomeSideEffect.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/HomeSideEffect.kt index 41db9104..72ab2d75 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/HomeSideEffect.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/HomeSideEffect.kt @@ -5,7 +5,6 @@ import com.threegap.bitnagil.presentation.common.mviviewmodel.MviSideEffect sealed interface HomeSideEffect : MviSideEffect { data class ShowToast(val message: String) : HomeSideEffect data class ShowToastWithIcon(val message: String) : HomeSideEffect - data class NavigateToEditRoutine(val routineId: String) : HomeSideEffect data object NavigateToRegisterRoutine : HomeSideEffect data object NavigateToEmotion : HomeSideEffect } diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/HomeState.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/HomeState.kt index c7f6b0a1..63b7fc66 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/HomeState.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/HomeState.kt @@ -9,24 +9,11 @@ import java.time.LocalDate data class HomeState( val isLoading: Boolean = false, val userNickname: String = "", - val myEmotion: EmotionBallType? = null, + val todayEmotion: TodayEmotionUiModel? = null, val selectedDate: LocalDate = LocalDate.now(), val currentWeeks: List = LocalDate.now().getCurrentWeekDays(), val routines: RoutinesUiModel = RoutinesUiModel(), - val currentSortType: RoutineSortType = RoutineSortType.TIME_ORDER, - val routineSortBottomSheetVisible: Boolean = false, - val routineDetailsBottomSheetVisible: Boolean = false, - val showDeleteConfirmDialog: Boolean = false, - val selectedRoutine: RoutineUiModel? = null, - val deletingRoutine: RoutineUiModel? = null, ) : MviState { val selectedDateRoutines: List - get() { - val routines = routines.routinesByDate[selectedDate.toString()] ?: emptyList() - return when (currentSortType) { - RoutineSortType.TIME_ORDER -> routines.sortedBy { it.executionTime } - RoutineSortType.COMPLETED_FIRST -> routines.sortedByDescending { it.isCompleted } - RoutineSortType.INCOMPLETE_FIRST -> routines.sortedBy { it.isCompleted } - } - } + get() = routines.routines[selectedDate.toString()]?.routineList ?: emptyList() } diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/RoutineSortType.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/RoutineSortType.kt deleted file mode 100644 index 08f215e9..00000000 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/RoutineSortType.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.threegap.bitnagil.presentation.home.model - -enum class RoutineSortType { - TIME_ORDER, - COMPLETED_FIRST, - INCOMPLETE_FIRST, -} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/RoutineUiModel.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/RoutineUiModel.kt index f5bab516..b1059f56 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/RoutineUiModel.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/RoutineUiModel.kt @@ -2,45 +2,32 @@ package com.threegap.bitnagil.presentation.home.model import android.os.Parcelable import com.threegap.bitnagil.domain.routine.model.DayOfWeek +import com.threegap.bitnagil.domain.routine.model.RecommendedRoutineType import com.threegap.bitnagil.domain.routine.model.Routine -import com.threegap.bitnagil.domain.routine.model.RoutineByDayDeletion -import com.threegap.bitnagil.domain.routine.model.RoutineType import kotlinx.parcelize.Parcelize @Parcelize data class RoutineUiModel( val routineId: String, - val historySeq: Int, - val repeatDay: List, val routineName: String, + val repeatDay: List, val executionTime: String, - val subRoutines: List, - val isModified: Boolean = false, - val routineCompletionId: Int?, - val isCompleted: Boolean = false, - val routineType: RoutineType, + val routineDate: String, + val routineCompleteYn: Boolean, + val subRoutineNames: List, + val subRoutineCompleteYn: List, + val recommendedRoutineType: RecommendedRoutineType?, ) : Parcelable fun Routine.toUiModel(): RoutineUiModel = RoutineUiModel( routineId = this.routineId, - historySeq = this.historySeq, - repeatDay = this.repeatDay, routineName = this.routineName, + repeatDay = this.repeatDay, executionTime = this.executionTime, - subRoutines = this.subRoutines.map { it.toUiModel() }, - isModified = this.isModified, - routineCompletionId = this.routineCompletionId, - isCompleted = this.isCompleted, - routineType = this.routineType, - ) - -fun RoutineUiModel.toRoutineByDayDeletion(performedDate: String): RoutineByDayDeletion = - RoutineByDayDeletion( - routineCompletionId = this.routineCompletionId, - routineId = this.routineId, - routineType = this.routineType, - subRoutineInfosForDelete = this.subRoutines.map { it.toSubRoutineDeletionInfo() }, - performedDate = performedDate, - historySeq = this.historySeq, + routineDate = this.routineDate, + routineCompleteYn = this.routineCompleteYn, + subRoutineNames = this.subRoutineNames, + subRoutineCompleteYn = this.subRoutineCompleteYn, + recommendedRoutineType = this.recommendedRoutineType, ) diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/RoutinesUiModel.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/RoutinesUiModel.kt index 31084eb0..928c7f63 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/RoutinesUiModel.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/RoutinesUiModel.kt @@ -6,12 +6,12 @@ import kotlinx.parcelize.Parcelize @Parcelize data class RoutinesUiModel( - val routinesByDate: Map> = emptyMap(), + val routines: Map = emptyMap(), ) : Parcelable fun Routines.toUiModel(): RoutinesUiModel = RoutinesUiModel( - routinesByDate = this.routinesByDate.mapValues { (_, routines) -> - routines.map { it.toUiModel() } + routines = this.routines.mapValues { (_, dayRoutines) -> + dayRoutines.toUiModel() }, ) diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/SubRoutineUiModel.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/SubRoutineUiModel.kt deleted file mode 100644 index ca442a40..00000000 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/SubRoutineUiModel.kt +++ /dev/null @@ -1,37 +0,0 @@ -package com.threegap.bitnagil.presentation.home.model - -import android.os.Parcelable -import com.threegap.bitnagil.domain.routine.model.RoutineType -import com.threegap.bitnagil.domain.routine.model.SubRoutine -import com.threegap.bitnagil.domain.routine.model.SubRoutineDeletionInfo -import kotlinx.parcelize.Parcelize - -@Parcelize -data class SubRoutineUiModel( - val subRoutineId: String, - val historySeq: Int, - val subRoutineName: String, - val sortOrder: Int, - val routineCompletionId: Int?, - val isCompleted: Boolean = false, - val isModified: Boolean = false, - val routineType: RoutineType, -) : Parcelable - -fun SubRoutine.toUiModel(): SubRoutineUiModel = - SubRoutineUiModel( - subRoutineId = this.subRoutineId, - historySeq = this.historySeq, - subRoutineName = this.subRoutineName, - routineCompletionId = this.routineCompletionId, - sortOrder = this.sortOrder, - isCompleted = this.isCompleted, - isModified = this.isModified, - routineType = this.routineType, - ) - -fun SubRoutineUiModel.toSubRoutineDeletionInfo(): SubRoutineDeletionInfo = - SubRoutineDeletionInfo( - routineCompletionId = this.routineCompletionId, - subRoutineId = this.subRoutineId, - ) diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/TodayEmotionUiModel.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/TodayEmotionUiModel.kt new file mode 100644 index 00000000..eaa9e79e --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/TodayEmotionUiModel.kt @@ -0,0 +1,16 @@ +package com.threegap.bitnagil.presentation.home.model + +import android.os.Parcelable +import com.threegap.bitnagil.domain.emotion.model.TodayEmotion +import kotlinx.parcelize.Parcelize + +@Parcelize +data class TodayEmotionUiModel( + val imageUrl: String, + val homeMessage: String, +) : Parcelable + +fun TodayEmotion.toUiModel() = TodayEmotionUiModel( + imageUrl = this.imageUrl, + homeMessage = this.homeMessage, +) diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/util/CollapsibleHeaderState.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/util/CollapsibleHeaderState.kt index cb21052b..979c5df7 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/util/CollapsibleHeaderState.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/util/CollapsibleHeaderState.kt @@ -163,6 +163,6 @@ private fun rememberScrollBehavior( private fun isScrollAtTop(lazyListState: LazyListState): Boolean = lazyListState.firstVisibleItemIndex == 0 && lazyListState.firstVisibleItemScrollOffset == 0 -private const val EXPANDED_HEADER_RATIO = 238f / 800f -private const val COLLAPSED_HEADER_RATIO = 65f / 800f +private const val EXPANDED_HEADER_RATIO = 225f / 722f +private const val COLLAPSED_HEADER_RATIO = 64f / 722f private val SCROLL_BUFFER_DISTANCE = 30.dp diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/util/LocalDateExtension.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/util/LocalDateExtension.kt index e96f976a..beec8cca 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/util/LocalDateExtension.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/util/LocalDateExtension.kt @@ -9,7 +9,7 @@ import java.util.Locale private val koreanLocale = Locale.KOREAN private val monthYearFormatter = DateTimeFormatter.ofPattern("yyyy년 M월", koreanLocale) -private val executionTimeFormatter = DateTimeFormatter.ofPattern("a h:mm", koreanLocale) +private val executionTimeFormatter24 = DateTimeFormatter.ofPattern("HH:mm", koreanLocale) fun LocalDate.getCurrentWeekDays(): List = (0..6).map { this.with(DayOfWeek.MONDAY).plusDays(it.toLong()) } @@ -25,7 +25,12 @@ fun LocalDate.formatDayOfMonth(): String = fun String.formatExecutionTime(): String = try { - LocalTime.parse(this).format(executionTimeFormatter) + val time = LocalTime.parse(this) + if (time == LocalTime.MIDNIGHT) { + "하루\n종일" + } else { + time.format(executionTimeFormatter24) + } } catch (e: Exception) { "시간 미정" } diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/mypage/MyPageScreen.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/mypage/MyPageScreen.kt index edf24a46..3feabcac 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/mypage/MyPageScreen.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/mypage/MyPageScreen.kt @@ -3,6 +3,7 @@ package com.threegap.bitnagil.presentation.mypage import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height @@ -25,10 +26,9 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import com.threegap.bitnagil.designsystem.BitnagilTheme import com.threegap.bitnagil.designsystem.R -import com.threegap.bitnagil.designsystem.component.atom.BitnagilIcon +import com.threegap.bitnagil.designsystem.component.atom.BitnagilIconButton import com.threegap.bitnagil.designsystem.component.block.BitnagilOptionButton import com.threegap.bitnagil.designsystem.component.block.BitnagilTopBar -import com.threegap.bitnagil.designsystem.modifier.clickableWithoutRipple import com.threegap.bitnagil.presentation.mypage.model.MyPageState @Composable @@ -68,11 +68,11 @@ private fun MyPageScreen( BitnagilTopBar( title = "마이페이지", actions = { - BitnagilIcon( + BitnagilIconButton( id = R.drawable.ic_setting, - modifier = Modifier - .clickableWithoutRipple(onClick = onClickSetting) - .padding(6.dp), + onClick = onClickSetting, + tint = null, + paddingValues = PaddingValues(6.dp), ) }, ) diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/RoutineListScreen.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/RoutineListScreen.kt new file mode 100644 index 00000000..19d0b035 --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/RoutineListScreen.kt @@ -0,0 +1,122 @@ +package com.threegap.bitnagil.presentation.routinelist + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.threegap.bitnagil.designsystem.BitnagilTheme +import com.threegap.bitnagil.designsystem.component.block.BitnagilTopBar +import com.threegap.bitnagil.presentation.common.flow.collectAsEffect +import com.threegap.bitnagil.presentation.home.util.getCurrentWeekDays +import com.threegap.bitnagil.presentation.routinelist.component.template.DeleteConfirmBottomSheet +import com.threegap.bitnagil.presentation.routinelist.component.template.RoutineDetailsCard +import com.threegap.bitnagil.presentation.routinelist.component.template.WeeklyDatePicker +import com.threegap.bitnagil.presentation.routinelist.model.RoutineListIntent +import com.threegap.bitnagil.presentation.routinelist.model.RoutineListSideEffect +import com.threegap.bitnagil.presentation.routinelist.model.RoutineListState +import java.time.LocalDate + +@Composable +fun RoutineListScreenContainer( + navigateToBack: () -> Unit, + viewModel: RoutineListViewModel = hiltViewModel(), +) { + val uiState by viewModel.stateFlow.collectAsStateWithLifecycle() + + viewModel.sideEffectFlow.collectAsEffect { sideEffect -> + when (sideEffect) { + is RoutineListSideEffect.NavigateToBack -> navigateToBack() + } + } + + if (uiState.deleteConfirmBottomSheetVisible) { + DeleteConfirmBottomSheet( + onDismissRequest = { viewModel.sendIntent(RoutineListIntent.HideDeleteConfirmBottomSheet) }, + ) + } + + RoutineListScreen( + uiState = uiState, + onDateSelect = { selectedDate -> + viewModel.sendIntent(RoutineListIntent.OnDateSelect(selectedDate)) + }, + onShowDeleteConfirmBottomSheet = { + viewModel.sendIntent(RoutineListIntent.ShowDeleteConfirmBottomSheet) + }, + onBackClick = { + viewModel.sendIntent(RoutineListIntent.NavigateToBack) + }, + ) +} + +@Composable +private fun RoutineListScreen( + uiState: RoutineListState, + onDateSelect: (LocalDate) -> Unit, + onShowDeleteConfirmBottomSheet: () -> Unit, + onBackClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .fillMaxSize() + .background(BitnagilTheme.colors.coolGray99) + .statusBarsPadding(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + BitnagilTopBar( + title = "루틴리스트", + showBackButton = true, + onBackClick = onBackClick, + ) + + Spacer(modifier = Modifier.height(4.dp)) + + WeeklyDatePicker( + selectedDate = uiState.selectedDate, + onDateSelect = onDateSelect, + weeklyDates = uiState.selectedDate.getCurrentWeekDays(), + modifier = Modifier + .padding(vertical = 10.dp, horizontal = 16.dp), + ) + + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + contentPadding = PaddingValues(bottom = 10.dp), + ) { + items(5) { + RoutineDetailsCard( + onDeleteClick = onShowDeleteConfirmBottomSheet, + ) + } + } + } +} + +@Preview +@Composable +private fun RoutineListScreenPreview() { + RoutineListScreen( + uiState = RoutineListState(), + onDateSelect = {}, + onShowDeleteConfirmBottomSheet = {}, + onBackClick = {}, + ) +} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/RoutineListViewModel.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/RoutineListViewModel.kt new file mode 100644 index 00000000..33c710c6 --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/RoutineListViewModel.kt @@ -0,0 +1,38 @@ +package com.threegap.bitnagil.presentation.routinelist + +import androidx.lifecycle.SavedStateHandle +import com.threegap.bitnagil.domain.routine.usecase.FetchWeeklyRoutinesUseCase +import com.threegap.bitnagil.presentation.common.mviviewmodel.MviViewModel +import com.threegap.bitnagil.presentation.routinelist.model.RoutineListIntent +import com.threegap.bitnagil.presentation.routinelist.model.RoutineListSideEffect +import com.threegap.bitnagil.presentation.routinelist.model.RoutineListState +import dagger.hilt.android.lifecycle.HiltViewModel +import org.orbitmvi.orbit.syntax.simple.SimpleSyntax +import javax.inject.Inject + +@HiltViewModel +class RoutineListViewModel @Inject constructor( + savedStateHandle: SavedStateHandle, + private val fetchWeeklyRoutinesUseCase: FetchWeeklyRoutinesUseCase, +) : MviViewModel( + savedStateHandle = savedStateHandle, + initState = RoutineListState(), +) { + override suspend fun SimpleSyntax.reduceState( + intent: RoutineListIntent, + state: RoutineListState, + ): RoutineListState? { + val newState = when (intent) { + is RoutineListIntent.OnDateSelect -> state.copy(selectedDate = intent.date) + is RoutineListIntent.ShowDeleteConfirmBottomSheet -> state.copy(deleteConfirmBottomSheetVisible = true) + is RoutineListIntent.HideDeleteConfirmBottomSheet -> state.copy(deleteConfirmBottomSheetVisible = false) + + is RoutineListIntent.NavigateToBack -> { + sendSideEffect(RoutineListSideEffect.NavigateToBack) + null + } + } + + return newState + } +} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/component/template/DeleteConfirmBottomSheet.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/component/template/DeleteConfirmBottomSheet.kt new file mode 100644 index 00000000..72f066c6 --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/component/template/DeleteConfirmBottomSheet.kt @@ -0,0 +1,139 @@ +package com.threegap.bitnagil.presentation.routinelist.component.template + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.threegap.bitnagil.designsystem.BitnagilTheme +import com.threegap.bitnagil.designsystem.component.atom.BitnagilTextButton +import com.threegap.bitnagil.designsystem.component.atom.BitnagilTextButtonColor + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun DeleteConfirmBottomSheet( + onDismissRequest: () -> Unit, + modifier: Modifier = Modifier, +) { + ModalBottomSheet( + modifier = modifier, + onDismissRequest = onDismissRequest, + contentColor = BitnagilTheme.colors.white, + containerColor = BitnagilTheme.colors.white, + ) { + RepeatRoutineDeleteContent( + modifier = Modifier + .padding(horizontal = 24.dp) + .padding(bottom = 26.dp), + ) + } +} + +@Composable +fun RepeatRoutineDeleteContent( + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier, + ) { + Text( + text = "이 루틴은 반복 설정되어 있어요", + color = BitnagilTheme.colors.coolGray10, + style = BitnagilTheme.typography.title3SemiBold, + ) + + Spacer(modifier = Modifier.height(10.dp)) + + Text( + text = "오늘만 삭제하거나, 전체 반복 일정에서 모두 삭제할 수\n있습니다. 삭제한 루틴은 되돌릴 수 없어요.", + color = BitnagilTheme.colors.coolGray40, + style = BitnagilTheme.typography.body2Medium, + modifier = Modifier + .padding(end = 40.dp) + .fillMaxWidth(), + ) + + Spacer(modifier = Modifier.height(24.dp)) + + BitnagilTextButton( + text = "오늘만 삭제", + onClick = { /*TODO*/ }, + modifier = Modifier.fillMaxWidth(), + textStyle = BitnagilTheme.typography.body2Medium, + ) + + Spacer(modifier = Modifier.height(12.dp)) + + BitnagilTextButton( + text = "모든 날짜에서 삭제", + onClick = { /*TODO*/ }, + colors = BitnagilTextButtonColor.delete(), + modifier = Modifier.fillMaxWidth(), + textStyle = BitnagilTheme.typography.body2Medium, + ) + } +} + +@Composable +fun SingleRoutineDeleteContent( + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier, + ) { + Text( + text = "루틴을 삭제하시겠어요?", + color = BitnagilTheme.colors.coolGray10, + style = BitnagilTheme.typography.title3SemiBold, + ) + + Spacer(modifier = Modifier.height(10.dp)) + + Text( + text = "이 루틴과 관련된 모든 기록이 함께 삭제되며, 삭제 후에는\n되돌릴 수 없습니다. 정말 삭제하시겠어요?", + color = BitnagilTheme.colors.coolGray40, + style = BitnagilTheme.typography.body2Medium, + modifier = Modifier + .padding(end = 40.dp) + .fillMaxWidth(), + ) + + Spacer(modifier = Modifier.height(24.dp)) + + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + BitnagilTextButton( + text = "취소", + onClick = { /*TODO*/ }, + colors = BitnagilTextButtonColor.cancel(), + modifier = Modifier.weight(1f), + textStyle = BitnagilTheme.typography.body2Medium, + ) + + BitnagilTextButton( + text = "모든 날짜에서 삭제", + onClick = { /*TODO*/ }, + modifier = Modifier.weight(1f), + textStyle = BitnagilTheme.typography.body2Medium, + ) + } + } +} + +@Preview +@Composable +private fun DeleteConfirmBottomSheetPreview() { + DeleteConfirmBottomSheet( + onDismissRequest = {}, + ) +} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/component/template/EmptyRoutineListView.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/component/template/EmptyRoutineListView.kt new file mode 100644 index 00000000..052ddeb8 --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/component/template/EmptyRoutineListView.kt @@ -0,0 +1,71 @@ +package com.threegap.bitnagil.presentation.routinelist.component.template + +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.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.threegap.bitnagil.designsystem.BitnagilTheme +import com.threegap.bitnagil.designsystem.modifier.clickableWithoutRipple + +@Composable +fun EmptyRoutineListView( + onRegisterRoutineClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + modifier = modifier, + ) { + Text( + text = "등록한 루틴이 없어요", + style = BitnagilTheme.typography.subtitle1SemiBold, + color = BitnagilTheme.colors.coolGray30, + modifier = Modifier.height(28.dp), + ) + + Text( + text = "루틴을 등록하고, 작은 변화부터 시작해보세요!", + style = BitnagilTheme.typography.body2Regular, + color = BitnagilTheme.colors.coolGray70, + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Box( + modifier = Modifier + .background( + color = BitnagilTheme.colors.coolGray96, + shape = RoundedCornerShape(8.dp), + ) + .clickableWithoutRipple { onRegisterRoutineClick() } + .padding( + vertical = 10.dp, + horizontal = 14.dp, + ), + contentAlignment = Alignment.Center, + ) { + Text( + text = "루틴 등록하기", + style = BitnagilTheme.typography.caption1SemiBold, + color = BitnagilTheme.colors.coolGray30, + ) + } + } +} + +@Preview +@Composable +private fun EmptyRoutineListViewPreview() { + EmptyRoutineListView(onRegisterRoutineClick = {}) +} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/component/template/RoutineDetailsCard.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/component/template/RoutineDetailsCard.kt new file mode 100644 index 00000000..4c8607fe --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/component/template/RoutineDetailsCard.kt @@ -0,0 +1,147 @@ +package com.threegap.bitnagil.presentation.routinelist.component.template + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +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.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.threegap.bitnagil.designsystem.BitnagilTheme +import com.threegap.bitnagil.designsystem.R +import com.threegap.bitnagil.designsystem.component.atom.BitnagilIcon +import com.threegap.bitnagil.designsystem.component.atom.BitnagilIconButton + +@Composable +fun RoutineDetailsCard( + onDeleteClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .background( + color = BitnagilTheme.colors.white, + shape = RoundedCornerShape(12.dp), + ) + .fillMaxWidth() + .padding(vertical = 14.dp), + ) { + Row( + modifier = Modifier + .padding(start = 16.dp, end = 2.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + BitnagilIcon( + id = R.drawable.ic_wakeup, + tint = null, + modifier = Modifier + .background( + color = BitnagilTheme.colors.orange25, + shape = RoundedCornerShape(4.dp), + ) + .padding(4.dp), + ) + + Text( + text = "개운하게 일어나기", + color = BitnagilTheme.colors.coolGray10, + style = BitnagilTheme.typography.body1SemiBold, + modifier = Modifier.padding(start = 10.dp), + ) + + Spacer(modifier = Modifier.weight(1f)) + + BitnagilIconButton( + id = R.drawable.ic_edit, + onClick = { /*TODO*/ }, + tint = null, + paddingValues = PaddingValues(12.dp), + ) + + BitnagilIconButton( + id = R.drawable.ic_trash, + onClick = onDeleteClick, + tint = null, + paddingValues = PaddingValues(12.dp), + ) + } + + HorizontalDivider( + thickness = 1.dp, + color = BitnagilTheme.colors.coolGray97, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 10.dp), + ) + + Column( + modifier = Modifier + .padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + Text( + text = "세부 루틴", + color = BitnagilTheme.colors.coolGray40, + style = BitnagilTheme.typography.body2Medium, + ) + Text( + text = "• 어쩌구", + color = BitnagilTheme.colors.coolGray40, + style = BitnagilTheme.typography.body2Medium, + ) + Text( + text = "• 어쩌구", + color = BitnagilTheme.colors.coolGray40, + style = BitnagilTheme.typography.body2Medium, + ) + Text( + text = "• 어쩌구", + color = BitnagilTheme.colors.coolGray40, + style = BitnagilTheme.typography.body2Medium, + ) + } + + HorizontalDivider( + thickness = 1.dp, + color = BitnagilTheme.colors.coolGray97, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 10.dp), + ) + + Column( + modifier = Modifier + .padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + Text( + text = "반복:", + color = BitnagilTheme.colors.coolGray40, + style = BitnagilTheme.typography.body2Medium, + ) + Text( + text = "기간:", + color = BitnagilTheme.colors.coolGray40, + style = BitnagilTheme.typography.body2Medium, + ) + Text( + text = "시간:", + color = BitnagilTheme.colors.coolGray40, + style = BitnagilTheme.typography.body2Medium, + ) + } + } +} + +@Preview +@Composable +private fun RoutineDetailsCardPreview() { + RoutineDetailsCard( + onDeleteClick = {}, + ) +} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/component/template/WeeklyDatePicker.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/component/template/WeeklyDatePicker.kt new file mode 100644 index 00000000..05a79a39 --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/component/template/WeeklyDatePicker.kt @@ -0,0 +1,101 @@ +package com.threegap.bitnagil.presentation.routinelist.component.template + +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.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +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.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.threegap.bitnagil.designsystem.BitnagilTheme +import com.threegap.bitnagil.designsystem.modifier.clickableWithoutRipple +import com.threegap.bitnagil.presentation.home.util.formatDayOfMonth +import com.threegap.bitnagil.presentation.home.util.formatDayOfWeekShort +import com.threegap.bitnagil.presentation.home.util.getCurrentWeekDays +import java.time.LocalDate + +@Composable +fun WeeklyDatePicker( + selectedDate: LocalDate, + weeklyDates: List, + onDateSelect: (LocalDate) -> Unit, + modifier: Modifier = Modifier, +) { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + modifier = modifier + .fillMaxWidth(), + ) { + weeklyDates.forEach { date -> + DateItem( + date = date, + isSelected = selectedDate == date, + isToday = date == LocalDate.now(), + onDateClick = { onDateSelect(date) }, + modifier = Modifier.padding(bottom = 18.dp), + ) + } + } +} + +@Composable +private fun DateItem( + date: LocalDate, + isSelected: Boolean, + isToday: Boolean, + onDateClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Column( + verticalArrangement = Arrangement.spacedBy(7.dp), + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.clickableWithoutRipple { onDateClick() }, + ) { + Text( + text = if (!isToday) date.formatDayOfWeekShort() else "오늘", + style = if (!isSelected) BitnagilTheme.typography.caption1Medium else BitnagilTheme.typography.caption1SemiBold, + color = if (!isSelected) BitnagilTheme.colors.coolGray70 else BitnagilTheme.colors.coolGray10, + ) + + Box( + contentAlignment = Alignment.Center, + modifier = modifier + .size(30.dp) + .background( + color = if (!isSelected) Color.Transparent else BitnagilTheme.colors.coolGray10, + shape = RoundedCornerShape(8.dp), + ), + ) { + Text( + text = date.formatDayOfMonth(), + style = if (!isSelected) BitnagilTheme.typography.body2Medium else BitnagilTheme.typography.body2SemiBold, + color = if (!isSelected) BitnagilTheme.colors.coolGray70 else BitnagilTheme.colors.white, + ) + } + } +} + +@Preview(showBackground = true) +@Composable +private fun WeeklyDatePickerPreview() { + var selectedDate by remember { mutableStateOf(LocalDate.now()) } + + WeeklyDatePicker( + selectedDate = selectedDate, + onDateSelect = { selectedDate = it }, + weeklyDates = selectedDate.getCurrentWeekDays(), + ) +} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/model/RoutineListIntent.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/model/RoutineListIntent.kt new file mode 100644 index 00000000..bc89c673 --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/model/RoutineListIntent.kt @@ -0,0 +1,11 @@ +package com.threegap.bitnagil.presentation.routinelist.model + +import com.threegap.bitnagil.presentation.common.mviviewmodel.MviIntent +import java.time.LocalDate + +sealed class RoutineListIntent : MviIntent { + data class OnDateSelect(val date: LocalDate) : RoutineListIntent() + data object ShowDeleteConfirmBottomSheet : RoutineListIntent() + data object HideDeleteConfirmBottomSheet : RoutineListIntent() + data object NavigateToBack : RoutineListIntent() +} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/model/RoutineListSideEffect.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/model/RoutineListSideEffect.kt new file mode 100644 index 00000000..33ec9ea6 --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/model/RoutineListSideEffect.kt @@ -0,0 +1,7 @@ +package com.threegap.bitnagil.presentation.routinelist.model + +import com.threegap.bitnagil.presentation.common.mviviewmodel.MviSideEffect + +sealed interface RoutineListSideEffect : MviSideEffect { + data object NavigateToBack : RoutineListSideEffect +} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/model/RoutineListState.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/model/RoutineListState.kt new file mode 100644 index 00000000..e3d53d81 --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/model/RoutineListState.kt @@ -0,0 +1,11 @@ +package com.threegap.bitnagil.presentation.routinelist.model + +import com.threegap.bitnagil.presentation.common.mviviewmodel.MviState +import kotlinx.parcelize.Parcelize +import java.time.LocalDate + +@Parcelize +data class RoutineListState( + val selectedDate: LocalDate = LocalDate.now(), + val deleteConfirmBottomSheetVisible: Boolean = false, +) : MviState 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 4460cc71..6794af98 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 @@ -1,7 +1,6 @@ package com.threegap.bitnagil.presentation.setting import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -27,9 +26,11 @@ import androidx.hilt.navigation.compose.hiltViewModel import com.threegap.bitnagil.designsystem.BitnagilTheme import com.threegap.bitnagil.designsystem.component.block.BitnagilOptionButton import com.threegap.bitnagil.designsystem.component.block.BitnagilTopBar +import com.threegap.bitnagil.designsystem.modifier.clickableWithoutRipple 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.block.ConfirmDialog +import com.threegap.bitnagil.presentation.setting.component.block.LogoutConfirmDialog +import com.threegap.bitnagil.presentation.setting.model.mvi.SettingIntent import com.threegap.bitnagil.presentation.setting.model.mvi.SettingSideEffect import com.threegap.bitnagil.presentation.setting.model.mvi.SettingState @@ -40,20 +41,21 @@ fun SettingScreenContainer( navigateToTermsOfService: () -> Unit, navigateToPrivacyPolicy: () -> Unit, navigateToLogin: () -> Unit, + navigateToWithdrawal: () -> Unit, ) { val state by viewModel.stateFlow.collectAsState() viewModel.sideEffectFlow.collectAsEffect { sideEffect -> when (sideEffect) { SettingSideEffect.NavigateToLogin -> navigateToLogin() + SettingSideEffect.NavigateToWithdrawal -> navigateToWithdrawal() } } - state.showConfirmDialog?.let { dialogType -> - ConfirmDialog( - type = dialogType, + if (state.logoutConfirmDialogVisible) { + LogoutConfirmDialog( onDismiss = viewModel::hideConfirmDialog, - onConfirm = viewModel::confirmDialogAction, + onConfirm = viewModel::logout, ) } @@ -66,7 +68,7 @@ fun SettingScreenContainer( onClickTermsOfService = navigateToTermsOfService, onClickPrivacyPolicy = navigateToPrivacyPolicy, onClickLogout = viewModel::showLogoutDialog, - onClickWithdrawal = viewModel::showWithdrawalDialog, + onClickWithdrawal = { viewModel.sendIntent(SettingIntent.OnWithdrawalClick) }, ) } @@ -116,7 +118,7 @@ private fun SettingScreen( Text( text = "버전 ", color = BitnagilTheme.colors.black, - style = BitnagilTheme.typography.body1Regular, + style = BitnagilTheme.typography.body1Medium, ) Text( text = state.version, @@ -124,30 +126,20 @@ private fun SettingScreen( style = BitnagilTheme.typography.body1SemiBold, ) } - if (state.version == state.latestVersion) { - Text( - "최신", - modifier = Modifier - .background( - color = BitnagilTheme.colors.coolGray98, - shape = RoundedCornerShape(4.dp), - ) - .padding(horizontal = 10.dp, vertical = 5.dp), - style = BitnagilTheme.typography.button2.copy(color = BitnagilTheme.colors.coolGray70), - ) - } else { - Text( - "업데이트", - modifier = Modifier - .background( - color = BitnagilTheme.colors.lightBlue200, - shape = RoundedCornerShape(4.dp), - ) - .clickable(onClick = onClickUpdate) - .padding(horizontal = 10.dp, vertical = 5.dp), - style = BitnagilTheme.typography.button2.copy(color = BitnagilTheme.colors.navy500), - ) - } + + val isLatest = state.version == state.latestVersion + Text( + text = if (isLatest) "최신" else "업데이트", + color = if (isLatest) BitnagilTheme.colors.coolGray70 else BitnagilTheme.colors.orange500, + style = BitnagilTheme.typography.button2, + modifier = Modifier + .background( + color = if (isLatest) BitnagilTheme.colors.coolGray98 else BitnagilTheme.colors.orange50, + shape = RoundedCornerShape(8.dp), + ) + .let { if (!isLatest) it.clickableWithoutRipple(onClick = onClickUpdate) else it } + .padding(horizontal = 10.dp, vertical = 5.dp), + ) } BitnagilOptionButton( @@ -191,7 +183,7 @@ fun SettingScreenPreview() { version = "1.0.1", latestVersion = "1.0.0", loading = false, - showConfirmDialog = null, + logoutConfirmDialogVisible = false, ), toggleServiceAlarm = {}, togglePushAlarm = {}, 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 539a00cb..9dba8c28 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 @@ -3,9 +3,7 @@ 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.ConfirmDialogType import com.threegap.bitnagil.presentation.setting.model.mvi.SettingIntent import com.threegap.bitnagil.presentation.setting.model.mvi.SettingSideEffect import com.threegap.bitnagil.presentation.setting.model.mvi.SettingState @@ -20,7 +18,6 @@ import javax.inject.Inject class SettingViewModel @Inject constructor( savedStateHandle: SavedStateHandle, private val logoutUseCase: LogoutUseCase, - private val withdrawalUseCase: WithdrawalUseCase, ) : MviViewModel( initState = SettingState.Init, savedStateHandle = savedStateHandle, @@ -48,58 +45,56 @@ class SettingViewModel @Inject constructor( return state.copy(useServiceAlarm = !state.useServiceAlarm) } - is SettingIntent.ShowConfirmDialog -> { - return state.copy(showConfirmDialog = intent.type) + is SettingIntent.ShowLogoutConfirmDialog -> { + return state.copy(logoutConfirmDialogVisible = true) } is SettingIntent.HideConfirmDialog -> { - return state.copy(showConfirmDialog = null) + return state.copy(logoutConfirmDialogVisible = false) } SettingIntent.LogoutSuccess -> { sendSideEffect(SettingSideEffect.NavigateToLogin) return null } + SettingIntent.LogoutLoading -> { return state.copy(loading = true) } + SettingIntent.LogoutFailure -> { return state.copy(loading = false) } - SettingIntent.WithdrawalSuccess -> { - sendSideEffect(SettingSideEffect.NavigateToLogin) + + SettingIntent.OnWithdrawalClick -> { + sendSideEffect(SettingSideEffect.NavigateToWithdrawal) return null } - SettingIntent.WithdrawalLoading -> { - return state.copy(loading = true) - } - SettingIntent.WithdrawalFailure -> { - return state.copy(loading = false) - } } } fun showLogoutDialog() { - sendIntent(SettingIntent.ShowConfirmDialog(ConfirmDialogType.LOGOUT)) - } - - fun showWithdrawalDialog() { - sendIntent(SettingIntent.ShowConfirmDialog(ConfirmDialogType.WITHDRAW)) + sendIntent(SettingIntent.ShowLogoutConfirmDialog) } fun hideConfirmDialog() { sendIntent(SettingIntent.HideConfirmDialog) } - fun confirmDialogAction() { - val currentDialogType = container.stateFlow.value.showConfirmDialog + fun logout() { + if (container.stateFlow.value.loading) return sendIntent(SettingIntent.HideConfirmDialog) - - when (currentDialogType) { - ConfirmDialogType.LOGOUT -> executeLogout() - ConfirmDialogType.WITHDRAW -> executeWithdrawal() - null -> {} + sendIntent(SettingIntent.LogoutLoading) + viewModelScope.launch { + logoutUseCase().fold( + onSuccess = { + sendIntent(SettingIntent.LogoutSuccess) + }, + onFailure = { + sendIntent(SettingIntent.LogoutFailure) + }, + ) } } @@ -118,26 +113,4 @@ class SettingViewModel @Inject constructor( delay(1000L) } } - - private fun executeLogout() { - viewModelScope.launch { - sendIntent(SettingIntent.LogoutLoading) - logoutUseCase().onSuccess { - sendIntent(SettingIntent.LogoutSuccess) - }.onFailure { - sendIntent(SettingIntent.LogoutFailure) - } - } - } - - private fun executeWithdrawal() { - 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/component/atom/settingtitle/SettingTitle.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/setting/component/atom/settingtitle/SettingTitle.kt index 53570202..529122f9 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/setting/component/atom/settingtitle/SettingTitle.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/setting/component/atom/settingtitle/SettingTitle.kt @@ -15,6 +15,6 @@ fun SettingTitle( Text( title, modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 3.dp), - style = BitnagilTheme.typography.caption1SemiBold.copy(color = BitnagilTheme.colors.coolGray50), + style = BitnagilTheme.typography.caption1SemiBold.copy(color = BitnagilTheme.colors.coolGray60), ) } diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/setting/component/block/ConfirmDialog.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/setting/component/block/LogoutConfirmDialog.kt similarity index 55% rename from presentation/src/main/java/com/threegap/bitnagil/presentation/setting/component/block/ConfirmDialog.kt rename to presentation/src/main/java/com/threegap/bitnagil/presentation/setting/component/block/LogoutConfirmDialog.kt index 6de79180..efe64301 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/setting/component/block/ConfirmDialog.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/setting/component/block/LogoutConfirmDialog.kt @@ -1,35 +1,28 @@ package com.threegap.bitnagil.presentation.setting.component.block import androidx.compose.foundation.background -import androidx.compose.foundation.border import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.BasicAlertDialog import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.window.DialogProperties import com.threegap.bitnagil.designsystem.BitnagilTheme -import com.threegap.bitnagil.designsystem.R -import com.threegap.bitnagil.designsystem.component.atom.BitnagilIcon import com.threegap.bitnagil.designsystem.component.atom.BitnagilTextButton import com.threegap.bitnagil.designsystem.component.atom.BitnagilTextButtonColor -import com.threegap.bitnagil.presentation.setting.model.ConfirmDialogType @OptIn(ExperimentalMaterial3Api::class) @Composable -fun ConfirmDialog( - type: ConfirmDialogType, +fun LogoutConfirmDialog( onDismiss: () -> Unit, onConfirm: () -> Unit, modifier: Modifier = Modifier, @@ -46,57 +39,43 @@ fun ConfirmDialog( modifier = Modifier .background( color = BitnagilTheme.colors.white, - shape = RoundedCornerShape(20.dp), + shape = RoundedCornerShape(12.dp), ) - .padding(vertical = 24.dp, horizontal = 16.dp), + .padding(vertical = 20.dp, horizontal = 24.dp), verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, ) { - BitnagilIcon( - id = R.drawable.ic_modal_warning, - tint = null, - modifier = Modifier.size(55.dp), - ) - - Spacer(modifier = Modifier.height(18.dp)) - Text( - text = type.titleText, + text = "로그아웃할까요?", color = BitnagilTheme.colors.coolGray10, - style = BitnagilTheme.typography.title2Bold, + style = BitnagilTheme.typography.subtitle1SemiBold, ) - Spacer(modifier = Modifier.height(2.dp)) + Spacer(modifier = Modifier.height(6.dp)) Text( - text = type.descriptionText, - color = BitnagilTheme.colors.coolGray10, - style = BitnagilTheme.typography.caption1Regular, + text = "이 기기에서 계정이 로그아웃되고, 다시\n로그인해야 서비스를 계속 이용할 수 있어요.", + color = BitnagilTheme.colors.coolGray40, + style = BitnagilTheme.typography.body2Medium, ) Spacer(modifier = Modifier.height(18.dp)) Row( - modifier = Modifier.height(44.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.height(48.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), ) { BitnagilTextButton( text = "취소", onClick = onDismiss, - colors = BitnagilTextButtonColor.skip(), - modifier = Modifier - .weight(1f) - .border( - width = 1.dp, - color = BitnagilTheme.colors.navy500, - shape = RoundedCornerShape(8.dp), - ), + colors = BitnagilTextButtonColor.cancel(), + textStyle = BitnagilTheme.typography.body2Medium, + modifier = Modifier.weight(1f), ) BitnagilTextButton( - text = type.confirmButtonText, + text = "로그아웃", onClick = onConfirm, - shape = RoundedCornerShape(8.dp), + textStyle = BitnagilTheme.typography.body2Medium, modifier = Modifier.weight(1f), ) } @@ -106,9 +85,8 @@ fun ConfirmDialog( @Preview @Composable -private fun ConfirmDialogPreview() { - ConfirmDialog( - type = ConfirmDialogType.LOGOUT, +private fun LogoutConfirmDialogPreview() { + LogoutConfirmDialog( onDismiss = {}, onConfirm = {}, ) diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/setting/model/ConfirmDialogType.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/setting/model/ConfirmDialogType.kt deleted file mode 100644 index c26c4cbd..00000000 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/setting/model/ConfirmDialogType.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.threegap.bitnagil.presentation.setting.model - -enum class ConfirmDialogType( - val titleText: String, - val descriptionText: String, - val confirmButtonText: String, -) { - LOGOUT( - titleText = "로그아웃 하시겠어요?", - descriptionText = "버튼을 누르면 로그인 페이지로 이동해요.", - confirmButtonText = "로그아웃", - ), - WITHDRAW( - titleText = "정말 탈퇴하시겠어요?", - descriptionText = "소중한 기록들이 모두 사라져요.", - confirmButtonText = "탈퇴하기", - ), -} 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 9438f5d4..97b77f65 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 @@ -1,7 +1,6 @@ package com.threegap.bitnagil.presentation.setting.model.mvi import com.threegap.bitnagil.presentation.common.mviviewmodel.MviIntent -import com.threegap.bitnagil.presentation.setting.model.ConfirmDialogType sealed class SettingIntent : MviIntent { data class LoadSettingSuccess( @@ -11,14 +10,12 @@ sealed class SettingIntent : MviIntent { val latestVersion: String, ) : SettingIntent() - data class ShowConfirmDialog(val type: ConfirmDialogType) : SettingIntent() + data object ShowLogoutConfirmDialog : SettingIntent() data object HideConfirmDialog : SettingIntent() 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() + data object OnWithdrawalClick : 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 cb457668..2502f31a 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 @@ -4,4 +4,5 @@ import com.threegap.bitnagil.presentation.common.mviviewmodel.MviSideEffect sealed class SettingSideEffect : MviSideEffect { data object NavigateToLogin : SettingSideEffect() + data object NavigateToWithdrawal : 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 a0b7166b..47b30630 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 @@ -1,7 +1,6 @@ package com.threegap.bitnagil.presentation.setting.model.mvi import com.threegap.bitnagil.presentation.common.mviviewmodel.MviState -import com.threegap.bitnagil.presentation.setting.model.ConfirmDialogType import kotlinx.parcelize.Parcelize @Parcelize @@ -11,7 +10,7 @@ data class SettingState( val version: String, val latestVersion: String, val loading: Boolean, - val showConfirmDialog: ConfirmDialogType?, + val logoutConfirmDialogVisible: Boolean, ) : MviState { companion object { val Init = SettingState( @@ -20,7 +19,7 @@ data class SettingState( version = "", latestVersion = "", loading = false, - showConfirmDialog = null, + logoutConfirmDialogVisible = false, ) } } diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/withdrawal/WithdrawalScreen.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/withdrawal/WithdrawalScreen.kt new file mode 100644 index 00000000..89737ec4 --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/withdrawal/WithdrawalScreen.kt @@ -0,0 +1,237 @@ +package com.threegap.bitnagil.presentation.withdrawal + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.ime +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.verticalScroll +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.draw.alpha +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.threegap.bitnagil.designsystem.BitnagilTheme +import com.threegap.bitnagil.designsystem.R +import com.threegap.bitnagil.designsystem.component.atom.BitnagilIcon +import com.threegap.bitnagil.designsystem.component.atom.BitnagilSelectButton +import com.threegap.bitnagil.designsystem.component.atom.BitnagilSelectButtonColor +import com.threegap.bitnagil.designsystem.component.atom.BitnagilTextButton +import com.threegap.bitnagil.designsystem.component.block.BitnagilTopBar +import com.threegap.bitnagil.designsystem.modifier.clickableWithoutRipple +import com.threegap.bitnagil.presentation.common.flow.collectAsEffect +import com.threegap.bitnagil.presentation.withdrawal.component.WithdrawalConfirmDialog +import com.threegap.bitnagil.presentation.withdrawal.model.WithdrawalIntent +import com.threegap.bitnagil.presentation.withdrawal.model.WithdrawalReason +import com.threegap.bitnagil.presentation.withdrawal.model.WithdrawalSideEffect +import com.threegap.bitnagil.presentation.withdrawal.model.WithdrawalState + +@Composable +fun WithdrawalScreenContainer( + navigateToBack: () -> Unit, + navigateToLogin: () -> Unit, + viewModel: WithdrawalViewModel = hiltViewModel(), +) { + val uiState by viewModel.container.stateFlow.collectAsStateWithLifecycle() + + viewModel.sideEffectFlow.collectAsEffect { sideEffect -> + when (sideEffect) { + is WithdrawalSideEffect.NavigateToBack -> navigateToBack() + is WithdrawalSideEffect.NavigateToLogin -> navigateToLogin() + } + } + + if (uiState.showSuccessDialog) { + WithdrawalConfirmDialog( + onConfirm = { viewModel.sendIntent(WithdrawalIntent.OnSuccessDialogConfirm) }, + ) + } + + WithdrawalScreen( + uiState = uiState, + onTermsToggle = { viewModel.sendIntent(WithdrawalIntent.OnTermsToggle) }, + onReasonSelect = { viewModel.sendIntent(WithdrawalIntent.OnReasonSelected(it)) }, + onCustomReasonChanged = { viewModel.sendIntent(WithdrawalIntent.OnCustomReasonChanged(it)) }, + onBackClick = { viewModel.sendIntent(WithdrawalIntent.OnBackClick) }, + onWithdrawalClick = viewModel::withdrawal, + ) +} + +@Composable +private fun WithdrawalScreen( + uiState: WithdrawalState, + onTermsToggle: () -> Unit, + onReasonSelect: (WithdrawalReason?) -> Unit, + onCustomReasonChanged: (String) -> Unit, + onBackClick: () -> Unit, + onWithdrawalClick: () -> Unit, +) { + val focusManager = LocalFocusManager.current + val focusRequester = remember { FocusRequester() } + val scrollState = rememberScrollState() + + Column( + modifier = Modifier + .fillMaxSize() + .background(BitnagilTheme.colors.white) + .statusBarsPadding() + .windowInsetsPadding(WindowInsets.ime), + ) { + BitnagilTopBar( + title = "탈퇴하기", + showBackButton = true, + onBackClick = onBackClick, + ) + + Column( + modifier = Modifier + .padding(horizontal = 16.dp) + .weight(1f) + .verticalScroll(scrollState), + ) { + Spacer(modifier = Modifier.height(46.dp)) + + Text( + text = "정말 탈퇴하시겠어요?", + color = BitnagilTheme.colors.coolGray10, + style = BitnagilTheme.typography.title3SemiBold, + modifier = Modifier.padding(bottom = 5.dp), + ) + + Text( + text = "탈퇴하면 보관 중인 데이터와 서비스 이용 내역이\n모두 삭제되고, 다시 가입해도 복구되지 않아요.", + color = BitnagilTheme.colors.coolGray50, + style = BitnagilTheme.typography.body1Medium, + ) + + Spacer(modifier = Modifier.height(26.dp)) + + Row( + modifier = Modifier + .fillMaxWidth() + .clickableWithoutRipple { onTermsToggle() }, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(10.dp), + ) { + BitnagilIcon( + id = if (uiState.isTermsChecked) R.drawable.ic_check_circle else R.drawable.ic_check_default, + tint = null, + ) + + Text( + text = "유의사항을 확인했어요.", + color = BitnagilTheme.colors.coolGray40, + style = BitnagilTheme.typography.body2Medium, + ) + } + + Spacer(modifier = Modifier.height(48.dp)) + + Column( + modifier = Modifier + .alpha(if (uiState.isTermsChecked) 1f else 0f), + ) { + Text( + text = "탈퇴 사유를 알려주실 수 있나요?", + color = BitnagilTheme.colors.coolGray10, + style = BitnagilTheme.typography.title3SemiBold, + modifier = Modifier.padding(bottom = 16.dp), + ) + + WithdrawalReason.entries.forEach { reason -> + BitnagilSelectButton( + title = reason.displayText, + selected = uiState.selectedReason == reason, + onClick = { + onReasonSelect(reason) + focusManager.clearFocus() + }, + titleTextStyle = BitnagilTheme.typography.body1Medium, + colors = BitnagilSelectButtonColor.withdrawal(), + modifier = Modifier.padding(bottom = 12.dp), + ) + } + + BasicTextField( + value = uiState.customReasonText, + onValueChange = onCustomReasonChanged, + textStyle = BitnagilTheme.typography.subtitle1Medium.copy( + color = BitnagilTheme.colors.coolGray10, + ), + modifier = Modifier + .fillMaxWidth() + .background( + color = BitnagilTheme.colors.coolGray99, + shape = RoundedCornerShape(12.dp), + ) + .height(112.dp) + .padding(vertical = 14.dp, horizontal = 20.dp) + .focusRequester(focusRequester) + .onFocusChanged { focusState -> + if (focusState.isFocused) { + onReasonSelect(null) + } + }, + decorationBox = { innerTextField -> + if (uiState.customReasonText.isEmpty()) { + Text( + text = "기타사항(직접 입력)", + color = BitnagilTheme.colors.coolGray80, + style = BitnagilTheme.typography.subtitle1Medium, + ) + } + innerTextField() + }, + ) + } + + Spacer(modifier = Modifier.height(54.dp)) + } + + BitnagilTextButton( + text = "탈퇴하기", + onClick = onWithdrawalClick, + enabled = uiState.isWithdrawalEnabled, + modifier = Modifier + .fillMaxWidth() + .alpha(if (uiState.isTermsChecked) 1f else 0f) + .padding(vertical = 14.dp, horizontal = 16.dp), + ) + } +} + +@Preview +@Composable +private fun WithdrawalScreenPreview() { + WithdrawalScreen( + uiState = WithdrawalState( + isTermsChecked = true, + ), + onTermsToggle = {}, + onReasonSelect = {}, + onCustomReasonChanged = {}, + onBackClick = {}, + onWithdrawalClick = {}, + ) +} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/withdrawal/WithdrawalViewModel.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/withdrawal/WithdrawalViewModel.kt new file mode 100644 index 00000000..28b80253 --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/withdrawal/WithdrawalViewModel.kt @@ -0,0 +1,72 @@ +package com.threegap.bitnagil.presentation.withdrawal + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.viewModelScope +import com.threegap.bitnagil.domain.auth.usecase.WithdrawalUseCase +import com.threegap.bitnagil.presentation.common.mviviewmodel.MviViewModel +import com.threegap.bitnagil.presentation.withdrawal.model.WithdrawalIntent +import com.threegap.bitnagil.presentation.withdrawal.model.WithdrawalSideEffect +import com.threegap.bitnagil.presentation.withdrawal.model.WithdrawalState +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import org.orbitmvi.orbit.syntax.simple.SimpleSyntax +import javax.inject.Inject + +@HiltViewModel +class WithdrawalViewModel @Inject constructor( + savedStateHandle: SavedStateHandle, + private val withdrawalUseCase: WithdrawalUseCase, +) : MviViewModel( + savedStateHandle = savedStateHandle, + initState = WithdrawalState(), +) { + override suspend fun SimpleSyntax.reduceState( + intent: WithdrawalIntent, + state: WithdrawalState, + ): WithdrawalState? { + val newState = when (intent) { + is WithdrawalIntent.UpdateLoading -> state.copy(isLoading = intent.isLoading) + is WithdrawalIntent.OnTermsToggle -> state.copy(isTermsChecked = !state.isTermsChecked) + is WithdrawalIntent.ShowSuccessDialog -> state.copy(showSuccessDialog = true) + + is WithdrawalIntent.OnCustomReasonChanged -> { + state.copy(customReasonText = intent.text) + } + + is WithdrawalIntent.OnReasonSelected -> { + state.copy( + selectedReason = intent.reason, + customReasonText = "", + ) + } + + is WithdrawalIntent.OnBackClick -> { + sendSideEffect(WithdrawalSideEffect.NavigateToBack) + null + } + + is WithdrawalIntent.OnSuccessDialogConfirm -> { + sendSideEffect(WithdrawalSideEffect.NavigateToLogin) + null + } + } + + return newState + } + + fun withdrawal() { + if (container.stateFlow.value.isLoading) return + sendIntent(WithdrawalIntent.UpdateLoading(true)) + viewModelScope.launch { + withdrawalUseCase().fold( + onSuccess = { + sendIntent(WithdrawalIntent.UpdateLoading(false)) + sendIntent(WithdrawalIntent.ShowSuccessDialog) + }, + onFailure = { + sendIntent(WithdrawalIntent.UpdateLoading(false)) + }, + ) + } + } +} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/withdrawal/component/WithdrawalConfirmDialog.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/withdrawal/component/WithdrawalConfirmDialog.kt new file mode 100644 index 00000000..5f43b375 --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/withdrawal/component/WithdrawalConfirmDialog.kt @@ -0,0 +1,75 @@ +package com.threegap.bitnagil.presentation.withdrawal.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.BasicAlertDialog +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.DialogProperties +import com.threegap.bitnagil.designsystem.BitnagilTheme +import com.threegap.bitnagil.designsystem.modifier.clickableWithoutRipple + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun WithdrawalConfirmDialog( + onConfirm: () -> Unit, + modifier: Modifier = Modifier, +) { + BasicAlertDialog( + onDismissRequest = {}, + modifier = modifier, + properties = DialogProperties( + dismissOnBackPress = false, + dismissOnClickOutside = false, + ), + ) { + Column( + modifier = Modifier + .background( + color = BitnagilTheme.colors.white, + shape = RoundedCornerShape(12.dp), + ) + .padding(vertical = 20.dp, horizontal = 24.dp), + ) { + Text( + text = "탈퇴가 완료되었어요", + color = BitnagilTheme.colors.coolGray10, + style = BitnagilTheme.typography.subtitle1SemiBold, + modifier = Modifier.padding(bottom = 6.dp), + ) + + Text( + text = "이용해 주셔서 감사합니다. 언제든 다시\n돌아오실 수 있어요:)", + color = BitnagilTheme.colors.coolGray40, + style = BitnagilTheme.typography.body2Medium, + modifier = Modifier.padding(bottom = 18.dp), + ) + + Text( + text = "확인", + color = BitnagilTheme.colors.orange500, + style = BitnagilTheme.typography.body2Medium, + textAlign = TextAlign.End, + modifier = Modifier + .fillMaxWidth() + .clickableWithoutRipple { onConfirm() }, + ) + } + } +} + +@Preview +@Composable +private fun WithdrawalConfirmDialogPreview() { + WithdrawalConfirmDialog( + onConfirm = {}, + ) +} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/withdrawal/model/WithdrawalIntent.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/withdrawal/model/WithdrawalIntent.kt new file mode 100644 index 00000000..54908d84 --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/withdrawal/model/WithdrawalIntent.kt @@ -0,0 +1,13 @@ +package com.threegap.bitnagil.presentation.withdrawal.model + +import com.threegap.bitnagil.presentation.common.mviviewmodel.MviIntent + +sealed class WithdrawalIntent : MviIntent { + data object OnTermsToggle : WithdrawalIntent() + data object OnBackClick : WithdrawalIntent() + data object ShowSuccessDialog : WithdrawalIntent() + data object OnSuccessDialogConfirm : WithdrawalIntent() + data class UpdateLoading(val isLoading: Boolean) : WithdrawalIntent() + data class OnReasonSelected(val reason: WithdrawalReason?) : WithdrawalIntent() + data class OnCustomReasonChanged(val text: String) : WithdrawalIntent() +} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/withdrawal/model/WithdrawalReason.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/withdrawal/model/WithdrawalReason.kt new file mode 100644 index 00000000..aa7e94c7 --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/withdrawal/model/WithdrawalReason.kt @@ -0,0 +1,9 @@ +package com.threegap.bitnagil.presentation.withdrawal.model + +enum class WithdrawalReason( + val displayText: String, +) { + ROUTINE_MISMATCH("루틴이 생활 패턴과 맞지 않아요."), + COMPLEX_UI("기능이 복잡하거나 사용이 불편해요."), + TECHNICAL_ISSUES("앱이 자주 멈추거나 오류가 발생해요."), +} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/withdrawal/model/WithdrawalSideEffect.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/withdrawal/model/WithdrawalSideEffect.kt new file mode 100644 index 00000000..d8e26c10 --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/withdrawal/model/WithdrawalSideEffect.kt @@ -0,0 +1,8 @@ +package com.threegap.bitnagil.presentation.withdrawal.model + +import com.threegap.bitnagil.presentation.common.mviviewmodel.MviSideEffect + +sealed interface WithdrawalSideEffect : MviSideEffect { + data object NavigateToBack : WithdrawalSideEffect + data object NavigateToLogin : WithdrawalSideEffect +} diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/withdrawal/model/WithdrawalState.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/withdrawal/model/WithdrawalState.kt new file mode 100644 index 00000000..79bbf035 --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/withdrawal/model/WithdrawalState.kt @@ -0,0 +1,19 @@ +package com.threegap.bitnagil.presentation.withdrawal.model + +import com.threegap.bitnagil.presentation.common.mviviewmodel.MviState +import kotlinx.parcelize.Parcelize + +@Parcelize +data class WithdrawalState( + val isLoading: Boolean = false, + val isTermsChecked: Boolean = false, + val selectedReason: WithdrawalReason? = null, + val customReasonText: String = "", + val showSuccessDialog: Boolean = false, +) : MviState { + val isWithdrawalEnabled: Boolean + get() = isTermsChecked && (selectedReason != null || customReasonText.isNotBlank()) + + val finalWithdrawalReason: String + get() = selectedReason?.displayText ?: customReasonText +} 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 cd49a39b..81b28f1c 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 @@ -5,12 +5,10 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.exclude import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.ime -import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.width @@ -107,9 +105,7 @@ private fun WriteRoutineScreen( .fillMaxSize() .background(color = BitnagilTheme.colors.white) .statusBarsPadding() - .windowInsetsPadding( - WindowInsets.ime.exclude(WindowInsets.navigationBars), - ), + .windowInsetsPadding(WindowInsets.ime), ) { BitnagilTopBar( title = if (state.writeRoutineType == WriteRoutineType.ADD) "루틴 등록" else "루틴 수정", diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/writeroutine/WriteRoutineViewModel.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/writeroutine/WriteRoutineViewModel.kt index 72c9ef82..79c9d21d 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/writeroutine/WriteRoutineViewModel.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/writeroutine/WriteRoutineViewModel.kt @@ -82,13 +82,14 @@ class WriteRoutineViewModel @AssistedInject constructor( sendIntent(WriteRoutineIntent.GetRoutineLoading) getRoutineUseCase(routineId).fold( onSuccess = { routine -> - oldSubRoutines = routine.subRoutines.map { SubRoutine.fromDomainSubRoutine(it) } +// oldSubRoutines = routine.subRoutines.map { SubRoutine.fromDomainSubRoutine(it) } sendIntent( WriteRoutineIntent.SetRoutine( name = routine.routineName, repeatDays = routine.repeatDay.map { Day.fromDayOfWeek(it) }, startTime = Time.fromDomainTimeString(routine.executionTime), - subRoutines = routine.subRoutines.map { it.subRoutineName }, +// subRoutines = routine.subRoutines.map { it.subRoutineName }, + subRoutines = emptyList(), ), ) }, diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/writeroutine/model/SubRoutine.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/writeroutine/model/SubRoutine.kt index f66cf678..89547aa2 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/writeroutine/model/SubRoutine.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/writeroutine/model/SubRoutine.kt @@ -1,19 +1,12 @@ package com.threegap.bitnagil.presentation.writeroutine.model + import com.threegap.bitnagil.domain.recommendroutine.model.RecommendSubRoutine -import com.threegap.bitnagil.domain.routine.model.SubRoutine as DomainSubRoutine data class SubRoutine( val id: String, val name: String, ) { companion object { - fun fromDomainSubRoutine(subRoutine: DomainSubRoutine): SubRoutine { - return SubRoutine( - id = subRoutine.subRoutineId, - name = subRoutine.subRoutineName, - ) - } - fun fromDomainRecommendSubRoutine(subRoutine: RecommendSubRoutine): SubRoutine { return SubRoutine( id = subRoutine.id.toString(),