From 17030b4670492e68c1437af71cf0f331e4ccea49 Mon Sep 17 00:00:00 2001 From: chanho0908 Date: Tue, 24 Feb 2026 14:09:01 +0900 Subject: [PATCH 01/17] =?UTF-8?q?=E2=9C=A8=20Feat:=20=EC=9B=94=EB=B3=84=20?= =?UTF-8?q?=ED=86=B5=EA=B3=84=20=EC=9D=B4=EB=8F=99=20=EA=B0=80=EB=8A=A5=20?= =?UTF-8?q?=EC=97=AC=EB=B6=80=20=ED=99=95=EC=9D=B8=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 이전/다음 달로 이동이 가능한지 확인하기 위해 `hasNext`와 `hasPrevious` 속성을 추가 - 기존 `isInProgressStatsDetail` 로직을 `selectedDate` null 체크에서 초기 로딩 상태를 나타내는 boolean 값으로 변경 --- .../stats/detail/contract/StatsDetailUiState.kt | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/feature/stats/detail/src/main/java/com/yapp/stats/detail/contract/StatsDetailUiState.kt b/feature/stats/detail/src/main/java/com/yapp/stats/detail/contract/StatsDetailUiState.kt index 11a5792b..226ec680 100644 --- a/feature/stats/detail/src/main/java/com/yapp/stats/detail/contract/StatsDetailUiState.kt +++ b/feature/stats/detail/src/main/java/com/yapp/stats/detail/contract/StatsDetailUiState.kt @@ -5,13 +5,28 @@ import com.twix.designsystem.components.stats.model.StatsCalendarUiModel import com.twix.domain.model.stats.detail.StatsDetail import com.twix.ui.base.State import java.time.LocalDate +import java.time.YearMonth @Immutable data class StatsDetailUiState( val goalId: Long = -1, val selectedDate: LocalDate? = null, val detail: StatsDetail = StatsDetail.EMPTY, + val isInProgressStatsDetail: Boolean = true, val calendarUiModel: StatsCalendarUiModel = StatsCalendarUiModel(), ) : State { - val isInProgressStatsDetail get() = selectedDate != null + val hasNext: Boolean + get() { + val limitYm = YearMonth.from(detail.statsSummary.endDate ?: LocalDate.now()) + val nextYm = YearMonth.from(selectedDate).plusMonths(1) + + return nextYm <= limitYm + } + + val hasPrevious: Boolean + get() { + val limitYm = YearMonth.from(detail.statsSummary.startDate) + val previousYm = YearMonth.from(selectedDate).minusMonths(1) + return previousYm >= limitYm + } } From 9d0f4d3fa049a61fe63af4eb45fe1e4c91bd865a Mon Sep 17 00:00:00 2001 From: chanho0908 Date: Tue, 24 Feb 2026 14:14:02 +0900 Subject: [PATCH 02/17] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20?= =?UTF-8?q?=EB=B0=98=EB=B3=B5=20=EC=A3=BC=EA=B8=B0(repeat=20type)=20?= =?UTF-8?q?=EB=AC=B8=EC=9E=90=EC=97=B4=20=EB=A6=AC=EC=86=8C=EC=8A=A4=20?= =?UTF-8?q?=EA=B3=B5=EC=9A=A9=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/design-system/src/main/res/values/strings.xml | 2 +- .../main/java/com/twix/goal_editor/component/GoalInfoCard.kt | 2 +- .../src/main/java/com/twix/goal_manage/GoalManageScreen.kt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/core/design-system/src/main/res/values/strings.xml b/core/design-system/src/main/res/values/strings.xml index 0f8851c9..17b7270e 100644 --- a/core/design-system/src/main/res/values/strings.xml +++ b/core/design-system/src/main/res/values/strings.xml @@ -11,6 +11,7 @@ 편집 시작일 종료일 + 반복 주기 미설정 취소 삭제 @@ -52,7 +53,6 @@ 목표 직접 만들기 목표 수정하기 목표를 입력해 보세요. - 반복 주기 시작 날짜 종료 날짜 종료 날짜 설정 diff --git a/feature/goal-editor/src/main/java/com/twix/goal_editor/component/GoalInfoCard.kt b/feature/goal-editor/src/main/java/com/twix/goal_editor/component/GoalInfoCard.kt index 80070668..388e8c49 100644 --- a/feature/goal-editor/src/main/java/com/twix/goal_editor/component/GoalInfoCard.kt +++ b/feature/goal-editor/src/main/java/com/twix/goal_editor/component/GoalInfoCard.kt @@ -113,7 +113,7 @@ private fun RepeatTypeSettings( Modifier .padding(16.dp), ) { - HeaderText(stringResource(R.string.header_repeat_type)) + HeaderText(stringResource(R.string.word_repeat_type)) Spacer(Modifier.height(12.dp)) diff --git a/feature/goal-manage/src/main/java/com/twix/goal_manage/GoalManageScreen.kt b/feature/goal-manage/src/main/java/com/twix/goal_manage/GoalManageScreen.kt index 4ff3c63c..3f124cc6 100644 --- a/feature/goal-manage/src/main/java/com/twix/goal_manage/GoalManageScreen.kt +++ b/feature/goal-manage/src/main/java/com/twix/goal_manage/GoalManageScreen.kt @@ -357,7 +357,7 @@ private fun GoalSummaryItem( ?: stringResource(R.string.word_not_set) GoalSummaryInfo( - label = stringResource(R.string.header_repeat_type), + label = stringResource(R.string.word_repeat_type), value = item.repeatCycle.label(), ) GoalSummaryInfo( From 7a836735625de608c4156e1590e807e30907722d Mon Sep 17 00:00:00 2001 From: chanho0908 Date: Tue, 24 Feb 2026 14:23:53 +0900 Subject: [PATCH 03/17] =?UTF-8?q?=E2=9C=A8=20Feat:=20=ED=86=B5=EA=B3=84=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=EC=9A=94=EC=95=BD=20=EC=A0=95=EB=B3=B4=20?= =?UTF-8?q?=EC=BB=B4=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 --- .../src/main/res/values/strings.xml | 4 + .../stats/detail/component/SummaryContent.kt | 177 ++++++++++++++++++ 2 files changed, 181 insertions(+) create mode 100644 feature/stats/detail/src/main/java/com/yapp/stats/detail/component/SummaryContent.kt diff --git a/core/design-system/src/main/res/values/strings.xml b/core/design-system/src/main/res/values/strings.xml index 17b7270e..0b116e30 100644 --- a/core/design-system/src/main/res/values/strings.xml +++ b/core/design-system/src/main/res/values/strings.xml @@ -12,6 +12,7 @@ 시작일 종료일 반복 주기 + 달성 횟수 미설정 취소 삭제 @@ -79,6 +80,9 @@ 진행중 종료 + + %1$s %2$d/%3$d + 종료 날짜가 시작 날짜보다 이전입니다. 목표 조회에 실패했습니다. diff --git a/feature/stats/detail/src/main/java/com/yapp/stats/detail/component/SummaryContent.kt b/feature/stats/detail/src/main/java/com/yapp/stats/detail/component/SummaryContent.kt new file mode 100644 index 00000000..b8a2868a --- /dev/null +++ b/feature/stats/detail/src/main/java/com/yapp/stats/detail/component/SummaryContent.kt @@ -0,0 +1,177 @@ +package com.yapp.stats.detail.component + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.VerticalDivider +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.twix.designsystem.R +import com.twix.designsystem.components.text.AppText +import com.twix.designsystem.extension.label +import com.twix.designsystem.theme.GrayColor +import com.twix.designsystem.theme.TwixTheme +import com.twix.domain.model.enums.AppTextStyle +import com.twix.domain.model.enums.RepeatCycle +import com.twix.domain.model.stats.detail.StatsSummary +import java.time.LocalDate + +@Composable +fun SummaryContent( + statsSummary: StatsSummary, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier.padding(horizontal = 20.dp), + ) { + HorizontalDivider( + thickness = 1.dp, + color = GrayColor.C500, + ) + + Spacer(Modifier.height(20.dp)) + + Row(modifier = Modifier.padding(horizontal = 20.dp)) { + Column( + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + AppText( + text = stringResource(R.string.word_end_count), + style = AppTextStyle.C1, + color = GrayColor.C400, + ) + AppText( + text = stringResource(R.string.word_repeat_type), + style = AppTextStyle.C1, + color = GrayColor.C400, + ) + AppText( + text = stringResource(R.string.word_start_date), + style = AppTextStyle.C1, + color = GrayColor.C400, + ) + AppText( + text = stringResource(R.string.word_end_date), + style = AppTextStyle.C1, + color = GrayColor.C400, + ) + } + + Spacer(Modifier.width(28.dp)) + + Column( + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + Image( + painter = painterResource(R.drawable.ic_checked_you), + contentDescription = null, + modifier = Modifier.size(14.dp), + ) + + AppText( + text = + stringResource( + R.string.stats_complete_count, + statsSummary.myNickname, + statsSummary.myCompletedCount, + statsSummary.totalCount, + ), + style = AppTextStyle.B4, + color = GrayColor.C500, + ) + + VerticalDivider( + modifier = Modifier.height(15.dp), + thickness = 1.dp, + color = GrayColor.C200, + ) + + AppText( + text = + stringResource( + R.string.stats_complete_count, + statsSummary.partnerNickname, + statsSummary.partnerCompletedCount, + statsSummary.totalCount, + ), + style = AppTextStyle.B4, + color = GrayColor.C500, + ) + } + + AppText( + text = statsSummary.repeatCycle.label(), + style = AppTextStyle.B4, + color = GrayColor.C500, + ) + + AppText( + text = + stringResource( + R.string.date_year_month_day, + statsSummary.startDate.year, + statsSummary.startDate.monthValue, + statsSummary.startDate.dayOfMonth, + ), + style = AppTextStyle.B4, + color = GrayColor.C500, + ) + + AppText( + text = + statsSummary.endDate?.let { + stringResource( + R.string.date_year_month_day, + it.year, + it.monthValue, + it.dayOfMonth, + ) + } ?: stringResource(R.string.word_not_set), + style = AppTextStyle.B4, + color = GrayColor.C500, + ) + } + } + + Spacer(Modifier.height(20.dp)) + + HorizontalDivider( + thickness = 1.dp, + color = GrayColor.C500, + ) + + Spacer(Modifier.height(52.dp)) + } +} + +@Preview(showBackground = true) +@Composable +private fun SummaryContentPreview() { + TwixTheme { + SummaryContent( + statsSummary = + StatsSummary( + myNickname = "페토", + myCompletedCount = 10, + partnerNickname = "찬호", + partnerCompletedCount = 8, + totalCount = 20, + repeatCycle = RepeatCycle.DAILY, + startDate = LocalDate.now(), + endDate = LocalDate.now(), + ), + ) + } +} From 228d59ca3047ffc4e8e8e26d0c843c7f22d58572 Mon Sep 17 00:00:00 2001 From: chanho0908 Date: Tue, 24 Feb 2026 14:24:14 +0900 Subject: [PATCH 04/17] =?UTF-8?q?=E2=9C=A8=20Feat:=20=ED=86=B5=EA=B3=84=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=ED=99=94=EB=A9=B4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/twix/util/bus/StatsRefreshBus.kt | 16 ++ .../com/yapp/stats/detail/StatsDetailRoute.kt | 7 - .../yapp/stats/detail/StatsDetailScreen.kt | 255 ++++++++++++++++++ .../yapp/stats/detail/StatsDetailViewModel.kt | 126 +++++++++ .../detail/contract/StatsDetailIntent.kt | 13 + .../yapp/stats/detail/di/StatsDetailModule.kt | 3 + 6 files changed, 413 insertions(+), 7 deletions(-) create mode 100644 core/util/src/main/java/com/twix/util/bus/StatsRefreshBus.kt delete mode 100644 feature/stats/detail/src/main/java/com/yapp/stats/detail/StatsDetailRoute.kt create mode 100644 feature/stats/detail/src/main/java/com/yapp/stats/detail/StatsDetailScreen.kt create mode 100644 feature/stats/detail/src/main/java/com/yapp/stats/detail/StatsDetailViewModel.kt create mode 100644 feature/stats/detail/src/main/java/com/yapp/stats/detail/contract/StatsDetailIntent.kt diff --git a/core/util/src/main/java/com/twix/util/bus/StatsRefreshBus.kt b/core/util/src/main/java/com/twix/util/bus/StatsRefreshBus.kt new file mode 100644 index 00000000..71b1fc9a --- /dev/null +++ b/core/util/src/main/java/com/twix/util/bus/StatsRefreshBus.kt @@ -0,0 +1,16 @@ +package com.twix.util.bus + +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow + +class StatsRefreshBus { + private val _events = + MutableSharedFlow( + replay = 0, + extraBufferCapacity = 1, + ) + + val events: SharedFlow = _events + + fun notifyChanged() = _events.tryEmit(Unit) +} diff --git a/feature/stats/detail/src/main/java/com/yapp/stats/detail/StatsDetailRoute.kt b/feature/stats/detail/src/main/java/com/yapp/stats/detail/StatsDetailRoute.kt deleted file mode 100644 index 20c196cf..00000000 --- a/feature/stats/detail/src/main/java/com/yapp/stats/detail/StatsDetailRoute.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.yapp.stats.detail - -import androidx.compose.runtime.Composable - -@Composable -fun StatsDetailRoute(onBack: () -> Unit) { -} diff --git a/feature/stats/detail/src/main/java/com/yapp/stats/detail/StatsDetailScreen.kt b/feature/stats/detail/src/main/java/com/yapp/stats/detail/StatsDetailScreen.kt new file mode 100644 index 00000000..dd9658c2 --- /dev/null +++ b/feature/stats/detail/src/main/java/com/yapp/stats/detail/StatsDetailScreen.kt @@ -0,0 +1,255 @@ +package com.yapp.stats.detail + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.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.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.twix.designsystem.R +import com.twix.designsystem.components.calendar.CalendarNavigator +import com.twix.designsystem.components.dialog.CommonDialog +import com.twix.designsystem.components.stats.StatsCalendar +import com.twix.designsystem.components.text.AppText +import com.twix.designsystem.extension.toRes +import com.twix.designsystem.theme.CommonColor +import com.twix.designsystem.theme.GrayColor +import com.twix.designsystem.theme.TwixTheme +import com.twix.domain.model.enums.AppTextStyle +import com.twix.domain.model.enums.GoalIconType +import com.yapp.stats.detail.component.StatsDetailTopbar +import com.yapp.stats.detail.component.SummaryContent +import com.yapp.stats.detail.contract.StatsDetailIntent +import com.yapp.stats.detail.contract.StatsDetailUiState +import com.yapp.stats.detail.preview.StatsDetailUiStatePreviewProvider +import org.koin.compose.viewmodel.koinViewModel +import java.time.LocalDate + +@Composable +fun StatsDetailRoute( + onBack: () -> Unit, + viewModel: StatsDetailViewModel = koinViewModel(), +) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + + StatsDetailScreen( + uiSate = uiState, + onBack = onBack, + onSelectDate = {}, + onPreviousMonth = { viewModel.dispatch(StatsDetailIntent.PreviousMonth) }, + onNextMonth = { viewModel.dispatch(StatsDetailIntent.NextMonth) }, + onClickDeleteStats = { }, + onClickPopupEdit = { }, + onClickPopupEnd = { }, + ) +} + +@Composable +fun StatsDetailScreen( + uiSate: StatsDetailUiState, + onBack: () -> Unit, + onSelectDate: (LocalDate) -> Unit, + onPreviousMonth: () -> Unit, + onNextMonth: () -> Unit, + onClickDeleteStats: () -> Unit, + onClickPopupEdit: () -> Unit, + onClickPopupEnd: () -> Unit, +) { + val scrollState = rememberScrollState() + var popupMenuVisibility by remember { mutableStateOf(false) } + var statsDeleteDialogVisibility by remember { mutableStateOf(false) } + + Box { + Column( + modifier = + Modifier + .fillMaxSize() + .background(GrayColor.C050) + .verticalScroll(scrollState), + ) { + StatsDetailTopbar( + goalName = uiSate.detail.goalName, + isInProgressStatsDetail = uiSate.isInProgressStatsDetail, + popupMenuVisibility = popupMenuVisibility, + onBack = onBack, + onClickAction = { + if (uiSate.isInProgressStatsDetail) { + popupMenuVisibility = true + } else { + statsDeleteDialogVisibility = true + } + }, + onDismiss = { popupMenuVisibility = false }, + onClickPopupEdit = onClickPopupEdit, + onClickPopupEnd = onClickPopupEnd, + onClickPopupDelete = { + popupMenuVisibility = false + statsDeleteDialogVisibility = true + }, + ) + + Box(modifier = Modifier.fillMaxWidth()) { + Image( + imageVector = ImageVector.vectorResource(R.drawable.ic_hug), + contentDescription = null, + modifier = + Modifier + .align(Alignment.TopStart) + .padding(start = 20.dp, top = 30.dp), + ) + + Column( + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Spacer(Modifier.height(32.dp)) + + CalendarNavigator( + currentDate = uiSate.selectedDate ?: LocalDate.now(), + onPreviousMonth = onPreviousMonth, + onNextMonth = onNextMonth, + hasPrevious = uiSate.hasPrevious, + hasNext = uiSate.hasNext, + ) + + Box( + Modifier + .fillMaxWidth() + .background(CommonColor.White, shape = RoundedCornerShape(16.dp)) + .border( + color = GrayColor.C500, + width = 1.dp, + shape = RoundedCornerShape(16.dp), + ).padding(horizontal = 12.dp) + .padding(top = 24.dp, bottom = 32.dp), + ) { + StatsCalendar( + uiModel = uiSate.calendarUiModel, + onSelectedDate = onSelectDate, + ) + } + } + + Image( + imageVector = ImageVector.vectorResource(R.drawable.ic_plane), + contentDescription = null, + modifier = + Modifier + .align(Alignment.TopEnd) + .padding(end = 27.dp, top = 30.dp), + ) + } + + Spacer(Modifier.height(44.dp)) + + SummaryContent(uiSate.detail.statsSummary) + } + + CommonDialog( + visible = statsDeleteDialogVisibility, + confirmText = stringResource(R.string.word_delete), + dismissText = stringResource(R.string.word_cancel), + onDismissRequest = { statsDeleteDialogVisibility = false }, + onConfirm = onClickDeleteStats, + onDismiss = { statsDeleteDialogVisibility = false }, + content = { + StatsDeleteDialogContent( + title = + stringResource( + R.string.dialog_delete_goal_title, + uiSate.detail.goalName, + ), + content = stringResource(R.string.dialog_delete_goal_content), + icon = uiSate.detail.goalIcon, + ) + }, + ) + } +} + +@Composable +private fun StatsDeleteDialogContent( + title: String, + content: String, + icon: GoalIconType, +) { + Column( + modifier = + Modifier + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Image( + painter = painterResource(icon.toRes()), + contentDescription = "emoji", + modifier = + Modifier + .size(60.dp), + ) + + Spacer(Modifier.height(12.dp)) + + AppText( + text = title, + style = AppTextStyle.T1, + color = GrayColor.C500, + textAlign = TextAlign.Center, + ) + + Spacer(Modifier.height(8.dp)) + + AppText( + text = content, + style = AppTextStyle.B2, + color = GrayColor.C500, + textAlign = TextAlign.Center, + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun StatsDetailScreenPreview( + @PreviewParameter(StatsDetailUiStatePreviewProvider::class) + uiState: StatsDetailUiState, +) { + TwixTheme { + StatsDetailScreen( + uiSate = uiState, + onBack = {}, + onSelectDate = {}, + onPreviousMonth = {}, + onNextMonth = {}, + onClickDeleteStats = {}, + onClickPopupEdit = {}, + onClickPopupEnd = {}, + ) + } +} diff --git a/feature/stats/detail/src/main/java/com/yapp/stats/detail/StatsDetailViewModel.kt b/feature/stats/detail/src/main/java/com/yapp/stats/detail/StatsDetailViewModel.kt new file mode 100644 index 00000000..2772215e --- /dev/null +++ b/feature/stats/detail/src/main/java/com/yapp/stats/detail/StatsDetailViewModel.kt @@ -0,0 +1,126 @@ +package com.yapp.stats.detail + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.viewModelScope +import com.twix.designsystem.R +import com.twix.designsystem.components.stats.model.StatsCalendarUiModel +import com.twix.designsystem.components.toast.model.ToastType +import com.twix.domain.repository.GoalRepository +import com.twix.domain.repository.StatsRepository +import com.twix.navigation.NavRoutes +import com.twix.ui.base.BaseViewModel +import com.twix.util.bus.StatsRefreshBus +import com.yapp.stats.detail.contract.StatsDetailIntent +import com.yapp.stats.detail.contract.StatsDetailSideEffect +import com.yapp.stats.detail.contract.StatsDetailUiState +import kotlinx.coroutines.launch +import java.time.LocalDate + +class StatsDetailViewModel( + private val statsRefreshBus: StatsRefreshBus, + private val goalRepository: GoalRepository, + private val statsRepository: StatsRepository, + savedSateHandle: SavedStateHandle, +) : BaseViewModel( + StatsDetailUiState(), + ) { + private val argGoalId: Long = + requireNotNull(savedSateHandle[NavRoutes.StatsDetailRoute.ARG_GOAL_ID]) { GOAL_ID_NOT_FOUND } + + private val argDate: String? = savedSateHandle.get(NavRoutes.StatsDetailRoute.ARG_DATE) + + init { + reduceNavArguments() + fetchStatsDetail(argDate?.let { LocalDate.parse(it) }) + } + + private fun reduceNavArguments() { + reduce { + copy( + goalId = argGoalId, + isInProgressStatsDetail = argDate != null, + ) + } + } + + private fun fetchStatsDetail(date: LocalDate?) { + launchResult( + block = { statsRepository.fetchStatsDetail(currentState.goalId, date) }, + onSuccess = { + reduce { + copy( + detail = it, + selectedDate = it.monthDate, + calendarUiModel = + StatsCalendarUiModel.create( + currentDate = it.monthDate, + completedDate = it.completedDate, + ), + ) + } + }, + onError = { showToast(R.string.toast_fetch_stats_failed, ToastType.ERROR) }, + ) + } + + override suspend fun handleIntent(intent: StatsDetailIntent) { + when (intent) { + StatsDetailIntent.PreviousMonth -> fetchPreviousMonth() + StatsDetailIntent.NextMonth -> fetchNextMonth() + StatsDetailIntent.GoalEnd -> { + // endGoal() + } + + StatsDetailIntent.GoalDelete -> { + // deleteGoal() + } + } + } + + private fun fetchPreviousMonth() { + val previousMonth = currentState.selectedDate?.minusMonths(1) ?: return + reduce { copy(selectedDate = previousMonth) } + fetchStatsDetail(previousMonth) + } + + private fun fetchNextMonth() { + val nextMonth = currentState.selectedDate?.plusMonths(1) ?: return + reduce { copy(selectedDate = nextMonth) } + fetchStatsDetail(nextMonth) + } + + private fun endGoal() { + launchResult( + block = { goalRepository.completeGoal(argGoalId) }, + onSuccess = { + statsRefreshBus.notifyChanged() + tryEmitSideEffect(StatsDetailSideEffect.NavigateToBack) + }, + onError = { showToast(R.string.toast_complete_goal_failed, ToastType.ERROR) }, + ) + } + + private fun deleteGoal() { + launchResult( + block = { goalRepository.completeGoal(argGoalId) }, + onSuccess = { + statsRefreshBus.notifyChanged() + tryEmitSideEffect(StatsDetailSideEffect.NavigateToBack) + }, + onError = { showToast(R.string.toast_delete_goal_failed, ToastType.ERROR) }, + ) + } + + private fun showToast( + message: Int, + type: ToastType, + ) { + viewModelScope.launch { + emitSideEffect(StatsDetailSideEffect.ShowToast(message, type)) + } + } + + companion object { + private const val GOAL_ID_NOT_FOUND = "Goal Id Argument Not Found" + } +} diff --git a/feature/stats/detail/src/main/java/com/yapp/stats/detail/contract/StatsDetailIntent.kt b/feature/stats/detail/src/main/java/com/yapp/stats/detail/contract/StatsDetailIntent.kt new file mode 100644 index 00000000..212be41b --- /dev/null +++ b/feature/stats/detail/src/main/java/com/yapp/stats/detail/contract/StatsDetailIntent.kt @@ -0,0 +1,13 @@ +package com.yapp.stats.detail.contract + +import com.twix.ui.base.Intent + +sealed interface StatsDetailIntent : Intent { + data object PreviousMonth : StatsDetailIntent + + data object NextMonth : StatsDetailIntent + + data object GoalEnd : StatsDetailIntent + + data object GoalDelete : StatsDetailIntent +} diff --git a/feature/stats/detail/src/main/java/com/yapp/stats/detail/di/StatsDetailModule.kt b/feature/stats/detail/src/main/java/com/yapp/stats/detail/di/StatsDetailModule.kt index d2cd76e6..95dd8dce 100644 --- a/feature/stats/detail/src/main/java/com/yapp/stats/detail/di/StatsDetailModule.kt +++ b/feature/stats/detail/src/main/java/com/yapp/stats/detail/di/StatsDetailModule.kt @@ -2,11 +2,14 @@ package com.yapp.stats.detail.di import com.twix.navigation.NavRoutes import com.twix.navigation.base.NavGraphContributor +import com.yapp.stats.detail.StatsDetailViewModel import com.yapp.stats.detail.navigation.StatsDetailGraph +import org.koin.core.module.dsl.viewModelOf import org.koin.core.qualifier.named import org.koin.dsl.module val statsDetailModule = module { single(named(NavRoutes.StatsDetailRoute.route)) { StatsDetailGraph } + viewModelOf(::StatsDetailViewModel) } From e5f24f543e2fc99ba343c59bdb2273c9f8883b03 Mon Sep 17 00:00:00 2001 From: chanho0908 Date: Tue, 24 Feb 2026 14:24:24 +0900 Subject: [PATCH 05/17] =?UTF-8?q?=E2=9C=A8=20Feat:=20StatsRefreshBus=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/util/src/main/java/com/twix/util/di/UtilModule.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/util/src/main/java/com/twix/util/di/UtilModule.kt b/core/util/src/main/java/com/twix/util/di/UtilModule.kt index 56a5a0f1..92b3dae9 100644 --- a/core/util/src/main/java/com/twix/util/di/UtilModule.kt +++ b/core/util/src/main/java/com/twix/util/di/UtilModule.kt @@ -1,6 +1,7 @@ package com.twix.util.di import com.twix.util.bus.GoalRefreshBus +import com.twix.util.bus.StatsRefreshBus import com.twix.util.bus.TaskCertificationRefreshBus import org.koin.dsl.module @@ -8,4 +9,5 @@ val utilModule = module { single { GoalRefreshBus() } single { TaskCertificationRefreshBus() } + single { StatsRefreshBus() } } From f538cd5af2833559dbffd541744c4ab227a4491f Mon Sep 17 00:00:00 2001 From: chanho0908 Date: Tue, 24 Feb 2026 14:28:45 +0900 Subject: [PATCH 06/17] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20?= =?UTF-8?q?=ED=86=B5=EA=B3=84=20=ED=99=94=EB=A9=B4=20=EB=AA=A9=EB=A1=9D=20?= =?UTF-8?q?=EC=9D=B4=EB=B2=A4=ED=8A=B8=20=EB=B2=84=EC=8A=A4=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/twix/util/bus/StatsRefreshBus.kt | 11 ++++++++--- .../main/java/com/twix/stats/StatsViewModel.kt | 16 ++++++++++++++++ 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/core/util/src/main/java/com/twix/util/bus/StatsRefreshBus.kt b/core/util/src/main/java/com/twix/util/bus/StatsRefreshBus.kt index 71b1fc9a..7881c3b3 100644 --- a/core/util/src/main/java/com/twix/util/bus/StatsRefreshBus.kt +++ b/core/util/src/main/java/com/twix/util/bus/StatsRefreshBus.kt @@ -4,13 +4,18 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow class StatsRefreshBus { + enum class Publisher { + InProgress, + End, + } + private val _events = - MutableSharedFlow( + MutableSharedFlow( replay = 0, extraBufferCapacity = 1, ) - val events: SharedFlow = _events + val events: SharedFlow = _events - fun notifyChanged() = _events.tryEmit(Unit) + fun notifyChanged(publisher: Publisher) = _events.tryEmit(publisher) } diff --git a/feature/main/src/main/java/com/twix/stats/StatsViewModel.kt b/feature/main/src/main/java/com/twix/stats/StatsViewModel.kt index 59951647..83b099d6 100644 --- a/feature/main/src/main/java/com/twix/stats/StatsViewModel.kt +++ b/feature/main/src/main/java/com/twix/stats/StatsViewModel.kt @@ -1,5 +1,6 @@ package com.twix.stats +import androidx.lifecycle.viewModelScope import com.twix.designsystem.R import com.twix.designsystem.components.toast.model.ToastType import com.twix.domain.repository.StatsRepository @@ -7,17 +8,21 @@ import com.twix.stats.contract.StatsIntent import com.twix.stats.contract.StatsSideEffect import com.twix.stats.contract.StatsUiState import com.twix.ui.base.BaseViewModel +import com.twix.util.bus.StatsRefreshBus import kotlinx.coroutines.Job +import kotlinx.coroutines.launch import java.time.LocalDate class StatsViewModel( private val statsRepository: StatsRepository, + private val eventBus: StatsRefreshBus, ) : BaseViewModel(StatsUiState()) { private var inProgressStatsJob: Job? = null init { fetchInProgressStats(LocalDate.now()) fetchEndStats() + collectEventBus() } override suspend fun handleIntent(intent: StatsIntent) { @@ -57,6 +62,17 @@ class StatsViewModel( ) } + private fun collectEventBus() { + viewModelScope.launch { + eventBus.events.collect { publisher -> + when (publisher) { + StatsRefreshBus.Publisher.InProgress -> fetchInProgressStats(currentState.currentDate) + StatsRefreshBus.Publisher.End -> fetchEndStats() + } + } + } + } + private suspend fun showToast( message: Int, type: ToastType, From 5fb14c3128c8baba95f537a89297c32b80af39a8 Mon Sep 17 00:00:00 2001 From: chanho0908 Date: Tue, 24 Feb 2026 14:47:57 +0900 Subject: [PATCH 07/17] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20?= =?UTF-8?q?=ED=86=B5=EA=B3=84=20=EC=83=81=EC=84=B8=20=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=ED=98=B8=EC=B6=9C=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=95=88=EC=A0=95=EC=84=B1=20=EA=B0=95=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `fetchStatsDetail` 호출 시 이전 Job을 취소하여 중복 호출 방지 - 목표 완료/삭제 시 `StatsRefreshBus`에 `Publisher` 정보를 전달하여 분기 처리 추가 --- .../yapp/stats/detail/StatsDetailViewModel.kt | 48 +++++++++++-------- 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/feature/stats/detail/src/main/java/com/yapp/stats/detail/StatsDetailViewModel.kt b/feature/stats/detail/src/main/java/com/yapp/stats/detail/StatsDetailViewModel.kt index 2772215e..085452a2 100644 --- a/feature/stats/detail/src/main/java/com/yapp/stats/detail/StatsDetailViewModel.kt +++ b/feature/stats/detail/src/main/java/com/yapp/stats/detail/StatsDetailViewModel.kt @@ -13,6 +13,7 @@ import com.twix.util.bus.StatsRefreshBus import com.yapp.stats.detail.contract.StatsDetailIntent import com.yapp.stats.detail.contract.StatsDetailSideEffect import com.yapp.stats.detail.contract.StatsDetailUiState +import kotlinx.coroutines.Job import kotlinx.coroutines.launch import java.time.LocalDate @@ -29,6 +30,8 @@ class StatsDetailViewModel( private val argDate: String? = savedSateHandle.get(NavRoutes.StatsDetailRoute.ARG_DATE) + private var fetchStatsDetailJob: Job? = null + init { reduceNavArguments() fetchStatsDetail(argDate?.let { LocalDate.parse(it) }) @@ -44,23 +47,25 @@ class StatsDetailViewModel( } private fun fetchStatsDetail(date: LocalDate?) { - launchResult( - block = { statsRepository.fetchStatsDetail(currentState.goalId, date) }, - onSuccess = { - reduce { - copy( - detail = it, - selectedDate = it.monthDate, - calendarUiModel = - StatsCalendarUiModel.create( - currentDate = it.monthDate, - completedDate = it.completedDate, - ), - ) - } - }, - onError = { showToast(R.string.toast_fetch_stats_failed, ToastType.ERROR) }, - ) + fetchStatsDetailJob?.cancel() + fetchStatsDetailJob = + launchResult( + block = { statsRepository.fetchStatsDetail(currentState.goalId, date) }, + onSuccess = { + reduce { + copy( + detail = it, + selectedDate = it.monthDate, + calendarUiModel = + StatsCalendarUiModel.create( + currentDate = it.monthDate, + completedDate = it.completedDate, + ), + ) + } + }, + onError = { showToast(R.string.toast_fetch_stats_failed, ToastType.ERROR) }, + ) } override suspend fun handleIntent(intent: StatsDetailIntent) { @@ -93,7 +98,7 @@ class StatsDetailViewModel( launchResult( block = { goalRepository.completeGoal(argGoalId) }, onSuccess = { - statsRefreshBus.notifyChanged() + statsRefreshBus.notifyChanged(StatsRefreshBus.Publisher.InProgress) tryEmitSideEffect(StatsDetailSideEffect.NavigateToBack) }, onError = { showToast(R.string.toast_complete_goal_failed, ToastType.ERROR) }, @@ -104,7 +109,12 @@ class StatsDetailViewModel( launchResult( block = { goalRepository.completeGoal(argGoalId) }, onSuccess = { - statsRefreshBus.notifyChanged() + val publisher = + when (currentState.isInProgressStatsDetail) { + true -> StatsRefreshBus.Publisher.InProgress + else -> StatsRefreshBus.Publisher.End + } + statsRefreshBus.notifyChanged(publisher) tryEmitSideEffect(StatsDetailSideEffect.NavigateToBack) }, onError = { showToast(R.string.toast_delete_goal_failed, ToastType.ERROR) }, From 26399515af8463dfe99ee3f27cba56d816b2a3ec Mon Sep 17 00:00:00 2001 From: chanho0908 Date: Tue, 24 Feb 2026 16:06:30 +0900 Subject: [PATCH 08/17] =?UTF-8?q?=E2=9C=A8=20Feat:=20=ED=86=B5=EA=B3=84=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=ED=99=94=EB=A9=B4=20`StatsDetailSideEffec?= =?UTF-8?q?t`=20=EA=B5=AC=EB=8F=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../yapp/stats/detail/StatsDetailScreen.kt | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/feature/stats/detail/src/main/java/com/yapp/stats/detail/StatsDetailScreen.kt b/feature/stats/detail/src/main/java/com/yapp/stats/detail/StatsDetailScreen.kt index dd9658c2..194cae01 100644 --- a/feature/stats/detail/src/main/java/com/yapp/stats/detail/StatsDetailScreen.kt +++ b/feature/stats/detail/src/main/java/com/yapp/stats/detail/StatsDetailScreen.kt @@ -18,10 +18,12 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.vectorResource @@ -35,27 +37,50 @@ import com.twix.designsystem.components.calendar.CalendarNavigator import com.twix.designsystem.components.dialog.CommonDialog import com.twix.designsystem.components.stats.StatsCalendar import com.twix.designsystem.components.text.AppText +import com.twix.designsystem.components.toast.ToastManager +import com.twix.designsystem.components.toast.model.ToastData import com.twix.designsystem.extension.toRes import com.twix.designsystem.theme.CommonColor import com.twix.designsystem.theme.GrayColor import com.twix.designsystem.theme.TwixTheme import com.twix.domain.model.enums.AppTextStyle import com.twix.domain.model.enums.GoalIconType +import com.twix.ui.base.ObserveAsEvents import com.yapp.stats.detail.component.StatsDetailTopbar import com.yapp.stats.detail.component.SummaryContent import com.yapp.stats.detail.contract.StatsDetailIntent +import com.yapp.stats.detail.contract.StatsDetailSideEffect import com.yapp.stats.detail.contract.StatsDetailUiState import com.yapp.stats.detail.preview.StatsDetailUiStatePreviewProvider +import org.koin.compose.koinInject import org.koin.compose.viewmodel.koinViewModel import java.time.LocalDate @Composable fun StatsDetailRoute( onBack: () -> Unit, + toastManager: ToastManager = koinInject(), viewModel: StatsDetailViewModel = koinViewModel(), ) { + val context = LocalContext.current + val currentContext by rememberUpdatedState(context) + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + ObserveAsEvents(viewModel.sideEffect) { sideEffect -> + when (sideEffect) { + StatsDetailSideEffect.NavigateToBack -> onBack() + is StatsDetailSideEffect.ShowToast -> { + toastManager.tryShow( + ToastData( + message = currentContext.getString(sideEffect.message), + type = sideEffect.type, + ), + ) + } + } + } + StatsDetailScreen( uiSate = uiState, onBack = onBack, From a14cac77af4ae429ab2386a092269469cdf45e15 Mon Sep 17 00:00:00 2001 From: chanho0908 Date: Tue, 24 Feb 2026 16:10:08 +0900 Subject: [PATCH 09/17] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Fix:=20=ED=86=B5?= =?UTF-8?q?=EA=B3=84=20=EC=83=81=EC=84=B8=20=ED=99=94=EB=A9=B4=20=EA=B3=A8?= =?UTF-8?q?=20=EC=82=AD=EC=A0=9C=20API=20=ED=95=A8=EC=88=98=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/com/yapp/stats/detail/StatsDetailViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/feature/stats/detail/src/main/java/com/yapp/stats/detail/StatsDetailViewModel.kt b/feature/stats/detail/src/main/java/com/yapp/stats/detail/StatsDetailViewModel.kt index 085452a2..16459501 100644 --- a/feature/stats/detail/src/main/java/com/yapp/stats/detail/StatsDetailViewModel.kt +++ b/feature/stats/detail/src/main/java/com/yapp/stats/detail/StatsDetailViewModel.kt @@ -107,7 +107,7 @@ class StatsDetailViewModel( private fun deleteGoal() { launchResult( - block = { goalRepository.completeGoal(argGoalId) }, + block = { goalRepository.deleteGoal(argGoalId) }, onSuccess = { val publisher = when (currentState.isInProgressStatsDetail) { From 6f4c7355517e890acf474f76c3804c77d51a3e59 Mon Sep 17 00:00:00 2001 From: chanho0908 Date: Tue, 24 Feb 2026 16:56:12 +0900 Subject: [PATCH 10/17] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20?= =?UTF-8?q?=EC=9B=94=EB=B3=84=20=EC=9D=B4=EB=8F=99=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../yapp/stats/detail/StatsDetailScreen.kt | 2 +- .../yapp/stats/detail/StatsDetailViewModel.kt | 99 ++++++++++++++----- .../detail/contract/StatsDetailUiState.kt | 5 +- .../StatsDetailUiStatePreviewProvider.kt | 2 - .../TaskCertificationDetailViewModel.kt | 4 +- 5 files changed, 77 insertions(+), 35 deletions(-) diff --git a/feature/stats/detail/src/main/java/com/yapp/stats/detail/StatsDetailScreen.kt b/feature/stats/detail/src/main/java/com/yapp/stats/detail/StatsDetailScreen.kt index 194cae01..2ae0dba0 100644 --- a/feature/stats/detail/src/main/java/com/yapp/stats/detail/StatsDetailScreen.kt +++ b/feature/stats/detail/src/main/java/com/yapp/stats/detail/StatsDetailScreen.kt @@ -157,7 +157,7 @@ fun StatsDetailScreen( Spacer(Modifier.height(32.dp)) CalendarNavigator( - currentDate = uiSate.selectedDate ?: LocalDate.now(), + currentDate = uiSate.detail.monthDate, onPreviousMonth = onPreviousMonth, onNextMonth = onNextMonth, hasPrevious = uiSate.hasPrevious, diff --git a/feature/stats/detail/src/main/java/com/yapp/stats/detail/StatsDetailViewModel.kt b/feature/stats/detail/src/main/java/com/yapp/stats/detail/StatsDetailViewModel.kt index 16459501..81b0d7b4 100644 --- a/feature/stats/detail/src/main/java/com/yapp/stats/detail/StatsDetailViewModel.kt +++ b/feature/stats/detail/src/main/java/com/yapp/stats/detail/StatsDetailViewModel.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.viewModelScope import com.twix.designsystem.R import com.twix.designsystem.components.stats.model.StatsCalendarUiModel import com.twix.designsystem.components.toast.model.ToastType +import com.twix.domain.model.stats.detail.StatsDetail import com.twix.domain.repository.GoalRepository import com.twix.domain.repository.StatsRepository import com.twix.navigation.NavRoutes @@ -13,10 +14,16 @@ import com.twix.util.bus.StatsRefreshBus import com.yapp.stats.detail.contract.StatsDetailIntent import com.yapp.stats.detail.contract.StatsDetailSideEffect import com.yapp.stats.detail.contract.StatsDetailUiState -import kotlinx.coroutines.Job +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch import java.time.LocalDate +import kotlin.collections.get +@OptIn(FlowPreview::class) class StatsDetailViewModel( private val statsRefreshBus: StatsRefreshBus, private val goalRepository: GoalRepository, @@ -30,7 +37,24 @@ class StatsDetailViewModel( private val argDate: String? = savedSateHandle.get(NavRoutes.StatsDetailRoute.ARG_DATE) - private var fetchStatsDetailJob: Job? = null + private val cache = mutableMapOf() + + private val monthChangeFlow = + MutableSharedFlow( + extraBufferCapacity = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST, + ) + + init { + viewModelScope.launch { + monthChangeFlow + .distinctUntilChanged() + .debounce(DEBOUNCE_INTERVAL) + .collect { date -> + fetchStatsDetail(date) + } + } + } init { reduceNavArguments() @@ -47,25 +71,45 @@ class StatsDetailViewModel( } private fun fetchStatsDetail(date: LocalDate?) { - fetchStatsDetailJob?.cancel() - fetchStatsDetailJob = - launchResult( - block = { statsRepository.fetchStatsDetail(currentState.goalId, date) }, - onSuccess = { - reduce { - copy( - detail = it, - selectedDate = it.monthDate, - calendarUiModel = - StatsCalendarUiModel.create( - currentDate = it.monthDate, - completedDate = it.completedDate, - ), - ) - } - }, - onError = { showToast(R.string.toast_fetch_stats_failed, ToastType.ERROR) }, - ) + val result = date?.let { checkCache(date) } + if (result == true) return + launchResult( + block = { statsRepository.fetchStatsDetail(currentState.goalId, date) }, + onSuccess = { + cache[it.monthDate] = it + reduce { + copy( + detail = it, + calendarUiModel = + StatsCalendarUiModel.create( + currentDate = it.monthDate, + completedDate = it.completedDate, + ), + ) + } + }, + onError = { + reduce { copy(detail = detail.copy(monthDate = date ?: LocalDate.now())) } + showToast(R.string.toast_fetch_stats_failed, ToastType.ERROR) + }, + ) + } + + private fun checkCache(date: LocalDate): Boolean { + cache[date]?.let { + reduce { + copy( + detail = it, + calendarUiModel = + StatsCalendarUiModel.create( + currentDate = it.monthDate, + completedDate = it.completedDate, + ), + ) + } + return true + } + return false } override suspend fun handleIntent(intent: StatsDetailIntent) { @@ -83,15 +127,15 @@ class StatsDetailViewModel( } private fun fetchPreviousMonth() { - val previousMonth = currentState.selectedDate?.minusMonths(1) ?: return - reduce { copy(selectedDate = previousMonth) } - fetchStatsDetail(previousMonth) + val previousMonth = currentState.detail.monthDate.minusMonths(1) + reduce { copy(detail = detail.copy(monthDate = previousMonth)) } + monthChangeFlow.tryEmit(previousMonth) } private fun fetchNextMonth() { - val nextMonth = currentState.selectedDate?.plusMonths(1) ?: return - reduce { copy(selectedDate = nextMonth) } - fetchStatsDetail(nextMonth) + val nextMonth = currentState.detail.monthDate.plusMonths(1) + reduce { copy(detail = detail.copy(monthDate = nextMonth)) } + monthChangeFlow.tryEmit(nextMonth) } private fun endGoal() { @@ -132,5 +176,6 @@ class StatsDetailViewModel( companion object { private const val GOAL_ID_NOT_FOUND = "Goal Id Argument Not Found" + private const val DEBOUNCE_INTERVAL = 600L } } diff --git a/feature/stats/detail/src/main/java/com/yapp/stats/detail/contract/StatsDetailUiState.kt b/feature/stats/detail/src/main/java/com/yapp/stats/detail/contract/StatsDetailUiState.kt index 226ec680..0fb4a5b8 100644 --- a/feature/stats/detail/src/main/java/com/yapp/stats/detail/contract/StatsDetailUiState.kt +++ b/feature/stats/detail/src/main/java/com/yapp/stats/detail/contract/StatsDetailUiState.kt @@ -10,7 +10,6 @@ import java.time.YearMonth @Immutable data class StatsDetailUiState( val goalId: Long = -1, - val selectedDate: LocalDate? = null, val detail: StatsDetail = StatsDetail.EMPTY, val isInProgressStatsDetail: Boolean = true, val calendarUiModel: StatsCalendarUiModel = StatsCalendarUiModel(), @@ -18,7 +17,7 @@ data class StatsDetailUiState( val hasNext: Boolean get() { val limitYm = YearMonth.from(detail.statsSummary.endDate ?: LocalDate.now()) - val nextYm = YearMonth.from(selectedDate).plusMonths(1) + val nextYm = YearMonth.from(detail.monthDate).plusMonths(1) return nextYm <= limitYm } @@ -26,7 +25,7 @@ data class StatsDetailUiState( val hasPrevious: Boolean get() { val limitYm = YearMonth.from(detail.statsSummary.startDate) - val previousYm = YearMonth.from(selectedDate).minusMonths(1) + val previousYm = YearMonth.from(detail.monthDate).minusMonths(1) return previousYm >= limitYm } } diff --git a/feature/stats/detail/src/main/java/com/yapp/stats/detail/preview/StatsDetailUiStatePreviewProvider.kt b/feature/stats/detail/src/main/java/com/yapp/stats/detail/preview/StatsDetailUiStatePreviewProvider.kt index 223c0ccc..10d57af5 100644 --- a/feature/stats/detail/src/main/java/com/yapp/stats/detail/preview/StatsDetailUiStatePreviewProvider.kt +++ b/feature/stats/detail/src/main/java/com/yapp/stats/detail/preview/StatsDetailUiStatePreviewProvider.kt @@ -61,13 +61,11 @@ class StatsDetailUiStatePreviewProvider : PreviewParameterProvider Date: Wed, 25 Feb 2026 16:10:05 +0900 Subject: [PATCH 11/17] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20`uiSate`?= =?UTF-8?q?=20=EB=B3=80=EC=88=98=EB=AA=85=EC=9D=84=20`uiState`=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../twix/stats/detail/StatsDetailScreen.kt | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/feature/stats/detail/src/main/java/com/twix/stats/detail/StatsDetailScreen.kt b/feature/stats/detail/src/main/java/com/twix/stats/detail/StatsDetailScreen.kt index 2f90c596..f37bd5c1 100644 --- a/feature/stats/detail/src/main/java/com/twix/stats/detail/StatsDetailScreen.kt +++ b/feature/stats/detail/src/main/java/com/twix/stats/detail/StatsDetailScreen.kt @@ -82,7 +82,7 @@ fun StatsDetailRoute( } StatsDetailScreen( - uiSate = uiState, + uiState = uiState, onBack = onBack, onSelectDate = {}, onPreviousMonth = { viewModel.dispatch(StatsDetailIntent.PreviousMonth) }, @@ -95,7 +95,7 @@ fun StatsDetailRoute( @Composable fun StatsDetailScreen( - uiSate: StatsDetailUiState, + uiState: StatsDetailUiState, onBack: () -> Unit, onSelectDate: (LocalDate) -> Unit, onPreviousMonth: () -> Unit, @@ -117,12 +117,12 @@ fun StatsDetailScreen( .verticalScroll(scrollState), ) { StatsDetailTopbar( - goalName = uiSate.detail.goalName, - isInProgressStatsDetail = uiSate.isInProgressStatsDetail, + goalName = uiState.detail.goalName, + isInProgressStatsDetail = uiState.isInProgressStatsDetail, popupMenuVisibility = popupMenuVisibility, onBack = onBack, onClickAction = { - if (uiSate.isInProgressStatsDetail) { + if (uiState.isInProgressStatsDetail) { popupMenuVisibility = true } else { statsDeleteDialogVisibility = true @@ -157,11 +157,11 @@ fun StatsDetailScreen( Spacer(Modifier.height(32.dp)) CalendarNavigator( - currentDate = uiSate.detail.monthDate, + currentDate = uiState.detail.monthDate, onPreviousMonth = onPreviousMonth, onNextMonth = onNextMonth, - hasPrevious = uiSate.hasPrevious, - hasNext = uiSate.hasNext, + hasPrevious = uiState.hasPrevious, + hasNext = uiState.hasNext, ) Box( @@ -176,7 +176,7 @@ fun StatsDetailScreen( .padding(top = 24.dp, bottom = 32.dp), ) { StatsCalendar( - uiModel = uiSate.calendarUiModel, + uiModel = uiState.calendarUiModel, onSelectedDate = onSelectDate, ) } @@ -194,7 +194,7 @@ fun StatsDetailScreen( Spacer(Modifier.height(44.dp)) - SummaryContent(uiSate.detail.statsSummary) + SummaryContent(uiState.detail.statsSummary) } CommonDialog( @@ -209,10 +209,10 @@ fun StatsDetailScreen( title = stringResource( R.string.dialog_delete_goal_title, - uiSate.detail.goalName, + uiState.detail.goalName, ), content = stringResource(R.string.dialog_delete_goal_content), - icon = uiSate.detail.goalIcon, + icon = uiState.detail.goalIcon, ) }, ) @@ -267,7 +267,7 @@ private fun StatsDetailScreenPreview( ) { TwixTheme { StatsDetailScreen( - uiSate = uiState, + uiState = uiState, onBack = {}, onSelectDate = {}, onPreviousMonth = {}, From 9f42116800283b4cfe12e0a1f6d83cb2d0efdc29 Mon Sep 17 00:00:00 2001 From: chanho0908 Date: Wed, 25 Feb 2026 16:12:13 +0900 Subject: [PATCH 12/17] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20StatsDet?= =?UTF-8?q?ailViewModel=20=ED=8C=8C=EB=9D=BC=EB=AF=B8=ED=84=B0=EB=AA=85=20?= =?UTF-8?q?=EC=98=A4=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 - `savedSateHandle`을 `savedStateHandle`로 수정 --- .../main/java/com/twix/stats/detail/StatsDetailViewModel.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/feature/stats/detail/src/main/java/com/twix/stats/detail/StatsDetailViewModel.kt b/feature/stats/detail/src/main/java/com/twix/stats/detail/StatsDetailViewModel.kt index fb6ca443..d080d8e5 100644 --- a/feature/stats/detail/src/main/java/com/twix/stats/detail/StatsDetailViewModel.kt +++ b/feature/stats/detail/src/main/java/com/twix/stats/detail/StatsDetailViewModel.kt @@ -27,14 +27,14 @@ class StatsDetailViewModel( private val statsRefreshBus: StatsRefreshBus, private val goalRepository: GoalRepository, private val statsRepository: StatsRepository, - savedSateHandle: SavedStateHandle, + savedStateHandle: SavedStateHandle, ) : BaseViewModel( StatsDetailUiState(), ) { private val argGoalId: Long = - requireNotNull(savedSateHandle[NavRoutes.StatsDetailRoute.ARG_GOAL_ID]) { GOAL_ID_NOT_FOUND } + requireNotNull(savedStateHandle[NavRoutes.StatsDetailRoute.ARG_GOAL_ID]) { GOAL_ID_NOT_FOUND } - private val argDate: String? = savedSateHandle.get(NavRoutes.StatsDetailRoute.ARG_DATE) + private val argDate: String? = savedStateHandle.get(NavRoutes.StatsDetailRoute.ARG_DATE) private val cache = mutableMapOf() From e87c2a694d8f653ce53c89e5e387e81d7c1c56f4 Mon Sep 17 00:00:00 2001 From: chanho0908 Date: Wed, 25 Feb 2026 16:14:14 +0900 Subject: [PATCH 13/17] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20?= =?UTF-8?q?=EC=8A=A4=ED=81=AC=EB=A1=A4=20=EB=B2=94=EC=9C=84=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=EC=9C=BC=EB=A1=9C=20Topbar=20=EA=B3=A0=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `verticalScroll` 수식 범위를 `Box`로 이동하여 Topbar는 스크롤되지 않도록 수정 --- .../java/com/twix/stats/detail/StatsDetailScreen.kt | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/feature/stats/detail/src/main/java/com/twix/stats/detail/StatsDetailScreen.kt b/feature/stats/detail/src/main/java/com/twix/stats/detail/StatsDetailScreen.kt index f37bd5c1..1dd78e1e 100644 --- a/feature/stats/detail/src/main/java/com/twix/stats/detail/StatsDetailScreen.kt +++ b/feature/stats/detail/src/main/java/com/twix/stats/detail/StatsDetailScreen.kt @@ -113,8 +113,7 @@ fun StatsDetailScreen( modifier = Modifier .fillMaxSize() - .background(GrayColor.C050) - .verticalScroll(scrollState), + .background(GrayColor.C050), ) { StatsDetailTopbar( goalName = uiState.detail.goalName, @@ -137,7 +136,12 @@ fun StatsDetailScreen( }, ) - Box(modifier = Modifier.fillMaxWidth()) { + Box( + modifier = + Modifier + .fillMaxWidth() + .verticalScroll(scrollState), + ) { Image( imageVector = ImageVector.vectorResource(R.drawable.ic_hug), contentDescription = null, From 7371166ee4fbb5d74c06f79d89a58d029859e17c Mon Sep 17 00:00:00 2001 From: chanho0908 Date: Wed, 25 Feb 2026 16:15:27 +0900 Subject: [PATCH 14/17] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20`init`?= =?UTF-8?q?=20=EB=B8=94=EB=A1=9D=20=EB=A1=9C=EC=A7=81=20`collectMonthChang?= =?UTF-8?q?eFlow`=20=EB=A9=94=EC=84=9C=EB=93=9C=EB=A1=9C=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `monthChangeFlow`를 구독하고 처리하는 로직을 별도의 `collectMonthChangeFlow` 메서드로 분리하여 `init` 블록의 가독성을 개선했습니다. - `init` 블록 내 메서드 호출 순서를 조정했습니다. --- .../com/twix/stats/detail/StatsDetailViewModel.kt | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/feature/stats/detail/src/main/java/com/twix/stats/detail/StatsDetailViewModel.kt b/feature/stats/detail/src/main/java/com/twix/stats/detail/StatsDetailViewModel.kt index d080d8e5..f4c322df 100644 --- a/feature/stats/detail/src/main/java/com/twix/stats/detail/StatsDetailViewModel.kt +++ b/feature/stats/detail/src/main/java/com/twix/stats/detail/StatsDetailViewModel.kt @@ -45,21 +45,20 @@ class StatsDetailViewModel( ) init { + collectMonthChangeFlow() + reduceNavArguments() + fetchStatsDetail(argDate?.let { LocalDate.parse(it) }) + } + + private fun collectMonthChangeFlow() { viewModelScope.launch { monthChangeFlow .distinctUntilChanged() .debounce(DEBOUNCE_INTERVAL) - .collect { date -> - fetchStatsDetail(date) - } + .collect { date -> fetchStatsDetail(date) } } } - init { - reduceNavArguments() - fetchStatsDetail(argDate?.let { LocalDate.parse(it) }) - } - private fun reduceNavArguments() { reduce { copy( From c200a1424ac463d9c3cb4d54ede306946393c691 Mon Sep 17 00:00:00 2001 From: chanho0908 Date: Wed, 25 Feb 2026 16:16:11 +0900 Subject: [PATCH 15/17] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20?= =?UTF-8?q?=ED=86=B5=EA=B3=84=20=EC=83=81=EC=84=B8=20=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?=EB=94=94=EB=B0=94=EC=9A=B4=EC=8A=A4=20=EC=8B=9C=EA=B0=84=20?= =?UTF-8?q?=EB=8B=A8=EC=B6=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `DEBOUNCE_INTERVAL` 값을 600L에서 300L로 수정 --- .../src/main/java/com/twix/stats/detail/StatsDetailViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/feature/stats/detail/src/main/java/com/twix/stats/detail/StatsDetailViewModel.kt b/feature/stats/detail/src/main/java/com/twix/stats/detail/StatsDetailViewModel.kt index f4c322df..356d1b6f 100644 --- a/feature/stats/detail/src/main/java/com/twix/stats/detail/StatsDetailViewModel.kt +++ b/feature/stats/detail/src/main/java/com/twix/stats/detail/StatsDetailViewModel.kt @@ -174,6 +174,6 @@ class StatsDetailViewModel( companion object { private const val GOAL_ID_NOT_FOUND = "Goal Id Argument Not Found" - private const val DEBOUNCE_INTERVAL = 600L + private const val DEBOUNCE_INTERVAL = 300L } } From db672396c62d9af8a579a85b23567f5b18a101bf Mon Sep 17 00:00:00 2001 From: chanho0908 Date: Wed, 25 Feb 2026 16:23:44 +0900 Subject: [PATCH 16/17] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20?= =?UTF-8?q?=ED=86=B5=EA=B3=84=20=EC=83=81=EC=84=B8=20=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?=EC=BA=90=EC=8B=9C=20=ED=82=A4=EB=A5=BC=20`LocalDate`=EC=97=90?= =?UTF-8?q?=EC=84=9C=20`YearMonth`=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 - 월별 통계 데이터를 캐시할 때, `LocalDate` 대신 `YearMonth`를 키로 사용하여 월 단위 캐싱의 정확성을 개선 --- .../twix/stats/detail/StatsDetailViewModel.kt | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/feature/stats/detail/src/main/java/com/twix/stats/detail/StatsDetailViewModel.kt b/feature/stats/detail/src/main/java/com/twix/stats/detail/StatsDetailViewModel.kt index 356d1b6f..316f29b2 100644 --- a/feature/stats/detail/src/main/java/com/twix/stats/detail/StatsDetailViewModel.kt +++ b/feature/stats/detail/src/main/java/com/twix/stats/detail/StatsDetailViewModel.kt @@ -21,6 +21,7 @@ import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch import java.time.LocalDate +import java.time.YearMonth @OptIn(FlowPreview::class) class StatsDetailViewModel( @@ -36,10 +37,10 @@ class StatsDetailViewModel( private val argDate: String? = savedStateHandle.get(NavRoutes.StatsDetailRoute.ARG_DATE) - private val cache = mutableMapOf() + private val cache = mutableMapOf() private val monthChangeFlow = - MutableSharedFlow( + MutableSharedFlow( extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST, ) @@ -55,7 +56,7 @@ class StatsDetailViewModel( monthChangeFlow .distinctUntilChanged() .debounce(DEBOUNCE_INTERVAL) - .collect { date -> fetchStatsDetail(date) } + .collect { yearMonth -> fetchStatsDetail(yearMonth.atDay(1)) } } } @@ -69,12 +70,12 @@ class StatsDetailViewModel( } private fun fetchStatsDetail(date: LocalDate?) { - val result = date?.let { checkCache(date) } + val result = date?.let { checkCache(YearMonth.from(it)) } if (result == true) return launchResult( block = { statsRepository.fetchStatsDetail(currentState.goalId, date) }, onSuccess = { - cache[it.monthDate] = it + cache[YearMonth.from(it.monthDate)] = it reduce { copy( detail = it, @@ -93,8 +94,8 @@ class StatsDetailViewModel( ) } - private fun checkCache(date: LocalDate): Boolean { - cache[date]?.let { + private fun checkCache(yearMonth: YearMonth): Boolean { + cache[yearMonth]?.let { reduce { copy( detail = it, @@ -127,13 +128,13 @@ class StatsDetailViewModel( private fun fetchPreviousMonth() { val previousMonth = currentState.detail.monthDate.minusMonths(1) reduce { copy(detail = detail.copy(monthDate = previousMonth)) } - monthChangeFlow.tryEmit(previousMonth) + monthChangeFlow.tryEmit(YearMonth.from(previousMonth)) } private fun fetchNextMonth() { val nextMonth = currentState.detail.monthDate.plusMonths(1) reduce { copy(detail = detail.copy(monthDate = nextMonth)) } - monthChangeFlow.tryEmit(nextMonth) + monthChangeFlow.tryEmit(YearMonth.from(nextMonth)) } private fun endGoal() { From 3b9d4c4255047bd452fcd6818f6d8bc10920cbaf Mon Sep 17 00:00:00 2001 From: chanho0908 Date: Wed, 25 Feb 2026 16:28:24 +0900 Subject: [PATCH 17/17] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20?= =?UTF-8?q?=ED=86=B5=EA=B3=84=20=EC=83=81=EC=84=B8=20=EC=9A=94=EC=95=BD=20?= =?UTF-8?q?UI=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `SummaryRow` 컴포저블을 새로 추가하여 반복되는 UI 로직을 개선 - 기존의 복잡한 `Row`/`Column` 구조를 `SummaryRow`를 사용하도록 수정하여 코드 가독성 및 재사용성 향상 --- .../stats/detail/component/SummaryContent.kt | 115 +++++++++--------- 1 file changed, 58 insertions(+), 57 deletions(-) diff --git a/feature/stats/detail/src/main/java/com/twix/stats/detail/component/SummaryContent.kt b/feature/stats/detail/src/main/java/com/twix/stats/detail/component/SummaryContent.kt index 5b4cce9f..b6f649fa 100644 --- a/feature/stats/detail/src/main/java/com/twix/stats/detail/component/SummaryContent.kt +++ b/feature/stats/detail/src/main/java/com/twix/stats/detail/component/SummaryContent.kt @@ -5,6 +5,7 @@ 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 @@ -42,38 +43,22 @@ fun SummaryContent( Spacer(Modifier.height(20.dp)) - Row(modifier = Modifier.padding(horizontal = 20.dp)) { - Column( - verticalArrangement = Arrangement.spacedBy(12.dp), - ) { + Column( + modifier = Modifier.padding(horizontal = 20.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + Row(modifier = Modifier.fillMaxWidth()) { AppText( text = stringResource(R.string.word_end_count), style = AppTextStyle.C1, color = GrayColor.C400, + modifier = Modifier.width(56.dp), ) - AppText( - text = stringResource(R.string.word_repeat_type), - style = AppTextStyle.C1, - color = GrayColor.C400, - ) - AppText( - text = stringResource(R.string.word_start_date), - style = AppTextStyle.C1, - color = GrayColor.C400, - ) - AppText( - text = stringResource(R.string.word_end_date), - style = AppTextStyle.C1, - color = GrayColor.C400, - ) - } - - Spacer(Modifier.width(28.dp)) - - Column( - verticalArrangement = Arrangement.spacedBy(12.dp), - ) { - Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + Spacer(Modifier.width(28.dp)) + Row( + modifier = Modifier.weight(1f), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { Image( painter = painterResource(R.drawable.ic_checked_you), contentDescription = null, @@ -110,39 +95,34 @@ fun SummaryContent( color = GrayColor.C500, ) } + } - AppText( - text = statsSummary.repeatCycle.label(), - style = AppTextStyle.B4, - color = GrayColor.C500, - ) - - AppText( - text = + SummaryRow( + label = stringResource(R.string.word_repeat_type), + value = statsSummary.repeatCycle.label(), + ) + SummaryRow( + label = stringResource(R.string.word_start_date), + value = + stringResource( + R.string.date_year_month_day, + statsSummary.startDate.year, + statsSummary.startDate.monthValue, + statsSummary.startDate.dayOfMonth, + ), + ) + SummaryRow( + label = stringResource(R.string.word_end_date), + value = + statsSummary.endDate?.let { stringResource( R.string.date_year_month_day, - statsSummary.startDate.year, - statsSummary.startDate.monthValue, - statsSummary.startDate.dayOfMonth, - ), - style = AppTextStyle.B4, - color = GrayColor.C500, - ) - - AppText( - text = - statsSummary.endDate?.let { - stringResource( - R.string.date_year_month_day, - it.year, - it.monthValue, - it.dayOfMonth, - ) - } ?: stringResource(R.string.word_not_set), - style = AppTextStyle.B4, - color = GrayColor.C500, - ) - } + it.year, + it.monthValue, + it.dayOfMonth, + ) + } ?: stringResource(R.string.word_not_set), + ) } Spacer(Modifier.height(20.dp)) @@ -156,6 +136,27 @@ fun SummaryContent( } } +@Composable +private fun SummaryRow( + label: String, + value: String, +) { + Row(modifier = Modifier.fillMaxWidth()) { + AppText( + text = label, + style = AppTextStyle.C1, + color = GrayColor.C400, + modifier = Modifier.width(56.dp), + ) + Spacer(Modifier.width(28.dp)) + AppText( + text = value, + style = AppTextStyle.B4, + color = GrayColor.C500, + ) + } +} + @Preview(showBackground = true) @Composable private fun SummaryContentPreview() {