From 36f0952fcf83946c5031a255a05a59ac94a3db00 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Sun, 10 Aug 2025 22:01:45 +0900 Subject: [PATCH 01/59] =?UTF-8?q?Refactor:=20=EB=A3=A8=ED=8B=B4=20?= =?UTF-8?q?=EC=97=A0=ED=8B=B0=EB=B7=B0=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=20=EB=A6=AC=EB=94=94=EC=9E=90=EC=9D=B8=20=EB=B0=98?= =?UTF-8?q?=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../bitnagil/presentation/home/HomeScreen.kt | 4 ++-- ...outineEmptyView.kt => EmptyRoutineView.kt} | 21 ++++++++++--------- 2 files changed, 13 insertions(+), 12 deletions(-) rename presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/template/{RoutineEmptyView.kt => EmptyRoutineView.kt} (78%) 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..05fdd52b 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 @@ -31,7 +31,7 @@ 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 @@ -211,7 +211,7 @@ private fun HomeScreen( ) { if (uiState.selectedDateRoutines.isEmpty()) { item { - RoutineEmptyView( + EmptyRoutineView( onRegisterRoutineClick = onRegisterRoutineClick, modifier = Modifier .fillMaxSize() 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 78% 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..1b4bcf8a 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,7 +18,7 @@ import com.threegap.bitnagil.designsystem.BitnagilTheme import com.threegap.bitnagil.designsystem.modifier.clickableWithoutRipple @Composable -fun RoutineEmptyView( +fun EmptyRoutineView( onRegisterRoutineClick: () -> Unit, modifier: 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( + EmptyRoutineView( onRegisterRoutineClick = {}, ) } From 3608ee4de64b7cbc47b7d99dbca72c60cb786a57 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Mon, 11 Aug 2025 00:24:55 +0900 Subject: [PATCH 02/59] =?UTF-8?q?Add:=20=EA=B8=B0=EB=B3=B8=20=EA=B0=90?= =?UTF-8?q?=EC=A0=95=20=EA=B7=B8=EB=9E=98=ED=94=BD=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/res/drawable-hdpi/default_emotion.png | Bin 0 -> 15374 bytes .../main/res/drawable-mdpi/default_emotion.png | Bin 0 -> 9013 bytes .../main/res/drawable-xhdpi/default_emotion.png | Bin 0 -> 23324 bytes .../res/drawable-xxhdpi/default_emotion.png | Bin 0 -> 41474 bytes .../res/drawable-xxxhdpi/default_emotion.png | Bin 0 -> 63184 bytes 5 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 core/designsystem/src/main/res/drawable-hdpi/default_emotion.png create mode 100644 core/designsystem/src/main/res/drawable-mdpi/default_emotion.png create mode 100644 core/designsystem/src/main/res/drawable-xhdpi/default_emotion.png create mode 100644 core/designsystem/src/main/res/drawable-xxhdpi/default_emotion.png create mode 100644 core/designsystem/src/main/res/drawable-xxxhdpi/default_emotion.png 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 0000000000000000000000000000000000000000..113a45342fae3e975155a43cbfadf8dc3fa463ea GIT binary patch literal 15374 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 bze)OkX29qTUqOqm00000NkvXXu0mjfgdK6~ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..92bb2867464df511b8af847adc7cacd2364d3bfb GIT binary patch literal 23324 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 z
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%0D1#%4TEyFp>aXFL$<_GAmbM6icG3JO5iw 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 B9~%Gw literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..6715cd7395e64ad9e33efc6ae9841d0f73ade2dd GIT binary patch literal 63184 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 Trvujj0Q!@aRFbF@GY Date: Mon, 11 Aug 2025 00:26:45 +0900 Subject: [PATCH 03/59] =?UTF-8?q?Feat:=20=EA=B0=90=EC=A0=95=20=EB=93=B1?= =?UTF-8?q?=EB=A1=9D=20=EB=B2=84=ED=8A=BC=20=EC=BB=B4=ED=8F=AC=EB=84=8C?= =?UTF-8?q?=ED=8A=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/atom/BitnagilTextButton.kt | 3 ++ .../component/atom/EmotionRegisterButton.kt | 46 +++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/atom/EmotionRegisterButton.kt 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..55954488 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), ) } } 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 = {}, + ) +} From b4aea4556b6f2292ecbd571444c7e8b35f465164 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Mon, 11 Aug 2025 00:28:13 +0900 Subject: [PATCH 04/59] =?UTF-8?q?Feat:=20=ED=99=88=20=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?=ED=97=A4=EB=8D=94=20=EB=A6=AC=EB=94=94=EC=9E=90=EC=9D=B8=20?= =?UTF-8?q?=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 소개글 변경 - 앱 아이콘 추가 - 기본 감정구슬 이미지 변경 --- .../home/component/atom/EmotionBall.kt | 15 +-- .../template/CollapsibleHomeHeader.kt | 114 +++++------------- .../home/util/CollapsibleHeaderState.kt | 4 +- 3 files changed, 32 insertions(+), 101 deletions(-) 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 index 0e71603f..8e27b316 100644 --- 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 @@ -15,13 +15,11 @@ 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) { @@ -30,7 +28,6 @@ fun EmotionBall( contentDescription = null, modifier = modifier .size(172.dp) - .clickableWithoutRipple { onClick() } .padding(10.dp) .fillMaxSize() .shadow( @@ -42,11 +39,10 @@ fun EmotionBall( ) } else { Image( - painter = painterResource(R.drawable.default_ball), + painter = painterResource(R.drawable.default_emotion), contentDescription = null, modifier = modifier - .size(172.dp) - .clickableWithoutRipple { onClick() } + .size(108.dp, 150.dp) .fillMaxSize(), ) } @@ -61,7 +57,6 @@ private fun EmotionBallDefaultPreview() { ) { EmotionBall( emotionType = null, - onClick = {}, ) } } @@ -75,7 +70,6 @@ private fun EmotionBallCalmPreview() { ) { EmotionBall( emotionType = EmotionBallType.CALM, - onClick = {}, ) } } @@ -89,7 +83,6 @@ private fun EmotionBallVitalityPreview() { ) { EmotionBall( emotionType = EmotionBallType.VITALITY, - onClick = {}, ) } } @@ -103,7 +96,6 @@ private fun EmotionBallLethargyPreview() { ) { EmotionBall( emotionType = EmotionBallType.LETHARGY, - onClick = {}, ) } } @@ -117,7 +109,6 @@ private fun EmotionBallAnxietyPreview() { ) { EmotionBall( emotionType = EmotionBallType.ANXIETY, - onClick = {}, ) } } @@ -131,7 +122,6 @@ private fun EmotionBallSatisfactionPreview() { ) { EmotionBall( emotionType = EmotionBallType.SATISFACTION, - onClick = {}, ) } } @@ -145,7 +135,6 @@ private fun EmotionBallFatiguePreview() { ) { EmotionBall( emotionType = EmotionBallType.FATIGUE, - onClick = {}, ) } } 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..86b4dfcb 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 @@ -4,28 +4,21 @@ 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.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.tooling.preview.Preview -import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Popup import com.threegap.bitnagil.designsystem.BitnagilTheme 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.component.atom.EmotionRegisterButton import com.threegap.bitnagil.presentation.home.model.EmotionBallType import com.threegap.bitnagil.presentation.home.util.CollapsibleHeaderState import com.threegap.bitnagil.presentation.home.util.rememberCollapsibleHeaderState @@ -38,107 +31,56 @@ fun CollapsibleHomeHeader( onEmotionRecordClick: () -> Unit, modifier: Modifier = Modifier, ) { - var showTooltip by remember { mutableStateOf(false) } - 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 = com.threegap.bitnagil.designsystem.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(1f - collapsibleHeaderState.collapseProgress), + ) { 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 = "${userName}님, 오셨군요!\n오늘 기분은 어떤가요?", + style = BitnagilTheme.typography.cafe24SsurroundAir, + color = BitnagilTheme.colors.white, ) - 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, - ) - } + EmotionRegisterButton( + onClick = onEmotionRecordClick, + ) } Box( modifier = modifier - .padding(10.dp) - .align(Alignment.BottomEnd), + .align(Alignment.BottomEnd) + .padding(end = 18.dp), ) { EmotionBall( emotionType = emotionBallType, - onClick = {}, - ) - } - } - } - } -} - -@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 = "감정 기록 시, 루틴을 추천 받아요!", ) } } 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 From 799a42432c8543c7cd844fdb9db57cab10af58c7 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Mon, 11 Aug 2025 03:12:17 +0900 Subject: [PATCH 05/59] =?UTF-8?q?Refactor:=20BitnagilFloatingButton=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EB=A6=AC=EB=94=94?= =?UTF-8?q?=EC=9E=90=EC=9D=B8=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../bitnagil/navigation/home/HomeNavHost.kt | 5 -- .../component/atom/BitnagilFloatingButton.kt | 54 ++++++++++++------- .../src/main/res/drawable/ic_add.xml | 13 +++++ .../src/main/res/drawable/ic_close.xml | 30 +++++------ 4 files changed, 62 insertions(+), 40 deletions(-) create mode 100644 core/designsystem/src/main/res/drawable/ic_add.xml 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..f52396c4 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 @@ -102,11 +102,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/component/atom/BitnagilFloatingButton.kt b/core/designsystem/src/main/java/com/threegap/bitnagil/designsystem/component/atom/BitnagilFloatingButton.kt index 98e94cd1..cab359e3 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,21 +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), + BitnagilIcon( + id = id, + tint = if (isActive) colors.activeIconColor else colors.defaultIconColor, modifier = Modifier.size(24.dp), ) } @@ -66,8 +65,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 +102,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 +125,8 @@ fun BitnagilFloatingActionMenu( BitnagilFloatingButton( id = if (isExpanded) activeIcon else defaultIcon, onClick = { onToggle(!isExpanded) }, + isActive = isExpanded, + colors = colors, ) } } @@ -136,6 +138,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 +179,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 +195,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/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_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"> + + + + From 934623b13320aac34f2136fce9e7f0eeb3bbd94d Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Mon, 11 Aug 2025 03:24:01 +0900 Subject: [PATCH 06/59] =?UTF-8?q?Refactor:=20WeeklyDatePicker=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EB=A6=AC=EB=94=94=EC=9E=90?= =?UTF-8?q?=EC=9D=B8=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/template/WeeklyDatePicker.kt | 49 ++++++++----------- 1 file changed, 20 insertions(+), 29 deletions(-) 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..d4e9d534 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 @@ -4,6 +4,7 @@ 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 @@ -22,7 +23,7 @@ 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 import com.threegap.bitnagil.designsystem.modifier.clickableWithoutRipple import com.threegap.bitnagil.presentation.home.util.formatDayOfMonth import com.threegap.bitnagil.presentation.home.util.formatDayOfWeekShort @@ -54,36 +55,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, + ) } } @@ -123,8 +114,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,14 +123,14 @@ 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, ) } } From b573427938c4e709278765638664a109735c4b7e Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Mon, 11 Aug 2025 03:24:45 +0900 Subject: [PATCH 07/59] =?UTF-8?q?Refactor:=20=EB=A3=A8=ED=8B=B4,=20?= =?UTF-8?q?=EC=84=9C=EB=B8=8C=EB=A3=A8=ED=8B=B4=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EB=A6=AC=EB=94=94=EC=9E=90=EC=9D=B8=20?= =?UTF-8?q?=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../home/component/block/RoutineItem.kt | 85 +++++++++++++------ .../home/component/block/SubRoutinesItem.kt | 37 ++------ 2 files changed, 68 insertions(+), 54 deletions(-) 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..f77a7857 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,7 +17,6 @@ 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 @@ -26,45 +27,58 @@ import com.threegap.bitnagil.presentation.home.model.SubRoutineUiModel fun RoutineItem( routine: RoutineUiModel, onRoutineToggle: (Boolean) -> Unit, - onMoreClick: () -> Unit, + onSubRoutineToggle: (String, 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.isCompleted) }, + 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.isCompleted) 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.subRoutines.isNotEmpty()) { + HorizontalDivider( + thickness = 1.dp, + color = BitnagilTheme.colors.coolGray97, + modifier = Modifier.padding(vertical = 10.dp), + ) + + SubRoutinesItem( + subRoutines = routine.subRoutines, + onSubRoutineToggle = { subRoutineId, isCompleted -> + onSubRoutineToggle(subRoutineId, isCompleted) + }, + ) + } } } @@ -116,6 +130,27 @@ private fun RoutineItemPreview() { routineType = RoutineType.ROUTINE, ), onRoutineToggle = { }, - onMoreClick = { }, + onSubRoutineToggle = { _, _ -> }, + ) +} + +@Preview +@Composable +private fun NoneSubRoutineRoutineItemPreview() { + RoutineItem( + routine = RoutineUiModel( + routineId = "uuid1", + routineName = "개운하게 일어나기", + executionTime = "20:30:00", + routineCompletionId = 1, + isCompleted = false, + isModified = false, + subRoutines = emptyList(), + historySeq = 1, + repeatDay = listOf(), + routineType = RoutineType.ROUTINE, + ), + onRoutineToggle = {}, + onSubRoutineToggle = { _, _ -> }, ) } 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..c782d485 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 @@ -29,47 +26,29 @@ fun SubRoutinesItem( ) { 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)) - 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, - ), + }, ) { BitnagilIcon( - id = R.drawable.ic_check, - tint = if (subRoutine.isCompleted) { - BitnagilTheme.colors.orange500 - } else { - BitnagilTheme.colors.coolGray96 - }, - modifier = Modifier.size(20.dp), + id = if (subRoutine.isCompleted) R.drawable.ic_check_circle else R.drawable.ic_check_default, + tint = null, + modifier = Modifier.size(24.dp), ) Text( text = subRoutine.subRoutineName, style = BitnagilTheme.typography.body2Medium, - color = BitnagilTheme.colors.coolGray20, + color = BitnagilTheme.colors.coolGray40, + modifier = Modifier.weight(1f), ) } } @@ -87,7 +66,7 @@ private fun SubRoutinesItemPreview() { subRoutineName = "물 마시기", sortOrder = 1, routineCompletionId = 1, - isCompleted = false, + isCompleted = true, isModified = false, routineType = RoutineType.SUB_ROUTINE, ), From dc849a1d0e602d39404d1625a9e71c0d8e0ec6ef Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Mon, 11 Aug 2025 15:54:03 +0900 Subject: [PATCH 08/59] =?UTF-8?q?Add:=20=EC=B6=94=EA=B0=80=EB=90=98?= =?UTF-8?q?=EB=8A=94=20=EC=95=84=EC=9D=B4=EC=BD=98=20=EC=97=90=EC=85=8B=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/res/drawable/ic_connect.xml | 50 +++++++++++++ .../src/main/res/drawable/ic_edit.xml | 24 ++++-- .../src/main/res/drawable/ic_grow.xml | 23 ++++++ .../src/main/res/drawable/ic_outside.xml | 33 ++++++++ .../src/main/res/drawable/ic_rest.xml | 30 ++++++++ .../src/main/res/drawable/ic_trash.xml | 52 ++++++++++--- .../src/main/res/drawable/ic_wakeup.xml | 75 +++++++++++++++++++ 7 files changed, 269 insertions(+), 18 deletions(-) create mode 100644 core/designsystem/src/main/res/drawable/ic_connect.xml create mode 100644 core/designsystem/src/main/res/drawable/ic_grow.xml create mode 100644 core/designsystem/src/main/res/drawable/ic_outside.xml create mode 100644 core/designsystem/src/main/res/drawable/ic_rest.xml create mode 100644 core/designsystem/src/main/res/drawable/ic_wakeup.xml 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_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_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 @@ + + + + + + + + + + + + + + + From a5d97934ca5fa12af6440cdc5c06612a5c7a637f Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Mon, 11 Aug 2025 15:56:49 +0900 Subject: [PATCH 09/59] =?UTF-8?q?Feat:=20RoutineDetailsCard=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/template/RoutineDetailsCard.kt | 144 ++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/component/template/RoutineDetailsCard.kt 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..e8200ef4 --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/component/template/RoutineDetailsCard.kt @@ -0,0 +1,144 @@ +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( + 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 = { /*TODO*/ }, + 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() +} From bc969c5a873d23279eb90e52f8011269aabbde40 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Mon, 11 Aug 2025 15:57:05 +0900 Subject: [PATCH 10/59] =?UTF-8?q?Feat:=20WeeklyDatePicker=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/template/WeeklyDatePicker.kt | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/component/template/WeeklyDatePicker.kt 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..d7205adb --- /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(), + ) +} From aca5a4fe4cebcd3264ec2d5bbb5334d214ba6aaf Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Mon, 11 Aug 2025 15:57:22 +0900 Subject: [PATCH 11/59] =?UTF-8?q?Feat:=20=EB=A3=A8=ED=8B=B4=20=EB=A6=AC?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=ED=99=94=EB=A9=B4=20ui=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../routinelist/RoutineListScreen.kt | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/RoutineListScreen.kt 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..4c085cb1 --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/RoutineListScreen.kt @@ -0,0 +1,81 @@ +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.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.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.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.threegap.bitnagil.designsystem.BitnagilTheme +import com.threegap.bitnagil.designsystem.component.block.BitnagilTopBar +import com.threegap.bitnagil.presentation.home.util.getCurrentWeekDays +import com.threegap.bitnagil.presentation.routinelist.component.template.RoutineDetailsCard +import com.threegap.bitnagil.presentation.routinelist.component.template.WeeklyDatePicker +import java.time.LocalDate + +@Composable +fun RoutineListScreenContainer( + +) { + RoutineListScreen() +} + +@Composable +private fun RoutineListScreen( + modifier: Modifier = Modifier +) { + Column( + modifier = modifier + .fillMaxSize() + .background(BitnagilTheme.colors.coolGray99) + .statusBarsPadding(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + BitnagilTopBar( + title = "루틴리스트", + showBackButton = true, + onBackClick = {}, + ) + + var selectedDate by remember { mutableStateOf(LocalDate.now()) } + + Spacer(modifier = Modifier.height(4.dp)) + + WeeklyDatePicker( + selectedDate = selectedDate, + onDateSelect = { selectedDate = it }, + weeklyDates = selectedDate.getCurrentWeekDays(), + modifier = Modifier + .padding(vertical = 10.dp, horizontal = 16.dp) + ) + + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + items(10) { + RoutineDetailsCard() + } + } + } +} + +@Preview +@Composable +private fun RoutineListScreenPreview() { + RoutineListScreen() +} From 3f6c5b2618447b585d2308a6235bfd2a44f4fff9 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Tue, 12 Aug 2025 03:26:19 +0900 Subject: [PATCH 12/59] =?UTF-8?q?Feat:=20=EC=82=AD=EC=A0=9C=20=ED=99=95?= =?UTF-8?q?=EC=9D=B8=20=EB=B0=94=ED=85=80=EC=8B=9C=ED=8A=B8=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../template/DeleteConfirmBottomSheet.kt | 139 ++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/component/template/DeleteConfirmBottomSheet.kt 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 = {}, + ) +} From 986c9e7c2a8a3441ee33a42c0144d22fdf036278 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Tue, 12 Aug 2025 03:27:03 +0900 Subject: [PATCH 13/59] =?UTF-8?q?Feat:=20RoutineList=ED=99=94=EB=A9=B4=20M?= =?UTF-8?q?VI=20=EB=AA=A8=EB=8D=B8=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../routinelist/model/RoutineListIntent.kt | 11 +++++++++++ .../routinelist/model/RoutineListSideEffect.kt | 7 +++++++ .../routinelist/model/RoutineListState.kt | 11 +++++++++++ 3 files changed, 29 insertions(+) create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/model/RoutineListIntent.kt create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/model/RoutineListSideEffect.kt create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/model/RoutineListState.kt 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 From 0463470c84b2d28667c0d94556abd48fa7949690 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Tue, 12 Aug 2025 03:28:09 +0900 Subject: [PATCH 14/59] =?UTF-8?q?Feat:=20=EB=A3=A8=ED=8B=B4=EB=A6=AC?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=ED=99=94=EB=A9=B4=20Ui=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/atom/BitnagilTextButton.kt | 20 +++++ .../src/main/res/drawable/ic_shine.xml | 15 ++++ .../routinelist/RoutineListScreen.kt | 77 ++++++++++++++----- .../routinelist/RoutineListViewModel.kt | 38 +++++++++ .../component/template/RoutineDetailsCard.kt | 31 ++++---- .../component/template/WeeklyDatePicker.kt | 2 +- 6 files changed, 150 insertions(+), 33 deletions(-) create mode 100644 core/designsystem/src/main/res/drawable/ic_shine.xml create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/RoutineListViewModel.kt 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..92fc237d 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 @@ -110,6 +110,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/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/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/RoutineListScreen.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/RoutineListScreen.kt index 4c085cb1..19d0b035 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/RoutineListScreen.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/RoutineListScreen.kt @@ -3,6 +3,7 @@ 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 @@ -11,64 +12,99 @@ 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.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.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(), ) { - RoutineListScreen() + 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( - modifier: Modifier = Modifier + uiState: RoutineListState, + onDateSelect: (LocalDate) -> Unit, + onShowDeleteConfirmBottomSheet: () -> Unit, + onBackClick: () -> Unit, + modifier: Modifier = Modifier, ) { Column( modifier = modifier .fillMaxSize() .background(BitnagilTheme.colors.coolGray99) .statusBarsPadding(), - horizontalAlignment = Alignment.CenterHorizontally + horizontalAlignment = Alignment.CenterHorizontally, ) { BitnagilTopBar( title = "루틴리스트", showBackButton = true, - onBackClick = {}, + onBackClick = onBackClick, ) - var selectedDate by remember { mutableStateOf(LocalDate.now()) } - Spacer(modifier = Modifier.height(4.dp)) WeeklyDatePicker( - selectedDate = selectedDate, - onDateSelect = { selectedDate = it }, - weeklyDates = selectedDate.getCurrentWeekDays(), + selectedDate = uiState.selectedDate, + onDateSelect = onDateSelect, + weeklyDates = uiState.selectedDate.getCurrentWeekDays(), modifier = Modifier - .padding(vertical = 10.dp, horizontal = 16.dp) + .padding(vertical = 10.dp, horizontal = 16.dp), ) LazyColumn( modifier = Modifier .fillMaxSize() .padding(horizontal = 16.dp), - verticalArrangement = Arrangement.spacedBy(12.dp) + verticalArrangement = Arrangement.spacedBy(12.dp), + contentPadding = PaddingValues(bottom = 10.dp), ) { - items(10) { - RoutineDetailsCard() + items(5) { + RoutineDetailsCard( + onDeleteClick = onShowDeleteConfirmBottomSheet, + ) } } } @@ -77,5 +113,10 @@ private fun RoutineListScreen( @Preview @Composable private fun RoutineListScreenPreview() { - RoutineListScreen() + 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/RoutineDetailsCard.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/component/template/RoutineDetailsCard.kt index e8200ef4..4c8607fe 100644 --- 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 @@ -23,7 +23,8 @@ import com.threegap.bitnagil.designsystem.component.atom.BitnagilIconButton @Composable fun RoutineDetailsCard( - modifier: Modifier = Modifier + onDeleteClick: () -> Unit, + modifier: Modifier = Modifier, ) { Column( modifier = modifier @@ -32,12 +33,12 @@ fun RoutineDetailsCard( shape = RoundedCornerShape(12.dp), ) .fillMaxWidth() - .padding(vertical = 14.dp) + .padding(vertical = 14.dp), ) { Row( modifier = Modifier .padding(start = 16.dp, end = 2.dp), - verticalAlignment = Alignment.CenterVertically + verticalAlignment = Alignment.CenterVertically, ) { BitnagilIcon( id = R.drawable.ic_wakeup, @@ -45,16 +46,16 @@ fun RoutineDetailsCard( modifier = Modifier .background( color = BitnagilTheme.colors.orange25, - shape = RoundedCornerShape(4.dp) + shape = RoundedCornerShape(4.dp), ) - .padding(4.dp) + .padding(4.dp), ) Text( text = "개운하게 일어나기", color = BitnagilTheme.colors.coolGray10, style = BitnagilTheme.typography.body1SemiBold, - modifier = Modifier.padding(start = 10.dp) + modifier = Modifier.padding(start = 10.dp), ) Spacer(modifier = Modifier.weight(1f)) @@ -63,27 +64,27 @@ fun RoutineDetailsCard( id = R.drawable.ic_edit, onClick = { /*TODO*/ }, tint = null, - paddingValues = PaddingValues(12.dp) + paddingValues = PaddingValues(12.dp), ) BitnagilIconButton( id = R.drawable.ic_trash, - onClick = { /*TODO*/ }, + onClick = onDeleteClick, tint = null, - paddingValues = PaddingValues(12.dp) + paddingValues = PaddingValues(12.dp), ) } HorizontalDivider( thickness = 1.dp, color = BitnagilTheme.colors.coolGray97, - modifier = Modifier.padding(horizontal = 16.dp, vertical = 10.dp) + modifier = Modifier.padding(horizontal = 16.dp, vertical = 10.dp), ) Column( modifier = Modifier .padding(horizontal = 16.dp), - verticalArrangement = Arrangement.spacedBy(2.dp) + verticalArrangement = Arrangement.spacedBy(2.dp), ) { Text( text = "세부 루틴", @@ -110,13 +111,13 @@ fun RoutineDetailsCard( HorizontalDivider( thickness = 1.dp, color = BitnagilTheme.colors.coolGray97, - modifier = Modifier.padding(horizontal = 16.dp, vertical = 10.dp) + modifier = Modifier.padding(horizontal = 16.dp, vertical = 10.dp), ) Column( modifier = Modifier .padding(horizontal = 16.dp), - verticalArrangement = Arrangement.spacedBy(2.dp) + verticalArrangement = Arrangement.spacedBy(2.dp), ) { Text( text = "반복:", @@ -140,5 +141,7 @@ fun RoutineDetailsCard( @Preview @Composable private fun RoutineDetailsCardPreview() { - RoutineDetailsCard() + 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 index d7205adb..05a79a39 100644 --- 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 @@ -45,7 +45,7 @@ fun WeeklyDatePicker( isSelected = selectedDate == date, isToday = date == LocalDate.now(), onDateClick = { onDateSelect(date) }, - modifier = Modifier.padding(bottom = 18.dp) + modifier = Modifier.padding(bottom = 18.dp), ) } } From 489b5d20fc90d454a8eb1d1a5fd7b13d56ee475d Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Tue, 12 Aug 2025 03:28:39 +0900 Subject: [PATCH 15/59] =?UTF-8?q?Feat:=20=EB=9D=BC=EC=9A=B0=ED=8A=B8=20?= =?UTF-8?q?=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/threegap/bitnagil/MainNavHost.kt | 11 +++++++++++ app/src/main/java/com/threegap/bitnagil/Route.kt | 3 +++ 2 files changed, 14 insertions(+) diff --git a/app/src/main/java/com/threegap/bitnagil/MainNavHost.kt b/app/src/main/java/com/threegap/bitnagil/MainNavHost.kt index 5716453b..21ba9cac 100644 --- a/app/src/main/java/com/threegap/bitnagil/MainNavHost.kt +++ b/app/src/main/java/com/threegap/bitnagil/MainNavHost.kt @@ -12,6 +12,7 @@ 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 @@ -223,5 +224,15 @@ fun MainNavHost( }, ) } + + composable { + RoutineListScreenContainer( + navigateToBack = { + if (navigator.navController.previousBackStackEntry != null) { + navigator.navController.popBackStack() + } + }, + ) + } } } diff --git a/app/src/main/java/com/threegap/bitnagil/Route.kt b/app/src/main/java/com/threegap/bitnagil/Route.kt index ced10832..fe64805d 100644 --- a/app/src/main/java/com/threegap/bitnagil/Route.kt +++ b/app/src/main/java/com/threegap/bitnagil/Route.kt @@ -38,4 +38,7 @@ sealed interface Route { @Serializable data object Emotion : Route + + @Serializable + data object RoutineList : Route } From d1b200211e439819f10fbf70f3c1e0321bb9bd4d Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Tue, 12 Aug 2025 13:10:22 +0900 Subject: [PATCH 16/59] =?UTF-8?q?Chore:=20=ED=88=B4=ED=8C=81=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../home/component/atom/RoundTriangleShape.kt | 52 ---------------- .../component/block/SpeechBubbleTooltip.kt | 61 ------------------- 2 files changed, 113 deletions(-) delete mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/atom/RoundTriangleShape.kt delete mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/block/SpeechBubbleTooltip.kt 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/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 = "감정 기록 시, 루틴을 추천 받아요!", - ) -} From 1d3563a64323e1966f05177896237c1187e4dd51 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Tue, 12 Aug 2025 13:23:25 +0900 Subject: [PATCH 17/59] =?UTF-8?q?Refactor:=20=EB=A3=A8=ED=8B=B4=20?= =?UTF-8?q?=EC=A0=95=EB=A0=AC=20=EA=B8=B0=EB=8A=A5=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 정렬 바텀 시트 컴포넌트 제거 - 정렬 타입 제거 - 정렬 state, intent 제거 --- .../main/res/drawable/ic_arrow_down_up.xml | 13 -- .../bitnagil/presentation/home/HomeScreen.kt | 29 ---- .../presentation/home/HomeViewModel.kt | 20 --- .../template/RoutineSortBottomSheet.kt | 134 ------------------ .../presentation/home/model/HomeIntent.kt | 3 - .../presentation/home/model/HomeState.kt | 11 +- .../home/model/RoutineSortType.kt | 7 - 7 files changed, 1 insertion(+), 216 deletions(-) delete mode 100644 core/designsystem/src/main/res/drawable/ic_arrow_down_up.xml delete mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/template/RoutineSortBottomSheet.kt delete mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/RoutineSortType.kt 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/presentation/src/main/java/com/threegap/bitnagil/presentation/home/HomeScreen.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/HomeScreen.kt index 05fdd52b..ca8d5f4a 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 @@ -33,7 +33,6 @@ import com.threegap.bitnagil.presentation.home.component.template.DeleteConfirmD import com.threegap.bitnagil.presentation.home.component.template.RoutineDetailsBottomSheet 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 @@ -75,18 +74,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( @@ -138,9 +125,6 @@ fun HomeScreenContainer( onSubRoutineCompletionToggle = { routineId, subRoutineId, isCompleted -> viewModel.toggleSubRoutineCompletion(routineId, subRoutineId, isCompleted) }, - onShowRoutineSortBottomSheet = { - viewModel.sendIntent(HomeIntent.ShowRoutineSortBottomSheet) - }, onShowRoutineDetailsBottomSheet = { routine -> viewModel.sendIntent(HomeIntent.ShowRoutineDetailsBottomSheet(routine)) }, @@ -161,7 +145,6 @@ private fun HomeScreen( onNextWeekClick: () -> Unit, onRoutineCompletionToggle: (String, Boolean) -> Unit, onSubRoutineCompletionToggle: (String, String, Boolean) -> Unit, - onShowRoutineSortBottomSheet: () -> Unit, onShowRoutineDetailsBottomSheet: (RoutineUiModel) -> Unit, onRegisterRoutineClick: () -> Unit, onRegisterEmotionClick: () -> Unit, @@ -249,18 +232,7 @@ private fun HomeScreen( .padding(horizontal = 16.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), ) - } - } } } } @@ -289,7 +261,6 @@ private fun HomeScreenPreview() { onNextWeekClick = {}, onRoutineCompletionToggle = { _, _ -> }, onSubRoutineCompletionToggle = { _, _, _ -> }, - onShowRoutineSortBottomSheet = {}, onShowRoutineDetailsBottomSheet = {}, onRegisterRoutineClick = {}, onRegisterEmotionClick = {}, 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..0f8709aa 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 @@ -19,7 +19,6 @@ 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 @@ -113,17 +112,6 @@ class HomeViewModel @Inject constructor( updateSubRoutine(state, intent.routineId, intent.subRoutineId, 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 } @@ -144,14 +132,6 @@ class HomeViewModel @Inject constructor( 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, 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/model/HomeIntent.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/HomeIntent.kt index 883b1533..06068891 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 @@ -11,7 +11,6 @@ sealed class HomeIntent : MviIntent { 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() @@ -26,8 +25,6 @@ sealed class HomeIntent : MviIntent { 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/HomeState.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/HomeState.kt index c7f6b0a1..74fc3c1d 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 @@ -13,20 +13,11 @@ data class HomeState( 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.routinesByDate[selectedDate.toString()] ?: 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, -} From 0c65ddefe6e85ded3d13f401e7d43613fc78fe82 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Tue, 12 Aug 2025 18:06:10 +0900 Subject: [PATCH 18/59] =?UTF-8?q?Refactor:=20RoutineSection=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EB=A6=AC=EB=94=94=EC=9E=90?= =?UTF-8?q?=EC=9D=B8=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../home/component/template/RoutineSection.kt | 91 ++++--------------- .../home/util/LocalDateExtension.kt | 9 +- 2 files changed, 24 insertions(+), 76 deletions(-) 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..065b5931 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,28 +1,17 @@ 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 @@ -32,76 +21,31 @@ fun RoutineSection( routine: RoutineUiModel, onRoutineToggle: (Boolean) -> Unit, onSubRoutineToggle: (String, Boolean) -> Unit, - onMoreClick: () -> 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() { +private fun RoutineSectionPreview() { RoutineSection( routine = RoutineUiModel( routineId = "uuid1", @@ -148,6 +92,5 @@ private fun RoutineTemplatePreview() { ), onRoutineToggle = {}, onSubRoutineToggle = { _, _ -> }, - onMoreClick = {}, ) } 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) { "시간 미정" } From 190b37c424b9893171bba74c93f1a225ba49d0d0 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Tue, 12 Aug 2025 18:14:48 +0900 Subject: [PATCH 19/59] =?UTF-8?q?Refactor:=20=ED=99=88=ED=99=94=EB=A9=B4?= =?UTF-8?q?=20=EB=A6=AC=EB=94=94=EC=9E=90=EC=9D=B8=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/atom/BitnagilFloatingButton.kt | 1 - .../bitnagil/presentation/home/HomeScreen.kt | 142 +++++++++--------- .../template/CollapsibleHomeHeader.kt | 9 +- .../component/template/EmptyRoutineView.kt | 2 +- 4 files changed, 77 insertions(+), 77 deletions(-) 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 cab359e3..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 @@ -54,7 +54,6 @@ fun BitnagilFloatingButton( BitnagilIcon( id = id, tint = if (isActive) colors.activeIconColor else colors.defaultIconColor, - modifier = Modifier.size(24.dp), ) } } 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 ca8d5f4a..cb045dd2 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,8 +1,10 @@ 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 @@ -10,34 +12,29 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn 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.EmptyRoutineView +import com.threegap.bitnagil.presentation.home.component.template.RoutineDetailsBottomSheet import com.threegap.bitnagil.presentation.home.component.template.RoutineSection 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 @@ -125,15 +122,15 @@ fun HomeScreenContainer( onSubRoutineCompletionToggle = { routineId, subRoutineId, isCompleted -> viewModel.toggleSubRoutineCompletion(routineId, subRoutineId, isCompleted) }, - onShowRoutineDetailsBottomSheet = { routine -> - viewModel.sendIntent(HomeIntent.ShowRoutineDetailsBottomSheet(routine)) - }, onRegisterRoutineClick = { viewModel.sendIntent(HomeIntent.OnRegisterRoutineClick) }, onRegisterEmotionClick = { viewModel.sendIntent(HomeIntent.OnRegisterEmotionClick) }, + onShowMoreRoutinesClick = { + // TODO: 루틴 리스트 화면으로 이동 + } ) } @@ -145,9 +142,9 @@ private fun HomeScreen( onNextWeekClick: () -> Unit, onRoutineCompletionToggle: (String, Boolean) -> Unit, onSubRoutineCompletionToggle: (String, String, Boolean) -> Unit, - onShowRoutineDetailsBottomSheet: (RoutineUiModel) -> Unit, onRegisterRoutineClick: () -> Unit, onRegisterEmotionClick: () -> Unit, + onShowMoreRoutinesClick: () -> Unit, modifier: Modifier = Modifier, ) { val collapsibleHeaderState = rememberCollapsibleHeaderState() @@ -155,16 +152,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)) @@ -177,7 +165,7 @@ private fun HomeScreen( onNextWeekClick = onNextWeekClick, modifier = Modifier .background( - color = BitnagilTheme.colors.white, + color = BitnagilTheme.colors.coolGray99, shape = RoundedCornerShape( topStart = 20.dp, topEnd = 20.dp, @@ -185,60 +173,72 @@ private fun HomeScreen( ), ) - LazyColumn( - state = collapsibleHeaderState.lazyListState, - modifier = Modifier - .fillMaxSize() - .background(BitnagilTheme.colors.white) - .nestedScroll(collapsibleHeaderState.nestedScrollConnection), - ) { - if (uiState.selectedDateRoutines.isEmpty()) { - item { - EmptyRoutineView( - onRegisterRoutineClick = onRegisterRoutineClick, - modifier = Modifier - .fillMaxSize() - .padding(top = 62.dp), - ) - } - } else { - uiState.selectedDateRoutines.forEachIndexed { index, routine -> + 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), + ) + } + + LazyColumn( + modifier = Modifier + .fillMaxSize() + .background(BitnagilTheme.colors.coolGray99) + .nestedScroll(collapsibleHeaderState.nestedScrollConnection) + .padding(horizontal = 16.dp) + .padding(bottom = 12.dp), + state = collapsibleHeaderState.lazyListState, + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + uiState.selectedDateRoutines.forEach { 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), - ) - + RoutineSection( + routine = routine, + onRoutineToggle = { isCompleted -> + onRoutineCompletionToggle( + routine.routineId, + isCompleted, + ) + }, + onSubRoutineToggle = { subRoutineId, isCompleted -> + onSubRoutineCompletionToggle( + routine.routineId, + subRoutineId, + isCompleted, ) + }, + ) } } } - item { - Spacer(modifier = Modifier.height(110.dp)) - } } } @@ -246,7 +246,7 @@ private fun HomeScreen( userName = uiState.userNickname, emotionBallType = uiState.myEmotion, collapsibleHeaderState = collapsibleHeaderState, - onEmotionRecordClick = onRegisterEmotionClick, + onRegisterEmotion = onRegisterEmotionClick, ) } } @@ -261,8 +261,8 @@ private fun HomeScreenPreview() { onNextWeekClick = {}, onRoutineCompletionToggle = { _, _ -> }, onSubRoutineCompletionToggle = { _, _, _ -> }, - onShowRoutineDetailsBottomSheet = {}, onRegisterRoutineClick = {}, onRegisterEmotionClick = {}, + onShowMoreRoutinesClick = {} ) } 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 86b4dfcb..57d9c892 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 @@ -28,7 +28,7 @@ fun CollapsibleHomeHeader( userName: String, emotionBallType: EmotionBallType?, collapsibleHeaderState: CollapsibleHeaderState, - onEmotionRecordClick: () -> Unit, + onRegisterEmotion: () -> Unit, modifier: Modifier = Modifier, ) { Column( @@ -70,7 +70,8 @@ fun CollapsibleHomeHeader( ) EmotionRegisterButton( - onClick = onEmotionRecordClick, + onClick = onRegisterEmotion, + enabled = emotionBallType == null, ) } @@ -90,11 +91,11 @@ fun CollapsibleHomeHeader( @Preview @Composable -private fun HomeTopBarPreview() { +private fun CollapsibleHomeHeaderPreview() { CollapsibleHomeHeader( userName = "대현", emotionBallType = null, collapsibleHeaderState = rememberCollapsibleHeaderState(), - onEmotionRecordClick = {}, + onRegisterEmotion = {}, ) } diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/template/EmptyRoutineView.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/template/EmptyRoutineView.kt index 1b4bcf8a..1a8bb254 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/template/EmptyRoutineView.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/template/EmptyRoutineView.kt @@ -23,7 +23,7 @@ fun EmptyRoutineView( modifier: Modifier = Modifier, ) { Column( - verticalArrangement = Arrangement.Center, + verticalArrangement = Arrangement.Top, horizontalAlignment = Alignment.CenterHorizontally, modifier = modifier, ) { From e9bdb65980f2915c72bdc45b9452b20670fd58ce Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Tue, 12 Aug 2025 22:34:00 +0900 Subject: [PATCH 20/59] =?UTF-8?q?Refactor:=20=EB=A7=88=EC=9D=B4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=ED=99=94=EB=A9=B4=20=EB=A6=AC=EB=94=94?= =?UTF-8?q?=EC=9E=90=EC=9D=B8=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../home/HomeBottomNavigationBar.kt | 50 +++++++++++-------- .../component/block/BitnagilOptionButton.kt | 8 +-- .../src/main/res/drawable/ic_setting.xml | 14 ++++-- .../presentation/mypage/MyPageScreen.kt | 12 ++--- 4 files changed, 49 insertions(+), 35 deletions(-) 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/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/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/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), ) }, ) From ec3c8ba77ed3ad56b7f28684f4c38f908ef0cc26 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Tue, 12 Aug 2025 22:34:20 +0900 Subject: [PATCH 21/59] =?UTF-8?q?Refactor:=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=ED=99=94=EB=A9=B4=20=EB=A6=AC=EB=94=94=EC=9E=90=EC=9D=B8=20?= =?UTF-8?q?=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/setting/SettingScreen.kt | 42 +++++++------------ .../atom/settingtitle/SettingTitle.kt | 2 +- 2 files changed, 17 insertions(+), 27 deletions(-) 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..c3114961 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,6 +26,7 @@ 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 @@ -116,7 +116,7 @@ private fun SettingScreen( Text( text = "버전 ", color = BitnagilTheme.colors.black, - style = BitnagilTheme.typography.body1Regular, + style = BitnagilTheme.typography.body1Medium, ) Text( text = state.version, @@ -124,30 +124,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( 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), ) } From 987e909fb1baa25e73915517a3e41ba6dbd6816c Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Wed, 13 Aug 2025 04:48:26 +0900 Subject: [PATCH 22/59] =?UTF-8?q?Feat:=20=EC=98=A4=EB=8A=98=EC=9D=98=20?= =?UTF-8?q?=EA=B0=90=EC=A0=95=20API=20v2=20=EC=A0=81=EC=9A=A9=20=EB=B0=8F?= =?UTF-8?q?=20UI=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../emotion/datasource/EmotionDataSource.kt | 4 +- .../datasourceImpl/EmotionDataSourceImpl.kt | 6 +- ...Response.kt => TodayEmotionResponseDto.kt} | 17 +++-- .../repositoryImpl/EmotionRepositoryImpl.kt | 5 +- .../data/emotion/service/EmotionService.kt | 8 +-- .../domain/emotion/model/TodayEmotion.kt | 8 +++ .../emotion/repository/EmotionRepository.kt | 3 +- .../usecase/FetchTodayEmotionUseCase.kt | 12 ++++ .../emotion/usecase/GetEmotionUseCase.kt | 12 ---- .../bitnagil/presentation/home/HomeScreen.kt | 6 +- .../presentation/home/HomeViewModel.kt | 16 ++--- .../template/CollapsibleHomeHeader.kt | 65 ++++++++++++++----- .../presentation/home/model/HomeIntent.kt | 2 +- .../presentation/home/model/HomeState.kt | 1 + .../home/model/TodayEmotionUiModel.kt | 16 +++++ 15 files changed, 121 insertions(+), 60 deletions(-) rename data/src/main/java/com/threegap/bitnagil/data/emotion/model/response/{GetEmotionResponse.kt => TodayEmotionResponseDto.kt} (52%) create mode 100644 domain/src/main/java/com/threegap/bitnagil/domain/emotion/model/TodayEmotion.kt create mode 100644 domain/src/main/java/com/threegap/bitnagil/domain/emotion/usecase/FetchTodayEmotionUseCase.kt delete mode 100644 domain/src/main/java/com/threegap/bitnagil/domain/emotion/usecase/GetEmotionUseCase.kt create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/TodayEmotionUiModel.kt 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..9538d342 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/{searchDate}") + suspend fun fetchTodayEmotion( @Path("searchDate") date: String, - ): BaseResponse + ): 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/presentation/src/main/java/com/threegap/bitnagil/presentation/home/HomeScreen.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/HomeScreen.kt index cb045dd2..4c38db64 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 @@ -130,7 +130,7 @@ fun HomeScreenContainer( }, onShowMoreRoutinesClick = { // TODO: 루틴 리스트 화면으로 이동 - } + }, ) } @@ -244,7 +244,7 @@ private fun HomeScreen( CollapsibleHomeHeader( userName = uiState.userNickname, - emotionBallType = uiState.myEmotion, + todayEmotion = uiState.todayEmotion, collapsibleHeaderState = collapsibleHeaderState, onRegisterEmotion = onRegisterEmotionClick, ) @@ -263,6 +263,6 @@ private fun HomeScreenPreview() { onSubRoutineCompletionToggle = { _, _, _ -> }, onRegisterRoutineClick = {}, onRegisterEmotionClick = {}, - onShowMoreRoutinesClick = {} + 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 0f8709aa..1e4a79c9 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,8 +3,8 @@ 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 @@ -15,7 +15,6 @@ 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 @@ -41,7 +40,7 @@ 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, @@ -186,8 +185,8 @@ class HomeViewModel @Inject constructor( is HomeIntent.ConfirmRoutineByDayDeletion -> null - is HomeIntent.LoadMyEmotion -> { - state.copy(myEmotion = intent.emotion) + is HomeIntent.LoadTodayEmotion -> { + state.copy(todayEmotion = intent.emotion) } is HomeIntent.OnRegisterEmotionClick -> { @@ -305,10 +304,9 @@ class HomeViewModel @Inject constructor( private fun getMyEmotion(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 -> 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 57d9c892..483d39cb 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,36 +1,61 @@ 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.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.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha +import androidx.compose.ui.graphics.FilterQuality +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.dp +import coil3.compose.AsyncImage +import coil3.request.ImageRequest +import coil3.request.crossfade +import coil3.size.Size import com.threegap.bitnagil.designsystem.BitnagilTheme +import com.threegap.bitnagil.designsystem.R import com.threegap.bitnagil.designsystem.component.atom.BitnagilIcon -import com.threegap.bitnagil.presentation.home.component.atom.EmotionBall import com.threegap.bitnagil.presentation.home.component.atom.EmotionRegisterButton -import com.threegap.bitnagil.presentation.home.model.EmotionBallType +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, onRegisterEmotion: () -> Unit, modifier: Modifier = Modifier, ) { + 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), @@ -43,7 +68,7 @@ fun CollapsibleHomeHeader( verticalAlignment = Alignment.CenterVertically, ) { BitnagilIcon( - id = com.threegap.bitnagil.designsystem.R.drawable.ic_logo, + id = R.drawable.ic_logo, tint = BitnagilTheme.colors.coolGray50, modifier = Modifier.padding(start = 16.dp), ) @@ -54,7 +79,7 @@ fun CollapsibleHomeHeader( modifier = Modifier .padding(top = 18.dp) .height(collapsibleHeaderState.currentHeaderHeight - collapsibleHeaderState.collapsedHeaderHeight) - .alpha(1f - collapsibleHeaderState.collapseProgress), + .alpha(alpha), ) { Column( modifier = Modifier @@ -64,26 +89,34 @@ fun CollapsibleHomeHeader( verticalArrangement = Arrangement.spacedBy(20.dp), ) { Text( - text = "${userName}님, 오셨군요!\n오늘 기분은 어떤가요?", + text = welcomeMessage, style = BitnagilTheme.typography.cafe24SsurroundAir, color = BitnagilTheme.colors.white, + fontWeight = FontWeight.SemiBold, ) EmotionRegisterButton( onClick = onRegisterEmotion, - enabled = emotionBallType == null, + enabled = !hasEmotion, ) } - Box( - modifier = modifier + AsyncImage( + model = ImageRequest.Builder(LocalContext.current) + .data(todayEmotion?.imageUrl) + .crossfade(true) + .size(Size.ORIGINAL) + .build(), + contentDescription = null, + placeholder = painterResource(R.drawable.default_emotion), + error = painterResource(R.drawable.default_emotion), + contentScale = ContentScale.Fit, + filterQuality = FilterQuality.High, + modifier = Modifier .align(Alignment.BottomEnd) - .padding(end = 18.dp), - ) { - EmotionBall( - emotionType = emotionBallType, - ) - } + .padding(end = 18.dp) + .aspectRatio(108f / 148f), + ) } } } @@ -94,7 +127,7 @@ fun CollapsibleHomeHeader( private fun CollapsibleHomeHeaderPreview() { CollapsibleHomeHeader( userName = "대현", - emotionBallType = null, + todayEmotion = null, collapsibleHeaderState = rememberCollapsibleHeaderState(), onRegisterEmotion = {}, ) 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 06068891..c45afe77 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,7 +6,7 @@ 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() 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 74fc3c1d..9f472f3b 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 @@ -10,6 +10,7 @@ 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(), 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, +) From 00834647d4bb6af7c52adb49f9c9c6cbc9e5237a Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Wed, 13 Aug 2025 17:53:04 +0900 Subject: [PATCH 23/59] =?UTF-8?q?Refactor:=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83,=20=ED=9A=8C=EC=9B=90=ED=83=88=ED=87=B4=20=EB=8B=A4?= =?UTF-8?q?=EC=9D=B4=EC=96=BC=EB=A1=9C=EA=B7=B8=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 로그아웃 다이얼로그 디자인 수정 - 회원탈퇴 로직 제거 --- .../component/atom/BitnagilTextButton.kt | 10 +++ .../main/res/drawable/ic_modal_warning.xml | 18 ------ .../presentation/setting/SettingScreen.kt | 13 ++-- .../presentation/setting/SettingViewModel.kt | 63 ++++--------------- ...onfirmDialog.kt => LogoutConfirmDialog.kt} | 58 ++++++----------- .../setting/model/ConfirmDialogType.kt | 18 ------ .../setting/model/mvi/SettingIntent.kt | 6 +- .../setting/model/mvi/SettingState.kt | 5 +- 8 files changed, 50 insertions(+), 141 deletions(-) delete mode 100644 core/designsystem/src/main/res/drawable/ic_modal_warning.xml rename presentation/src/main/java/com/threegap/bitnagil/presentation/setting/component/block/{ConfirmDialog.kt => LogoutConfirmDialog.kt} (55%) delete mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/setting/model/ConfirmDialogType.kt 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..d1fb0c9a 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 @@ -110,6 +110,16 @@ data class BitnagilTextButtonColor( pressedTextColor = BitnagilTheme.colors.navy500, disabledTextColor = BitnagilTheme.colors.navy500, ) + + @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/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/presentation/src/main/java/com/threegap/bitnagil/presentation/setting/SettingScreen.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/setting/SettingScreen.kt index c3114961..e727bfbf 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 @@ -29,7 +29,7 @@ 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.SettingSideEffect import com.threegap.bitnagil.presentation.setting.model.mvi.SettingState @@ -49,11 +49,10 @@ fun SettingScreenContainer( } } - state.showConfirmDialog?.let { dialogType -> - ConfirmDialog( - type = dialogType, + if (state.logoutConfirmDialogVisible) { + LogoutConfirmDialog( onDismiss = viewModel::hideConfirmDialog, - onConfirm = viewModel::confirmDialogAction, + onConfirm = viewModel::logout, ) } @@ -66,7 +65,7 @@ fun SettingScreenContainer( onClickTermsOfService = navigateToTermsOfService, onClickPrivacyPolicy = navigateToPrivacyPolicy, onClickLogout = viewModel::showLogoutDialog, - onClickWithdrawal = viewModel::showWithdrawalDialog, + onClickWithdrawal = {}, ) } @@ -181,7 +180,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..03f9e827 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, @@ -49,57 +46,45 @@ class SettingViewModel @Inject constructor( } is SettingIntent.ShowConfirmDialog -> { - return state.copy(showConfirmDialog = intent.type) + 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) - 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.ShowConfirmDialog) } fun hideConfirmDialog() { sendIntent(SettingIntent.HideConfirmDialog) } - fun confirmDialogAction() { - val currentDialogType = container.stateFlow.value.showConfirmDialog - + fun logout() { sendIntent(SettingIntent.HideConfirmDialog) - - when (currentDialogType) { - ConfirmDialogType.LOGOUT -> executeLogout() - ConfirmDialogType.WITHDRAW -> executeWithdrawal() - null -> {} + viewModelScope.launch { + sendIntent(SettingIntent.LogoutLoading) + logoutUseCase().onSuccess { + sendIntent(SettingIntent.LogoutSuccess) + }.onFailure { + sendIntent(SettingIntent.LogoutFailure) + } } } @@ -118,26 +103,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/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..5358865f 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,11 @@ sealed class SettingIntent : MviIntent { val latestVersion: String, ) : SettingIntent() - data class ShowConfirmDialog(val type: ConfirmDialogType) : SettingIntent() + data object ShowConfirmDialog : 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() } 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, ) } } From b61c73b583c732fd4fdc8399ea25ac7472d32c90 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Wed, 13 Aug 2025 18:13:22 +0900 Subject: [PATCH 24/59] =?UTF-8?q?Refactor:=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83=20=EC=A4=91=EB=B3=B5=20=ED=81=B4=EB=A6=AD=20=EB=B0=A9?= =?UTF-8?q?=EC=A7=80=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/setting/SettingViewModel.kt | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) 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 03f9e827..e65ee24e 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 @@ -77,14 +77,19 @@ class SettingViewModel @Inject constructor( } fun logout() { + if (container.stateFlow.value.loading) return + sendIntent(SettingIntent.HideConfirmDialog) viewModelScope.launch { sendIntent(SettingIntent.LogoutLoading) - logoutUseCase().onSuccess { - sendIntent(SettingIntent.LogoutSuccess) - }.onFailure { - sendIntent(SettingIntent.LogoutFailure) - } + logoutUseCase().fold( + onSuccess = { + sendIntent(SettingIntent.LogoutSuccess) + }, + onFailure = { + sendIntent(SettingIntent.LogoutFailure) + }, + ) } } From 7c7cbc54e9fcdb3cb55e37e973d87e0bd4243dfc Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Wed, 13 Aug 2025 18:21:43 +0900 Subject: [PATCH 25/59] =?UTF-8?q?Refactor:=20=EB=A1=9C=EB=94=A9=20?= =?UTF-8?q?=ED=98=B8=EC=B6=9C=20=EC=9C=84=EC=B9=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../threegap/bitnagil/presentation/setting/SettingViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 e65ee24e..fe174569 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 @@ -80,8 +80,8 @@ class SettingViewModel @Inject constructor( if (container.stateFlow.value.loading) return sendIntent(SettingIntent.HideConfirmDialog) + sendIntent(SettingIntent.LogoutLoading) viewModelScope.launch { - sendIntent(SettingIntent.LogoutLoading) logoutUseCase().fold( onSuccess = { sendIntent(SettingIntent.LogoutSuccess) From d4e567b678b95812f986a65f07daa7b7464ef406 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Wed, 13 Aug 2025 18:42:31 +0900 Subject: [PATCH 26/59] =?UTF-8?q?Refactor:=20=EB=A3=A8=ED=8B=B4=20?= =?UTF-8?q?=EC=88=98=EC=A0=95,=20=EC=82=AD=EC=A0=9C=20=EB=A1=9C=EC=A7=81?= =?UTF-8?q?=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 루틴 상세 바텀시트 컴포넌트 제거 - 루틴 삭제 모달 컴포넌트 제가 - 수정, 삭제 관련 state, intent 제거 --- .../java/com/threegap/bitnagil/MainNavHost.kt | 3 - .../bitnagil/navigation/home/HomeNavHost.kt | 2 - .../bitnagil/presentation/home/HomeScreen.kt | 41 --- .../presentation/home/HomeViewModel.kt | 144 +------- .../home/component/atom/EmotionBall.kt | 140 -------- .../component/template/DeleteConfirmDialog.kt | 126 ------- .../template/RoutineDetailsBottomSheet.kt | 329 ------------------ .../presentation/home/model/HomeIntent.kt | 11 - .../presentation/home/model/HomeSideEffect.kt | 1 - .../presentation/home/model/HomeState.kt | 5 - 10 files changed, 4 insertions(+), 798 deletions(-) delete mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/atom/EmotionBall.kt delete mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/template/DeleteConfirmDialog.kt delete mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/template/RoutineDetailsBottomSheet.kt diff --git a/app/src/main/java/com/threegap/bitnagil/MainNavHost.kt b/app/src/main/java/com/threegap/bitnagil/MainNavHost.kt index 5d40ed48..c46de6be 100644 --- a/app/src/main/java/com/threegap/bitnagil/MainNavHost.kt +++ b/app/src/main/java/com/threegap/bitnagil/MainNavHost.kt @@ -121,9 +121,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) }, 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 f52396c4..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, ) } 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 4c38db64..6fb0d40b 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 @@ -27,9 +27,7 @@ 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.EmptyRoutineView -import com.threegap.bitnagil.presentation.home.component.template.RoutineDetailsBottomSheet import com.threegap.bitnagil.presentation.home.component.template.RoutineSection import com.threegap.bitnagil.presentation.home.component.template.WeeklyDatePicker import com.threegap.bitnagil.presentation.home.model.HomeIntent @@ -42,7 +40,6 @@ import java.time.LocalDate fun HomeScreenContainer( viewModel: HomeViewModel = hiltViewModel(), navigateToRegisterRoutine: () -> Unit, - navigateToEditRoutine: (String) -> Unit, navigateToEmotion: () -> Unit, ) { val uiState by viewModel.stateFlow.collectAsStateWithLifecycle() @@ -57,10 +54,6 @@ fun HomeScreenContainer( navigateToEmotion() } - is HomeSideEffect.NavigateToEditRoutine -> { - navigateToEditRoutine(sideEffect.routineId) - } - is HomeSideEffect.ShowToastWithIcon -> { GlobalBitnagilToast.showCheck(sideEffect.message) } @@ -71,40 +64,6 @@ fun HomeScreenContainer( } } - 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 -> 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 1e4a79c9..1450bed0 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 @@ -8,8 +8,6 @@ import com.threegap.bitnagil.domain.emotion.usecase.GetEmotionChangeEventFlowUse 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.usecase.FetchWeeklyRoutinesUseCase import com.threegap.bitnagil.domain.routine.usecase.RoutineCompletionUseCase import com.threegap.bitnagil.domain.user.usecase.FetchUserProfileUseCase @@ -20,7 +18,6 @@ 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.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,8 +39,6 @@ class HomeViewModel @Inject constructor( private val fetchUserProfileUseCase: FetchUserProfileUseCase, 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, @@ -63,7 +58,7 @@ class HomeViewModel @Inject constructor( observeRoutineUpdates() fetchWeeklyRoutines(container.stateFlow.value.currentWeeks) fetchUserProfile() - getMyEmotion(container.stateFlow.value.selectedDate) + fetchTodayEmotion(container.stateFlow.value.selectedDate) } override suspend fun SimpleSyntax.reduceState( @@ -111,90 +106,12 @@ class HomeViewModel @Inject constructor( updateSubRoutine(state, intent.routineId, intent.subRoutineId, intent.isCompleted) } - 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.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.LoadTodayEmotion -> { state.copy(todayEmotion = intent.emotion) } is HomeIntent.OnRegisterEmotionClick -> { - if (state.myEmotion == null) { - sendSideEffect(HomeSideEffect.NavigateToEmotion) - } else { - sendSideEffect(HomeSideEffect.ShowToastWithIcon("선택한 감정 구슬이 이미 반영되었어요.")) - } + sendSideEffect(HomeSideEffect.NavigateToEmotion) null } @@ -207,11 +124,6 @@ class HomeViewModel @Inject constructor( sendSideEffect(HomeSideEffect.ShowToast("루틴 완료 상태 저장에 실패했어요.\n다시 시도해 주세요.")) null } - - is HomeIntent.NavigateToEditRoutine -> { - sendSideEffect(HomeSideEffect.NavigateToEditRoutine(intent.routineId)) - null - } } return newState } @@ -228,7 +140,7 @@ class HomeViewModel @Inject constructor( viewModelScope.launch { getEmotionChangeEventFlowUseCase().collect { val currentDate = LocalDate.now() - getMyEmotion(currentDate) + fetchTodayEmotion(currentDate) } } } @@ -301,7 +213,7 @@ class HomeViewModel @Inject constructor( } } - private fun getMyEmotion(currentDate: LocalDate) { + private fun fetchTodayEmotion(currentDate: LocalDate) { sendIntent(HomeIntent.UpdateLoading(true)) viewModelScope.launch { fetchTodayEmotionUseCase(currentDate.toString()).fold( @@ -494,52 +406,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 8e27b316..00000000 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/atom/EmotionBall.kt +++ /dev/null @@ -1,140 +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.presentation.home.model.EmotionBallType - -@Composable -fun EmotionBall( - emotionType: EmotionBallType?, - modifier: Modifier = Modifier, -) { - if (emotionType != null) { - Image( - painter = painterResource(emotionType.drawableId), - contentDescription = null, - modifier = modifier - .size(172.dp) - .padding(10.dp) - .fillMaxSize() - .shadow( - elevation = 24.dp, - shape = CircleShape, - ambientColor = emotionType.ambientColor, - spotColor = emotionType.spotColor, - ), - ) - } else { - Image( - painter = painterResource(R.drawable.default_emotion), - contentDescription = null, - modifier = modifier - .size(108.dp, 150.dp) - .fillMaxSize(), - ) - } -} - -@Preview(showBackground = true, name = "Default (Null)") -@Composable -private fun EmotionBallDefaultPreview() { - Column( - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - EmotionBall( - emotionType = null, - ) - } -} - -@Preview(showBackground = true, name = "Calm") -@Composable -private fun EmotionBallCalmPreview() { - Column( - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - EmotionBall( - emotionType = EmotionBallType.CALM, - ) - } -} - -@Preview(showBackground = true, name = "Vitality") -@Composable -private fun EmotionBallVitalityPreview() { - Column( - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - EmotionBall( - emotionType = EmotionBallType.VITALITY, - ) - } -} - -@Preview(showBackground = true, name = "Lethargy") -@Composable -private fun EmotionBallLethargyPreview() { - Column( - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - EmotionBall( - emotionType = EmotionBallType.LETHARGY, - ) - } -} - -@Preview(showBackground = true, name = "Anxiety") -@Composable -private fun EmotionBallAnxietyPreview() { - Column( - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - EmotionBall( - emotionType = EmotionBallType.ANXIETY, - ) - } -} - -@Preview(showBackground = true, name = "Satisfaction") -@Composable -private fun EmotionBallSatisfactionPreview() { - Column( - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - EmotionBall( - emotionType = EmotionBallType.SATISFACTION, - ) - } -} - -@Preview(showBackground = true, name = "Fatigue") -@Composable -private fun EmotionBallFatiguePreview() { - Column( - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - EmotionBall( - emotionType = EmotionBallType.FATIGUE, - ) - } -} 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/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/model/HomeIntent.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/HomeIntent.kt index c45afe77..ab01bb04 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 @@ -11,20 +11,9 @@ sealed class HomeIntent : MviIntent { 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 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 object RoutineToggleCompletionFailure : HomeIntent() data object OnRegisterEmotionClick : HomeIntent() data object OnRegisterRoutineClick : HomeIntent() data object OnPreviousWeekClick : HomeIntent() data object OnNextWeekClick : 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 9f472f3b..c73306c6 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,15 +9,10 @@ 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 routineDetailsBottomSheetVisible: Boolean = false, - val showDeleteConfirmDialog: Boolean = false, - val selectedRoutine: RoutineUiModel? = null, - val deletingRoutine: RoutineUiModel? = null, ) : MviState { val selectedDateRoutines: List get() = routines.routinesByDate[selectedDate.toString()] ?: emptyList() From a13305a2c6eb84d34e1ae6119bc7bdf2d3a7c2e4 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Wed, 13 Aug 2025 19:01:14 +0900 Subject: [PATCH 27/59] =?UTF-8?q?Feat:=20=EB=94=94=EC=9E=90=EC=9D=B8=20?= =?UTF-8?q?=EC=BB=AC=EB=9F=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/threegap/bitnagil/designsystem/color/BitnagilColors.kt | 1 + .../main/java/com/threegap/bitnagil/designsystem/color/Color.kt | 1 + 2 files changed, 2 insertions(+) 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) From b94a04c851c200cee90304ef016747b4faf868a4 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Wed, 13 Aug 2025 19:19:52 +0900 Subject: [PATCH 28/59] =?UTF-8?q?Feat:=20EmptyRoutineListView=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../template/EmptyRoutineListView.kt | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/component/template/EmptyRoutineListView.kt 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 = {}) +} From 932081fe886fc3757141afabe404a1cf1807ac65 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Wed, 13 Aug 2025 19:28:40 +0900 Subject: [PATCH 29/59] =?UTF-8?q?Chore:=20=EB=84=A4=EC=9D=B4=EB=B0=8D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ShowConfirmDialog -> ShowLogoutConfirmDialog --- .../bitnagil/presentation/setting/SettingViewModel.kt | 4 ++-- .../bitnagil/presentation/setting/model/mvi/SettingIntent.kt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) 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 fe174569..e97f0776 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 @@ -45,7 +45,7 @@ class SettingViewModel @Inject constructor( return state.copy(useServiceAlarm = !state.useServiceAlarm) } - is SettingIntent.ShowConfirmDialog -> { + is SettingIntent.ShowLogoutConfirmDialog -> { return state.copy(logoutConfirmDialogVisible = true) } @@ -69,7 +69,7 @@ class SettingViewModel @Inject constructor( } fun showLogoutDialog() { - sendIntent(SettingIntent.ShowConfirmDialog) + sendIntent(SettingIntent.ShowLogoutConfirmDialog) } fun hideConfirmDialog() { 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 5358865f..c16591a4 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 @@ -10,7 +10,7 @@ sealed class SettingIntent : MviIntent { val latestVersion: String, ) : SettingIntent() - data object ShowConfirmDialog : SettingIntent() + data object ShowLogoutConfirmDialog : SettingIntent() data object HideConfirmDialog : SettingIntent() data object ToggleServiceAlarm : SettingIntent() data object TogglePushAlarm : SettingIntent() From d6bcec798a02a6dc5a726e4a13e3d9a10c09352d Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Thu, 14 Aug 2025 07:39:10 +0900 Subject: [PATCH 30/59] =?UTF-8?q?Feat:=20=ED=83=88=ED=87=B4=20=EC=82=AC?= =?UTF-8?q?=EC=9C=A0=20enum=20class=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/withdrawal/model/WithdrawalReason.kt | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/withdrawal/model/WithdrawalReason.kt 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("앱이 자주 멈추거나 오류가 발생해요."), +} From f534ef40adee4ad69f8a94572ef60679163c42c9 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Thu, 14 Aug 2025 07:39:42 +0900 Subject: [PATCH 31/59] =?UTF-8?q?Feat:=20=ED=9A=8C=EC=9B=90=ED=83=88?= =?UTF-8?q?=ED=87=B4=20MVI=20=EB=AA=A8=EB=8D=B8=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../withdrawal/model/WithdrawalIntent.kt | 13 +++++++++++++ .../withdrawal/model/WithdrawalSideEffect.kt | 8 ++++++++ .../withdrawal/model/WithdrawalState.kt | 19 +++++++++++++++++++ 3 files changed, 40 insertions(+) create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/withdrawal/model/WithdrawalIntent.kt create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/withdrawal/model/WithdrawalSideEffect.kt create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/withdrawal/model/WithdrawalState.kt 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/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 +} From 202b3bc0460ac3bfe66bbd6a32f8151a615773e0 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Thu, 14 Aug 2025 07:40:09 +0900 Subject: [PATCH 32/59] =?UTF-8?q?Feat:=20=ED=83=88=ED=87=B4=20=ED=99=95?= =?UTF-8?q?=EC=9D=B8=20=EB=8B=A4=EC=9D=B4=EC=96=BC=EB=A1=9C=EA=B7=B8=20?= =?UTF-8?q?=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/WithdrawalConfirmDialog.kt | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/withdrawal/component/WithdrawalConfirmDialog.kt 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 = {}, + ) +} From 3b1c1bdecc9a374befba8559e1ab503db39f5370 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Thu, 14 Aug 2025 07:41:11 +0900 Subject: [PATCH 33/59] =?UTF-8?q?Refactor:=20BitnagilSelectButton=20?= =?UTF-8?q?=EA=B3=B5=ED=86=B5=20=EC=BB=B4=ED=8F=AC=EB=84=8C=ED=8A=B8=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 선택시 아이콘 추가 --- .../component/atom/BitnagilSelectButton.kt | 96 ++++++++++++++----- .../res/drawable/ic_check_circle_orange.xml | 23 +++++ 2 files changed, 95 insertions(+), 24 deletions(-) create mode 100644 core/designsystem/src/main/res/drawable/ic_check_circle_orange.xml 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/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 @@ + + + + + From 124fa237bcffe3062f329df5d894f6e305efea84 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Thu, 14 Aug 2025 07:41:45 +0900 Subject: [PATCH 34/59] =?UTF-8?q?Feat:=20=ED=9A=8C=EC=9B=90=ED=83=88?= =?UTF-8?q?=ED=87=B4=20=ED=99=94=EB=A9=B4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../withdrawal/WithdrawalScreen.kt | 232 ++++++++++++++++++ .../withdrawal/WithdrawalViewModel.kt | 72 ++++++ 2 files changed, 304 insertions(+) create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/withdrawal/WithdrawalScreen.kt create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/withdrawal/WithdrawalViewModel.kt 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..2cf5912a --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/withdrawal/WithdrawalScreen.kt @@ -0,0 +1,232 @@ +package com.threegap.bitnagil.presentation.withdrawal + +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.layout.statusBarsPadding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +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.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, + modifier: Modifier = Modifier, +) { + val focusManager = LocalFocusManager.current + val focusRequester = remember { FocusRequester() } + + Column( + modifier = modifier + .fillMaxSize() + .background(BitnagilTheme.colors.white) + .statusBarsPadding(), + ) { + BitnagilTopBar( + title = "탈퇴하기", + showBackButton = true, + onBackClick = onBackClick, + ) + + Column( + modifier = Modifier.padding(horizontal = 16.dp), + ) { + 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, + ) + } + } + + if (uiState.isTermsChecked) { + Spacer(modifier = Modifier.height(48.dp)) + + Column( + modifier = Modifier.padding(horizontal = 16.dp), + ) { + 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 + .focusRequester(focusRequester) + .onFocusChanged { focusState -> + if (focusState.isFocused) { + onReasonSelect(null) + } + }, + decorationBox = { innerTextField -> + Box( + modifier = Modifier + .fillMaxWidth() + .background( + color = BitnagilTheme.colors.coolGray99, + shape = RoundedCornerShape(12.dp), + ) + .height(112.dp) + .padding(vertical = 14.dp, horizontal = 20.dp), + ) { + if (uiState.customReasonText.isEmpty()) { + Text( + text = "기타사항(직접 입력)", + color = BitnagilTheme.colors.coolGray80, + style = BitnagilTheme.typography.subtitle1Medium, + ) + } + innerTextField() + } + }, + ) + } + } + + Spacer(modifier = Modifier.weight(1f)) + + BitnagilTextButton( + text = "탈퇴하기", + onClick = onWithdrawalClick, + enabled = uiState.isWithdrawalEnabled, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 14.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)) + }, + ) + } + } +} From 9a1bfab4c730e68d9a8a743fa27fe5b385cfe607 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Thu, 14 Aug 2025 07:42:36 +0900 Subject: [PATCH 35/59] =?UTF-8?q?Feat:=20=ED=83=88=ED=87=B4=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20navigation=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/threegap/bitnagil/MainNavHost.kt | 21 +++++++++++++++++++ .../main/java/com/threegap/bitnagil/Route.kt | 3 +++ .../presentation/setting/SettingScreen.kt | 5 ++++- .../presentation/setting/SettingViewModel.kt | 5 +++++ .../setting/model/mvi/SettingIntent.kt | 1 + .../setting/model/mvi/SettingSideEffect.kt | 1 + 6 files changed, 35 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/threegap/bitnagil/MainNavHost.kt b/app/src/main/java/com/threegap/bitnagil/MainNavHost.kt index 5716453b..dec3eb86 100644 --- a/app/src/main/java/com/threegap/bitnagil/MainNavHost.kt +++ b/app/src/main/java/com/threegap/bitnagil/MainNavHost.kt @@ -16,6 +16,7 @@ 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 @@ -166,6 +167,9 @@ fun MainNavHost( } } }, + navigateToWithdrawal = { + navigator.navController.navigate(Route.Withdrawal) + }, ) } @@ -223,5 +227,22 @@ fun MainNavHost( }, ) } + + composable { + WithdrawalScreenContainer( + navigateToBack = { + if (navigator.navController.previousBackStackEntry != null) { + navigator.navController.popBackStack() + } + }, + navigateToLogin = { + navigator.navController.navigate(Route.Login) { + popUpTo(0) { + inclusive = true + } + } + }, + ) + } } } diff --git a/app/src/main/java/com/threegap/bitnagil/Route.kt b/app/src/main/java/com/threegap/bitnagil/Route.kt index ced10832..cb9eacde 100644 --- a/app/src/main/java/com/threegap/bitnagil/Route.kt +++ b/app/src/main/java/com/threegap/bitnagil/Route.kt @@ -38,4 +38,7 @@ sealed interface Route { @Serializable data object Emotion : Route + + @Serializable + data object Withdrawal : Route } 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 e727bfbf..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 @@ -30,6 +30,7 @@ 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.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,12 +41,14 @@ 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() } } @@ -65,7 +68,7 @@ fun SettingScreenContainer( onClickTermsOfService = navigateToTermsOfService, onClickPrivacyPolicy = navigateToPrivacyPolicy, onClickLogout = viewModel::showLogoutDialog, - onClickWithdrawal = {}, + onClickWithdrawal = { viewModel.sendIntent(SettingIntent.OnWithdrawalClick) }, ) } 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 e97f0776..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 @@ -65,6 +65,11 @@ class SettingViewModel @Inject constructor( SettingIntent.LogoutFailure -> { return state.copy(loading = false) } + + SettingIntent.OnWithdrawalClick -> { + sendSideEffect(SettingSideEffect.NavigateToWithdrawal) + return null + } } } 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 c16591a4..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 @@ -17,4 +17,5 @@ sealed class SettingIntent : MviIntent { data object LogoutLoading : SettingIntent() data object LogoutSuccess : SettingIntent() data object LogoutFailure : 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() } From 89c095f63f049699163eec9ee172624371265f20 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Thu, 14 Aug 2025 16:07:17 +0900 Subject: [PATCH 36/59] =?UTF-8?q?Feat:=20=EB=A3=A8=ED=8B=B4=ED=83=88?= =?UTF-8?q?=ED=87=B4=20=ED=99=94=EB=A9=B4=20=EC=8A=A4=ED=81=AC=EB=A1=A4=20?= =?UTF-8?q?=EC=98=81=EC=97=AD=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/AndroidManifest.xml | 1 + .../java/com/threegap/bitnagil/MainScreen.kt | 8 +-- .../withdrawal/WithdrawalScreen.kt | 63 ++++++++++--------- .../writeroutine/WriteRoutineScreen.kt | 6 +- 4 files changed, 40 insertions(+), 38 deletions(-) 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/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/presentation/src/main/java/com/threegap/bitnagil/presentation/withdrawal/WithdrawalScreen.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/withdrawal/WithdrawalScreen.kt index 2cf5912a..89737ec4 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/withdrawal/WithdrawalScreen.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/withdrawal/WithdrawalScreen.kt @@ -2,23 +2,28 @@ package com.threegap.bitnagil.presentation.withdrawal 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.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 @@ -81,16 +86,17 @@ private fun WithdrawalScreen( onCustomReasonChanged: (String) -> Unit, onBackClick: () -> Unit, onWithdrawalClick: () -> Unit, - modifier: Modifier = Modifier, ) { val focusManager = LocalFocusManager.current val focusRequester = remember { FocusRequester() } + val scrollState = rememberScrollState() Column( - modifier = modifier + modifier = Modifier .fillMaxSize() .background(BitnagilTheme.colors.white) - .statusBarsPadding(), + .statusBarsPadding() + .windowInsetsPadding(WindowInsets.ime), ) { BitnagilTopBar( title = "탈퇴하기", @@ -99,7 +105,10 @@ private fun WithdrawalScreen( ) Column( - modifier = Modifier.padding(horizontal = 16.dp), + modifier = Modifier + .padding(horizontal = 16.dp) + .weight(1f) + .verticalScroll(scrollState), ) { Spacer(modifier = Modifier.height(46.dp)) @@ -136,13 +145,12 @@ private fun WithdrawalScreen( style = BitnagilTheme.typography.body2Medium, ) } - } - if (uiState.isTermsChecked) { Spacer(modifier = Modifier.height(48.dp)) Column( - modifier = Modifier.padding(horizontal = 16.dp), + modifier = Modifier + .alpha(if (uiState.isTermsChecked) 1f else 0f), ) { Text( text = "탈퇴 사유를 알려주실 수 있나요?", @@ -172,6 +180,13 @@ private fun WithdrawalScreen( 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) { @@ -179,31 +194,20 @@ private fun WithdrawalScreen( } }, decorationBox = { innerTextField -> - Box( - modifier = Modifier - .fillMaxWidth() - .background( - color = BitnagilTheme.colors.coolGray99, - shape = RoundedCornerShape(12.dp), - ) - .height(112.dp) - .padding(vertical = 14.dp, horizontal = 20.dp), - ) { - if (uiState.customReasonText.isEmpty()) { - Text( - text = "기타사항(직접 입력)", - color = BitnagilTheme.colors.coolGray80, - style = BitnagilTheme.typography.subtitle1Medium, - ) - } - innerTextField() + if (uiState.customReasonText.isEmpty()) { + Text( + text = "기타사항(직접 입력)", + color = BitnagilTheme.colors.coolGray80, + style = BitnagilTheme.typography.subtitle1Medium, + ) } + innerTextField() }, ) } - } - Spacer(modifier = Modifier.weight(1f)) + Spacer(modifier = Modifier.height(54.dp)) + } BitnagilTextButton( text = "탈퇴하기", @@ -211,7 +215,8 @@ private fun WithdrawalScreen( enabled = uiState.isWithdrawalEnabled, modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 14.dp), + .alpha(if (uiState.isTermsChecked) 1f else 0f) + .padding(vertical = 14.dp, horizontal = 16.dp), ) } } 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 "루틴 수정", From 02f7973d0ce8e361dad8e0066e0a69af73b21e46 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Fri, 15 Aug 2025 01:41:47 +0900 Subject: [PATCH 37/59] =?UTF-8?q?Chore:=20api=20url=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../threegap/bitnagil/data/emotion/service/EmotionService.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 9538d342..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 @@ -19,7 +19,7 @@ interface EmotionService { @Body request: RegisterEmotionRequest, ): BaseResponse - @GET("/api/v2/{searchDate}") + @GET("/api/v2/emotion-marbles/{searchDate}") suspend fun fetchTodayEmotion( @Path("searchDate") date: String, ): BaseResponse From 8edf0ee73d85d66a3617b99f2edb1ea444269c2b Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Fri, 15 Aug 2025 04:01:22 +0900 Subject: [PATCH 38/59] =?UTF-8?q?Chore:=20=ED=8F=B0=ED=8A=B8=20=EB=94=94?= =?UTF-8?q?=EC=9E=90=EC=9D=B8=20=EC=8B=9C=EC=8A=A4=ED=85=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/threegap/bitnagil/designsystem/typography/Type.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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), ), ) { From 5b57cc8a39692e020e40a9ef6582b5376781c037 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Fri, 15 Aug 2025 04:29:23 +0900 Subject: [PATCH 39/59] =?UTF-8?q?Refactor:=20=EB=A3=A8=ED=8B=B4=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20api=20=EB=B2=84=EC=A0=84=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - v1 -> v2 버전업 - response 변화에 따른 모델 수정 --- .../routine/model/response/DayRoutinesDto.kt | 19 +++++++++ .../data/routine/model/response/RoutineDto.kt | 38 ++++++++++++------ .../model/response/RoutinesResponseDto.kt | 10 ++++- .../data/routine/service/RoutineService.kt | 2 +- .../domain/routine/model/DayRoutines.kt | 6 +++ .../routine/model/RecommendedRoutineType.kt | 18 +++++++++ .../bitnagil/domain/routine/model/Routine.kt | 16 +++----- .../bitnagil/domain/routine/model/Routines.kt | 11 +----- .../usecase/FetchWeeklyRoutinesUseCase.kt | 1 - .../home/model/DayRoutinesUiModel.kt | 17 ++++++++ .../presentation/home/model/HomeState.kt | 2 +- .../presentation/home/model/RoutineUiModel.kt | 39 +++++++------------ .../home/model/RoutinesUiModel.kt | 6 +-- .../writeroutine/WriteRoutineViewModel.kt | 5 ++- 14 files changed, 124 insertions(+), 66 deletions(-) create mode 100644 data/src/main/java/com/threegap/bitnagil/data/routine/model/response/DayRoutinesDto.kt create mode 100644 domain/src/main/java/com/threegap/bitnagil/domain/routine/model/DayRoutines.kt create mode 100644 domain/src/main/java/com/threegap/bitnagil/domain/routine/model/RecommendedRoutineType.kt create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/DayRoutinesUiModel.kt 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/service/RoutineService.kt b/data/src/main/java/com/threegap/bitnagil/data/routine/service/RoutineService.kt index 403789eb..16798690 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 @@ -14,7 +14,7 @@ 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, 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/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/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/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/HomeState.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/HomeState.kt index c73306c6..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 @@ -15,5 +15,5 @@ data class HomeState( val routines: RoutinesUiModel = RoutinesUiModel(), ) : MviState { val selectedDateRoutines: List - get() = routines.routinesByDate[selectedDate.toString()] ?: emptyList() + get() = routines.routines[selectedDate.toString()]?.routineList ?: emptyList() } 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/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(), ), ) }, From e61f183a1640c53aba3635993f21785db4f88d03 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Fri, 15 Aug 2025 04:32:52 +0900 Subject: [PATCH 40/59] =?UTF-8?q?Refactor:=20=EB=A3=A8=ED=8B=B4=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C=20api=20=EB=B2=84=EC=A0=84=20=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - v1 -> v2로 마이그레이션 - 루틴 완료 로직 인덱스 값 기반으로 수정 - 루틴 완료 requestDto 수정 --- .../model/request/RoutineCompletionInfoDto.kt | 18 ++-- .../request/RoutineCompletionRequestDto.kt | 10 +- .../repositoryImpl/RoutineRepositoryImpl.kt | 9 +- .../data/routine/service/RoutineService.kt | 4 +- .../routine/model/RoutineCompletionInfo.kt | 5 +- .../routine/model/RoutineCompletionInfos.kt | 5 + .../routine/repository/RoutineRepository.kt | 4 +- .../usecase/RoutineCompletionUseCase.kt | 6 +- .../bitnagil/presentation/home/HomeScreen.kt | 13 ++- .../presentation/home/HomeViewModel.kt | 89 ++++++++--------- .../home/component/block/RoutineItem.kt | 74 ++++---------- .../home/component/block/SubRoutinesItem.kt | 56 +++-------- .../home/component/template/RoutineSection.kt | 98 +++++++++---------- .../presentation/home/model/HomeIntent.kt | 2 +- .../writeroutine/model/SubRoutine.kt | 9 +- 15 files changed, 167 insertions(+), 235 deletions(-) create mode 100644 domain/src/main/java/com/threegap/bitnagil/domain/routine/model/RoutineCompletionInfos.kt 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/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 16798690..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,7 +9,7 @@ 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 @@ -20,7 +20,7 @@ interface RoutineService { @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/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/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/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 6fb0d40b..3af8cd7b 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 @@ -78,8 +78,8 @@ fun HomeScreenContainer( onRoutineCompletionToggle = { routineId, isCompleted -> viewModel.toggleRoutineCompletion(routineId, isCompleted) }, - onSubRoutineCompletionToggle = { routineId, subRoutineId, isCompleted -> - viewModel.toggleSubRoutineCompletion(routineId, subRoutineId, isCompleted) + onSubRoutineCompletionToggle = { routineId, subRoutineIndex, isCompleted -> + viewModel.toggleSubRoutineCompletion(routineId, subRoutineIndex, isCompleted) }, onRegisterRoutineClick = { viewModel.sendIntent(HomeIntent.OnRegisterRoutineClick) @@ -100,7 +100,7 @@ private fun HomeScreen( onPreviousWeekClick: () -> Unit, onNextWeekClick: () -> Unit, onRoutineCompletionToggle: (String, Boolean) -> Unit, - onSubRoutineCompletionToggle: (String, String, Boolean) -> Unit, + onSubRoutineCompletionToggle: (String, Int, Boolean) -> Unit, onRegisterRoutineClick: () -> Unit, onRegisterEmotionClick: () -> Unit, onShowMoreRoutinesClick: () -> Unit, @@ -170,8 +170,7 @@ private fun HomeScreen( .fillMaxSize() .background(BitnagilTheme.colors.coolGray99) .nestedScroll(collapsibleHeaderState.nestedScrollConnection) - .padding(horizontal = 16.dp) - .padding(bottom = 12.dp), + .padding(horizontal = 16.dp), state = collapsibleHeaderState.lazyListState, verticalArrangement = Arrangement.spacedBy(12.dp), ) { @@ -187,10 +186,10 @@ private fun HomeScreen( isCompleted, ) }, - onSubRoutineToggle = { subRoutineId, isCompleted -> + onSubRoutineToggle = { subRoutineIndex, isCompleted -> onSubRoutineCompletionToggle( routine.routineId, - subRoutineId, + subRoutineIndex, isCompleted, ) }, 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 1450bed0..5f358c20 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 @@ -6,8 +6,8 @@ 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.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.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 @@ -103,7 +103,7 @@ 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.LoadTodayEmotion -> { @@ -201,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 -> @@ -237,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) } @@ -278,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 @@ -293,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 -> @@ -301,19 +299,26 @@ 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 < 0 || subRoutineIndex >= routine.subRoutineNames.size) { + return@updateRoutinesForDate false + } + + val updatedSubRoutineCompleteYn = routine.subRoutineCompleteYn.toMutableList().apply { + if (subRoutineIndex < size) { + this[subRoutineIndex] = isCompleted } } - val routineCompleted = if (isCompleted) updatedSubRoutines.all { it.isCompleted } else false + val routineCompleted = if (isCompleted) { + updatedSubRoutineCompleteYn.all { it } + } else { + false + } val updatedRoutine = routine.copy( - subRoutines = updatedSubRoutines, - isCompleted = routineCompleted, + subRoutineCompleteYn = updatedSubRoutineCompleteYn, + routineCompleteYn = routineCompleted, ) routinesForDate[routineIndex] = updatedRoutine @@ -326,12 +331,13 @@ class HomeViewModel @Inject constructor( updateLogic: (MutableList) -> Boolean, ): HomeState { val dateKey = state.selectedDate.toString() - val routinesForDate = state.routines.routinesByDate[dateKey]?.toMutableList() ?: return state + val routinesForDate = state.routines.routines[dateKey]?.routineList?.toMutableList() ?: return state if (!updateLogic(routinesForDate)) return state - val updatedRoutinesByDate = state.routines.routinesByDate.toMutableMap() - updatedRoutinesByDate[dateKey] = routinesForDate + val updatedRoutinesByDate = state.routines.routines.toMutableMap() + val dayRoutines = updatedRoutinesByDate[dateKey] ?: return state + updatedRoutinesByDate[dateKey] = dayRoutines.copy(routineList = routinesForDate) return state.copy(routines = RoutinesUiModel(updatedRoutinesByDate)) } @@ -342,39 +348,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, - ), - ) - } - } } } } @@ -385,9 +377,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( 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 f77a7857..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 @@ -19,15 +19,13 @@ 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.RoutineUiModel -import com.threegap.bitnagil.presentation.home.model.SubRoutineUiModel @Composable fun RoutineItem( routine: RoutineUiModel, onRoutineToggle: (Boolean) -> Unit, - onSubRoutineToggle: (String, Boolean) -> Unit, + onSubRoutineToggle: (Int, Boolean) -> Unit, modifier: Modifier = Modifier, ) { Column( @@ -45,7 +43,7 @@ fun RoutineItem( Row( modifier = Modifier .fillMaxWidth() - .clickableWithoutRipple { onRoutineToggle(!routine.isCompleted) }, + .clickableWithoutRipple { onRoutineToggle(!routine.routineCompleteYn) }, horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { @@ -57,7 +55,7 @@ fun RoutineItem( ) BitnagilIcon( - id = if (routine.isCompleted) R.drawable.ic_check_circle else R.drawable.ic_check_default, + id = if (routine.routineCompleteYn) R.drawable.ic_check_circle else R.drawable.ic_check_default, tint = null, modifier = Modifier .padding(start = 10.dp) @@ -65,7 +63,7 @@ fun RoutineItem( ) } - if (routine.subRoutines.isNotEmpty()) { + if (routine.subRoutineNames.isNotEmpty()) { HorizontalDivider( thickness = 1.dp, color = BitnagilTheme.colors.coolGray97, @@ -73,9 +71,10 @@ fun RoutineItem( ) SubRoutinesItem( - subRoutines = routine.subRoutines, - onSubRoutineToggle = { subRoutineId, isCompleted -> - onSubRoutineToggle(subRoutineId, isCompleted) + subRoutineNames = routine.subRoutineNames, + subRoutineCompleteYn = routine.subRoutineCompleteYn, + onSubRoutineToggle = { index, isCompleted -> + onSubRoutineToggle(index, isCompleted) }, ) } @@ -88,46 +87,14 @@ 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 = { }, onSubRoutineToggle = { _, _ -> }, @@ -141,14 +108,13 @@ private fun NoneSubRoutineRoutineItemPreview() { routine = RoutineUiModel( routineId = "uuid1", routineName = "개운하게 일어나기", + repeatDay = emptyList(), executionTime = "20:30:00", - routineCompletionId = 1, - isCompleted = false, - isModified = false, - subRoutines = emptyList(), - historySeq = 1, - repeatDay = listOf(), - routineType = RoutineType.ROUTINE, + 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/SubRoutinesItem.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/block/SubRoutinesItem.kt index c782d485..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 @@ -15,37 +15,39 @@ 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), ) { - subRoutines.forEach { subRoutine -> + repeat(minSize) { index -> + val subRoutineName = subRoutineNames[index] + val isCompleted = subRoutineCompleteYn.getOrElse(index) { false } + Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp), modifier = Modifier .fillMaxWidth() - .clickableWithoutRipple { - onSubRoutineToggle(subRoutine.subRoutineId, !subRoutine.isCompleted) - }, + .clickableWithoutRipple { onSubRoutineToggle(index, !isCompleted) }, ) { BitnagilIcon( - id = if (subRoutine.isCompleted) R.drawable.ic_check_circle else R.drawable.ic_check_default, + 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.coolGray40, modifier = Modifier.weight(1f), @@ -59,38 +61,8 @@ fun SubRoutinesItem( @Composable private fun SubRoutinesItemPreview() { SubRoutinesItem( - subRoutines = listOf( - SubRoutineUiModel( - subRoutineId = "uuid1", - historySeq = 1, - subRoutineName = "물 마시기", - sortOrder = 1, - routineCompletionId = 1, - isCompleted = true, - 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/RoutineSection.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/template/RoutineSection.kt index 065b5931..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 @@ -10,17 +10,15 @@ 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.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, + onSubRoutineToggle: (Int, Boolean) -> Unit, modifier: Modifier = Modifier, ) { Row( @@ -46,51 +44,51 @@ fun RoutineSection( @Preview(showBackground = true) @Composable 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 = { _, _ -> }, - ) +// 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/model/HomeIntent.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/HomeIntent.kt index ab01bb04..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 @@ -10,7 +10,7 @@ sealed class HomeIntent : MviIntent { 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 OnSubRoutineCompletionToggle(val routineId: String, val subRoutineIndex: Int, val isCompleted: Boolean) : HomeIntent() data object RoutineToggleCompletionFailure : HomeIntent() data object OnRegisterEmotionClick : HomeIntent() data object OnRegisterRoutineClick : HomeIntent() 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(), From 9ee1b7ad9670723a5231713ccf9e2a82226668f8 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Fri, 15 Aug 2025 04:33:24 +0900 Subject: [PATCH 41/59] =?UTF-8?q?Chore:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20=EC=BD=94=EB=93=9C=ED=8C=8C=EC=9D=BC=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data/routine/mapper/RoutineMapper.kt | 63 ------------------- .../routine/model/response/SubRoutineDto.kt | 24 ------- .../domain/routine/model/RoutineCompletion.kt | 6 -- .../domain/routine/model/SubRoutine.kt | 12 ---- .../home/model/EmotionBallType.kt | 48 -------------- .../home/model/SubRoutineUiModel.kt | 37 ----------- 6 files changed, 190 deletions(-) delete mode 100644 data/src/main/java/com/threegap/bitnagil/data/routine/mapper/RoutineMapper.kt delete mode 100644 data/src/main/java/com/threegap/bitnagil/data/routine/model/response/SubRoutineDto.kt delete mode 100644 domain/src/main/java/com/threegap/bitnagil/domain/routine/model/RoutineCompletion.kt delete mode 100644 domain/src/main/java/com/threegap/bitnagil/domain/routine/model/SubRoutine.kt delete mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/EmotionBallType.kt delete mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/home/model/SubRoutineUiModel.kt 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/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/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/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/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/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, - ) From e19aca653cd436c98e6d84f1000cc7307a95e6f7 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Fri, 15 Aug 2025 05:19:25 +0900 Subject: [PATCH 42/59] =?UTF-8?q?Feat:=20=EB=8B=B9=EC=9D=BC=20=EB=AA=A8?= =?UTF-8?q?=EB=93=A0=20=EB=8B=AC=EC=84=B1=20=EC=8B=9C=20=EC=99=84=EB=A3=8C?= =?UTF-8?q?=20=ED=91=9C=EC=8B=9C=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../bitnagil/presentation/home/HomeScreen.kt | 1 + .../presentation/home/HomeViewModel.kt | 11 +++- .../component/template/WeeklyDatePicker.kt | 61 ++++++++++++++++++- 3 files changed, 68 insertions(+), 5 deletions(-) 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 3af8cd7b..1d4b3591 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 @@ -119,6 +119,7 @@ private fun HomeScreen( WeeklyDatePicker( selectedDate = uiState.selectedDate, weeklyDates = uiState.currentWeeks, + routines = uiState.routines, onDateSelect = onDateSelect, onPreviousWeekClick = onPreviousWeekClick, onNextWeekClick = onNextWeekClick, 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 5f358c20..33e3b786 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 @@ -331,13 +331,18 @@ class HomeViewModel @Inject constructor( updateLogic: (MutableList) -> Boolean, ): HomeState { val dateKey = state.selectedDate.toString() - val routinesForDate = state.routines.routines[dateKey]?.routineList?.toMutableList() ?: return state + val dayRoutines = state.routines.routines[dateKey] ?: return state + val routinesForDate = dayRoutines.routineList.toMutableList() if (!updateLogic(routinesForDate)) return state + val allCompleted = routinesForDate.all { it.routineCompleteYn } + val updatedRoutinesByDate = state.routines.routines.toMutableMap() - val dayRoutines = updatedRoutinesByDate[dateKey] ?: return state - updatedRoutinesByDate[dateKey] = dayRoutines.copy(routineList = routinesForDate) + updatedRoutinesByDate[dateKey] = dayRoutines.copy( + routineList = routinesForDate, + allCompleted = allCompleted, + ) return state.copy(routines = RoutinesUiModel(updatedRoutinesByDate)) } 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 d4e9d534..15cbddc3 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,5 +1,16 @@ 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 @@ -12,6 +23,7 @@ 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.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -23,8 +35,10 @@ 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 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 @@ -35,11 +49,21 @@ 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 by remember(routines) { + derivedStateOf { + weeklyDates.associateWith { date -> + routines.routines[date.toString()]?.allCompleted ?: false + } + } + } + Column( modifier = modifier, ) { @@ -91,7 +115,8 @@ fun WeeklyDatePicker( DateItem( date = date, isSelected = selectedDate == date, - isToday = date == LocalDate.now(), + isToday = date == today, + isCompleted = completionStates[date] ?: false, onDateClick = { onDateSelect(date) }, ) } @@ -104,6 +129,7 @@ private fun DateItem( date: LocalDate, isSelected: Boolean, isToday: Boolean, + isCompleted: Boolean, onDateClick: () -> Unit, modifier: Modifier = Modifier, ) { @@ -133,6 +159,36 @@ private fun DateItem( 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, + ) + } + } } } @@ -143,8 +199,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) }, ) From 97e319d3fbe1769e865840c51377efdb9e66eabd Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Fri, 15 Aug 2025 12:42:13 +0900 Subject: [PATCH 43/59] =?UTF-8?q?Chore:=20=EC=BD=94=EB=93=9C=EB=9E=98?= =?UTF-8?q?=EB=B9=97=20=EB=A6=AC=EB=B7=B0=EC=82=AC=ED=95=AD=20=EB=B0=98?= =?UTF-8?q?=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 네이밍 변경 - 감정 조회 호출 시 LocalDate.now()를 통하도록 변경 --- .../com/threegap/bitnagil/presentation/home/HomeViewModel.kt | 2 +- .../presentation/home/component/template/EmptyRoutineView.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 33e3b786..4e28298f 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 @@ -58,7 +58,7 @@ class HomeViewModel @Inject constructor( observeRoutineUpdates() fetchWeeklyRoutines(container.stateFlow.value.currentWeeks) fetchUserProfile() - fetchTodayEmotion(container.stateFlow.value.selectedDate) + fetchTodayEmotion(LocalDate.now()) } override suspend fun SimpleSyntax.reduceState( diff --git a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/template/EmptyRoutineView.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/template/EmptyRoutineView.kt index 1a8bb254..e0df124c 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/template/EmptyRoutineView.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/home/component/template/EmptyRoutineView.kt @@ -64,7 +64,7 @@ fun EmptyRoutineView( @Preview(showBackground = true) @Composable -private fun RoutineEmptyViewPreview() { +private fun EmptyRoutineViewPreview() { EmptyRoutineView( onRegisterRoutineClick = {}, ) From 86f33b95c0a7ede4338a9e9ae5011fc7cadb88b9 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Fri, 15 Aug 2025 12:50:13 +0900 Subject: [PATCH 44/59] =?UTF-8?q?Refactor:=20Routine=20LazyColumn=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - items() API를 사용해 키/콘텐츠 타입을 명시 --- .../bitnagil/presentation/home/HomeScreen.kt | 35 ++++++++----------- 1 file changed, 14 insertions(+), 21 deletions(-) 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 1d4b3591..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 @@ -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.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -175,27 +176,19 @@ private fun HomeScreen( state = collapsibleHeaderState.lazyListState, verticalArrangement = Arrangement.spacedBy(12.dp), ) { - uiState.selectedDateRoutines.forEach { routine -> - item( - key = "${routine.routineId}_${uiState.selectedDate}", - ) { - RoutineSection( - routine = routine, - onRoutineToggle = { isCompleted -> - onRoutineCompletionToggle( - routine.routineId, - isCompleted, - ) - }, - onSubRoutineToggle = { subRoutineIndex, isCompleted -> - onSubRoutineCompletionToggle( - routine.routineId, - subRoutineIndex, - isCompleted, - ) - }, - ) - } + 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) + }, + ) } } } From 5c17495c889ca03abbfeb18a9c3c691115c1c658 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Fri, 15 Aug 2025 13:01:56 +0900 Subject: [PATCH 45/59] =?UTF-8?q?Refactor:=20WeeklyDatePicker=EC=97=90?= =?UTF-8?q?=EC=84=9C=20derivedStateOf=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../home/component/template/WeeklyDatePicker.kt | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) 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 15cbddc3..86b4991c 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 @@ -23,7 +23,6 @@ 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.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -56,12 +55,8 @@ fun WeeklyDatePicker( modifier: Modifier = Modifier, ) { val today = remember { LocalDate.now() } - val completionStates by remember(routines) { - derivedStateOf { - weeklyDates.associateWith { date -> - routines.routines[date.toString()]?.allCompleted ?: false - } - } + val completionStates = weeklyDates.associateWith { date -> + routines.routines[date.toString()]?.allCompleted ?: false } Column( From 3581c0584f6bd68a4daf1c00e2d8a5311aa3dd53 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Fri, 15 Aug 2025 13:17:52 +0900 Subject: [PATCH 46/59] =?UTF-8?q?Refactor:=20=EA=B0=90=EC=A0=95=EA=B5=AC?= =?UTF-8?q?=EC=8A=AC=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EB=A1=9C=EB=94=A9=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AsyncImage의 model 생성 시 remember를 사용하여 불필요한 재생성 방지 --- .../component/template/CollapsibleHomeHeader.kt | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) 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 483d39cb..7635c663 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 @@ -14,6 +14,7 @@ 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.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha @@ -44,6 +45,7 @@ fun CollapsibleHomeHeader( onRegisterEmotion: () -> Unit, modifier: Modifier = Modifier, ) { + val context = LocalContext.current val alpha by animateFloatAsState( targetValue = 1f - collapsibleHeaderState.collapseProgress, animationSpec = tween(durationMillis = 300), @@ -102,16 +104,16 @@ fun CollapsibleHomeHeader( } AsyncImage( - model = ImageRequest.Builder(LocalContext.current) - .data(todayEmotion?.imageUrl) - .crossfade(true) - .size(Size.ORIGINAL) - .build(), + 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, - filterQuality = FilterQuality.High, modifier = Modifier .align(Alignment.BottomEnd) .padding(end = 18.dp) From 5d01e7a3d090430391c82b71a030d944a6163744 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Fri, 15 Aug 2025 13:18:29 +0900 Subject: [PATCH 47/59] Chore: ktlintFormat --- .../home/component/template/CollapsibleHomeHeader.kt | 2 -- 1 file changed, 2 deletions(-) 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 7635c663..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 @@ -18,7 +18,6 @@ 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.graphics.FilterQuality import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource @@ -28,7 +27,6 @@ import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage import coil3.request.ImageRequest import coil3.request.crossfade -import coil3.size.Size import com.threegap.bitnagil.designsystem.BitnagilTheme import com.threegap.bitnagil.designsystem.R import com.threegap.bitnagil.designsystem.component.atom.BitnagilIcon From 2a655e928b509de1d80585533b5328ea40b6cdb2 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Fri, 15 Aug 2025 14:35:42 +0900 Subject: [PATCH 48/59] =?UTF-8?q?Fix:=20=EB=A3=A8=ED=8B=B4=20=EC=99=84?= =?UTF-8?q?=EB=A3=8C=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 중복 검증 처리 제거 --- .../bitnagil/presentation/home/HomeViewModel.kt | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) 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 4e28298f..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 @@ -300,21 +300,15 @@ class HomeViewModel @Inject constructor( val routine = routinesForDate[routineIndex] - if (subRoutineIndex < 0 || subRoutineIndex >= routine.subRoutineNames.size) { + if (subRoutineIndex !in routine.subRoutineCompleteYn.indices) { return@updateRoutinesForDate false } - val updatedSubRoutineCompleteYn = routine.subRoutineCompleteYn.toMutableList().apply { - if (subRoutineIndex < size) { - this[subRoutineIndex] = isCompleted - } + val updatedSubRoutineCompleteYn = routine.subRoutineCompleteYn.toMutableList().also { + it[subRoutineIndex] = isCompleted } - val routineCompleted = if (isCompleted) { - updatedSubRoutineCompleteYn.all { it } - } else { - false - } + val routineCompleted = updatedSubRoutineCompleteYn.all { it } val updatedRoutine = routine.copy( subRoutineCompleteYn = updatedSubRoutineCompleteYn, From 17600db9a981598e3699f5688a70f3e086bbe296 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Fri, 15 Aug 2025 23:54:20 +0900 Subject: [PATCH 49/59] =?UTF-8?q?Fix:=20=EB=B6=88=ED=95=84=EC=9A=94?= =?UTF-8?q?=ED=95=9C=20Map=20=EC=9E=AC=EC=83=9D=EC=84=B1=20=EB=B0=A9?= =?UTF-8?q?=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../home/component/template/WeeklyDatePicker.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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 86b4991c..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 @@ -55,8 +55,10 @@ fun WeeklyDatePicker( modifier: Modifier = Modifier, ) { val today = remember { LocalDate.now() } - val completionStates = weeklyDates.associateWith { date -> - routines.routines[date.toString()]?.allCompleted ?: false + val completionStates = remember(weeklyDates, routines) { + weeklyDates.associateWith { date -> + routines.routines[date.toString()]?.allCompleted ?: false + } } Column( From 56496ff20f28c6fbac6f0923f4416794fc28fe44 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Mon, 11 Aug 2025 15:54:03 +0900 Subject: [PATCH 50/59] =?UTF-8?q?Add:=20=EC=B6=94=EA=B0=80=EB=90=98?= =?UTF-8?q?=EB=8A=94=20=EC=95=84=EC=9D=B4=EC=BD=98=20=EC=97=90=EC=85=8B=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/res/drawable/ic_connect.xml | 50 +++++++++++++ .../src/main/res/drawable/ic_edit.xml | 24 ++++-- .../src/main/res/drawable/ic_grow.xml | 23 ++++++ .../src/main/res/drawable/ic_outside.xml | 33 ++++++++ .../src/main/res/drawable/ic_rest.xml | 30 ++++++++ .../src/main/res/drawable/ic_trash.xml | 52 ++++++++++--- .../src/main/res/drawable/ic_wakeup.xml | 75 +++++++++++++++++++ 7 files changed, 269 insertions(+), 18 deletions(-) create mode 100644 core/designsystem/src/main/res/drawable/ic_connect.xml create mode 100644 core/designsystem/src/main/res/drawable/ic_grow.xml create mode 100644 core/designsystem/src/main/res/drawable/ic_outside.xml create mode 100644 core/designsystem/src/main/res/drawable/ic_rest.xml create mode 100644 core/designsystem/src/main/res/drawable/ic_wakeup.xml 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_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_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 @@ + + + + + + + + + + + + + + + From 269d20177d1a3de4beac8dfc725e0b5e9ab0f4d1 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Mon, 11 Aug 2025 15:56:49 +0900 Subject: [PATCH 51/59] =?UTF-8?q?Feat:=20RoutineDetailsCard=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/template/RoutineDetailsCard.kt | 144 ++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/component/template/RoutineDetailsCard.kt 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..e8200ef4 --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/component/template/RoutineDetailsCard.kt @@ -0,0 +1,144 @@ +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( + 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 = { /*TODO*/ }, + 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() +} From d4b38a7508cec1bec7a07fd52f891873ac02f2f3 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Mon, 11 Aug 2025 15:57:05 +0900 Subject: [PATCH 52/59] =?UTF-8?q?Feat:=20WeeklyDatePicker=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/template/WeeklyDatePicker.kt | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/component/template/WeeklyDatePicker.kt 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..d7205adb --- /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(), + ) +} From 580a52c534b84078c68606dd55311bcb763ac958 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Mon, 11 Aug 2025 15:57:22 +0900 Subject: [PATCH 53/59] =?UTF-8?q?Feat:=20=EB=A3=A8=ED=8B=B4=20=EB=A6=AC?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=ED=99=94=EB=A9=B4=20ui=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../routinelist/RoutineListScreen.kt | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/RoutineListScreen.kt 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..4c085cb1 --- /dev/null +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/RoutineListScreen.kt @@ -0,0 +1,81 @@ +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.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.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.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.threegap.bitnagil.designsystem.BitnagilTheme +import com.threegap.bitnagil.designsystem.component.block.BitnagilTopBar +import com.threegap.bitnagil.presentation.home.util.getCurrentWeekDays +import com.threegap.bitnagil.presentation.routinelist.component.template.RoutineDetailsCard +import com.threegap.bitnagil.presentation.routinelist.component.template.WeeklyDatePicker +import java.time.LocalDate + +@Composable +fun RoutineListScreenContainer( + +) { + RoutineListScreen() +} + +@Composable +private fun RoutineListScreen( + modifier: Modifier = Modifier +) { + Column( + modifier = modifier + .fillMaxSize() + .background(BitnagilTheme.colors.coolGray99) + .statusBarsPadding(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + BitnagilTopBar( + title = "루틴리스트", + showBackButton = true, + onBackClick = {}, + ) + + var selectedDate by remember { mutableStateOf(LocalDate.now()) } + + Spacer(modifier = Modifier.height(4.dp)) + + WeeklyDatePicker( + selectedDate = selectedDate, + onDateSelect = { selectedDate = it }, + weeklyDates = selectedDate.getCurrentWeekDays(), + modifier = Modifier + .padding(vertical = 10.dp, horizontal = 16.dp) + ) + + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + items(10) { + RoutineDetailsCard() + } + } + } +} + +@Preview +@Composable +private fun RoutineListScreenPreview() { + RoutineListScreen() +} From 25b936f695ea2eabf893e5ebea553d4603f544ce Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Tue, 12 Aug 2025 03:26:19 +0900 Subject: [PATCH 54/59] =?UTF-8?q?Feat:=20=EC=82=AD=EC=A0=9C=20=ED=99=95?= =?UTF-8?q?=EC=9D=B8=20=EB=B0=94=ED=85=80=EC=8B=9C=ED=8A=B8=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../template/DeleteConfirmBottomSheet.kt | 139 ++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/component/template/DeleteConfirmBottomSheet.kt 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 = {}, + ) +} From 3d40afd630fe917a36f4151b1d9dcff7b6653256 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Tue, 12 Aug 2025 03:27:03 +0900 Subject: [PATCH 55/59] =?UTF-8?q?Feat:=20RoutineList=ED=99=94=EB=A9=B4=20M?= =?UTF-8?q?VI=20=EB=AA=A8=EB=8D=B8=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../routinelist/model/RoutineListIntent.kt | 11 +++++++++++ .../routinelist/model/RoutineListSideEffect.kt | 7 +++++++ .../routinelist/model/RoutineListState.kt | 11 +++++++++++ 3 files changed, 29 insertions(+) create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/model/RoutineListIntent.kt create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/model/RoutineListSideEffect.kt create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/model/RoutineListState.kt 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 From a62f22a953bd7bfe2680c69f907b1014725cf8cd Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Tue, 12 Aug 2025 03:28:09 +0900 Subject: [PATCH 56/59] =?UTF-8?q?Feat:=20=EB=A3=A8=ED=8B=B4=EB=A6=AC?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=ED=99=94=EB=A9=B4=20Ui=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../component/atom/BitnagilTextButton.kt | 10 +++ .../src/main/res/drawable/ic_shine.xml | 15 ++++ .../routinelist/RoutineListScreen.kt | 77 ++++++++++++++----- .../routinelist/RoutineListViewModel.kt | 38 +++++++++ .../component/template/RoutineDetailsCard.kt | 31 ++++---- .../component/template/WeeklyDatePicker.kt | 2 +- 6 files changed, 140 insertions(+), 33 deletions(-) create mode 100644 core/designsystem/src/main/res/drawable/ic_shine.xml create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/RoutineListViewModel.kt 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 c6a27184..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 @@ -114,6 +114,16 @@ data class BitnagilTextButtonColor( 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, 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/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/RoutineListScreen.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/RoutineListScreen.kt index 4c085cb1..19d0b035 100644 --- a/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/RoutineListScreen.kt +++ b/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/RoutineListScreen.kt @@ -3,6 +3,7 @@ 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 @@ -11,64 +12,99 @@ 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.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.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(), ) { - RoutineListScreen() + 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( - modifier: Modifier = Modifier + uiState: RoutineListState, + onDateSelect: (LocalDate) -> Unit, + onShowDeleteConfirmBottomSheet: () -> Unit, + onBackClick: () -> Unit, + modifier: Modifier = Modifier, ) { Column( modifier = modifier .fillMaxSize() .background(BitnagilTheme.colors.coolGray99) .statusBarsPadding(), - horizontalAlignment = Alignment.CenterHorizontally + horizontalAlignment = Alignment.CenterHorizontally, ) { BitnagilTopBar( title = "루틴리스트", showBackButton = true, - onBackClick = {}, + onBackClick = onBackClick, ) - var selectedDate by remember { mutableStateOf(LocalDate.now()) } - Spacer(modifier = Modifier.height(4.dp)) WeeklyDatePicker( - selectedDate = selectedDate, - onDateSelect = { selectedDate = it }, - weeklyDates = selectedDate.getCurrentWeekDays(), + selectedDate = uiState.selectedDate, + onDateSelect = onDateSelect, + weeklyDates = uiState.selectedDate.getCurrentWeekDays(), modifier = Modifier - .padding(vertical = 10.dp, horizontal = 16.dp) + .padding(vertical = 10.dp, horizontal = 16.dp), ) LazyColumn( modifier = Modifier .fillMaxSize() .padding(horizontal = 16.dp), - verticalArrangement = Arrangement.spacedBy(12.dp) + verticalArrangement = Arrangement.spacedBy(12.dp), + contentPadding = PaddingValues(bottom = 10.dp), ) { - items(10) { - RoutineDetailsCard() + items(5) { + RoutineDetailsCard( + onDeleteClick = onShowDeleteConfirmBottomSheet, + ) } } } @@ -77,5 +113,10 @@ private fun RoutineListScreen( @Preview @Composable private fun RoutineListScreenPreview() { - RoutineListScreen() + 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/RoutineDetailsCard.kt b/presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/component/template/RoutineDetailsCard.kt index e8200ef4..4c8607fe 100644 --- 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 @@ -23,7 +23,8 @@ import com.threegap.bitnagil.designsystem.component.atom.BitnagilIconButton @Composable fun RoutineDetailsCard( - modifier: Modifier = Modifier + onDeleteClick: () -> Unit, + modifier: Modifier = Modifier, ) { Column( modifier = modifier @@ -32,12 +33,12 @@ fun RoutineDetailsCard( shape = RoundedCornerShape(12.dp), ) .fillMaxWidth() - .padding(vertical = 14.dp) + .padding(vertical = 14.dp), ) { Row( modifier = Modifier .padding(start = 16.dp, end = 2.dp), - verticalAlignment = Alignment.CenterVertically + verticalAlignment = Alignment.CenterVertically, ) { BitnagilIcon( id = R.drawable.ic_wakeup, @@ -45,16 +46,16 @@ fun RoutineDetailsCard( modifier = Modifier .background( color = BitnagilTheme.colors.orange25, - shape = RoundedCornerShape(4.dp) + shape = RoundedCornerShape(4.dp), ) - .padding(4.dp) + .padding(4.dp), ) Text( text = "개운하게 일어나기", color = BitnagilTheme.colors.coolGray10, style = BitnagilTheme.typography.body1SemiBold, - modifier = Modifier.padding(start = 10.dp) + modifier = Modifier.padding(start = 10.dp), ) Spacer(modifier = Modifier.weight(1f)) @@ -63,27 +64,27 @@ fun RoutineDetailsCard( id = R.drawable.ic_edit, onClick = { /*TODO*/ }, tint = null, - paddingValues = PaddingValues(12.dp) + paddingValues = PaddingValues(12.dp), ) BitnagilIconButton( id = R.drawable.ic_trash, - onClick = { /*TODO*/ }, + onClick = onDeleteClick, tint = null, - paddingValues = PaddingValues(12.dp) + paddingValues = PaddingValues(12.dp), ) } HorizontalDivider( thickness = 1.dp, color = BitnagilTheme.colors.coolGray97, - modifier = Modifier.padding(horizontal = 16.dp, vertical = 10.dp) + modifier = Modifier.padding(horizontal = 16.dp, vertical = 10.dp), ) Column( modifier = Modifier .padding(horizontal = 16.dp), - verticalArrangement = Arrangement.spacedBy(2.dp) + verticalArrangement = Arrangement.spacedBy(2.dp), ) { Text( text = "세부 루틴", @@ -110,13 +111,13 @@ fun RoutineDetailsCard( HorizontalDivider( thickness = 1.dp, color = BitnagilTheme.colors.coolGray97, - modifier = Modifier.padding(horizontal = 16.dp, vertical = 10.dp) + modifier = Modifier.padding(horizontal = 16.dp, vertical = 10.dp), ) Column( modifier = Modifier .padding(horizontal = 16.dp), - verticalArrangement = Arrangement.spacedBy(2.dp) + verticalArrangement = Arrangement.spacedBy(2.dp), ) { Text( text = "반복:", @@ -140,5 +141,7 @@ fun RoutineDetailsCard( @Preview @Composable private fun RoutineDetailsCardPreview() { - RoutineDetailsCard() + 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 index d7205adb..05a79a39 100644 --- 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 @@ -45,7 +45,7 @@ fun WeeklyDatePicker( isSelected = selectedDate == date, isToday = date == LocalDate.now(), onDateClick = { onDateSelect(date) }, - modifier = Modifier.padding(bottom = 18.dp) + modifier = Modifier.padding(bottom = 18.dp), ) } } From 0ec9dff80868ea9528857db11091c6f7bd1852e9 Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Tue, 12 Aug 2025 03:28:39 +0900 Subject: [PATCH 57/59] =?UTF-8?q?Feat:=20=EB=9D=BC=EC=9A=B0=ED=8A=B8=20?= =?UTF-8?q?=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../main/java/com/threegap/bitnagil/MainNavHost.kt | 11 +++++++++++ app/src/main/java/com/threegap/bitnagil/Route.kt | 3 +++ 2 files changed, 14 insertions(+) diff --git a/app/src/main/java/com/threegap/bitnagil/MainNavHost.kt b/app/src/main/java/com/threegap/bitnagil/MainNavHost.kt index f158156f..b344ac1e 100644 --- a/app/src/main/java/com/threegap/bitnagil/MainNavHost.kt +++ b/app/src/main/java/com/threegap/bitnagil/MainNavHost.kt @@ -12,6 +12,7 @@ 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 @@ -241,5 +242,15 @@ fun MainNavHost( }, ) } + + composable { + RoutineListScreenContainer( + navigateToBack = { + if (navigator.navController.previousBackStackEntry != null) { + navigator.navController.popBackStack() + } + }, + ) + } } } diff --git a/app/src/main/java/com/threegap/bitnagil/Route.kt b/app/src/main/java/com/threegap/bitnagil/Route.kt index cb9eacde..49ddc465 100644 --- a/app/src/main/java/com/threegap/bitnagil/Route.kt +++ b/app/src/main/java/com/threegap/bitnagil/Route.kt @@ -41,4 +41,7 @@ sealed interface Route { @Serializable data object Withdrawal : Route + + @Serializable + data object RoutineList : Route } From 48b4f4c2c40410ff6dbdba11a58eb3b056865fae Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Wed, 13 Aug 2025 19:01:14 +0900 Subject: [PATCH 58/59] =?UTF-8?q?Feat:=20=EB=94=94=EC=9E=90=EC=9D=B8=20?= =?UTF-8?q?=EC=BB=AC=EB=9F=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/threegap/bitnagil/designsystem/color/BitnagilColors.kt | 1 + .../main/java/com/threegap/bitnagil/designsystem/color/Color.kt | 1 + 2 files changed, 2 insertions(+) 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) From 3cfc4db8bf63f066980dc49e8568709f176f005d Mon Sep 17 00:00:00 2001 From: wjdrjs00 Date: Wed, 13 Aug 2025 19:19:52 +0900 Subject: [PATCH 59/59] =?UTF-8?q?Feat:=20EmptyRoutineListView=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../template/EmptyRoutineListView.kt | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 presentation/src/main/java/com/threegap/bitnagil/presentation/routinelist/component/template/EmptyRoutineListView.kt 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 = {}) +}