From e3168e189bb61314063e3d15f5c5a23aa953631c Mon Sep 17 00:00:00 2001 From: yangsooplus Date: Wed, 24 Jan 2024 00:12:43 +0900 Subject: [PATCH 01/14] =?UTF-8?q?feat:=20RecentSpendGraph=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../feature/statistics/StatisticsScreen.kt | 9 +- .../statistics/component/RecentSpentGraph.kt | 230 ++++++++++++++++++ .../src/main/res/values/strings.xml | 6 + 3 files changed, 244 insertions(+), 1 deletion(-) create mode 100644 feature/statistics/src/main/java/com/susu/feature/statistics/component/RecentSpentGraph.kt create mode 100644 feature/statistics/src/main/res/values/strings.xml diff --git a/feature/statistics/src/main/java/com/susu/feature/statistics/StatisticsScreen.kt b/feature/statistics/src/main/java/com/susu/feature/statistics/StatisticsScreen.kt index 1321af41..5d8b375b 100644 --- a/feature/statistics/src/main/java/com/susu/feature/statistics/StatisticsScreen.kt +++ b/feature/statistics/src/main/java/com/susu/feature/statistics/StatisticsScreen.kt @@ -10,8 +10,15 @@ import androidx.compose.ui.unit.dp import com.susu.core.designsystem.theme.SusuTheme @Composable -fun StatisticsScreen( +fun StatisticsRoute( padding: PaddingValues, +) { + StatisticsScreen() +} + +@Composable +fun StatisticsScreen( + padding: PaddingValues = PaddingValues(), ) { Text( modifier = Modifier.padding(padding), diff --git a/feature/statistics/src/main/java/com/susu/feature/statistics/component/RecentSpentGraph.kt b/feature/statistics/src/main/java/com/susu/feature/statistics/component/RecentSpentGraph.kt new file mode 100644 index 00000000..fe117567 --- /dev/null +++ b/feature/statistics/src/main/java/com/susu/feature/statistics/component/RecentSpentGraph.kt @@ -0,0 +1,230 @@ +package com.susu.feature.statistics.component + +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.EaseInOutCubic +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.blur +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.susu.core.designsystem.theme.Blue60 +import com.susu.core.designsystem.theme.Gray10 +import com.susu.core.designsystem.theme.Gray100 +import com.susu.core.designsystem.theme.Gray30 +import com.susu.core.designsystem.theme.Gray40 +import com.susu.core.designsystem.theme.Gray60 +import com.susu.core.designsystem.theme.Gray90 +import com.susu.core.designsystem.theme.Orange30 +import com.susu.core.designsystem.theme.Orange60 +import com.susu.core.designsystem.theme.SusuTheme +import com.susu.feature.statistics.R +import kotlin.random.Random + +@Composable +fun RecentSpentGraph( + modifier: Modifier = Modifier, + isActive: Boolean = true, + spentData: List> = emptyList(), +) { + val totalAmount by remember { mutableStateOf(spentData.sumOf { it.second } / 10000) } + val maximumAmount by remember { mutableStateOf(spentData.maxOf { it.second }) } + + Column( + modifier = modifier + .fillMaxWidth() + .background(color = Gray10, shape = RoundedCornerShape(4.dp)) + .padding(SusuTheme.spacing.spacing_xxs), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Row( + modifier = Modifier.fillMaxWidth().padding(SusuTheme.spacing.spacing_xxs), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + text = stringResource(R.string.statistics_recent_8_total_money), + style = SusuTheme.typography.title_xs, + color = Gray100, + ) + if (isActive) { + Text( + text = stringResource(R.string.statistics_total_man_format, totalAmount.toString()), + style = SusuTheme.typography.title_xs, + color = Blue60, + ) + } else { + Text( + text = stringResource(R.string.statistics_total_man_format, stringResource(R.string.word_unkown)), + style = SusuTheme.typography.title_xs, + color = Gray40, + ) + } + } + Spacer(modifier = Modifier.height(SusuTheme.spacing.spacing_xxs)) + Row( + modifier = if (isActive) { + Modifier.fillMaxWidth().padding(SusuTheme.spacing.spacing_xxs) + } else { + Modifier.fillMaxWidth().blur(8.dp).padding(SusuTheme.spacing.spacing_xxs) + }, + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + spentData.forEachIndexed { i, data -> + Column( + horizontalAlignment = Alignment.CenterHorizontally, + ) { + StickGraph( + ratio = data.second.toFloat() / maximumAmount, + color = if (isActive) { + if (i == spentData.lastIndex) { + Orange60 + } else { + Orange30 + } + } else { + if (i == spentData.lastIndex) { + Gray60 + } else { + Gray30 + } + }, + ) + Spacer(modifier = Modifier.height(SusuTheme.spacing.spacing_xxxxs)) + Text( + text = data.first, + style = SusuTheme.typography.title_xxxs, + color = if (i == spentData.lastIndex) { + Gray90 + } else { + Gray40 + }, + ) + } + Spacer(modifier = Modifier.width(SusuTheme.spacing.spacing_s)) + } + } + } +} + +@Composable +fun StickGraph( + ratio: Float, + color: Color, + modifier: Modifier = Modifier, + width: Dp = 24.dp, + maximumHeight: Dp = 80.dp, + delay: Int = 0, + duration: Int = 800, +) { + var initialHeight by remember { mutableStateOf(0f) } + val fillHeight = remember { Animatable(initialHeight) } + val graphHeight = with(LocalDensity.current) { maximumHeight.toPx() * ratio } + + LaunchedEffect(key1 = ratio) { + fillHeight.animateTo( + targetValue = graphHeight, + animationSpec = tween( + delayMillis = delay, + durationMillis = duration, + easing = EaseInOutCubic, + ), + ) + initialHeight = ratio + } + + Spacer( + modifier = modifier + .size(width = width, height = maximumHeight) + .drawBehind { + drawRoundRect( + color = color, + cornerRadius = CornerRadius(4.dp.toPx()), + topLeft = Offset(0f, maximumHeight.toPx() - fillHeight.value), + size = Size(width.toPx(), fillHeight.value), + ) + }, + ) +} + +@Preview +@Composable +fun StickGraphPreview() { + SusuTheme { + var ratio by remember { mutableStateOf(1f) } + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + StickGraph(ratio, Orange30) + StickGraph(0.3f, Orange60) + Button( + onClick = { ratio = Random.nextFloat() }, + ) { + Text(text = "그래프 값 변경") + } + } + } +} + +@Preview +@Composable +fun RecentSpentGraphPreview() { + SusuTheme { + Column { + RecentSpentGraph( + modifier = Modifier.padding(16.dp), + spentData = listOf( + Pair("1월", 10000), + Pair("2월", 20000), + Pair("3월", 30000), + Pair("4월", 40000), + Pair("5월", 50000), + Pair("6월", 60000), + Pair("7월", 70000), + Pair("8월", 80000), + ), + ) + RecentSpentGraph( + modifier = Modifier.padding(16.dp), + spentData = listOf( + Pair("1월", 10000), + Pair("2월", 20000), + Pair("3월", 30000), + Pair("4월", 40000), + Pair("5월", 50000), + Pair("6월", 60000), + Pair("7월", 70000), + Pair("8월", 80000), + ), + isActive = false, + ) + } + } +} diff --git a/feature/statistics/src/main/res/values/strings.xml b/feature/statistics/src/main/res/values/strings.xml new file mode 100644 index 00000000..42528091 --- /dev/null +++ b/feature/statistics/src/main/res/values/strings.xml @@ -0,0 +1,6 @@ + + + 최근 8개월 간 쓴 금액 + 총 %s만원 + \? + From ad00c3f0c05d3afff35ea3b45eedb795c44470b4 Mon Sep 17 00:00:00 2001 From: yangsooplus Date: Wed, 24 Jan 2024 01:14:39 +0900 Subject: [PATCH 02/14] =?UTF-8?q?feat:=20StatisticsItem=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../statistics/component/StatisticsItem.kt | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 feature/statistics/src/main/java/com/susu/feature/statistics/component/StatisticsItem.kt diff --git a/feature/statistics/src/main/java/com/susu/feature/statistics/component/StatisticsItem.kt b/feature/statistics/src/main/java/com/susu/feature/statistics/component/StatisticsItem.kt new file mode 100644 index 00000000..613f3f3c --- /dev/null +++ b/feature/statistics/src/main/java/com/susu/feature/statistics/component/StatisticsItem.kt @@ -0,0 +1,105 @@ +package com.susu.feature.statistics.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +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.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.susu.core.designsystem.theme.Gray10 +import com.susu.core.designsystem.theme.Gray100 +import com.susu.core.designsystem.theme.Gray50 +import com.susu.core.designsystem.theme.Gray60 +import com.susu.core.designsystem.theme.Gray80 +import com.susu.core.designsystem.theme.Orange60 +import com.susu.core.designsystem.theme.SusuTheme +import com.susu.core.ui.extension.toMoneyFormat + +@Composable +fun StatisticsVerticalItem( + title: String, + content: String, + description: String, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .background(color = Gray10, shape = RoundedCornerShape(4.dp)) + .padding(SusuTheme.spacing.spacing_m), + ) { + Text( + modifier = Modifier.align(Alignment.Start), + text = title, + style = SusuTheme.typography.title_xxs, + color = Gray100, + ) + Spacer(modifier = Modifier.height(SusuTheme.spacing.spacing_xxs)) + Text( + modifier = Modifier.align(Alignment.CenterHorizontally), + text = content, + style = SusuTheme.typography.title_l, + color = Orange60, + ) + Text( + modifier = Modifier.align(Alignment.CenterHorizontally), + text = description, + style = SusuTheme.typography.title_xxxs, + color = Gray60, + ) + } +} + +@Composable +fun StatisticsHorizontalItem( + title: String, + name: String, + money: Int, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .fillMaxWidth() + .background(color = Gray10, shape = RoundedCornerShape(4.dp)) + .padding(SusuTheme.spacing.spacing_m), + ) { + Text( + modifier = Modifier.align(Alignment.Start), + text = title, + style = SusuTheme.typography.title_xxs, + color = Gray50, + ) + Spacer(modifier = Modifier.height(SusuTheme.spacing.spacing_xxxxs)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text(text = name, style = SusuTheme.typography.title_s, color = Gray80) + Text( + text = stringResource(id = com.susu.core.ui.R.string.money_unit_format, money.toMoneyFormat()), + style = SusuTheme.typography.title_s, + color = Gray100, + ) + } + } +} + +@Preview(showBackground = true) +@Composable +fun StatisticsItemPreview() { + SusuTheme { + Column { + StatisticsVerticalItem(title = "자주 개발하는 시간", content = "밤", description = "낮에 좀 해라") + StatisticsHorizontalItem(title = "이번달에 허투루 쓴 돈", name = "배달음식", money = 60000) + } + } +} From 1ca8b529a29aeca6f0b7c2f8b63e1fe11c451a2e Mon Sep 17 00:00:00 2001 From: yangsooplus Date: Wed, 24 Jan 2024 02:16:13 +0900 Subject: [PATCH 03/14] =?UTF-8?q?chore:=20string=20resource=20=EC=98=A4?= =?UTF-8?q?=ED=83=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/susu/feature/statistics/component/RecentSpentGraph.kt | 2 +- feature/statistics/src/main/res/values/strings.xml | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/feature/statistics/src/main/java/com/susu/feature/statistics/component/RecentSpentGraph.kt b/feature/statistics/src/main/java/com/susu/feature/statistics/component/RecentSpentGraph.kt index fe117567..a1edb662 100644 --- a/feature/statistics/src/main/java/com/susu/feature/statistics/component/RecentSpentGraph.kt +++ b/feature/statistics/src/main/java/com/susu/feature/statistics/component/RecentSpentGraph.kt @@ -81,7 +81,7 @@ fun RecentSpentGraph( ) } else { Text( - text = stringResource(R.string.statistics_total_man_format, stringResource(R.string.word_unkown)), + text = stringResource(R.string.statistics_total_man_format, stringResource(R.string.word_unknown)), style = SusuTheme.typography.title_xs, color = Gray40, ) diff --git a/feature/statistics/src/main/res/values/strings.xml b/feature/statistics/src/main/res/values/strings.xml index 42528091..29f3b1d0 100644 --- a/feature/statistics/src/main/res/values/strings.xml +++ b/feature/statistics/src/main/res/values/strings.xml @@ -2,5 +2,6 @@ 최근 8개월 간 쓴 금액 총 %s만원 - \? + \? + 총 ?번 From cfb1716ec59f10261e557936e9574f5f57e552a3 Mon Sep 17 00:00:00 2001 From: yangsooplus Date: Wed, 24 Jan 2024 02:16:44 +0900 Subject: [PATCH 04/14] =?UTF-8?q?feat:=20StatisticsItem=20=EB=B9=84?= =?UTF-8?q?=ED=99=9C=EC=84=B1=ED=99=94=20=EC=83=81=ED=83=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../statistics/component/StatisticsItem.kt | 66 ++++++++++++++----- 1 file changed, 48 insertions(+), 18 deletions(-) diff --git a/feature/statistics/src/main/java/com/susu/feature/statistics/component/StatisticsItem.kt b/feature/statistics/src/main/java/com/susu/feature/statistics/component/StatisticsItem.kt index 613f3f3c..b8d438b2 100644 --- a/feature/statistics/src/main/java/com/susu/feature/statistics/component/StatisticsItem.kt +++ b/feature/statistics/src/main/java/com/susu/feature/statistics/component/StatisticsItem.kt @@ -18,12 +18,14 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.susu.core.designsystem.theme.Gray10 import com.susu.core.designsystem.theme.Gray100 +import com.susu.core.designsystem.theme.Gray40 import com.susu.core.designsystem.theme.Gray50 import com.susu.core.designsystem.theme.Gray60 import com.susu.core.designsystem.theme.Gray80 import com.susu.core.designsystem.theme.Orange60 import com.susu.core.designsystem.theme.SusuTheme import com.susu.core.ui.extension.toMoneyFormat +import com.susu.feature.statistics.R @Composable fun StatisticsVerticalItem( @@ -31,6 +33,7 @@ fun StatisticsVerticalItem( content: String, description: String, modifier: Modifier = Modifier, + isActive: Boolean = true, ) { Column( modifier = modifier @@ -44,18 +47,33 @@ fun StatisticsVerticalItem( color = Gray100, ) Spacer(modifier = Modifier.height(SusuTheme.spacing.spacing_xxs)) - Text( - modifier = Modifier.align(Alignment.CenterHorizontally), - text = content, - style = SusuTheme.typography.title_l, - color = Orange60, - ) - Text( - modifier = Modifier.align(Alignment.CenterHorizontally), - text = description, - style = SusuTheme.typography.title_xxxs, - color = Gray60, - ) + if (isActive) { + Text( + modifier = Modifier.align(Alignment.CenterHorizontally), + text = content, + style = SusuTheme.typography.title_l, + color = Orange60, + ) + Text( + modifier = Modifier.align(Alignment.CenterHorizontally), + text = description, + style = SusuTheme.typography.title_xxxs, + color = Gray60, + ) + } else { + Text( + modifier = Modifier.align(Alignment.CenterHorizontally), + text = stringResource(id = R.string.word_unknown), + style = SusuTheme.typography.title_l, + color = Gray40, + ) + Text( + modifier = Modifier.align(Alignment.CenterHorizontally), + text = stringResource(R.string.word_unknown_count), + style = SusuTheme.typography.title_xxxs, + color = Gray40, + ) + } } } @@ -65,6 +83,7 @@ fun StatisticsHorizontalItem( name: String, money: Int, modifier: Modifier = Modifier, + isActive: Boolean = true, ) { Column( modifier = modifier @@ -83,12 +102,21 @@ fun StatisticsHorizontalItem( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, ) { - Text(text = name, style = SusuTheme.typography.title_s, color = Gray80) - Text( - text = stringResource(id = com.susu.core.ui.R.string.money_unit_format, money.toMoneyFormat()), - style = SusuTheme.typography.title_s, - color = Gray100, - ) + if (isActive) { + Text(text = name, style = SusuTheme.typography.title_s, color = Gray80) + Text( + text = stringResource(id = com.susu.core.ui.R.string.money_unit_format, money.toMoneyFormat()), + style = SusuTheme.typography.title_s, + color = Gray100, + ) + } else { + Text(text = stringResource(id = R.string.word_unknown), style = SusuTheme.typography.title_s, color = Gray40) + Text( + text = stringResource(id = com.susu.core.ui.R.string.money_unit_format, stringResource(id = R.string.word_unknown)), + style = SusuTheme.typography.title_s, + color = Gray40, + ) + } } } } @@ -100,6 +128,8 @@ fun StatisticsItemPreview() { Column { StatisticsVerticalItem(title = "자주 개발하는 시간", content = "밤", description = "낮에 좀 해라") StatisticsHorizontalItem(title = "이번달에 허투루 쓴 돈", name = "배달음식", money = 60000) + StatisticsVerticalItem(title = "자주 개발하는 시간", content = "밤", description = "낮에 좀 해라", isActive = false) + StatisticsHorizontalItem(title = "이번달에 허투루 쓴 돈", name = "배달음식", money = 60000, isActive = false) } } } From be2d9a4368a017d2be8ec110e2b8016265452d00 Mon Sep 17 00:00:00 2001 From: yangsooplus Date: Wed, 24 Jan 2024 03:15:18 +0900 Subject: [PATCH 05/14] =?UTF-8?q?feat:=20=EB=82=98=EC=9D=98=20=ED=86=B5?= =?UTF-8?q?=EA=B3=84=20UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/susu/feature/navigator/MainScreen.kt | 4 +- .../feature/statistics/StatisticsContract.kt | 8 ++ .../feature/statistics/StatisticsScreen.kt | 111 ++++++++++++++++-- .../statistics/component/RecentSpentGraph.kt | 64 +++++----- .../statistics/component/StatisticsItem.kt | 4 +- .../statistics/component/StatisticsTab.kt | 80 +++++++++++++ .../navigation/StatisticsNavigation.kt | 9 +- .../src/main/res/values/strings.xml | 11 +- 8 files changed, 238 insertions(+), 53 deletions(-) create mode 100644 feature/statistics/src/main/java/com/susu/feature/statistics/StatisticsContract.kt create mode 100644 feature/statistics/src/main/java/com/susu/feature/statistics/component/StatisticsTab.kt diff --git a/feature/navigator/src/main/java/com/susu/feature/navigator/MainScreen.kt b/feature/navigator/src/main/java/com/susu/feature/navigator/MainScreen.kt index 2c52a1c0..7e8c7489 100644 --- a/feature/navigator/src/main/java/com/susu/feature/navigator/MainScreen.kt +++ b/feature/navigator/src/main/java/com/susu/feature/navigator/MainScreen.kt @@ -121,9 +121,7 @@ internal fun MainScreen( handleException = viewModel::handleException, ) - statisticsNavGraph( - padding = innerPadding, - ) + statisticsNavGraph() communityNavGraph( padding = innerPadding, diff --git a/feature/statistics/src/main/java/com/susu/feature/statistics/StatisticsContract.kt b/feature/statistics/src/main/java/com/susu/feature/statistics/StatisticsContract.kt new file mode 100644 index 00000000..8620d450 --- /dev/null +++ b/feature/statistics/src/main/java/com/susu/feature/statistics/StatisticsContract.kt @@ -0,0 +1,8 @@ +package com.susu.feature.statistics + +import androidx.annotation.StringRes + +enum class StatisticsTab(@StringRes val stringId: Int) { + MY(R.string.statistics_tab_my), + AVERAGE(R.string.statistics_tab_average) +} diff --git a/feature/statistics/src/main/java/com/susu/feature/statistics/StatisticsScreen.kt b/feature/statistics/src/main/java/com/susu/feature/statistics/StatisticsScreen.kt index 5d8b375b..1566cfde 100644 --- a/feature/statistics/src/main/java/com/susu/feature/statistics/StatisticsScreen.kt +++ b/feature/statistics/src/main/java/com/susu/feature/statistics/StatisticsScreen.kt @@ -1,35 +1,124 @@ package com.susu.feature.statistics -import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.susu.core.designsystem.component.appbar.SusuDefaultAppBar +import com.susu.core.designsystem.component.appbar.icon.LogoIcon +import com.susu.core.designsystem.theme.Gray10 +import com.susu.core.designsystem.theme.Gray100 +import com.susu.core.designsystem.theme.Gray40 import com.susu.core.designsystem.theme.SusuTheme +import com.susu.feature.statistics.component.RecentSpentGraph +import com.susu.feature.statistics.component.StatisticsHorizontalItem +import com.susu.feature.statistics.component.StatisticsTab +import com.susu.feature.statistics.component.StatisticsVerticalItem @Composable -fun StatisticsRoute( - padding: PaddingValues, -) { +fun StatisticsRoute() { StatisticsScreen() } @Composable fun StatisticsScreen( - padding: PaddingValues = PaddingValues(), + spentData: List> = emptyList(), // TODO: Preview 데이터 ) { - Text( - modifier = Modifier.padding(padding), - text = "통계", - ) + Column( + modifier = Modifier.fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(horizontal = SusuTheme.spacing.spacing_m), + verticalArrangement = Arrangement.spacedBy(SusuTheme.spacing.spacing_xxs), + ) { + SusuDefaultAppBar( + modifier = Modifier.padding(horizontal = SusuTheme.spacing.spacing_xs), + leftIcon = { LogoIcon() }, + title = stringResource(R.string.statistics_word), + ) + StatisticsTab( + modifier = Modifier + .height(52.dp) + .padding(vertical = SusuTheme.spacing.spacing_xxs), + selectedTab = StatisticsTab.MY, + onTabSelect = {}, + ) + RecentSpentGraph( + spentData = spentData, // TODO: 서버 값으로 교체 + ) + Row( + modifier = Modifier + .fillMaxWidth() + .background(color = Gray10, shape = RoundedCornerShape(4.dp)) + .padding(SusuTheme.spacing.spacing_m), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text(text = stringResource(R.string.statistics_most_spent_month), style = SusuTheme.typography.title_xs, color = Gray100) + Text( + text = stringResource(R.string.word_month_format, stringResource(id = R.string.word_unknown)), + style = SusuTheme.typography.title_xs, + color = Gray40, + ) + } + Row( + modifier = Modifier.fillMaxWidth(), + ) { + StatisticsVerticalItem( + modifier = Modifier.weight(1f), + title = stringResource(R.string.statistics_most_susu_relationship), + content = "", + description = "", + ) + Spacer(modifier = Modifier.width(SusuTheme.spacing.spacing_xxs)) + StatisticsVerticalItem( + modifier = Modifier.weight(1f), + title = stringResource(R.string.statistics_most_susu_event), + content = "", + description = "", + ) + } + StatisticsHorizontalItem( + title = stringResource(R.string.statistics_most_received_money), + name = "김수수", + money = 0, + ) + StatisticsHorizontalItem( + title = stringResource(R.string.statistics_most_sent_money), + name = "양수수", + money = 0, + ) + } } -@Preview +@Preview(showBackground = true) @Composable fun SentScreenPreview() { SusuTheme { - StatisticsScreen(padding = PaddingValues(0.dp)) + StatisticsScreen( + spentData = listOf( + Pair("1월", 10000), + Pair("2월", 20000), + Pair("3월", 30000), + Pair("4월", 40000), + Pair("5월", 50000), + Pair("6월", 60000), + Pair("7월", 70000), + Pair("8월", 80000), + ), + ) } } diff --git a/feature/statistics/src/main/java/com/susu/feature/statistics/component/RecentSpentGraph.kt b/feature/statistics/src/main/java/com/susu/feature/statistics/component/RecentSpentGraph.kt index a1edb662..cad213ca 100644 --- a/feature/statistics/src/main/java/com/susu/feature/statistics/component/RecentSpentGraph.kt +++ b/feature/statistics/src/main/java/com/susu/feature/statistics/component/RecentSpentGraph.kt @@ -55,7 +55,7 @@ fun RecentSpentGraph( spentData: List> = emptyList(), ) { val totalAmount by remember { mutableStateOf(spentData.sumOf { it.second } / 10000) } - val maximumAmount by remember { mutableStateOf(spentData.maxOf { it.second }) } + val maximumAmount by remember { mutableStateOf(spentData.maxOfOrNull { it.second }) } Column( modifier = modifier @@ -97,38 +97,42 @@ fun RecentSpentGraph( horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, ) { - spentData.forEachIndexed { i, data -> - Column( - horizontalAlignment = Alignment.CenterHorizontally, - ) { - StickGraph( - ratio = data.second.toFloat() / maximumAmount, - color = if (isActive) { - if (i == spentData.lastIndex) { - Orange60 + if (maximumAmount != null) { + spentData.forEachIndexed { i, data -> + Column( + horizontalAlignment = Alignment.CenterHorizontally, + ) { + StickGraph( + ratio = data.second.toFloat() / maximumAmount!!, + color = if (isActive) { + if (i == spentData.lastIndex) { + Orange60 + } else { + Orange30 + } } else { - Orange30 - } - } else { - if (i == spentData.lastIndex) { - Gray60 + if (i == spentData.lastIndex) { + Gray60 + } else { + Gray30 + } + }, + ) + Spacer(modifier = Modifier.height(SusuTheme.spacing.spacing_xxxxs)) + Text( + text = data.first, + style = SusuTheme.typography.title_xxxs, + color = if (i == spentData.lastIndex) { + Gray90 } else { - Gray30 - } - }, - ) - Spacer(modifier = Modifier.height(SusuTheme.spacing.spacing_xxxxs)) - Text( - text = data.first, - style = SusuTheme.typography.title_xxxs, - color = if (i == spentData.lastIndex) { - Gray90 - } else { - Gray40 - }, - ) + Gray40 + }, + ) + } + Spacer(modifier = Modifier.width(SusuTheme.spacing.spacing_s)) } - Spacer(modifier = Modifier.width(SusuTheme.spacing.spacing_s)) + } else { + Spacer(modifier = Modifier.height(100.dp)) // 임시 } } } diff --git a/feature/statistics/src/main/java/com/susu/feature/statistics/component/StatisticsItem.kt b/feature/statistics/src/main/java/com/susu/feature/statistics/component/StatisticsItem.kt index b8d438b2..1eaa02dc 100644 --- a/feature/statistics/src/main/java/com/susu/feature/statistics/component/StatisticsItem.kt +++ b/feature/statistics/src/main/java/com/susu/feature/statistics/component/StatisticsItem.kt @@ -56,7 +56,7 @@ fun StatisticsVerticalItem( ) Text( modifier = Modifier.align(Alignment.CenterHorizontally), - text = description, + text = stringResource(R.string.word_entire_count, description), style = SusuTheme.typography.title_xxxs, color = Gray60, ) @@ -69,7 +69,7 @@ fun StatisticsVerticalItem( ) Text( modifier = Modifier.align(Alignment.CenterHorizontally), - text = stringResource(R.string.word_unknown_count), + text = stringResource(R.string.word_entire_count, stringResource(id = R.string.word_unknown)), style = SusuTheme.typography.title_xxxs, color = Gray40, ) diff --git a/feature/statistics/src/main/java/com/susu/feature/statistics/component/StatisticsTab.kt b/feature/statistics/src/main/java/com/susu/feature/statistics/component/StatisticsTab.kt new file mode 100644 index 00000000..d51aee3a --- /dev/null +++ b/feature/statistics/src/main/java/com/susu/feature/statistics/component/StatisticsTab.kt @@ -0,0 +1,80 @@ +package com.susu.feature.statistics.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +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.res.stringResource +import androidx.compose.ui.unit.dp +import com.susu.core.designsystem.theme.Gray10 +import com.susu.core.designsystem.theme.Gray100 +import com.susu.core.designsystem.theme.Gray20 +import com.susu.core.designsystem.theme.Gray50 +import com.susu.core.designsystem.theme.SusuTheme +import com.susu.core.ui.extension.susuClickable +import com.susu.feature.statistics.StatisticsTab + +@Composable +fun StatisticsTab( + modifier: Modifier = Modifier, + selectedTab: StatisticsTab = StatisticsTab.MY, + onTabSelect: (StatisticsTab) -> Unit = {}, +) { + Row( + modifier = modifier + .background(color = Gray10, shape = RoundedCornerShape(4.dp)) + .padding(SusuTheme.spacing.spacing_xxxxs), + ) { + StatisticsTabItem( + modifier = Modifier.weight(1f), + text = stringResource(id = StatisticsTab.MY.stringId), + isSelected = selectedTab == StatisticsTab.MY, + onClick = { onTabSelect(StatisticsTab.MY) }, + ) + Spacer(modifier = Modifier.width(SusuTheme.spacing.spacing_xxxxs)) + StatisticsTabItem( + modifier = Modifier.weight(1f), + text = stringResource(id = StatisticsTab.AVERAGE.stringId), + isSelected = selectedTab == StatisticsTab.AVERAGE, + onClick = { onTabSelect(StatisticsTab.AVERAGE) }, + ) + } +} + +@Composable +fun StatisticsTabItem( + isSelected: Boolean, + text: String, + modifier: Modifier = Modifier, + onClick: () -> Unit, +) { + Box( + modifier = modifier.fillMaxSize().background( + color = if (isSelected) { + Gray20 + } else { + Gray10 + }, + shape = RoundedCornerShape(4.dp), + ).susuClickable(onClick = onClick), + ) { + Text( + modifier = Modifier.align(Alignment.Center), + text = text, + style = SusuTheme.typography.title_xxxs, + color = if (isSelected) { + Gray100 + } else { + Gray50 + }, + ) + } +} diff --git a/feature/statistics/src/main/java/com/susu/feature/statistics/navigation/StatisticsNavigation.kt b/feature/statistics/src/main/java/com/susu/feature/statistics/navigation/StatisticsNavigation.kt index a357c9cd..78d6979b 100644 --- a/feature/statistics/src/main/java/com/susu/feature/statistics/navigation/StatisticsNavigation.kt +++ b/feature/statistics/src/main/java/com/susu/feature/statistics/navigation/StatisticsNavigation.kt @@ -1,21 +1,18 @@ package com.susu.feature.statistics.navigation -import androidx.compose.foundation.layout.PaddingValues import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import androidx.navigation.compose.composable -import com.susu.feature.statistics.StatisticsScreen +import com.susu.feature.statistics.StatisticsRoute fun NavController.navigateStatistics(navOptions: NavOptions) { navigate(StatisticsRoute.route, navOptions) } -fun NavGraphBuilder.statisticsNavGraph( - padding: PaddingValues, -) { +fun NavGraphBuilder.statisticsNavGraph() { composable(route = StatisticsRoute.route) { - StatisticsScreen(padding) + StatisticsRoute() } } diff --git a/feature/statistics/src/main/res/values/strings.xml b/feature/statistics/src/main/res/values/strings.xml index 29f3b1d0..f6dbcedf 100644 --- a/feature/statistics/src/main/res/values/strings.xml +++ b/feature/statistics/src/main/res/values/strings.xml @@ -2,6 +2,15 @@ 최근 8개월 간 쓴 금액 총 %s만원 + 나의 수수 + 수수 평균 \? - 총 ?번 + 총 %s번 + 통계 + 경조사비를 가장 많이 쓴 달 + %s월 + 최다 수수 관계 + 최다 수수 경조사 + 가장 많이 받은 금액 + 가장 많이 보낸 금액 From 3e4fc83c11e67bcbc9f3fcf7cb4f5ff7d9a1f8e5 Mon Sep 17 00:00:00 2001 From: yangsooplus Date: Wed, 24 Jan 2024 14:28:32 +0900 Subject: [PATCH 06/14] =?UTF-8?q?feat:=20=ED=86=B5=EA=B3=84=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20=EA=B5=AC=EC=A1=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/susu/feature/navigator/MainScreen.kt | 4 +- .../feature/statistics/StatisticsContract.kt | 12 ++ .../feature/statistics/StatisticsScreen.kt | 136 +++++++----------- .../feature/statistics/StatisticsViewModel.kt | 23 +++ .../statistics/content/MyStatisticsContent.kt | 134 +++++++++++++++++ .../navigation/StatisticsNavigation.kt | 8 +- 6 files changed, 230 insertions(+), 87 deletions(-) create mode 100644 feature/statistics/src/main/java/com/susu/feature/statistics/StatisticsViewModel.kt create mode 100644 feature/statistics/src/main/java/com/susu/feature/statistics/content/MyStatisticsContent.kt diff --git a/feature/navigator/src/main/java/com/susu/feature/navigator/MainScreen.kt b/feature/navigator/src/main/java/com/susu/feature/navigator/MainScreen.kt index 7e8c7489..aef9d3b5 100644 --- a/feature/navigator/src/main/java/com/susu/feature/navigator/MainScreen.kt +++ b/feature/navigator/src/main/java/com/susu/feature/navigator/MainScreen.kt @@ -121,7 +121,9 @@ internal fun MainScreen( handleException = viewModel::handleException, ) - statisticsNavGraph() + statisticsNavGraph( + navigateToMyInfo = navigator::navigateMyPageInfo, + ) communityNavGraph( padding = innerPadding, diff --git a/feature/statistics/src/main/java/com/susu/feature/statistics/StatisticsContract.kt b/feature/statistics/src/main/java/com/susu/feature/statistics/StatisticsContract.kt index 8620d450..1a8b1fdd 100644 --- a/feature/statistics/src/main/java/com/susu/feature/statistics/StatisticsContract.kt +++ b/feature/statistics/src/main/java/com/susu/feature/statistics/StatisticsContract.kt @@ -1,8 +1,20 @@ package com.susu.feature.statistics import androidx.annotation.StringRes +import com.susu.core.ui.base.SideEffect +import com.susu.core.ui.base.UiState enum class StatisticsTab(@StringRes val stringId: Int) { MY(R.string.statistics_tab_my), AVERAGE(R.string.statistics_tab_average) } + +data class StatisticsState( + val isLoading: Boolean = false, + val isBlind: Boolean = false, + val currentTab: StatisticsTab = StatisticsTab.MY, +) : UiState + +sealed interface StatisticsEffect : SideEffect { + data object NavigateToMyInfo : StatisticsEffect +} diff --git a/feature/statistics/src/main/java/com/susu/feature/statistics/StatisticsScreen.kt b/feature/statistics/src/main/java/com/susu/feature/statistics/StatisticsScreen.kt index 1566cfde..58a178e1 100644 --- a/feature/statistics/src/main/java/com/susu/feature/statistics/StatisticsScreen.kt +++ b/feature/statistics/src/main/java/com/susu/feature/statistics/StatisticsScreen.kt @@ -1,106 +1,85 @@ package com.susu.feature.statistics -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.width import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll -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.res.stringResource 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.susu.core.designsystem.component.appbar.SusuDefaultAppBar import com.susu.core.designsystem.component.appbar.icon.LogoIcon -import com.susu.core.designsystem.theme.Gray10 -import com.susu.core.designsystem.theme.Gray100 -import com.susu.core.designsystem.theme.Gray40 +import com.susu.core.designsystem.component.screen.LoadingScreen import com.susu.core.designsystem.theme.SusuTheme -import com.susu.feature.statistics.component.RecentSpentGraph -import com.susu.feature.statistics.component.StatisticsHorizontalItem +import com.susu.core.ui.extension.collectWithLifecycle import com.susu.feature.statistics.component.StatisticsTab -import com.susu.feature.statistics.component.StatisticsVerticalItem +import com.susu.feature.statistics.content.MyStatisticsRoute @Composable -fun StatisticsRoute() { - StatisticsScreen() +fun StatisticsRoute( + viewModel: StatisticsViewModel = hiltViewModel(), + navigateToMyInfo: () -> Unit, +) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + + viewModel.sideEffect.collectWithLifecycle { sideEffect -> + when (sideEffect) { + StatisticsEffect.NavigateToMyInfo -> navigateToMyInfo() + } + } + + StatisticsScreen( + uiState = uiState, + ) } @Composable fun StatisticsScreen( - spentData: List> = emptyList(), // TODO: Preview 데이터 + uiState: StatisticsState = StatisticsState(), ) { - Column( + Box( modifier = Modifier.fillMaxSize() - .verticalScroll(rememberScrollState()) - .padding(horizontal = SusuTheme.spacing.spacing_m), - verticalArrangement = Arrangement.spacedBy(SusuTheme.spacing.spacing_xxs), + .verticalScroll(rememberScrollState()), ) { - SusuDefaultAppBar( - modifier = Modifier.padding(horizontal = SusuTheme.spacing.spacing_xs), - leftIcon = { LogoIcon() }, - title = stringResource(R.string.statistics_word), - ) - StatisticsTab( - modifier = Modifier - .height(52.dp) - .padding(vertical = SusuTheme.spacing.spacing_xxs), - selectedTab = StatisticsTab.MY, - onTabSelect = {}, - ) - RecentSpentGraph( - spentData = spentData, // TODO: 서버 값으로 교체 - ) - Row( - modifier = Modifier - .fillMaxWidth() - .background(color = Gray10, shape = RoundedCornerShape(4.dp)) - .padding(SusuTheme.spacing.spacing_m), - horizontalArrangement = Arrangement.SpaceBetween, + Column( + modifier = Modifier.fillMaxSize().padding(horizontal = SusuTheme.spacing.spacing_m), + verticalArrangement = Arrangement.spacedBy(SusuTheme.spacing.spacing_xxs), ) { - Text(text = stringResource(R.string.statistics_most_spent_month), style = SusuTheme.typography.title_xs, color = Gray100) - Text( - text = stringResource(R.string.word_month_format, stringResource(id = R.string.word_unknown)), - style = SusuTheme.typography.title_xs, - color = Gray40, + SusuDefaultAppBar( + modifier = Modifier.padding(horizontal = SusuTheme.spacing.spacing_xs), + leftIcon = { LogoIcon() }, + title = stringResource(R.string.statistics_word), ) - } - Row( - modifier = Modifier.fillMaxWidth(), - ) { - StatisticsVerticalItem( - modifier = Modifier.weight(1f), - title = stringResource(R.string.statistics_most_susu_relationship), - content = "", - description = "", - ) - Spacer(modifier = Modifier.width(SusuTheme.spacing.spacing_xxs)) - StatisticsVerticalItem( - modifier = Modifier.weight(1f), - title = stringResource(R.string.statistics_most_susu_event), - content = "", - description = "", + StatisticsTab( + modifier = Modifier + .height(52.dp) + .padding(vertical = SusuTheme.spacing.spacing_xxs), + selectedTab = StatisticsTab.MY, + onTabSelect = {}, ) + when (uiState.currentTab) { + StatisticsTab.MY -> MyStatisticsRoute( + isBlind = uiState.isBlind, + modifier = Modifier.fillMaxSize(), + ) + + StatisticsTab.AVERAGE -> {} + } + } + + if (uiState.isLoading) { + LoadingScreen(modifier = Modifier.align(Alignment.Center)) } - StatisticsHorizontalItem( - title = stringResource(R.string.statistics_most_received_money), - name = "김수수", - money = 0, - ) - StatisticsHorizontalItem( - title = stringResource(R.string.statistics_most_sent_money), - name = "양수수", - money = 0, - ) } } @@ -108,17 +87,6 @@ fun StatisticsScreen( @Composable fun SentScreenPreview() { SusuTheme { - StatisticsScreen( - spentData = listOf( - Pair("1월", 10000), - Pair("2월", 20000), - Pair("3월", 30000), - Pair("4월", 40000), - Pair("5월", 50000), - Pair("6월", 60000), - Pair("7월", 70000), - Pair("8월", 80000), - ), - ) + StatisticsScreen() } } diff --git a/feature/statistics/src/main/java/com/susu/feature/statistics/StatisticsViewModel.kt b/feature/statistics/src/main/java/com/susu/feature/statistics/StatisticsViewModel.kt new file mode 100644 index 00000000..da9957ce --- /dev/null +++ b/feature/statistics/src/main/java/com/susu/feature/statistics/StatisticsViewModel.kt @@ -0,0 +1,23 @@ +package com.susu.feature.statistics + +import androidx.lifecycle.viewModelScope +import com.susu.core.ui.base.BaseViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class StatisticsViewModel @Inject constructor() : BaseViewModel(StatisticsState()) { + + fun checkAdditionalInfo() { + viewModelScope.launch { + intent { copy(isLoading = true) } + // TODO: 정보 입력 여부 확인 + intent { copy(isLoading = false) } + } + } + + fun selectStatisticsTab(tab: StatisticsTab) = intent { + copy(currentTab = tab) + } +} diff --git a/feature/statistics/src/main/java/com/susu/feature/statistics/content/MyStatisticsContent.kt b/feature/statistics/src/main/java/com/susu/feature/statistics/content/MyStatisticsContent.kt new file mode 100644 index 00000000..42682604 --- /dev/null +++ b/feature/statistics/src/main/java/com/susu/feature/statistics/content/MyStatisticsContent.kt @@ -0,0 +1,134 @@ +package com.susu.feature.statistics.content + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.susu.core.designsystem.theme.Gray10 +import com.susu.core.designsystem.theme.Gray100 +import com.susu.core.designsystem.theme.Gray40 +import com.susu.core.designsystem.theme.SusuTheme +import com.susu.feature.statistics.R +import com.susu.feature.statistics.component.RecentSpentGraph +import com.susu.feature.statistics.component.StatisticsHorizontalItem +import com.susu.feature.statistics.component.StatisticsVerticalItem + +@Composable +fun MyStatisticsRoute( + isBlind: Boolean, + modifier: Modifier, +) { + MyStatisticsContent( + isBlind = isBlind, + modifier = modifier, + ) +} + +@Composable +fun MyStatisticsContent( + isBlind: Boolean, + modifier: Modifier = Modifier, + spentData: List> = emptyList(), // TODO: Preview 데이터 +) { + Column( + modifier = modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(SusuTheme.spacing.spacing_xxs), + ) { + RecentSpentGraph( + spentData = spentData, // TODO: 서버 값으로 교체 + ) + Row( + modifier = Modifier + .fillMaxWidth() + .background(color = Gray10, shape = RoundedCornerShape(4.dp)) + .padding(SusuTheme.spacing.spacing_m), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text(text = stringResource(R.string.statistics_most_spent_month), style = SusuTheme.typography.title_xs, color = Gray100) + Text( + text = stringResource(R.string.word_month_format, stringResource(id = R.string.word_unknown)), + style = SusuTheme.typography.title_xs, + color = Gray40, + ) + } + Row( + modifier = Modifier.fillMaxWidth(), + ) { + StatisticsVerticalItem( + modifier = Modifier.weight(1f), + title = stringResource(R.string.statistics_most_susu_relationship), + content = "", + description = "", + ) + Spacer(modifier = Modifier.width(SusuTheme.spacing.spacing_xxs)) + StatisticsVerticalItem( + modifier = Modifier.weight(1f), + title = stringResource(R.string.statistics_most_susu_event), + content = "", + description = "", + ) + } + StatisticsHorizontalItem( + title = stringResource(R.string.statistics_most_received_money), + name = "김수수", + money = 0, + ) + StatisticsHorizontalItem( + title = stringResource(R.string.statistics_most_sent_money), + name = "양수수", + money = 0, + ) + } +} + +@Preview +@Composable +fun MyStatisticsContentPreview() { + SusuTheme { + MyStatisticsContent( + isBlind = false, + spentData = listOf( + Pair("1월", 10000), + Pair("2월", 20000), + Pair("3월", 30000), + Pair("4월", 40000), + Pair("5월", 50000), + Pair("6월", 60000), + Pair("7월", 70000), + Pair("8월", 80000), + ), + ) + } +} + +@Preview +@Composable +fun MyStatisticsContentBlindPreview() { + SusuTheme { + MyStatisticsContent( + isBlind = true, + spentData = listOf( + Pair("1월", 10000), + Pair("2월", 20000), + Pair("3월", 30000), + Pair("4월", 40000), + Pair("5월", 50000), + Pair("6월", 60000), + Pair("7월", 70000), + Pair("8월", 80000), + ), + ) + } +} diff --git a/feature/statistics/src/main/java/com/susu/feature/statistics/navigation/StatisticsNavigation.kt b/feature/statistics/src/main/java/com/susu/feature/statistics/navigation/StatisticsNavigation.kt index 78d6979b..7997fd57 100644 --- a/feature/statistics/src/main/java/com/susu/feature/statistics/navigation/StatisticsNavigation.kt +++ b/feature/statistics/src/main/java/com/susu/feature/statistics/navigation/StatisticsNavigation.kt @@ -10,9 +10,13 @@ fun NavController.navigateStatistics(navOptions: NavOptions) { navigate(StatisticsRoute.route, navOptions) } -fun NavGraphBuilder.statisticsNavGraph() { +fun NavGraphBuilder.statisticsNavGraph( + navigateToMyInfo: () -> Unit, +) { composable(route = StatisticsRoute.route) { - StatisticsRoute() + StatisticsRoute( + navigateToMyInfo = navigateToMyInfo + ) } } From 1ff4ed181210d61c2527f686965231147c59b30f Mon Sep 17 00:00:00 2001 From: yangsooplus Date: Wed, 24 Jan 2024 14:53:41 +0900 Subject: [PATCH 07/14] =?UTF-8?q?feat:=20=EC=B6=94=EA=B0=80=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EB=AF=B8=EC=9E=85=EB=A0=A5=20=EC=8B=9C=20UI?= =?UTF-8?q?=EC=99=80=20=EB=8B=A4=EC=9D=B4=EC=96=BC=EB=A1=9C=EA=B7=B8=20?= =?UTF-8?q?=EC=B6=9C=EB=A0=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CheckAdditionalUserInfoUseCase.kt | 14 +++++++++++ .../com/susu/feature/navigator/MainScreen.kt | 2 ++ .../feature/statistics/StatisticsContract.kt | 5 ++-- .../feature/statistics/StatisticsScreen.kt | 24 ++++++++++++++++--- .../feature/statistics/StatisticsViewModel.kt | 15 ++++++++++-- .../statistics/content/MyStatisticsContent.kt | 24 +++++++++++++++---- .../navigation/StatisticsNavigation.kt | 7 +++++- 7 files changed, 78 insertions(+), 13 deletions(-) create mode 100644 domain/src/main/java/com/susu/domain/usecase/statistics/CheckAdditionalUserInfoUseCase.kt diff --git a/domain/src/main/java/com/susu/domain/usecase/statistics/CheckAdditionalUserInfoUseCase.kt b/domain/src/main/java/com/susu/domain/usecase/statistics/CheckAdditionalUserInfoUseCase.kt new file mode 100644 index 00000000..40aadce8 --- /dev/null +++ b/domain/src/main/java/com/susu/domain/usecase/statistics/CheckAdditionalUserInfoUseCase.kt @@ -0,0 +1,14 @@ +package com.susu.domain.usecase.statistics + +import com.susu.core.common.runCatchingIgnoreCancelled +import com.susu.domain.repository.UserRepository +import javax.inject.Inject + +class CheckAdditionalUserInfoUseCase @Inject constructor( + private val userRepository: UserRepository, +) { + suspend operator fun invoke() = runCatchingIgnoreCancelled { + val userInfo = userRepository.getUserInfo() + userInfo.birth in 1930..2030 && (userInfo.gender == "M" || userInfo.gender == "F") + } +} diff --git a/feature/navigator/src/main/java/com/susu/feature/navigator/MainScreen.kt b/feature/navigator/src/main/java/com/susu/feature/navigator/MainScreen.kt index aef9d3b5..5d6582cc 100644 --- a/feature/navigator/src/main/java/com/susu/feature/navigator/MainScreen.kt +++ b/feature/navigator/src/main/java/com/susu/feature/navigator/MainScreen.kt @@ -123,6 +123,8 @@ internal fun MainScreen( statisticsNavGraph( navigateToMyInfo = navigator::navigateMyPageInfo, + onShowDialog = viewModel::onShowDialog, + handleException = viewModel::handleException, ) communityNavGraph( diff --git a/feature/statistics/src/main/java/com/susu/feature/statistics/StatisticsContract.kt b/feature/statistics/src/main/java/com/susu/feature/statistics/StatisticsContract.kt index 1a8b1fdd..3abeaab4 100644 --- a/feature/statistics/src/main/java/com/susu/feature/statistics/StatisticsContract.kt +++ b/feature/statistics/src/main/java/com/susu/feature/statistics/StatisticsContract.kt @@ -11,10 +11,11 @@ enum class StatisticsTab(@StringRes val stringId: Int) { data class StatisticsState( val isLoading: Boolean = false, - val isBlind: Boolean = false, + val isBlind: Boolean = true, val currentTab: StatisticsTab = StatisticsTab.MY, ) : UiState sealed interface StatisticsEffect : SideEffect { - data object NavigateToMyInfo : StatisticsEffect + data object ShowAdditionalInfoDialog : StatisticsEffect + data class HandleException(val throwable: Throwable, val retry: () -> Unit) : StatisticsEffect } diff --git a/feature/statistics/src/main/java/com/susu/feature/statistics/StatisticsScreen.kt b/feature/statistics/src/main/java/com/susu/feature/statistics/StatisticsScreen.kt index 58a178e1..630315a9 100644 --- a/feature/statistics/src/main/java/com/susu/feature/statistics/StatisticsScreen.kt +++ b/feature/statistics/src/main/java/com/susu/feature/statistics/StatisticsScreen.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -21,6 +22,7 @@ import com.susu.core.designsystem.component.appbar.SusuDefaultAppBar import com.susu.core.designsystem.component.appbar.icon.LogoIcon import com.susu.core.designsystem.component.screen.LoadingScreen import com.susu.core.designsystem.theme.SusuTheme +import com.susu.core.ui.DialogToken import com.susu.core.ui.extension.collectWithLifecycle import com.susu.feature.statistics.component.StatisticsTab import com.susu.feature.statistics.content.MyStatisticsRoute @@ -29,15 +31,30 @@ import com.susu.feature.statistics.content.MyStatisticsRoute fun StatisticsRoute( viewModel: StatisticsViewModel = hiltViewModel(), navigateToMyInfo: () -> Unit, + onShowDialog: (DialogToken) -> Unit, + handleException: (Throwable, () -> Unit) -> Unit, ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() viewModel.sideEffect.collectWithLifecycle { sideEffect -> when (sideEffect) { - StatisticsEffect.NavigateToMyInfo -> navigateToMyInfo() + is StatisticsEffect.HandleException -> handleException(sideEffect.throwable, sideEffect.retry) + StatisticsEffect.ShowAdditionalInfoDialog -> onShowDialog( + DialogToken( + title = "통계를 위한 정보를 알려주세요", + text = "나의 평균 거래 상황을 분석하기 위해\n필요한 정보가 있어요", + dismissText = "닫기", + confirmText = "정보 입력하기", + onConfirmRequest = navigateToMyInfo, + ), + ) } } + LaunchedEffect(key1 = Unit) { + viewModel.checkAdditionalInfo() + } + StatisticsScreen( uiState = uiState, ) @@ -46,6 +63,7 @@ fun StatisticsRoute( @Composable fun StatisticsScreen( uiState: StatisticsState = StatisticsState(), + onTabSelected: (StatisticsTab) -> Unit = {}, ) { Box( modifier = Modifier.fillMaxSize() @@ -64,8 +82,8 @@ fun StatisticsScreen( modifier = Modifier .height(52.dp) .padding(vertical = SusuTheme.spacing.spacing_xxs), - selectedTab = StatisticsTab.MY, - onTabSelect = {}, + selectedTab = uiState.currentTab, + onTabSelect = onTabSelected, ) when (uiState.currentTab) { StatisticsTab.MY -> MyStatisticsRoute( diff --git a/feature/statistics/src/main/java/com/susu/feature/statistics/StatisticsViewModel.kt b/feature/statistics/src/main/java/com/susu/feature/statistics/StatisticsViewModel.kt index da9957ce..59539915 100644 --- a/feature/statistics/src/main/java/com/susu/feature/statistics/StatisticsViewModel.kt +++ b/feature/statistics/src/main/java/com/susu/feature/statistics/StatisticsViewModel.kt @@ -2,17 +2,28 @@ package com.susu.feature.statistics import androidx.lifecycle.viewModelScope import com.susu.core.ui.base.BaseViewModel +import com.susu.domain.usecase.statistics.CheckAdditionalUserInfoUseCase import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel -class StatisticsViewModel @Inject constructor() : BaseViewModel(StatisticsState()) { +class StatisticsViewModel @Inject constructor( + private val checkAdditionalUserInfoUseCase: CheckAdditionalUserInfoUseCase, +) : BaseViewModel(StatisticsState()) { fun checkAdditionalInfo() { viewModelScope.launch { intent { copy(isLoading = true) } - // TODO: 정보 입력 여부 확인 + checkAdditionalUserInfoUseCase() + .onSuccess { + if (!it) { + postSideEffect(StatisticsEffect.ShowAdditionalInfoDialog) + } + intent { copy(isBlind = !it) } + }.onFailure { + postSideEffect(StatisticsEffect.HandleException(it, ::checkAdditionalInfo)) + } intent { copy(isLoading = false) } } } diff --git a/feature/statistics/src/main/java/com/susu/feature/statistics/content/MyStatisticsContent.kt b/feature/statistics/src/main/java/com/susu/feature/statistics/content/MyStatisticsContent.kt index 42682604..0c0ea8b0 100644 --- a/feature/statistics/src/main/java/com/susu/feature/statistics/content/MyStatisticsContent.kt +++ b/feature/statistics/src/main/java/com/susu/feature/statistics/content/MyStatisticsContent.kt @@ -16,6 +16,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.susu.core.designsystem.theme.Blue60 import com.susu.core.designsystem.theme.Gray10 import com.susu.core.designsystem.theme.Gray100 import com.susu.core.designsystem.theme.Gray40 @@ -47,6 +48,7 @@ fun MyStatisticsContent( verticalArrangement = Arrangement.spacedBy(SusuTheme.spacing.spacing_xxs), ) { RecentSpentGraph( + isActive = !isBlind, spentData = spentData, // TODO: 서버 값으로 교체 ) Row( @@ -57,11 +59,19 @@ fun MyStatisticsContent( horizontalArrangement = Arrangement.SpaceBetween, ) { Text(text = stringResource(R.string.statistics_most_spent_month), style = SusuTheme.typography.title_xs, color = Gray100) - Text( - text = stringResource(R.string.word_month_format, stringResource(id = R.string.word_unknown)), - style = SusuTheme.typography.title_xs, - color = Gray40, - ) + if (isBlind) { + Text( + text = stringResource(R.string.word_month_format, stringResource(id = R.string.word_unknown)), + style = SusuTheme.typography.title_xs, + color = Gray40, + ) + } else { + Text( + text = stringResource(R.string.word_month_format, "3"), + style = SusuTheme.typography.title_xs, + color = Blue60, + ) + } } Row( modifier = Modifier.fillMaxWidth(), @@ -71,6 +81,7 @@ fun MyStatisticsContent( title = stringResource(R.string.statistics_most_susu_relationship), content = "", description = "", + isActive = !isBlind, ) Spacer(modifier = Modifier.width(SusuTheme.spacing.spacing_xxs)) StatisticsVerticalItem( @@ -78,17 +89,20 @@ fun MyStatisticsContent( title = stringResource(R.string.statistics_most_susu_event), content = "", description = "", + isActive = !isBlind, ) } StatisticsHorizontalItem( title = stringResource(R.string.statistics_most_received_money), name = "김수수", money = 0, + isActive = !isBlind, ) StatisticsHorizontalItem( title = stringResource(R.string.statistics_most_sent_money), name = "양수수", money = 0, + isActive = !isBlind, ) } } diff --git a/feature/statistics/src/main/java/com/susu/feature/statistics/navigation/StatisticsNavigation.kt b/feature/statistics/src/main/java/com/susu/feature/statistics/navigation/StatisticsNavigation.kt index 7997fd57..5a4f4afe 100644 --- a/feature/statistics/src/main/java/com/susu/feature/statistics/navigation/StatisticsNavigation.kt +++ b/feature/statistics/src/main/java/com/susu/feature/statistics/navigation/StatisticsNavigation.kt @@ -4,6 +4,7 @@ import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import androidx.navigation.compose.composable +import com.susu.core.ui.DialogToken import com.susu.feature.statistics.StatisticsRoute fun NavController.navigateStatistics(navOptions: NavOptions) { @@ -12,10 +13,14 @@ fun NavController.navigateStatistics(navOptions: NavOptions) { fun NavGraphBuilder.statisticsNavGraph( navigateToMyInfo: () -> Unit, + onShowDialog: (DialogToken) -> Unit, + handleException: (Throwable, () -> Unit) -> Unit, ) { composable(route = StatisticsRoute.route) { StatisticsRoute( - navigateToMyInfo = navigateToMyInfo + navigateToMyInfo = navigateToMyInfo, + onShowDialog = onShowDialog, + handleException = handleException, ) } } From a9e9df7517d583dca6138cdbf663c11cee434b0a Mon Sep 17 00:00:00 2001 From: yangsooplus Date: Wed, 24 Jan 2024 15:01:22 +0900 Subject: [PATCH 08/14] =?UTF-8?q?fix:=20=EC=88=98=EC=A0=95=EB=90=9C=20?= =?UTF-8?q?=EC=9C=A0=EC=A0=80=20=EC=A0=95=EB=B3=B4=EA=B0=80=20=EB=A1=9C?= =?UTF-8?q?=EC=BB=AC=EC=97=90=20=EC=A0=80=EC=9E=A5=EB=90=98=EC=A7=80=20?= =?UTF-8?q?=EC=95=8A=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/susu/data/data/repository/UserRepositoryImpl.kt | 6 +++++- .../java/com/susu/feature/mypage/info/MyPageInfoScreen.kt | 6 +++++- .../com/susu/feature/mypage/info/MyPageInfoViewModel.kt | 6 +----- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/data/src/main/java/com/susu/data/data/repository/UserRepositoryImpl.kt b/data/src/main/java/com/susu/data/data/repository/UserRepositoryImpl.kt index b2eb8cf4..b11e4ead 100644 --- a/data/src/main/java/com/susu/data/data/repository/UserRepositoryImpl.kt +++ b/data/src/main/java/com/susu/data/data/repository/UserRepositoryImpl.kt @@ -46,7 +46,11 @@ class UserRepositoryImpl @Inject constructor( }.firstOrNull() ?: throw UserNotFoundException() val uid = json.decodeFromString(localUserInfo).id - return userService.patchUserInfo(uid = uid, UserPatchRequest(name, gender, birth)).getOrThrow().toModel() + val patchedUserInfo = userService.patchUserInfo(uid = uid, UserPatchRequest(name, gender, birth)).getOrThrow() + dataStore.edit { preferences -> + preferences[userKey] = json.encodeToString(patchedUserInfo) + } + return patchedUserInfo.toModel() } override suspend fun logout() { diff --git a/feature/mypage/src/main/java/com/susu/feature/mypage/info/MyPageInfoScreen.kt b/feature/mypage/src/main/java/com/susu/feature/mypage/info/MyPageInfoScreen.kt index 58b3ea01..d46e360a 100644 --- a/feature/mypage/src/main/java/com/susu/feature/mypage/info/MyPageInfoScreen.kt +++ b/feature/mypage/src/main/java/com/susu/feature/mypage/info/MyPageInfoScreen.kt @@ -16,8 +16,8 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -63,6 +63,10 @@ fun MyPageInfoRoute( val uiState by viewModel.uiState.collectAsStateWithLifecycle() + LaunchedEffect(key1 = Unit) { + viewModel.getUserInfo() + } + BackHandler { if (uiState.isEditing) { viewModel.cancelEdit() diff --git a/feature/mypage/src/main/java/com/susu/feature/mypage/info/MyPageInfoViewModel.kt b/feature/mypage/src/main/java/com/susu/feature/mypage/info/MyPageInfoViewModel.kt index 146697ea..f98953d3 100644 --- a/feature/mypage/src/main/java/com/susu/feature/mypage/info/MyPageInfoViewModel.kt +++ b/feature/mypage/src/main/java/com/susu/feature/mypage/info/MyPageInfoViewModel.kt @@ -17,11 +17,7 @@ class MyPageInfoViewModel @Inject constructor( private val patchUserUseCase: PatchUserUseCase, ) : BaseViewModel(MyPageInfoState()) { - init { - getUserInfo() - } - - private fun getUserInfo() { + fun getUserInfo() { viewModelScope.launch { intent { copy(isLoading = true) } getUserUseCase().onSuccess { From c8ae6741498b07affa8a41aaf8906548b758cb66 Mon Sep 17 00:00:00 2001 From: yangsooplus Date: Wed, 24 Jan 2024 15:17:27 +0900 Subject: [PATCH 09/14] =?UTF-8?q?chore:=20StatisticsScreen=20string=20Reso?= =?UTF-8?q?urce=20=EB=B6=84=EB=A6=AC=20=EB=B0=8F=20=EC=86=8C=EC=86=8C?= =?UTF-8?q?=ED=95=9C=20UI=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/ui/src/main/res/values/strings.xml | 1 + .../com/susu/feature/statistics/StatisticsScreen.kt | 12 +++++++----- feature/statistics/src/main/res/values/strings.xml | 3 +++ 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/core/ui/src/main/res/values/strings.xml b/core/ui/src/main/res/values/strings.xml index 1a97107f..b8da13b2 100644 --- a/core/ui/src/main/res/values/strings.xml +++ b/core/ui/src/main/res/values/strings.xml @@ -41,4 +41,5 @@ 아니요 연락처 메모 + 닫기 diff --git a/feature/statistics/src/main/java/com/susu/feature/statistics/StatisticsScreen.kt b/feature/statistics/src/main/java/com/susu/feature/statistics/StatisticsScreen.kt index 630315a9..9f10f4e4 100644 --- a/feature/statistics/src/main/java/com/susu/feature/statistics/StatisticsScreen.kt +++ b/feature/statistics/src/main/java/com/susu/feature/statistics/StatisticsScreen.kt @@ -13,6 +13,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -35,16 +36,17 @@ fun StatisticsRoute( handleException: (Throwable, () -> Unit) -> Unit, ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() + val context = LocalContext.current viewModel.sideEffect.collectWithLifecycle { sideEffect -> when (sideEffect) { is StatisticsEffect.HandleException -> handleException(sideEffect.throwable, sideEffect.retry) StatisticsEffect.ShowAdditionalInfoDialog -> onShowDialog( DialogToken( - title = "통계를 위한 정보를 알려주세요", - text = "나의 평균 거래 상황을 분석하기 위해\n필요한 정보가 있어요", - dismissText = "닫기", - confirmText = "정보 입력하기", + title = context.getString(R.string.statistics_dialog_title), + text = context.getString(R.string.statistics_dialog_description), + dismissText = context.getString(com.susu.core.ui.R.string.word_close), + confirmText = context.getString(R.string.statistics_dialog_confirm), onConfirmRequest = navigateToMyInfo, ), ) @@ -57,6 +59,7 @@ fun StatisticsRoute( StatisticsScreen( uiState = uiState, + onTabSelected = viewModel::selectStatisticsTab, ) } @@ -74,7 +77,6 @@ fun StatisticsScreen( verticalArrangement = Arrangement.spacedBy(SusuTheme.spacing.spacing_xxs), ) { SusuDefaultAppBar( - modifier = Modifier.padding(horizontal = SusuTheme.spacing.spacing_xs), leftIcon = { LogoIcon() }, title = stringResource(R.string.statistics_word), ) diff --git a/feature/statistics/src/main/res/values/strings.xml b/feature/statistics/src/main/res/values/strings.xml index f6dbcedf..a6b24c05 100644 --- a/feature/statistics/src/main/res/values/strings.xml +++ b/feature/statistics/src/main/res/values/strings.xml @@ -13,4 +13,7 @@ 최다 수수 경조사 가장 많이 받은 금액 가장 많이 보낸 금액 + 통계를 위한 정보를 알려주세요 + 나의 평균 거래 상황을 분석하기 위해\n필요한 정보가 있어요 + 정보 입력하기 From 92c49a60b454bb91fe3db407a808be4d9ae0c7cf Mon Sep 17 00:00:00 2001 From: yangsooplus Date: Wed, 24 Jan 2024 16:25:21 +0900 Subject: [PATCH 10/14] =?UTF-8?q?feat:=20=EB=82=98=EC=9D=98=20=ED=86=B5?= =?UTF-8?q?=EA=B3=84=20=EC=84=9C=EB=B2=84=20=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/susu/core/model/MyStatistics.kt | 10 + .../com/susu/core/model/StatisticsElement.kt | 6 + .../com/susu/data/data/di/RepositoryModule.kt | 7 + .../repository/StatisticsRepositoryImpl.kt | 12 ++ .../susu/data/remote/api/StatisticsService.kt | 10 + .../susu/data/remote/di/ApiServiceModule.kt | 7 + .../model/response/MyStatisticsResponse.kt | 34 ++++ .../domain/repository/StatisticsRepository.kt | 7 + .../statistics/GetMyStatisticsUseCase.kt | 13 ++ .../feature/statistics/StatisticsScreen.kt | 5 +- .../statistics/component/RecentSpentGraph.kt | 56 +++--- .../statistics/component/StatisticsItem.kt | 8 +- .../statistics/content/MyStatisticsContent.kt | 176 +++++++++--------- .../content/MyStatisticsContract.kt | 14 ++ .../content/MyStatisticsViewModel.kt | 26 +++ 15 files changed, 269 insertions(+), 122 deletions(-) create mode 100644 core/model/src/main/java/com/susu/core/model/MyStatistics.kt create mode 100644 core/model/src/main/java/com/susu/core/model/StatisticsElement.kt create mode 100644 data/src/main/java/com/susu/data/data/repository/StatisticsRepositoryImpl.kt create mode 100644 data/src/main/java/com/susu/data/remote/api/StatisticsService.kt create mode 100644 data/src/main/java/com/susu/data/remote/model/response/MyStatisticsResponse.kt create mode 100644 domain/src/main/java/com/susu/domain/repository/StatisticsRepository.kt create mode 100644 domain/src/main/java/com/susu/domain/usecase/statistics/GetMyStatisticsUseCase.kt create mode 100644 feature/statistics/src/main/java/com/susu/feature/statistics/content/MyStatisticsContract.kt create mode 100644 feature/statistics/src/main/java/com/susu/feature/statistics/content/MyStatisticsViewModel.kt diff --git a/core/model/src/main/java/com/susu/core/model/MyStatistics.kt b/core/model/src/main/java/com/susu/core/model/MyStatistics.kt new file mode 100644 index 00000000..2cfd9791 --- /dev/null +++ b/core/model/src/main/java/com/susu/core/model/MyStatistics.kt @@ -0,0 +1,10 @@ +package com.susu.core.model + +data class MyStatistics( + val highestAmountReceived: StatisticsElement = StatisticsElement(), + val highestAmountSent: StatisticsElement = StatisticsElement(), + val mostCategory: StatisticsElement = StatisticsElement(), + val mostRelationship: StatisticsElement = StatisticsElement(), + val mostSpentMonth: Int = 0, + val recentSpent: List = emptyList(), +) diff --git a/core/model/src/main/java/com/susu/core/model/StatisticsElement.kt b/core/model/src/main/java/com/susu/core/model/StatisticsElement.kt new file mode 100644 index 00000000..f284b7ac --- /dev/null +++ b/core/model/src/main/java/com/susu/core/model/StatisticsElement.kt @@ -0,0 +1,6 @@ +package com.susu.core.model + +data class StatisticsElement( + val title: String = "", + val value: Int = 0, +) diff --git a/data/src/main/java/com/susu/data/data/di/RepositoryModule.kt b/data/src/main/java/com/susu/data/data/di/RepositoryModule.kt index 9eaffd4a..389d5660 100644 --- a/data/src/main/java/com/susu/data/data/di/RepositoryModule.kt +++ b/data/src/main/java/com/susu/data/data/di/RepositoryModule.kt @@ -6,6 +6,7 @@ import com.susu.data.data.repository.LedgerRecentSearchRepositoryImpl import com.susu.data.data.repository.LedgerRepositoryImpl import com.susu.data.data.repository.LoginRepositoryImpl import com.susu.data.data.repository.SignUpRepositoryImpl +import com.susu.data.data.repository.StatisticsRepositoryImpl import com.susu.data.data.repository.TermRepositoryImpl import com.susu.data.data.repository.TokenRepositoryImpl import com.susu.data.data.repository.UserRepositoryImpl @@ -15,6 +16,7 @@ import com.susu.domain.repository.LedgerRecentSearchRepository import com.susu.domain.repository.LedgerRepository import com.susu.domain.repository.LoginRepository import com.susu.domain.repository.SignUpRepository +import com.susu.domain.repository.StatisticsRepository import com.susu.domain.repository.TermRepository import com.susu.domain.repository.TokenRepository import com.susu.domain.repository.UserRepository @@ -71,4 +73,9 @@ abstract class RepositoryModule { abstract fun bindExcelRepository( excelRepositoryImpl: ExcelRepositoryImpl, ): ExcelRepository + + @Binds + abstract fun bindStatisticsRepository( + statisticsRepositoryImpl: StatisticsRepositoryImpl, + ): StatisticsRepository } diff --git a/data/src/main/java/com/susu/data/data/repository/StatisticsRepositoryImpl.kt b/data/src/main/java/com/susu/data/data/repository/StatisticsRepositoryImpl.kt new file mode 100644 index 00000000..b617e311 --- /dev/null +++ b/data/src/main/java/com/susu/data/data/repository/StatisticsRepositoryImpl.kt @@ -0,0 +1,12 @@ +package com.susu.data.data.repository + +import com.susu.data.remote.api.StatisticsService +import com.susu.data.remote.model.response.toModel +import com.susu.domain.repository.StatisticsRepository +import javax.inject.Inject + +class StatisticsRepositoryImpl @Inject constructor( + private val statisticsService: StatisticsService, +) : StatisticsRepository { + override suspend fun getMyStatistics() = statisticsService.getMyStatistics().getOrThrow().toModel() +} diff --git a/data/src/main/java/com/susu/data/remote/api/StatisticsService.kt b/data/src/main/java/com/susu/data/remote/api/StatisticsService.kt new file mode 100644 index 00000000..9f8a0932 --- /dev/null +++ b/data/src/main/java/com/susu/data/remote/api/StatisticsService.kt @@ -0,0 +1,10 @@ +package com.susu.data.remote.api + +import com.susu.data.remote.model.response.MyStatisticsResponse +import com.susu.data.remote.retrofit.ApiResult +import retrofit2.http.GET + +interface StatisticsService { + @GET("statistics/mine") + suspend fun getMyStatistics(): ApiResult +} diff --git a/data/src/main/java/com/susu/data/remote/di/ApiServiceModule.kt b/data/src/main/java/com/susu/data/remote/di/ApiServiceModule.kt index 5b4f7dd4..bed7d817 100644 --- a/data/src/main/java/com/susu/data/remote/di/ApiServiceModule.kt +++ b/data/src/main/java/com/susu/data/remote/di/ApiServiceModule.kt @@ -4,6 +4,7 @@ import com.susu.data.remote.api.AuthService import com.susu.data.remote.api.CategoryService import com.susu.data.remote.api.LedgerService import com.susu.data.remote.api.SignUpService +import com.susu.data.remote.api.StatisticsService import com.susu.data.remote.api.TermService import com.susu.data.remote.api.TokenService import com.susu.data.remote.api.UserService @@ -59,4 +60,10 @@ object ApiServiceModule { fun providesUserService(retrofit: Retrofit): UserService { return retrofit.create(UserService::class.java) } + + @Singleton + @Provides + fun providesStatisticsService(retrofit: Retrofit): StatisticsService { + return retrofit.create(StatisticsService::class.java) + } } diff --git a/data/src/main/java/com/susu/data/remote/model/response/MyStatisticsResponse.kt b/data/src/main/java/com/susu/data/remote/model/response/MyStatisticsResponse.kt new file mode 100644 index 00000000..47f45164 --- /dev/null +++ b/data/src/main/java/com/susu/data/remote/model/response/MyStatisticsResponse.kt @@ -0,0 +1,34 @@ +package com.susu.data.remote.model.response + +import com.susu.core.model.MyStatistics +import kotlinx.serialization.Serializable + +@Serializable +data class MyStatisticsResponse( + val highestAmountReceived: StatisticsElement, + val highestAmountSent: StatisticsElement, + val mostCategory: StatisticsElement, + val mostRelationship: StatisticsElement, + val mostSpentMonth: Int, + val recentSpent: List, +) + +@Serializable +data class StatisticsElement( + val title: String, + val value: Int, +) + +fun MyStatisticsResponse.toModel() = MyStatistics( + highestAmountReceived = highestAmountReceived.toModel(), + highestAmountSent = highestAmountSent.toModel(), + mostCategory = mostCategory.toModel(), + mostRelationship = mostRelationship.toModel(), + mostSpentMonth = mostSpentMonth, + recentSpent = recentSpent.map { it.toModel() }, +) + +fun StatisticsElement.toModel() = com.susu.core.model.StatisticsElement( + title = title, + value = value, +) diff --git a/domain/src/main/java/com/susu/domain/repository/StatisticsRepository.kt b/domain/src/main/java/com/susu/domain/repository/StatisticsRepository.kt new file mode 100644 index 00000000..5806e125 --- /dev/null +++ b/domain/src/main/java/com/susu/domain/repository/StatisticsRepository.kt @@ -0,0 +1,7 @@ +package com.susu.domain.repository + +import com.susu.core.model.MyStatistics + +interface StatisticsRepository { + suspend fun getMyStatistics(): MyStatistics +} diff --git a/domain/src/main/java/com/susu/domain/usecase/statistics/GetMyStatisticsUseCase.kt b/domain/src/main/java/com/susu/domain/usecase/statistics/GetMyStatisticsUseCase.kt new file mode 100644 index 00000000..cee303e0 --- /dev/null +++ b/domain/src/main/java/com/susu/domain/usecase/statistics/GetMyStatisticsUseCase.kt @@ -0,0 +1,13 @@ +package com.susu.domain.usecase.statistics + +import com.susu.core.common.runCatchingIgnoreCancelled +import com.susu.domain.repository.StatisticsRepository +import javax.inject.Inject + +class GetMyStatisticsUseCase @Inject constructor( + private val statisticsRepository: StatisticsRepository, +) { + suspend operator fun invoke() = runCatchingIgnoreCancelled { + statisticsRepository.getMyStatistics() + } +} diff --git a/feature/statistics/src/main/java/com/susu/feature/statistics/StatisticsScreen.kt b/feature/statistics/src/main/java/com/susu/feature/statistics/StatisticsScreen.kt index 9f10f4e4..04e13826 100644 --- a/feature/statistics/src/main/java/com/susu/feature/statistics/StatisticsScreen.kt +++ b/feature/statistics/src/main/java/com/susu/feature/statistics/StatisticsScreen.kt @@ -60,6 +60,7 @@ fun StatisticsRoute( StatisticsScreen( uiState = uiState, onTabSelected = viewModel::selectStatisticsTab, + handleException = handleException, ) } @@ -67,6 +68,7 @@ fun StatisticsRoute( fun StatisticsScreen( uiState: StatisticsState = StatisticsState(), onTabSelected: (StatisticsTab) -> Unit = {}, + handleException: (Throwable, () -> Unit) -> Unit = { _, _ -> }, ) { Box( modifier = Modifier.fillMaxSize() @@ -91,6 +93,7 @@ fun StatisticsScreen( StatisticsTab.MY -> MyStatisticsRoute( isBlind = uiState.isBlind, modifier = Modifier.fillMaxSize(), + handleException = handleException, ) StatisticsTab.AVERAGE -> {} @@ -98,7 +101,7 @@ fun StatisticsScreen( } if (uiState.isLoading) { - LoadingScreen(modifier = Modifier.align(Alignment.Center)) + LoadingScreen(modifier = Modifier.fillMaxSize().align(Alignment.Center)) } } } diff --git a/feature/statistics/src/main/java/com/susu/feature/statistics/component/RecentSpentGraph.kt b/feature/statistics/src/main/java/com/susu/feature/statistics/component/RecentSpentGraph.kt index cad213ca..f132086e 100644 --- a/feature/statistics/src/main/java/com/susu/feature/statistics/component/RecentSpentGraph.kt +++ b/feature/statistics/src/main/java/com/susu/feature/statistics/component/RecentSpentGraph.kt @@ -45,6 +45,7 @@ import com.susu.core.designsystem.theme.Gray90 import com.susu.core.designsystem.theme.Orange30 import com.susu.core.designsystem.theme.Orange60 import com.susu.core.designsystem.theme.SusuTheme +import com.susu.core.model.StatisticsElement import com.susu.feature.statistics.R import kotlin.random.Random @@ -52,10 +53,10 @@ import kotlin.random.Random fun RecentSpentGraph( modifier: Modifier = Modifier, isActive: Boolean = true, - spentData: List> = emptyList(), + spentData: List = emptyList(), ) { - val totalAmount by remember { mutableStateOf(spentData.sumOf { it.second } / 10000) } - val maximumAmount by remember { mutableStateOf(spentData.maxOfOrNull { it.second }) } + val totalAmount by remember { mutableStateOf(spentData.sumOf { it.value }) } + val maximumAmount by remember { mutableStateOf(spentData.maxOfOrNull { it.value }) } Column( modifier = modifier @@ -65,7 +66,9 @@ fun RecentSpentGraph( horizontalAlignment = Alignment.CenterHorizontally, ) { Row( - modifier = Modifier.fillMaxWidth().padding(SusuTheme.spacing.spacing_xxs), + modifier = Modifier + .fillMaxWidth() + .padding(SusuTheme.spacing.spacing_xxs), horizontalArrangement = Arrangement.SpaceBetween, ) { Text( @@ -90,9 +93,14 @@ fun RecentSpentGraph( Spacer(modifier = Modifier.height(SusuTheme.spacing.spacing_xxs)) Row( modifier = if (isActive) { - Modifier.fillMaxWidth().padding(SusuTheme.spacing.spacing_xxs) + Modifier + .fillMaxWidth() + .padding(SusuTheme.spacing.spacing_xxs) } else { - Modifier.fillMaxWidth().blur(8.dp).padding(SusuTheme.spacing.spacing_xxs) + Modifier + .fillMaxWidth() + .blur(8.dp) + .padding(SusuTheme.spacing.spacing_xxs) }, horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, @@ -103,7 +111,7 @@ fun RecentSpentGraph( horizontalAlignment = Alignment.CenterHorizontally, ) { StickGraph( - ratio = data.second.toFloat() / maximumAmount!!, + ratio = data.value.toFloat() / maximumAmount!!, color = if (isActive) { if (i == spentData.lastIndex) { Orange60 @@ -120,7 +128,7 @@ fun RecentSpentGraph( ) Spacer(modifier = Modifier.height(SusuTheme.spacing.spacing_xxxxs)) Text( - text = data.first, + text = stringResource(id = R.string.word_month_format, data.title), style = SusuTheme.typography.title_xxxs, color = if (i == spentData.lastIndex) { Gray90 @@ -205,27 +213,27 @@ fun RecentSpentGraphPreview() { RecentSpentGraph( modifier = Modifier.padding(16.dp), spentData = listOf( - Pair("1월", 10000), - Pair("2월", 20000), - Pair("3월", 30000), - Pair("4월", 40000), - Pair("5월", 50000), - Pair("6월", 60000), - Pair("7월", 70000), - Pair("8월", 80000), + StatisticsElement("1월", 10000), + StatisticsElement("2월", 20000), + StatisticsElement("3월", 30000), + StatisticsElement("4월", 40000), + StatisticsElement("5월", 50000), + StatisticsElement("6월", 60000), + StatisticsElement("7월", 70000), + StatisticsElement("8월", 80000), ), ) RecentSpentGraph( modifier = Modifier.padding(16.dp), spentData = listOf( - Pair("1월", 10000), - Pair("2월", 20000), - Pair("3월", 30000), - Pair("4월", 40000), - Pair("5월", 50000), - Pair("6월", 60000), - Pair("7월", 70000), - Pair("8월", 80000), + StatisticsElement("1월", 10000), + StatisticsElement("2월", 20000), + StatisticsElement("3월", 30000), + StatisticsElement("4월", 40000), + StatisticsElement("5월", 50000), + StatisticsElement("6월", 60000), + StatisticsElement("7월", 70000), + StatisticsElement("8월", 80000), ), isActive = false, ) diff --git a/feature/statistics/src/main/java/com/susu/feature/statistics/component/StatisticsItem.kt b/feature/statistics/src/main/java/com/susu/feature/statistics/component/StatisticsItem.kt index 1eaa02dc..80a4495c 100644 --- a/feature/statistics/src/main/java/com/susu/feature/statistics/component/StatisticsItem.kt +++ b/feature/statistics/src/main/java/com/susu/feature/statistics/component/StatisticsItem.kt @@ -31,7 +31,7 @@ import com.susu.feature.statistics.R fun StatisticsVerticalItem( title: String, content: String, - description: String, + count: Int, modifier: Modifier = Modifier, isActive: Boolean = true, ) { @@ -56,7 +56,7 @@ fun StatisticsVerticalItem( ) Text( modifier = Modifier.align(Alignment.CenterHorizontally), - text = stringResource(R.string.word_entire_count, description), + text = stringResource(R.string.word_entire_count, count.toString()), style = SusuTheme.typography.title_xxxs, color = Gray60, ) @@ -126,9 +126,9 @@ fun StatisticsHorizontalItem( fun StatisticsItemPreview() { SusuTheme { Column { - StatisticsVerticalItem(title = "자주 개발하는 시간", content = "밤", description = "낮에 좀 해라") + StatisticsVerticalItem(title = "자주 개발하는 시간", content = "밤", count = 1) StatisticsHorizontalItem(title = "이번달에 허투루 쓴 돈", name = "배달음식", money = 60000) - StatisticsVerticalItem(title = "자주 개발하는 시간", content = "밤", description = "낮에 좀 해라", isActive = false) + StatisticsVerticalItem(title = "자주 개발하는 시간", content = "밤", count = 1, isActive = false) StatisticsHorizontalItem(title = "이번달에 허투루 쓴 돈", name = "배달음식", money = 60000, isActive = false) } } diff --git a/feature/statistics/src/main/java/com/susu/feature/statistics/content/MyStatisticsContent.kt b/feature/statistics/src/main/java/com/susu/feature/statistics/content/MyStatisticsContent.kt index 0c0ea8b0..6adbd905 100644 --- a/feature/statistics/src/main/java/com/susu/feature/statistics/content/MyStatisticsContent.kt +++ b/feature/statistics/src/main/java/com/susu/feature/statistics/content/MyStatisticsContent.kt @@ -2,6 +2,7 @@ package com.susu.feature.statistics.content 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 @@ -12,15 +13,21 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -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.susu.core.designsystem.component.screen.LoadingScreen import com.susu.core.designsystem.theme.Blue60 import com.susu.core.designsystem.theme.Gray10 import com.susu.core.designsystem.theme.Gray100 import com.susu.core.designsystem.theme.Gray40 import com.susu.core.designsystem.theme.SusuTheme +import com.susu.core.ui.extension.collectWithLifecycle import com.susu.feature.statistics.R import com.susu.feature.statistics.component.RecentSpentGraph import com.susu.feature.statistics.component.StatisticsHorizontalItem @@ -30,8 +37,23 @@ import com.susu.feature.statistics.component.StatisticsVerticalItem fun MyStatisticsRoute( isBlind: Boolean, modifier: Modifier, + viewModel: MyStatisticsViewModel = hiltViewModel(), + handleException: (Throwable, () -> Unit) -> Unit, ) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + + viewModel.sideEffect.collectWithLifecycle { sideEffect -> + when (sideEffect) { + is MyStatisticsEffect.HandleException -> handleException(sideEffect.throwable, sideEffect.retry) + } + } + + LaunchedEffect(key1 = Unit) { + viewModel.getMyStatistics() + } + MyStatisticsContent( + uiState = uiState, isBlind = isBlind, modifier = modifier, ) @@ -39,110 +61,78 @@ fun MyStatisticsRoute( @Composable fun MyStatisticsContent( + uiState: MyStatisticsState, isBlind: Boolean, modifier: Modifier = Modifier, - spentData: List> = emptyList(), // TODO: Preview 데이터 ) { - Column( - modifier = modifier.fillMaxSize(), - verticalArrangement = Arrangement.spacedBy(SusuTheme.spacing.spacing_xxs), - ) { - RecentSpentGraph( - isActive = !isBlind, - spentData = spentData, // TODO: 서버 값으로 교체 - ) - Row( - modifier = Modifier - .fillMaxWidth() - .background(color = Gray10, shape = RoundedCornerShape(4.dp)) - .padding(SusuTheme.spacing.spacing_m), - horizontalArrangement = Arrangement.SpaceBetween, + Box(modifier = modifier.fillMaxSize()) { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(SusuTheme.spacing.spacing_xxs), ) { - Text(text = stringResource(R.string.statistics_most_spent_month), style = SusuTheme.typography.title_xs, color = Gray100) - if (isBlind) { - Text( - text = stringResource(R.string.word_month_format, stringResource(id = R.string.word_unknown)), - style = SusuTheme.typography.title_xs, - color = Gray40, + RecentSpentGraph( + isActive = !isBlind, + spentData = uiState.statistics.recentSpent, + ) + Row( + modifier = Modifier + .fillMaxWidth() + .background(color = Gray10, shape = RoundedCornerShape(4.dp)) + .padding(SusuTheme.spacing.spacing_m), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text(text = stringResource(R.string.statistics_most_spent_month), style = SusuTheme.typography.title_xs, color = Gray100) + if (isBlind) { + Text( + text = stringResource(R.string.word_month_format, stringResource(id = R.string.word_unknown)), + style = SusuTheme.typography.title_xs, + color = Gray40, + ) + } else { + Text( + text = stringResource(R.string.word_month_format, uiState.statistics.mostSpentMonth.toString()), + style = SusuTheme.typography.title_xs, + color = Blue60, + ) + } + } + Row( + modifier = Modifier.fillMaxWidth(), + ) { + StatisticsVerticalItem( + modifier = Modifier.weight(1f), + title = stringResource(R.string.statistics_most_susu_relationship), + content = uiState.statistics.mostRelationship.title, + count = uiState.statistics.mostRelationship.value, + isActive = !isBlind, ) - } else { - Text( - text = stringResource(R.string.word_month_format, "3"), - style = SusuTheme.typography.title_xs, - color = Blue60, + Spacer(modifier = Modifier.width(SusuTheme.spacing.spacing_xxs)) + StatisticsVerticalItem( + modifier = Modifier.weight(1f), + title = stringResource(R.string.statistics_most_susu_event), + content = uiState.statistics.mostCategory.title, + count = uiState.statistics.mostCategory.value, + isActive = !isBlind, ) } - } - Row( - modifier = Modifier.fillMaxWidth(), - ) { - StatisticsVerticalItem( - modifier = Modifier.weight(1f), - title = stringResource(R.string.statistics_most_susu_relationship), - content = "", - description = "", + StatisticsHorizontalItem( + title = stringResource(R.string.statistics_most_received_money), + name = uiState.statistics.highestAmountReceived.title, + money = uiState.statistics.highestAmountReceived.value, isActive = !isBlind, ) - Spacer(modifier = Modifier.width(SusuTheme.spacing.spacing_xxs)) - StatisticsVerticalItem( - modifier = Modifier.weight(1f), - title = stringResource(R.string.statistics_most_susu_event), - content = "", - description = "", + StatisticsHorizontalItem( + title = stringResource(R.string.statistics_most_sent_money), + name = uiState.statistics.highestAmountSent.title, + money = uiState.statistics.highestAmountSent.value, isActive = !isBlind, ) } - StatisticsHorizontalItem( - title = stringResource(R.string.statistics_most_received_money), - name = "김수수", - money = 0, - isActive = !isBlind, - ) - StatisticsHorizontalItem( - title = stringResource(R.string.statistics_most_sent_money), - name = "양수수", - money = 0, - isActive = !isBlind, - ) - } -} - -@Preview -@Composable -fun MyStatisticsContentPreview() { - SusuTheme { - MyStatisticsContent( - isBlind = false, - spentData = listOf( - Pair("1월", 10000), - Pair("2월", 20000), - Pair("3월", 30000), - Pair("4월", 40000), - Pair("5월", 50000), - Pair("6월", 60000), - Pair("7월", 70000), - Pair("8월", 80000), - ), - ) - } -} -@Preview -@Composable -fun MyStatisticsContentBlindPreview() { - SusuTheme { - MyStatisticsContent( - isBlind = true, - spentData = listOf( - Pair("1월", 10000), - Pair("2월", 20000), - Pair("3월", 30000), - Pair("4월", 40000), - Pair("5월", 50000), - Pair("6월", 60000), - Pair("7월", 70000), - Pair("8월", 80000), - ), - ) + if (uiState.isLoading) { + LoadingScreen( + modifier = Modifier.align(Alignment.Center), + ) + } } } diff --git a/feature/statistics/src/main/java/com/susu/feature/statistics/content/MyStatisticsContract.kt b/feature/statistics/src/main/java/com/susu/feature/statistics/content/MyStatisticsContract.kt new file mode 100644 index 00000000..98384780 --- /dev/null +++ b/feature/statistics/src/main/java/com/susu/feature/statistics/content/MyStatisticsContract.kt @@ -0,0 +1,14 @@ +package com.susu.feature.statistics.content + +import com.susu.core.model.MyStatistics +import com.susu.core.ui.base.SideEffect +import com.susu.core.ui.base.UiState + +sealed interface MyStatisticsEffect : SideEffect { + data class HandleException(val throwable: Throwable, val retry: () -> Unit) : MyStatisticsEffect +} + +data class MyStatisticsState( + val isLoading: Boolean = false, + val statistics: MyStatistics = MyStatistics(), +) : UiState diff --git a/feature/statistics/src/main/java/com/susu/feature/statistics/content/MyStatisticsViewModel.kt b/feature/statistics/src/main/java/com/susu/feature/statistics/content/MyStatisticsViewModel.kt new file mode 100644 index 00000000..77854360 --- /dev/null +++ b/feature/statistics/src/main/java/com/susu/feature/statistics/content/MyStatisticsViewModel.kt @@ -0,0 +1,26 @@ +package com.susu.feature.statistics.content + +import androidx.lifecycle.viewModelScope +import com.susu.core.ui.base.BaseViewModel +import com.susu.domain.usecase.statistics.GetMyStatisticsUseCase +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class MyStatisticsViewModel @Inject constructor( + private val getMyStatisticsUseCase: GetMyStatisticsUseCase, +) : BaseViewModel(MyStatisticsState()) { + fun getMyStatistics() { + viewModelScope.launch { + intent { copy(isLoading = true) } + getMyStatisticsUseCase() + .onSuccess { + intent { copy(statistics = it) } + }.onFailure { + postSideEffect(MyStatisticsEffect.HandleException(it, ::getMyStatistics)) + } + intent { copy(isLoading = false) } + } + } +} From cd8fea720fee48e85d065efb7dd0121470a32935 Mon Sep 17 00:00:00 2001 From: yangsooplus Date: Thu, 25 Jan 2024 18:15:54 +0900 Subject: [PATCH 11/14] =?UTF-8?q?refactor:=20=EC=B5=9C=EA=B7=BC=20?= =?UTF-8?q?=EA=B8=88=EC=95=A1=20=EB=8D=B0=EC=9D=B4=ED=84=B0=EB=A5=BC=20Rep?= =?UTF-8?q?ository=EC=97=90=EC=84=9C=20=EA=B0=80=EA=B3=B5=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/susu/core/model/MyStatistics.kt | 2 + .../repository/StatisticsRepositoryImpl.kt | 19 +++++- .../model/response/MyStatisticsResponse.kt | 2 + .../statistics/component/RecentSpentGraph.kt | 67 +++++++++---------- .../statistics/content/MyStatisticsContent.kt | 2 + 5 files changed, 55 insertions(+), 37 deletions(-) diff --git a/core/model/src/main/java/com/susu/core/model/MyStatistics.kt b/core/model/src/main/java/com/susu/core/model/MyStatistics.kt index 2cfd9791..866ef535 100644 --- a/core/model/src/main/java/com/susu/core/model/MyStatistics.kt +++ b/core/model/src/main/java/com/susu/core/model/MyStatistics.kt @@ -7,4 +7,6 @@ data class MyStatistics( val mostRelationship: StatisticsElement = StatisticsElement(), val mostSpentMonth: Int = 0, val recentSpent: List = emptyList(), + val recentTotalSpent: Int = 0, + val recentMaximumSpent: Int = 0, ) diff --git a/data/src/main/java/com/susu/data/data/repository/StatisticsRepositoryImpl.kt b/data/src/main/java/com/susu/data/data/repository/StatisticsRepositoryImpl.kt index b617e311..0f111dc1 100644 --- a/data/src/main/java/com/susu/data/data/repository/StatisticsRepositoryImpl.kt +++ b/data/src/main/java/com/susu/data/data/repository/StatisticsRepositoryImpl.kt @@ -1,5 +1,7 @@ package com.susu.data.data.repository +import com.susu.core.model.MyStatistics +import com.susu.core.model.StatisticsElement import com.susu.data.remote.api.StatisticsService import com.susu.data.remote.model.response.toModel import com.susu.domain.repository.StatisticsRepository @@ -8,5 +10,20 @@ import javax.inject.Inject class StatisticsRepositoryImpl @Inject constructor( private val statisticsService: StatisticsService, ) : StatisticsRepository { - override suspend fun getMyStatistics() = statisticsService.getMyStatistics().getOrThrow().toModel() + override suspend fun getMyStatistics(): MyStatistics { + val originalStatistic = statisticsService.getMyStatistics().getOrThrow().toModel() + val sortedRecentSpent = originalStatistic.recentSpent.sortedBy { it.title.replace(".", "").toInt() } // api 수정 후 replace 제거 + .map { StatisticsElement(title = it.title.removeRange(0 until 5).toInt().toString(), value = it.value) } // api 수정 후 range 수정 + + return MyStatistics( + highestAmountReceived = originalStatistic.highestAmountReceived, + highestAmountSent = originalStatistic.highestAmountSent, + mostCategory = originalStatistic.mostCategory, + mostRelationship = originalStatistic.mostRelationship, + mostSpentMonth = originalStatistic.mostSpentMonth % 100, + recentSpent = sortedRecentSpent, + recentTotalSpent = originalStatistic.recentTotalSpent, + recentMaximumSpent = originalStatistic.recentMaximumSpent, + ) + } } diff --git a/data/src/main/java/com/susu/data/remote/model/response/MyStatisticsResponse.kt b/data/src/main/java/com/susu/data/remote/model/response/MyStatisticsResponse.kt index 47f45164..966823eb 100644 --- a/data/src/main/java/com/susu/data/remote/model/response/MyStatisticsResponse.kt +++ b/data/src/main/java/com/susu/data/remote/model/response/MyStatisticsResponse.kt @@ -26,6 +26,8 @@ fun MyStatisticsResponse.toModel() = MyStatistics( mostRelationship = mostRelationship.toModel(), mostSpentMonth = mostSpentMonth, recentSpent = recentSpent.map { it.toModel() }, + recentMaximumSpent = recentSpent.maxOfOrNull { it.value } ?: 0, + recentTotalSpent = recentSpent.sumOf { it.value } ) fun StatisticsElement.toModel() = com.susu.core.model.StatisticsElement( diff --git a/feature/statistics/src/main/java/com/susu/feature/statistics/component/RecentSpentGraph.kt b/feature/statistics/src/main/java/com/susu/feature/statistics/component/RecentSpentGraph.kt index f132086e..7d7a554e 100644 --- a/feature/statistics/src/main/java/com/susu/feature/statistics/component/RecentSpentGraph.kt +++ b/feature/statistics/src/main/java/com/susu/feature/statistics/component/RecentSpentGraph.kt @@ -54,10 +54,9 @@ fun RecentSpentGraph( modifier: Modifier = Modifier, isActive: Boolean = true, spentData: List = emptyList(), + totalAmount: Int = 0, + maximumAmount: Int = 0, ) { - val totalAmount by remember { mutableStateOf(spentData.sumOf { it.value }) } - val maximumAmount by remember { mutableStateOf(spentData.maxOfOrNull { it.value }) } - Column( modifier = modifier .fillMaxWidth() @@ -105,42 +104,38 @@ fun RecentSpentGraph( horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, ) { - if (maximumAmount != null) { - spentData.forEachIndexed { i, data -> - Column( - horizontalAlignment = Alignment.CenterHorizontally, - ) { - StickGraph( - ratio = data.value.toFloat() / maximumAmount!!, - color = if (isActive) { - if (i == spentData.lastIndex) { - Orange60 - } else { - Orange30 - } + spentData.forEachIndexed { i, data -> + Column( + horizontalAlignment = Alignment.CenterHorizontally, + ) { + StickGraph( + ratio = data.value.toFloat() / maximumAmount, + color = if (isActive) { + if (i == spentData.lastIndex) { + Orange60 } else { - if (i == spentData.lastIndex) { - Gray60 - } else { - Gray30 - } - }, - ) - Spacer(modifier = Modifier.height(SusuTheme.spacing.spacing_xxxxs)) - Text( - text = stringResource(id = R.string.word_month_format, data.title), - style = SusuTheme.typography.title_xxxs, - color = if (i == spentData.lastIndex) { - Gray90 + Orange30 + } + } else { + if (i == spentData.lastIndex) { + Gray60 } else { - Gray40 - }, - ) - } - Spacer(modifier = Modifier.width(SusuTheme.spacing.spacing_s)) + Gray30 + } + }, + ) + Spacer(modifier = Modifier.height(SusuTheme.spacing.spacing_xxxxs)) + Text( + text = stringResource(id = R.string.word_month_format, data.title), + style = SusuTheme.typography.title_xxxs, + color = if (i == spentData.lastIndex) { + Gray90 + } else { + Gray40 + }, + ) } - } else { - Spacer(modifier = Modifier.height(100.dp)) // 임시 + Spacer(modifier = Modifier.width(SusuTheme.spacing.spacing_s)) } } } diff --git a/feature/statistics/src/main/java/com/susu/feature/statistics/content/MyStatisticsContent.kt b/feature/statistics/src/main/java/com/susu/feature/statistics/content/MyStatisticsContent.kt index 6adbd905..c6e446b0 100644 --- a/feature/statistics/src/main/java/com/susu/feature/statistics/content/MyStatisticsContent.kt +++ b/feature/statistics/src/main/java/com/susu/feature/statistics/content/MyStatisticsContent.kt @@ -73,6 +73,8 @@ fun MyStatisticsContent( RecentSpentGraph( isActive = !isBlind, spentData = uiState.statistics.recentSpent, + maximumAmount = uiState.statistics.recentMaximumSpent, + totalAmount = uiState.statistics.recentTotalSpent, ) Row( modifier = Modifier From 5c00d78141092458d3c6693f9341bb9ceed73b9d Mon Sep 17 00:00:00 2001 From: yangsooplus Date: Thu, 25 Jan 2024 22:15:21 +0900 Subject: [PATCH 12/14] =?UTF-8?q?refactor:=20@Stable=20=EC=B6=94=EA=B0=80,?= =?UTF-8?q?=20PersistentList=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/com/susu/core/model/MyStatistics.kt | 3 +++ .../main/java/com/susu/core/model/StatisticsElement.kt | 3 +++ .../feature/statistics/component/RecentSpentGraph.kt | 9 ++++++--- .../feature/statistics/content/MyStatisticsContent.kt | 3 ++- 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/core/model/src/main/java/com/susu/core/model/MyStatistics.kt b/core/model/src/main/java/com/susu/core/model/MyStatistics.kt index 866ef535..8badbda1 100644 --- a/core/model/src/main/java/com/susu/core/model/MyStatistics.kt +++ b/core/model/src/main/java/com/susu/core/model/MyStatistics.kt @@ -1,5 +1,8 @@ package com.susu.core.model +import androidx.compose.runtime.Stable + +@Stable data class MyStatistics( val highestAmountReceived: StatisticsElement = StatisticsElement(), val highestAmountSent: StatisticsElement = StatisticsElement(), diff --git a/core/model/src/main/java/com/susu/core/model/StatisticsElement.kt b/core/model/src/main/java/com/susu/core/model/StatisticsElement.kt index f284b7ac..363d620a 100644 --- a/core/model/src/main/java/com/susu/core/model/StatisticsElement.kt +++ b/core/model/src/main/java/com/susu/core/model/StatisticsElement.kt @@ -1,5 +1,8 @@ package com.susu.core.model +import androidx.compose.runtime.Stable + +@Stable data class StatisticsElement( val title: String = "", val value: Int = 0, diff --git a/feature/statistics/src/main/java/com/susu/feature/statistics/component/RecentSpentGraph.kt b/feature/statistics/src/main/java/com/susu/feature/statistics/component/RecentSpentGraph.kt index 7d7a554e..fad28860 100644 --- a/feature/statistics/src/main/java/com/susu/feature/statistics/component/RecentSpentGraph.kt +++ b/feature/statistics/src/main/java/com/susu/feature/statistics/component/RecentSpentGraph.kt @@ -47,13 +47,16 @@ import com.susu.core.designsystem.theme.Orange60 import com.susu.core.designsystem.theme.SusuTheme import com.susu.core.model.StatisticsElement import com.susu.feature.statistics.R +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList import kotlin.random.Random @Composable fun RecentSpentGraph( modifier: Modifier = Modifier, isActive: Boolean = true, - spentData: List = emptyList(), + spentData: PersistentList = persistentListOf(), totalAmount: Int = 0, maximumAmount: Int = 0, ) { @@ -216,7 +219,7 @@ fun RecentSpentGraphPreview() { StatisticsElement("6월", 60000), StatisticsElement("7월", 70000), StatisticsElement("8월", 80000), - ), + ).toPersistentList(), ) RecentSpentGraph( modifier = Modifier.padding(16.dp), @@ -229,7 +232,7 @@ fun RecentSpentGraphPreview() { StatisticsElement("6월", 60000), StatisticsElement("7월", 70000), StatisticsElement("8월", 80000), - ), + ).toPersistentList(), isActive = false, ) } diff --git a/feature/statistics/src/main/java/com/susu/feature/statistics/content/MyStatisticsContent.kt b/feature/statistics/src/main/java/com/susu/feature/statistics/content/MyStatisticsContent.kt index c6e446b0..cee4fd65 100644 --- a/feature/statistics/src/main/java/com/susu/feature/statistics/content/MyStatisticsContent.kt +++ b/feature/statistics/src/main/java/com/susu/feature/statistics/content/MyStatisticsContent.kt @@ -32,6 +32,7 @@ import com.susu.feature.statistics.R import com.susu.feature.statistics.component.RecentSpentGraph import com.susu.feature.statistics.component.StatisticsHorizontalItem import com.susu.feature.statistics.component.StatisticsVerticalItem +import kotlinx.collections.immutable.toPersistentList @Composable fun MyStatisticsRoute( @@ -72,7 +73,7 @@ fun MyStatisticsContent( ) { RecentSpentGraph( isActive = !isBlind, - spentData = uiState.statistics.recentSpent, + spentData = uiState.statistics.recentSpent.toPersistentList(), maximumAmount = uiState.statistics.recentMaximumSpent, totalAmount = uiState.statistics.recentTotalSpent, ) From 4102354c4998e585d01e50a1f20d61f946384a19 Mon Sep 17 00:00:00 2001 From: yangsooplus Date: Thu, 25 Jan 2024 22:17:39 +0900 Subject: [PATCH 13/14] chore: ktlint, detekt check --- data/src/main/java/com/susu/data/data/di/RepositoryModule.kt | 1 - data/src/main/java/com/susu/data/remote/di/ApiServiceModule.kt | 1 + .../com/susu/data/remote/model/response/MyStatisticsResponse.kt | 2 +- .../main/java/com/susu/feature/statistics/StatisticsContract.kt | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) diff --git a/data/src/main/java/com/susu/data/data/di/RepositoryModule.kt b/data/src/main/java/com/susu/data/data/di/RepositoryModule.kt index 750bc5c9..daf25d15 100644 --- a/data/src/main/java/com/susu/data/data/di/RepositoryModule.kt +++ b/data/src/main/java/com/susu/data/data/di/RepositoryModule.kt @@ -85,5 +85,4 @@ abstract class RepositoryModule { abstract fun bindVoteRepository( voteRepositoryImpl: VoteRepositoryImpl, ): VoteRepository - } diff --git a/data/src/main/java/com/susu/data/remote/di/ApiServiceModule.kt b/data/src/main/java/com/susu/data/remote/di/ApiServiceModule.kt index 681f7805..7c6452dd 100644 --- a/data/src/main/java/com/susu/data/remote/di/ApiServiceModule.kt +++ b/data/src/main/java/com/susu/data/remote/di/ApiServiceModule.kt @@ -66,6 +66,7 @@ object ApiServiceModule { @Provides fun providesStatisticsService(retrofit: Retrofit): StatisticsService { return retrofit.create(StatisticsService::class.java) + } @Singleton @Provides diff --git a/data/src/main/java/com/susu/data/remote/model/response/MyStatisticsResponse.kt b/data/src/main/java/com/susu/data/remote/model/response/MyStatisticsResponse.kt index 966823eb..e9be8886 100644 --- a/data/src/main/java/com/susu/data/remote/model/response/MyStatisticsResponse.kt +++ b/data/src/main/java/com/susu/data/remote/model/response/MyStatisticsResponse.kt @@ -27,7 +27,7 @@ fun MyStatisticsResponse.toModel() = MyStatistics( mostSpentMonth = mostSpentMonth, recentSpent = recentSpent.map { it.toModel() }, recentMaximumSpent = recentSpent.maxOfOrNull { it.value } ?: 0, - recentTotalSpent = recentSpent.sumOf { it.value } + recentTotalSpent = recentSpent.sumOf { it.value }, ) fun StatisticsElement.toModel() = com.susu.core.model.StatisticsElement( diff --git a/feature/statistics/src/main/java/com/susu/feature/statistics/StatisticsContract.kt b/feature/statistics/src/main/java/com/susu/feature/statistics/StatisticsContract.kt index 3abeaab4..52bfe0b9 100644 --- a/feature/statistics/src/main/java/com/susu/feature/statistics/StatisticsContract.kt +++ b/feature/statistics/src/main/java/com/susu/feature/statistics/StatisticsContract.kt @@ -6,7 +6,7 @@ import com.susu.core.ui.base.UiState enum class StatisticsTab(@StringRes val stringId: Int) { MY(R.string.statistics_tab_my), - AVERAGE(R.string.statistics_tab_average) + AVERAGE(R.string.statistics_tab_average), } data class StatisticsState( From 120a00f96d9c0236c060ac0da9e8d996aeb225ed Mon Sep 17 00:00:00 2001 From: yangsooplus Date: Thu, 25 Jan 2024 22:34:00 +0900 Subject: [PATCH 14/14] =?UTF-8?q?refactor:=20=EC=B5=9C=EA=B7=BC=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=20=EB=82=B4=EC=97=AD=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=EB=90=9C=20api=20=ED=98=95=EC=8B=9D=EC=97=90=20=EB=A7=9E?= =?UTF-8?q?=EA=B2=8C=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EA=B0=80=EA=B3=B5?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/susu/data/data/repository/StatisticsRepositoryImpl.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/data/src/main/java/com/susu/data/data/repository/StatisticsRepositoryImpl.kt b/data/src/main/java/com/susu/data/data/repository/StatisticsRepositoryImpl.kt index 0f111dc1..deae1020 100644 --- a/data/src/main/java/com/susu/data/data/repository/StatisticsRepositoryImpl.kt +++ b/data/src/main/java/com/susu/data/data/repository/StatisticsRepositoryImpl.kt @@ -12,8 +12,8 @@ class StatisticsRepositoryImpl @Inject constructor( ) : StatisticsRepository { override suspend fun getMyStatistics(): MyStatistics { val originalStatistic = statisticsService.getMyStatistics().getOrThrow().toModel() - val sortedRecentSpent = originalStatistic.recentSpent.sortedBy { it.title.replace(".", "").toInt() } // api 수정 후 replace 제거 - .map { StatisticsElement(title = it.title.removeRange(0 until 5).toInt().toString(), value = it.value) } // api 수정 후 range 수정 + val sortedRecentSpent = originalStatistic.recentSpent.sortedBy { it.title.toInt() } + .map { StatisticsElement(title = it.title.substring(it.title.length - 2).toInt().toString(), value = it.value) } return MyStatistics( highestAmountReceived = originalStatistic.highestAmountReceived,