From f253b4632a07cd0a5dfb860e086aecda58fe5aaf Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Fri, 15 Aug 2025 17:44:24 +0900 Subject: [PATCH 01/20] =?UTF-8?q?[REFACTOR/#323]=20=ED=99=88=ED=99=94?= =?UTF-8?q?=EB=A9=B4=EC=9D=98=20UI=20=EA=B5=AC=EC=A1=B0=EB=A5=BC=20?= =?UTF-8?q?=EA=B0=9C=ED=8E=B8=ED=95=98=EC=97=AC=20=EC=BB=B4=ED=8F=AC?= =?UTF-8?q?=EB=84=8C=ED=8A=B8=20=EA=B3=84=EC=B8=B5=20=EA=B5=AC=EC=A1=B0?= =?UTF-8?q?=EB=A5=BC=20=EA=B0=84=EC=86=8C=ED=99=94=ED=95=A9=EB=8B=88?= =?UTF-8?q?=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/auth/signup/page/TermsOfServicePage.kt | 8 +- .../auth/timereminder/TimeReminderScreen.kt | 8 +- .../ui/home/calendar/ClodyCalendar.kt | 85 ------- .../ui/home/calendar/component/DayItem.kt | 93 -------- .../calendar/component/HorizontalDivider.kt | 26 --- .../ui/home/calendar/component/MonthlyItem.kt | 100 -------- .../ui/home/calendar/component/WeekHeader.kt | 53 ----- .../home/calendar/model/CalendarDateData.kt | 21 -- .../ui/home/component/CalendarDate.kt | 7 + .../ui/home/component/CloverCount.kt | 40 ---- .../DailyDiary.kt} | 4 +- ...iaryStateButton.kt => DailyStateButton.kt} | 10 +- .../model => component}/DiaryDateData.kt | 2 +- .../ui/home/component/MonthlyCalendar.kt | 201 ++++++++++++++++ .../component/MonthlyCalendarAndDailyDiary.kt | 133 +++++++++++ .../presentation/ui/home/screen/HomeScreen.kt | 216 +++++++++--------- .../ui/home/screen/HomeViewModel.kt | 2 +- .../ui/home/screen/ScrollableCalendar.kt | 64 ------ 18 files changed, 461 insertions(+), 612 deletions(-) delete mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/ClodyCalendar.kt delete mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/component/DayItem.kt delete mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/component/HorizontalDivider.kt delete mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/component/MonthlyItem.kt delete mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/component/WeekHeader.kt delete mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/model/CalendarDateData.kt create mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/home/component/CalendarDate.kt delete mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/home/component/CloverCount.kt rename app/src/main/java/com/sopt/clody/presentation/ui/home/{calendar/component/DailyDiaryListItem.kt => component/DailyDiary.kt} (98%) rename app/src/main/java/com/sopt/clody/presentation/ui/home/component/{DiaryStateButton.kt => DailyStateButton.kt} (89%) rename app/src/main/java/com/sopt/clody/presentation/ui/home/{calendar/model => component}/DiaryDateData.kt (71%) create mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/home/component/MonthlyCalendar.kt create mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/home/component/MonthlyCalendarAndDailyDiary.kt delete mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/home/screen/ScrollableCalendar.kt diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/page/TermsOfServicePage.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/page/TermsOfServicePage.kt index 6b2dd169..80075453 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/page/TermsOfServicePage.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/signup/page/TermsOfServicePage.kt @@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.width +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.IconButton import androidx.compose.material3.Scaffold import androidx.compose.material3.Text @@ -25,7 +26,6 @@ import androidx.compose.ui.unit.dp import com.sopt.clody.R import com.sopt.clody.presentation.ui.auth.component.checkbox.CustomCheckbox import com.sopt.clody.presentation.ui.component.button.ClodyButton -import com.sopt.clody.presentation.ui.home.calendar.component.HorizontalDivider import com.sopt.clody.presentation.utils.base.BasePreview import com.sopt.clody.presentation.utils.base.ClodyPreview import com.sopt.clody.presentation.utils.extension.heightForScreenPercentage @@ -107,7 +107,11 @@ fun TermsOfServicePage( ) } Spacer(modifier = Modifier.height(18.dp)) - HorizontalDivider(color = ClodyTheme.colors.gray07, thickness = 1.dp) + HorizontalDivider( + color = ClodyTheme.colors.gray07, + thickness = 1.dp, + modifier = Modifier.fillMaxWidth(), + ) Spacer(modifier = Modifier.height(16.dp)) TermsCheckboxRow( text = stringResource(R.string.terms_service_use), diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/auth/timereminder/TimeReminderScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/auth/timereminder/TimeReminderScreen.kt index 2c536ff8..95667467 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/auth/timereminder/TimeReminderScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/auth/timereminder/TimeReminderScreen.kt @@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -35,7 +36,6 @@ import com.sopt.clody.presentation.ui.component.LoadingScreen import com.sopt.clody.presentation.ui.component.button.ClodyButton import com.sopt.clody.presentation.ui.component.dialog.FailureDialog import com.sopt.clody.presentation.ui.component.popup.ClodyPopupBottomSheet -import com.sopt.clody.presentation.ui.home.calendar.component.HorizontalDivider import com.sopt.clody.presentation.utils.amplitude.AmplitudeConstraints import com.sopt.clody.presentation.utils.amplitude.AmplitudeUtils import com.sopt.clody.presentation.utils.extension.TimePeriod @@ -166,7 +166,11 @@ fun TimeReminderScreen( modifier = Modifier.fillMaxWidth(), onClick = { showBottomSheet = true }, ) - HorizontalDivider(color = ClodyTheme.colors.gray07, thickness = 1.dp) + HorizontalDivider( + color = ClodyTheme.colors.gray07, + thickness = 1.dp, + modifier = Modifier.fillMaxWidth(), + ) } if (showBottomSheet) { diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/ClodyCalendar.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/ClodyCalendar.kt deleted file mode 100644 index 3ce9cd36..00000000 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/ClodyCalendar.kt +++ /dev/null @@ -1,85 +0,0 @@ -package com.sopt.clody.presentation.ui.home.calendar - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.sopt.clody.data.remote.dto.response.MonthlyCalendarResponseDto -import com.sopt.clody.presentation.ui.component.FailureScreen -import com.sopt.clody.presentation.ui.component.LoadingScreen -import com.sopt.clody.presentation.ui.home.calendar.component.DailyDiaryListItem -import com.sopt.clody.presentation.ui.home.calendar.component.HorizontalDivider -import com.sopt.clody.presentation.ui.home.calendar.component.MonthlyItem -import com.sopt.clody.presentation.ui.home.calendar.model.generateCalendarDates -import com.sopt.clody.presentation.ui.home.screen.DailyDiariesState -import com.sopt.clody.presentation.ui.home.screen.HomeViewModel -import java.time.LocalDate -import java.time.YearMonth - -@Composable -fun ClodyCalendar( - selectedYear: Int, - selectedMonth: Int, - selectedDate: LocalDate, - onDateSelected: (LocalDate) -> Unit, - diaries: List, - homeViewModel: HomeViewModel, - onShowDiaryDeleteStateChange: (Boolean) -> Unit, -) { - val currentMonth = YearMonth.of(selectedYear, selectedMonth) - val dateList = remember(currentMonth.year, currentMonth.monthValue) { - generateCalendarDates(currentMonth.year, currentMonth.monthValue) - } - val initialDayOfWeek = selectedDate.dayOfWeek - - val dailyDiariesUiState by homeViewModel.dailyDiariesState.collectAsStateWithLifecycle() - - Column( - modifier = Modifier - .fillMaxSize(), - verticalArrangement = Arrangement.spacedBy(16.dp), - ) { - MonthlyItem( - dateList = dateList, - selectedDate = selectedDate, - onDayClick = { date -> - onDateSelected(date) - homeViewModel.updateDiaryState(diaries) - }, - getDiaryDataForDate = { date -> - diaries.getOrNull(date.dayOfMonth - 1) - }, - ) - Spacer(modifier = Modifier.height(10.dp)) - HorizontalDivider() - - when (val state = dailyDiariesUiState) { - is DailyDiariesState.Idle -> { - } - - is DailyDiariesState.Loading -> { - LoadingScreen() - } - - is DailyDiariesState.Success -> { - DailyDiaryListItem( - date = selectedDate, - dayOfWeek = initialDayOfWeek, - dailyDiary = state.data, - onShowDiaryDeleteStateChange = onShowDiaryDeleteStateChange, - ) - } - - is DailyDiariesState.Error -> { - FailureScreen() - } - } - } -} diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/component/DayItem.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/component/DayItem.kt deleted file mode 100644 index aa618e95..00000000 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/component/DayItem.kt +++ /dev/null @@ -1,93 +0,0 @@ -package com.sopt.clody.presentation.ui.home.calendar.component - -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import com.sopt.clody.R -import com.sopt.clody.data.remote.dto.response.MonthlyCalendarResponseDto -import com.sopt.clody.domain.model.ReplyStatus -import com.sopt.clody.presentation.ui.type.DiaryCloverType -import com.sopt.clody.ui.theme.ClodyTheme -import kotlinx.datetime.DayOfWeek -import java.time.LocalDate - -@Composable -fun DayItem( - date: LocalDate, - dayOfWeek: DayOfWeek, - onDayClick: (LocalDate) -> Unit, - isSelected: Boolean, - diaryData: MonthlyCalendarResponseDto.Diary, - modifier: Modifier = Modifier, -) { - val today = LocalDate.now() - val isToday = date == today - - val iconRes = DiaryCloverType.getCalendarCloverType(diaryData, isToday).iconRes - - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - modifier = modifier - .padding(8.dp) - .clip(RoundedCornerShape(12.dp)) - .clickable { onDayClick(date) }, - ) { - Box( - modifier = Modifier - .size(48.dp), - contentAlignment = Alignment.Center, - ) { - Image( - painter = painterResource(id = iconRes), - contentDescription = "Diary clover icon", - ) - if (diaryData.replyStatus == ReplyStatus.READY_NOT_READ && diaryData.diaryCount > 0) { - Image( - painter = painterResource(id = R.drawable.ic_home_unread_reply), - contentDescription = "Unread replies icon", - modifier = Modifier - .align(Alignment.BottomEnd) - .padding(end = 0.dp, bottom = 8.dp) - .size(12.dp), - ) - } - } - Box( - contentAlignment = Alignment.Center, - modifier = Modifier - .background( - if (isSelected) Color.Black else Color.Transparent, - shape = RoundedCornerShape(12.dp), - ) - .padding(horizontal = 6.dp), - ) { - Text( - text = date.dayOfMonth.toString(), - style = ClodyTheme.typography.detail1SemiBold.copy( - color = when { - isSelected -> ClodyTheme.colors.white - isToday -> ClodyTheme.colors.gray02 - else -> ClodyTheme.colors.gray05 - }, - ), - textAlign = TextAlign.Center, - ) - } - } -} diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/component/HorizontalDivider.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/component/HorizontalDivider.kt deleted file mode 100644 index 13855dd9..00000000 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/component/HorizontalDivider.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.sopt.clody.presentation.ui.home.calendar.component - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import com.sopt.clody.ui.theme.ClodyTheme - -@Composable -fun HorizontalDivider( - color: Color = ClodyTheme.colors.gray08, - thickness: Dp = 6.dp, - modifier: Modifier = Modifier, -) { - Box( - modifier = modifier - .fillMaxWidth() - .height(thickness) - .background(color), - ) -} diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/component/MonthlyItem.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/component/MonthlyItem.kt deleted file mode 100644 index 55ae5155..00000000 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/component/MonthlyItem.kt +++ /dev/null @@ -1,100 +0,0 @@ -package com.sopt.clody.presentation.ui.home.calendar.component - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.unit.dp -import com.sopt.clody.data.remote.dto.response.MonthlyCalendarResponseDto -import com.sopt.clody.presentation.ui.home.calendar.model.CalendarDate -import com.sopt.clody.presentation.utils.amplitude.AmplitudeConstraints -import com.sopt.clody.presentation.utils.amplitude.AmplitudeUtils -import kotlinx.datetime.DayOfWeek -import java.time.LocalDate - -@Composable -fun MonthlyItem( - dateList: List, - selectedDate: LocalDate, - onDayClick: (LocalDate) -> Unit, - getDiaryDataForDate: (LocalDate) -> MonthlyCalendarResponseDto.Diary?, -) { - val itemWidth = (LocalConfiguration.current.screenWidthDp.dp - 40.dp) / 7 - - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 20.dp), - ) { - WeekHeader(itemWidth = itemWidth) - Spacer(modifier = Modifier.height(8.dp)) - Box( - modifier = Modifier.fillMaxWidth(), - contentAlignment = Alignment.Center, - ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.fillMaxWidth(), - ) { - val firstDate = dateList.firstOrNull()?.let { LocalDate.of(it.year, it.month, it.date) } - val firstDayOfWeek = firstDate?.dayOfWeek ?: DayOfWeek.SUNDAY - val emptyDays = (firstDayOfWeek.value % 7) - - val paddedDateList = List(emptyDays) { null } + dateList - - paddedDateList.chunked(7).forEach { weekDates -> - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - weekDates.forEach { date -> - Box( - modifier = Modifier - .weight(1f) - .padding(vertical = 2.dp), - contentAlignment = Alignment.Center, - ) { - if (date != null) { - val localDate = LocalDate.of(date.year, date.month, date.date) - val diaryData = getDiaryDataForDate(localDate) - if (diaryData != null) { - DayItem( - date = localDate, - dayOfWeek = localDate.dayOfWeek, - onDayClick = { clickedDate -> - AmplitudeUtils.trackEvent(AmplitudeConstraints.HOME_CALENDAR_CLOVER) - onDayClick(clickedDate) - }, - isSelected = localDate == selectedDate, - diaryData = diaryData, - modifier = Modifier.fillMaxWidth(), - ) - } - } - } - } - if (weekDates.size < 7) { - repeat(7 - weekDates.size) { - Box( - modifier = Modifier - .width(itemWidth) - .padding(vertical = 2.dp), - ) - } - } - } - } - } - } - } -} diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/component/WeekHeader.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/component/WeekHeader.kt deleted file mode 100644 index 41e86052..00000000 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/component/WeekHeader.kt +++ /dev/null @@ -1,53 +0,0 @@ -package com.sopt.clody.presentation.ui.home.calendar.component - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.width -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import com.sopt.clody.ui.theme.ClodyTheme -import kotlinx.datetime.DayOfWeek -import java.time.format.TextStyle - -@Composable -fun WeekHeader( - modifier: Modifier = Modifier, - itemWidth: Dp = (LocalConfiguration.current.screenWidthDp.dp - 40.dp) / 7, -) { - val dayOfWeekArray = listOf( - DayOfWeek.SUNDAY, - DayOfWeek.MONDAY, - DayOfWeek.TUESDAY, - DayOfWeek.WEDNESDAY, - DayOfWeek.THURSDAY, - DayOfWeek.FRIDAY, - DayOfWeek.SATURDAY, - ) - - Row( - horizontalArrangement = Arrangement.SpaceBetween, - modifier = modifier.fillMaxWidth(), - ) { - dayOfWeekArray.forEach { week -> - Box( - modifier = Modifier.width(itemWidth), - contentAlignment = Alignment.Center, - ) { - Text( - text = week.getDisplayName(TextStyle.NARROW, LocalConfiguration.current.locales[0]), - color = ClodyTheme.colors.gray05, - style = ClodyTheme.typography.detail1Medium, - textAlign = TextAlign.Center, - ) - } - } - } -} diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/model/CalendarDateData.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/model/CalendarDateData.kt deleted file mode 100644 index fcbbc34f..00000000 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/model/CalendarDateData.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.sopt.clody.presentation.ui.home.calendar.model - -import java.time.YearMonth - -data class CalendarDate( - val date: Int, - val month: Int, - val year: Int, -) - -fun daysInMonth(month: Int, year: Int): Int { - return YearMonth.of(year, month).lengthOfMonth() -} - -fun generateCalendarDates(year: Int, month: Int): List { - val daysInCurrentMonth = daysInMonth(month, year) - - return (1..daysInCurrentMonth).map { day -> - CalendarDate(day, month, year) - } -} diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/component/CalendarDate.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/CalendarDate.kt new file mode 100644 index 00000000..d8e638c7 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/CalendarDate.kt @@ -0,0 +1,7 @@ +package com.sopt.clody.presentation.ui.home.component + +data class CalendarDate( + val date: Int, + val month: Int, + val year: Int, +) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/component/CloverCount.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/CloverCount.kt deleted file mode 100644 index f5f76ac9..00000000 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/component/CloverCount.kt +++ /dev/null @@ -1,40 +0,0 @@ -package com.sopt.clody.presentation.ui.home.component - -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxWidth -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.text.style.TextAlign -import androidx.compose.ui.unit.dp -import com.sopt.clody.R -import com.sopt.clody.ui.theme.ClodyTheme - -@Composable -fun CloverCount(cloverCount: Int) { - val text = stringResource(R.string.home_total_clover, cloverCount) - - Box( - modifier = Modifier - .fillMaxWidth() - .padding(top = 10.dp, bottom = 5.dp, end = 20.dp), - contentAlignment = Alignment.TopEnd, - ) { - Text( - text = text, - style = ClodyTheme.typography.detail1SemiBold, - color = ClodyTheme.colors.darkGreen, - modifier = Modifier - .border(9.dp, ClodyTheme.colors.lightGreenBack, shape = RoundedCornerShape(9.dp)) - .background(ClodyTheme.colors.lightGreenBack, shape = RoundedCornerShape(9.dp)) - .padding(horizontal = 12.dp, vertical = 8.dp), - textAlign = TextAlign.Center, - ) - } -} diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/component/DailyDiaryListItem.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/DailyDiary.kt similarity index 98% rename from app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/component/DailyDiaryListItem.kt rename to app/src/main/java/com/sopt/clody/presentation/ui/home/component/DailyDiary.kt index 7268a3aa..1766c06b 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/component/DailyDiaryListItem.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/DailyDiary.kt @@ -1,4 +1,4 @@ -package com.sopt.clody.presentation.ui.home.calendar.component +package com.sopt.clody.presentation.ui.home.component import androidx.compose.foundation.Image import androidx.compose.foundation.background @@ -30,7 +30,7 @@ import java.time.LocalDate import java.time.format.TextStyle @Composable -fun DailyDiaryListItem( +fun DailyDiary( date: LocalDate, dayOfWeek: DayOfWeek, dailyDiary: DailyDiariesResponseDto, diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/component/DiaryStateButton.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/DailyStateButton.kt similarity index 89% rename from app/src/main/java/com/sopt/clody/presentation/ui/home/component/DiaryStateButton.kt rename to app/src/main/java/com/sopt/clody/presentation/ui/home/component/DailyStateButton.kt index 37431fde..724f4837 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/component/DiaryStateButton.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/DailyStateButton.kt @@ -1,18 +1,15 @@ package com.sopt.clody.presentation.ui.home.component -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp import com.sopt.clody.R import com.sopt.clody.presentation.ui.component.button.ClodyButton import com.sopt.clody.presentation.ui.component.button.ClodyReplyButton import com.sopt.clody.ui.theme.ClodyTheme @Composable -fun DiaryStateButton( +fun DailyStateButton( hasDraft: Boolean, canWrite: Boolean, canReply: Boolean, @@ -22,11 +19,8 @@ fun DiaryStateButton( day: Int, onClickWriteDiary: (Int, Int, Int) -> Unit, onClickReplyDiary: () -> Unit, + modifier: Modifier = Modifier, ) { - val modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp) - when { hasDraft -> { ClodyButton( diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/model/DiaryDateData.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/DiaryDateData.kt similarity index 71% rename from app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/model/DiaryDateData.kt rename to app/src/main/java/com/sopt/clody/presentation/ui/home/component/DiaryDateData.kt index b8d5b3aa..e02accde 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/calendar/model/DiaryDateData.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/DiaryDateData.kt @@ -1,4 +1,4 @@ -package com.sopt.clody.presentation.ui.home.calendar.model +package com.sopt.clody.presentation.ui.home.component import java.time.LocalDate diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/component/MonthlyCalendar.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/MonthlyCalendar.kt new file mode 100644 index 00000000..0846f392 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/MonthlyCalendar.kt @@ -0,0 +1,201 @@ +package com.sopt.clody.presentation.ui.home.component + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.sopt.clody.R +import com.sopt.clody.data.remote.dto.response.MonthlyCalendarResponseDto +import com.sopt.clody.domain.model.ReplyStatus +import com.sopt.clody.presentation.ui.type.DiaryCloverType +import com.sopt.clody.presentation.utils.amplitude.AmplitudeConstraints +import com.sopt.clody.presentation.utils.amplitude.AmplitudeUtils +import com.sopt.clody.ui.theme.ClodyTheme +import kotlinx.datetime.DayOfWeek +import java.time.LocalDate +import java.time.format.TextStyle + +@Composable +fun MonthlyCalendar( + dateList: List, + selectedDate: LocalDate, + onDayClick: (LocalDate) -> Unit, + getDiaryDataForDate: (LocalDate) -> MonthlyCalendarResponseDto.Diary?, +) { + val locale = LocalConfiguration.current.locales[0] + val days = remember { + List(7) { i -> DayOfWeek.SUNDAY.plus(i.toLong()) } + } + + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp), + ) { + // 요일 헤더 부분 + Row( + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp), + ) { + days.forEach { week -> + Box( + modifier = Modifier.width((LocalConfiguration.current.screenWidthDp.dp - 40.dp) / 7), + contentAlignment = Alignment.Center, + ) { + Text( + text = week.getDisplayName(TextStyle.NARROW, locale), + color = ClodyTheme.colors.gray05, + style = ClodyTheme.typography.detail1Medium, + textAlign = TextAlign.Center, + ) + } + } + } + + // 월별 캘린더의 클로버 + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.Center, + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxWidth(), + ) { + val firstDate = dateList.firstOrNull()?.let { LocalDate.of(it.year, it.month, it.date) } + val firstDayOfWeek = firstDate?.dayOfWeek ?: DayOfWeek.SUNDAY + val emptyDays = (firstDayOfWeek.value % 7) + + val paddedDateList = List(emptyDays) { null } + dateList + + paddedDateList.chunked(7).forEach { weekDates -> + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + weekDates.forEach { date -> + Box( + modifier = Modifier + .weight(1f) + .padding(vertical = 2.dp), + contentAlignment = Alignment.Center, + ) { + if (date != null) { + val localDate = LocalDate.of(date.year, date.month, date.date) + val diaryData = getDiaryDataForDate(localDate) + if (diaryData != null) { + DailyClover( + date = localDate, + onDayClick = { clickedDate -> + AmplitudeUtils.trackEvent(AmplitudeConstraints.HOME_CALENDAR_CLOVER) + onDayClick(clickedDate) + }, + isSelected = localDate == selectedDate, + diaryData = diaryData, + modifier = Modifier.fillMaxWidth(), + ) + } + } + } + } + if (weekDates.size < 7) { + repeat(7 - weekDates.size) { + Box( + modifier = Modifier + .width((LocalConfiguration.current.screenWidthDp.dp - 40.dp) / 7) + .padding(vertical = 2.dp), + ) + } + } + } + } + } + } + } +} + +@Composable +fun DailyClover( + date: LocalDate, + onDayClick: (LocalDate) -> Unit, + isSelected: Boolean, + diaryData: MonthlyCalendarResponseDto.Diary, + modifier: Modifier = Modifier, +) { + val today = LocalDate.now() + val isToday = date == today + + val iconRes = DiaryCloverType.getCalendarCloverType(diaryData, isToday).iconRes + + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = modifier + .padding(8.dp) + .clip(RoundedCornerShape(12.dp)) + .clickable { onDayClick(date) }, + ) { + Box( + modifier = Modifier + .size(48.dp), + contentAlignment = Alignment.Center, + ) { + Image( + painter = painterResource(id = iconRes), + contentDescription = "Diary clover icon", + ) + if (diaryData.replyStatus == ReplyStatus.READY_NOT_READ && diaryData.diaryCount > 0) { + Image( + painter = painterResource(id = R.drawable.ic_home_unread_reply), + contentDescription = "Unread replies icon", + modifier = Modifier + .align(Alignment.BottomEnd) + .padding(end = 0.dp, bottom = 8.dp) + .size(12.dp), + ) + } + } + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .background( + if (isSelected) Color.Black else Color.Transparent, + shape = RoundedCornerShape(12.dp), + ) + .padding(horizontal = 6.dp), + ) { + Text( + text = date.dayOfMonth.toString(), + style = ClodyTheme.typography.detail1SemiBold.copy( + color = when { + isSelected -> ClodyTheme.colors.white + isToday -> ClodyTheme.colors.gray02 + else -> ClodyTheme.colors.gray05 + }, + ), + textAlign = TextAlign.Center, + ) + } + } +} diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/component/MonthlyCalendarAndDailyDiary.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/MonthlyCalendarAndDailyDiary.kt new file mode 100644 index 00000000..b8ce222a --- /dev/null +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/MonthlyCalendarAndDailyDiary.kt @@ -0,0 +1,133 @@ +package com.sopt.clody.presentation.ui.home.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.sopt.clody.R +import com.sopt.clody.data.remote.dto.response.DailyDiariesResponseDto +import com.sopt.clody.data.remote.dto.response.MonthlyCalendarResponseDto +import com.sopt.clody.domain.model.ReplyStatus +import com.sopt.clody.presentation.ui.component.FailureScreen +import com.sopt.clody.presentation.ui.component.LoadingScreen +import com.sopt.clody.presentation.ui.home.screen.DailyDiariesState +import com.sopt.clody.presentation.ui.home.screen.HomeViewModel +import com.sopt.clody.ui.theme.ClodyTheme +import java.time.LocalDate +import java.time.YearMonth + +@Composable +fun MonthlyCalendarAndDailyDiary( + selectedYear: Int, + selectedMonth: Int, + cloverCount: Int, + homeViewModel: HomeViewModel, + diaries: List, + onShowDiaryDeleteStateChange: (Boolean) -> Unit, + selectedDate: LocalDate, + onDiaryDataUpdated: (Int, ReplyStatus) -> Unit, + modifier: Modifier = Modifier, + dailyDiariesState: DailyDiariesState, +) { + val scrollState = rememberScrollState() + val currentMonth = YearMonth.of(selectedYear, selectedMonth) + val dateList = remember(currentMonth.year, currentMonth.monthValue) { + (1..YearMonth.of(currentMonth.year, currentMonth.monthValue).lengthOfMonth()).map { day -> + CalendarDate(day, currentMonth.monthValue, currentMonth.year) + } + } + val initialDayOfWeek = selectedDate.dayOfWeek + + LaunchedEffect(selectedDate, diaries) { + if (selectedDate.year == selectedYear && selectedDate.monthValue == selectedMonth) { + homeViewModel.updateDiaryState(diaries) + onDiaryDataUpdated( + homeViewModel.diaryCount.value, + homeViewModel.replyStatus.value, + ) + } + } + + Column( + modifier = modifier + .fillMaxSize() + .verticalScroll(scrollState) + .background(ClodyTheme.colors.white), + ) { + // 클로버 총 갯수 + Text( + text = stringResource(R.string.home_total_clover, cloverCount), + style = ClodyTheme.typography.detail1SemiBold, + color = ClodyTheme.colors.darkGreen, + modifier = Modifier + .align(Alignment.End) + .padding(top = 10.dp, bottom = 25.dp, end = 20.dp) + .border(9.dp, ClodyTheme.colors.lightGreenBack, shape = RoundedCornerShape(9.dp)) + .background(ClodyTheme.colors.lightGreenBack, shape = RoundedCornerShape(9.dp)) + .padding(horizontal = 12.dp, vertical = 8.dp), + textAlign = TextAlign.Center, + ) + + // 캘린더 + 일별 일기 리스트 + Column( + modifier = Modifier.fillMaxSize(), + ) { + MonthlyCalendar( + dateList = dateList, + selectedDate = selectedDate, + onDayClick = { date -> + homeViewModel.updateSelectedDate(date) + homeViewModel.updateDiaryState(diaries) + }, + getDiaryDataForDate = { date -> + diaries.getOrNull(date.dayOfMonth - 1) + }, + ) + + HorizontalDivider( + color = ClodyTheme.colors.gray08, + thickness = 6.dp, + modifier = Modifier + .fillMaxWidth() + .padding(top = 30.dp, bottom = 20.dp), + ) + + when (dailyDiariesState) { + is DailyDiariesState.Idle -> { + } + + is DailyDiariesState.Loading -> { + LoadingScreen() + } + + is DailyDiariesState.Success -> { + DailyDiary( + date = selectedDate, + dayOfWeek = initialDayOfWeek, + dailyDiary = dailyDiariesState.data, + onShowDiaryDeleteStateChange = onShowDiaryDeleteStateChange, + ) + } + + is DailyDiariesState.Error -> { + FailureScreen() + } + } + } + } +} diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt index 83772e74..6af3aefb 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt @@ -22,7 +22,7 @@ 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.mutableLongStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment @@ -36,6 +36,7 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.sopt.clody.R import com.sopt.clody.core.review.InAppReviewManager +import com.sopt.clody.data.remote.dto.response.DailyDiariesResponseDto import com.sopt.clody.data.remote.dto.response.MonthlyCalendarResponseDto import com.sopt.clody.domain.model.ReplyStatus import com.sopt.clody.presentation.ui.component.FailureScreen @@ -46,8 +47,9 @@ import com.sopt.clody.presentation.ui.component.dialog.ClodyDialog import com.sopt.clody.presentation.ui.component.popup.ClodyPopupBottomSheet import com.sopt.clody.presentation.ui.component.timepicker.YearMonthPicker import com.sopt.clody.presentation.ui.component.toast.ClodyToastMessage -import com.sopt.clody.presentation.ui.home.component.DiaryStateButton +import com.sopt.clody.presentation.ui.home.component.DailyStateButton import com.sopt.clody.presentation.ui.home.component.HomeTopAppBar +import com.sopt.clody.presentation.ui.home.component.MonthlyCalendarAndDailyDiary import com.sopt.clody.presentation.utils.amplitude.AmplitudeConstraints import com.sopt.clody.presentation.utils.amplitude.AmplitudeUtils import com.sopt.clody.presentation.utils.extension.toLocalizedMonthLabel @@ -72,6 +74,7 @@ fun HomeRoute( homeViewModel: HomeViewModel = hiltViewModel(), ) { val calendarState by homeViewModel.calendarState.collectAsStateWithLifecycle() + val dailyDiariesState by homeViewModel.dailyDiariesState.collectAsStateWithLifecycle() val replyStatus by homeViewModel.replyStatus.collectAsStateWithLifecycle() val showFirstDraftPopup by homeViewModel.showFirstDraftPopup.collectAsStateWithLifecycle() val draftAlarmEnableToast by homeViewModel.draftAlarmEnableToast.collectAsStateWithLifecycle() @@ -86,6 +89,8 @@ fun HomeRoute( val (isError, errorMessage) = homeViewModel.errorState.collectAsStateWithLifecycle().value val showYearMonthPickerState by homeViewModel.showYearMonthPickerState.collectAsStateWithLifecycle() val hasDraft by homeViewModel.hasDraft.collectAsStateWithLifecycle() + var backPressedTime by remember { mutableLongStateOf(0L) } + val backPressThreshold = 2000 val requestPermissionLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.RequestPermission(), @@ -93,6 +98,15 @@ fun HomeRoute( homeViewModel.sendNotification(isGranted) } + BackHandler { + val currentTime = System.currentTimeMillis() + if (currentTime - backPressedTime <= backPressThreshold) { + (context as? Activity)?.finish() + } else { + backPressedTime = currentTime + } + } + LaunchedEffect(Unit) { AmplitudeUtils.trackEvent(eventName = AmplitudeConstraints.HOME) @@ -139,6 +153,7 @@ fun HomeRoute( HomeScreen( homeViewModel = homeViewModel, calendarState = calendarState, + dailyDiariesState = dailyDiariesState, deleteDiaryState = deleteDiaryState, showYearMonthPickerState = showYearMonthPickerState, onClickDiaryList = navigateToDiaryList, @@ -161,8 +176,6 @@ fun HomeRoute( replyStatus, ) }, - isError = isError, - errorMessage = errorMessage, selectedYear = selectedDiaryDate.year, selectedMonth = selectedDiaryDate.month, selectedDate = selectedDate, @@ -306,6 +319,7 @@ fun HomeRoute( fun HomeScreen( homeViewModel: HomeViewModel, calendarState: CalendarState, + dailyDiariesState: DailyDiariesState, deleteDiaryState: DeleteDiaryState, showYearMonthPickerState: Boolean, onClickDiaryList: (Int, Int) -> Unit, @@ -317,8 +331,6 @@ fun HomeScreen( date: Int, replyStatus: Route.ReplyLoading.ReplyLoadingFrom, ) -> Unit, - isError: Boolean, - errorMessage: String, selectedYear: Int, selectedMonth: Int, selectedDate: LocalDate, @@ -327,125 +339,101 @@ fun HomeScreen( canReply: Boolean, isInvalidDraft: Boolean, ) { - if (isError) { - FailureScreen( - message = errorMessage, - confirmAction = { - homeViewModel.updateYearMonthAndLoadData(selectedYear, selectedMonth, selectedDate.dayOfMonth) - }, - ) - } else { - var backPressedTime by remember { mutableStateOf(0L) } - val backPressThreshold = 2000 - val context = LocalContext.current - - BackHandler { - val currentTime = System.currentTimeMillis() - if (currentTime - backPressedTime <= backPressThreshold) { - (context as? Activity)?.finish() - } else { - backPressedTime = currentTime - } - } - - Scaffold( - topBar = { - HomeTopAppBar( - onClickDiaryList = { - AmplitudeUtils.trackEvent(eventName = AmplitudeConstraints.HOME_LIST_DIARY) - onClickDiaryList(selectedYear, selectedMonth) - }, - onClickSetting = onClickSetting, - onShowYearMonthPickerStateChange = { newState -> homeViewModel.setShowYearMonthPickerState(newState) }, - selectedYear = selectedYear.toLocalizedYearLabel(), - selectedMonth = selectedMonth.toLocalizedMonthLabel(), - ) - }, - containerColor = ClodyTheme.colors.white, - content = { innerPadding -> - when (calendarState) { - is CalendarState.Idle -> {} + Scaffold( + topBar = { + HomeTopAppBar( + onClickDiaryList = { + AmplitudeUtils.trackEvent(eventName = AmplitudeConstraints.HOME_LIST_DIARY) + onClickDiaryList(selectedYear, selectedMonth) + }, + onClickSetting = onClickSetting, + onShowYearMonthPickerStateChange = { newState -> homeViewModel.setShowYearMonthPickerState(newState) }, + selectedYear = selectedYear.toLocalizedYearLabel(), + selectedMonth = selectedMonth.toLocalizedMonthLabel(), + ) + }, + containerColor = ClodyTheme.colors.white, + content = { innerPadding -> + when (calendarState) { + is CalendarState.Idle -> {} - is CalendarState.Loading -> { - LoadingScreen() - } + is CalendarState.Loading -> { + LoadingScreen() + } - is CalendarState.Success -> { - ScrollableCalendar( - selectedYear = selectedYear, - selectedMonth = selectedMonth, - cloverCount = calendarState.data.totalCloverCount, - diaries = calendarState.data.diaries, - homeViewModel = homeViewModel, - onShowDiaryDeleteStateChange = { newState -> homeViewModel.setShowDiaryDeleteState(newState) }, - selectedDate = selectedDate, - onDiaryDataUpdated = { _, _ -> - homeViewModel.updateDiaryState(calendarState.data.diaries) - }, - modifier = Modifier.padding(innerPadding), - ) - } + is CalendarState.Success -> { + MonthlyCalendarAndDailyDiary( + selectedYear = selectedYear, + selectedMonth = selectedMonth, + cloverCount = calendarState.data.totalCloverCount, + diaries = calendarState.data.diaries, + homeViewModel = homeViewModel, + onShowDiaryDeleteStateChange = { newState -> homeViewModel.setShowDiaryDeleteState(newState) }, + selectedDate = selectedDate, + onDiaryDataUpdated = { _, _ -> + homeViewModel.updateDiaryState(calendarState.data.diaries) + }, + modifier = Modifier.padding(innerPadding), + dailyDiariesState = dailyDiariesState, + ) + } - is CalendarState.Error -> { - homeViewModel.setErrorState(true, calendarState.message) - } + is CalendarState.Error -> { + homeViewModel.setErrorState(true, calendarState.message) } + } - when (deleteDiaryState) { - is DeleteDiaryState.Idle -> {} + when (deleteDiaryState) { + is DeleteDiaryState.Idle -> {} - is DeleteDiaryState.Loading -> { - LoadingScreen() - } + is DeleteDiaryState.Loading -> { + LoadingScreen() + } - is DeleteDiaryState.Success -> {} + is DeleteDiaryState.Success -> {} - is DeleteDiaryState.Failure -> { - homeViewModel.setErrorState(true, stringResource(R.string.home_error_delete_diary)) - } + is DeleteDiaryState.Failure -> { + homeViewModel.setErrorState(true, stringResource(R.string.home_error_delete_diary)) } - }, - bottomBar = { - Column( - modifier = Modifier - .navigationBarsPadding() - .background(ClodyTheme.colors.white), - ) { - Spacer(modifier = Modifier.height(14.dp)) - DiaryStateButton( - hasDraft = hasDraft, - canWrite = canWrite, - canReply = canReply, - isInvalidDraft = isInvalidDraft, - year = selectedYear, - month = selectedMonth, - day = selectedDate.dayOfMonth, - onClickWriteDiary = onClickWriteDiary, - onClickReplyDiary = { - onClickReplyDiary( - selectedYear, - selectedMonth, - selectedDate.dayOfMonth, - Route.ReplyLoading.ReplyLoadingFrom.HOME, - ) - }, + } + }, + bottomBar = { + DailyStateButton( + hasDraft = hasDraft, + canWrite = canWrite, + canReply = canReply, + isInvalidDraft = isInvalidDraft, + year = selectedYear, + month = selectedMonth, + day = selectedDate.dayOfMonth, + onClickWriteDiary = onClickWriteDiary, + onClickReplyDiary = { + onClickReplyDiary( + selectedYear, + selectedMonth, + selectedDate.dayOfMonth, + Route.ReplyLoading.ReplyLoadingFrom.HOME, ) - Spacer(modifier = Modifier.height(14.dp)) - } - }, - ) + }, + modifier = Modifier + .fillMaxWidth() + .navigationBarsPadding() + .background(ClodyTheme.colors.white) + .padding(horizontal = 16.dp, vertical = 14.dp), + ) + }, + ) - if (showYearMonthPickerState) { - ClodyPopupBottomSheet(onDismissRequest = { homeViewModel.setShowYearMonthPickerState(false) }) { - YearMonthPicker( - onDismissRequest = { homeViewModel.setShowYearMonthPickerState(false) }, - selectedYear = selectedYear, - selectedMonth = selectedMonth, - onYearMonthSelected = { year, month -> - homeViewModel.updateYearMonthAndLoadData(year, month) - }, - ) - } + if (showYearMonthPickerState) { + ClodyPopupBottomSheet(onDismissRequest = { homeViewModel.setShowYearMonthPickerState(false) }) { + YearMonthPicker( + onDismissRequest = { homeViewModel.setShowYearMonthPickerState(false) }, + selectedYear = selectedYear, + selectedMonth = selectedMonth, + onYearMonthSelected = { year, month -> + homeViewModel.updateYearMonthAndLoadData(year, month) + }, + ) } } } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt index b37f7330..de13a266 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt @@ -14,7 +14,7 @@ import com.sopt.clody.domain.repository.DiaryRepository import com.sopt.clody.domain.repository.DraftRepository import com.sopt.clody.domain.repository.NotificationRepository import com.sopt.clody.domain.repository.ReviewRepository -import com.sopt.clody.presentation.ui.home.calendar.model.DiaryDateData +import com.sopt.clody.presentation.ui.home.component.DiaryDateData import com.sopt.clody.presentation.ui.setting.notificationsetting.screen.NotificationChangeState import com.sopt.clody.presentation.utils.network.ErrorMessageProvider import dagger.hilt.android.lifecycle.HiltViewModel diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/ScrollableCalendar.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/ScrollableCalendar.kt deleted file mode 100644 index 920c1616..00000000 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/ScrollableCalendar.kt +++ /dev/null @@ -1,64 +0,0 @@ -package com.sopt.clody.presentation.ui.home.screen - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import com.sopt.clody.data.remote.dto.response.MonthlyCalendarResponseDto -import com.sopt.clody.domain.model.ReplyStatus -import com.sopt.clody.presentation.ui.home.calendar.ClodyCalendar -import com.sopt.clody.presentation.ui.home.component.CloverCount -import com.sopt.clody.ui.theme.ClodyTheme -import java.time.LocalDate - -@Composable -fun ScrollableCalendar( - selectedYear: Int, - selectedMonth: Int, - cloverCount: Int, - homeViewModel: HomeViewModel, - diaries: List, - onShowDiaryDeleteStateChange: (Boolean) -> Unit, - selectedDate: LocalDate, - onDiaryDataUpdated: (Int, ReplyStatus) -> Unit, - modifier: Modifier = Modifier, -) { - LaunchedEffect(selectedDate, diaries) { - if (selectedDate.year == selectedYear && selectedDate.monthValue == selectedMonth) { - homeViewModel.updateDiaryState(diaries) - onDiaryDataUpdated( - homeViewModel.diaryCount.value, - homeViewModel.replyStatus.value, - ) - } - } - val scrollState = rememberScrollState() - - Column( - modifier = modifier - .fillMaxSize() - .verticalScroll(scrollState) - .background(ClodyTheme.colors.white), - ) { - CloverCount(cloverCount = cloverCount) - Spacer(modifier = Modifier.height(20.dp)) - ClodyCalendar( - selectedYear = selectedYear, - selectedMonth = selectedMonth, - selectedDate = selectedDate, - onDateSelected = { date -> - homeViewModel.updateSelectedDate(date) - }, - diaries = diaries, - homeViewModel = homeViewModel, - onShowDiaryDeleteStateChange = onShowDiaryDeleteStateChange, - ) - } -} From 196e0f5be76ae26830deb82a7a4936a987ebb548 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Sun, 17 Aug 2025 17:51:36 +0900 Subject: [PATCH 02/20] =?UTF-8?q?[CHORE/#323]=20ReplyStatus=EB=A5=BC=20pre?= =?UTF-8?q?sentation=20layer=EB=A1=9C=20=EC=9D=B4=EB=8F=99=ED=95=98?= =?UTF-8?q?=EA=B3=A0,=20DiaryCloverType=20->=20DailyCloverType=EB=A1=9C=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=EC=9D=84=20=EB=B3=80=EA=B2=BD=ED=95=A9?= =?UTF-8?q?=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data/remote/dto/response/MonthlyCalendarResponseDto.kt | 2 +- .../data/remote/dto/response/MonthlyDiaryResponseDto.kt | 2 +- .../presentation/ui/diarylist/component/DailyDiaryCard.kt | 2 +- .../presentation/ui/diarylist/component/MonthlyDiaryList.kt | 2 +- .../ui/diarylist/navigation/DiaryListNavigation.kt | 2 +- .../presentation/ui/diarylist/screen/DiaryListScreen.kt | 2 +- .../clody/presentation/ui/home/component/MonthlyCalendar.kt | 6 +++--- .../ui/home/component/MonthlyCalendarAndDailyDiary.kt | 2 +- .../clody/presentation/ui/home/navigation/HomeNavigation.kt | 2 +- .../sopt/clody/presentation/ui/home/screen/HomeScreen.kt | 2 +- .../sopt/clody/presentation/ui/home/screen/HomeViewModel.kt | 2 +- .../clody/presentation/ui/replydiary/ReplyDiaryScreen.kt | 2 +- .../ui/replydiary/navigation/ReplyDiaryNavigation.kt | 2 +- .../ui/replyloading/navigation/ReplyLoadingNavigation.kt | 2 +- .../ui/replyloading/screen/ReplyLoadingScreen.kt | 2 +- .../ui/type/{DiaryCloverType.kt => DailyCloverType.kt} | 5 ++--- .../{domain/model => presentation/ui/type}/ReplyStatus.kt | 2 +- .../com/sopt/clody/presentation/utils/navigation/Route.kt | 2 +- 18 files changed, 21 insertions(+), 22 deletions(-) rename app/src/main/java/com/sopt/clody/presentation/ui/type/{DiaryCloverType.kt => DailyCloverType.kt} (94%) rename app/src/main/java/com/sopt/clody/{domain/model => presentation/ui/type}/ReplyStatus.kt (84%) diff --git a/app/src/main/java/com/sopt/clody/data/remote/dto/response/MonthlyCalendarResponseDto.kt b/app/src/main/java/com/sopt/clody/data/remote/dto/response/MonthlyCalendarResponseDto.kt index 2a4e63ec..9742f1dc 100644 --- a/app/src/main/java/com/sopt/clody/data/remote/dto/response/MonthlyCalendarResponseDto.kt +++ b/app/src/main/java/com/sopt/clody/data/remote/dto/response/MonthlyCalendarResponseDto.kt @@ -1,6 +1,6 @@ package com.sopt.clody.data.remote.dto.response -import com.sopt.clody.domain.model.ReplyStatus +import com.sopt.clody.presentation.ui.type.ReplyStatus import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable diff --git a/app/src/main/java/com/sopt/clody/data/remote/dto/response/MonthlyDiaryResponseDto.kt b/app/src/main/java/com/sopt/clody/data/remote/dto/response/MonthlyDiaryResponseDto.kt index 695d959c..bdad746d 100644 --- a/app/src/main/java/com/sopt/clody/data/remote/dto/response/MonthlyDiaryResponseDto.kt +++ b/app/src/main/java/com/sopt/clody/data/remote/dto/response/MonthlyDiaryResponseDto.kt @@ -1,6 +1,6 @@ package com.sopt.clody.data.remote.dto.response -import com.sopt.clody.domain.model.ReplyStatus +import com.sopt.clody.presentation.ui.type.ReplyStatus import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/component/DailyDiaryCard.kt b/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/component/DailyDiaryCard.kt index ed6029f9..3a8a3974 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/component/DailyDiaryCard.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/component/DailyDiaryCard.kt @@ -28,7 +28,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.sopt.clody.R import com.sopt.clody.data.remote.dto.response.MonthlyDiaryResponseDto -import com.sopt.clody.domain.model.ReplyStatus +import com.sopt.clody.presentation.ui.type.ReplyStatus import com.sopt.clody.presentation.ui.diarylist.screen.DiaryListViewModel import com.sopt.clody.ui.theme.ClodyTheme diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/component/MonthlyDiaryList.kt b/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/component/MonthlyDiaryList.kt index 401257a1..46007358 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/component/MonthlyDiaryList.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/component/MonthlyDiaryList.kt @@ -9,7 +9,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.sopt.clody.data.remote.dto.response.MonthlyDiaryResponseDto -import com.sopt.clody.domain.model.ReplyStatus +import com.sopt.clody.presentation.ui.type.ReplyStatus import com.sopt.clody.presentation.ui.diarylist.screen.DiaryListViewModel import com.sopt.clody.presentation.utils.extension.getDayOfWeek diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/navigation/DiaryListNavigation.kt b/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/navigation/DiaryListNavigation.kt index fa10a207..6574514c 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/navigation/DiaryListNavigation.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/navigation/DiaryListNavigation.kt @@ -4,7 +4,7 @@ import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable import androidx.navigation.toRoute -import com.sopt.clody.domain.model.ReplyStatus +import com.sopt.clody.presentation.ui.type.ReplyStatus import com.sopt.clody.presentation.ui.diarylist.screen.DiaryListRoute import com.sopt.clody.presentation.utils.navigation.Route diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/screen/DiaryListScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/screen/DiaryListScreen.kt index ae5e732a..9afbf62c 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/screen/DiaryListScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/screen/DiaryListScreen.kt @@ -12,7 +12,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.res.stringResource import androidx.hilt.navigation.compose.hiltViewModel import com.sopt.clody.R -import com.sopt.clody.domain.model.ReplyStatus +import com.sopt.clody.presentation.ui.type.ReplyStatus import com.sopt.clody.presentation.ui.component.FailureScreen import com.sopt.clody.presentation.ui.component.LoadingScreen import com.sopt.clody.presentation.ui.component.bottomsheet.DiaryDeleteSheet diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/component/MonthlyCalendar.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/MonthlyCalendar.kt index 0846f392..77542c82 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/component/MonthlyCalendar.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/MonthlyCalendar.kt @@ -25,8 +25,8 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import com.sopt.clody.R import com.sopt.clody.data.remote.dto.response.MonthlyCalendarResponseDto -import com.sopt.clody.domain.model.ReplyStatus -import com.sopt.clody.presentation.ui.type.DiaryCloverType +import com.sopt.clody.presentation.ui.type.DailyCloverType +import com.sopt.clody.presentation.ui.type.ReplyStatus import com.sopt.clody.presentation.utils.amplitude.AmplitudeConstraints import com.sopt.clody.presentation.utils.amplitude.AmplitudeUtils import com.sopt.clody.ui.theme.ClodyTheme @@ -146,7 +146,7 @@ fun DailyClover( val today = LocalDate.now() val isToday = date == today - val iconRes = DiaryCloverType.getCalendarCloverType(diaryData, isToday).iconRes + val iconRes = DailyCloverType.getCalendarCloverType(diaryData, isToday).iconRes Column( horizontalAlignment = Alignment.CenterHorizontally, diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/component/MonthlyCalendarAndDailyDiary.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/MonthlyCalendarAndDailyDiary.kt index b8ce222a..21b223a6 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/component/MonthlyCalendarAndDailyDiary.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/MonthlyCalendarAndDailyDiary.kt @@ -22,11 +22,11 @@ import androidx.compose.ui.unit.dp import com.sopt.clody.R import com.sopt.clody.data.remote.dto.response.DailyDiariesResponseDto import com.sopt.clody.data.remote.dto.response.MonthlyCalendarResponseDto -import com.sopt.clody.domain.model.ReplyStatus import com.sopt.clody.presentation.ui.component.FailureScreen import com.sopt.clody.presentation.ui.component.LoadingScreen import com.sopt.clody.presentation.ui.home.screen.DailyDiariesState import com.sopt.clody.presentation.ui.home.screen.HomeViewModel +import com.sopt.clody.presentation.ui.type.ReplyStatus import com.sopt.clody.ui.theme.ClodyTheme import java.time.LocalDate import java.time.YearMonth diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/navigation/HomeNavigation.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/navigation/HomeNavigation.kt index bc68535a..16fd8a1f 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/navigation/HomeNavigation.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/navigation/HomeNavigation.kt @@ -5,7 +5,7 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptionsBuilder import androidx.navigation.compose.composable import androidx.navigation.toRoute -import com.sopt.clody.domain.model.ReplyStatus +import com.sopt.clody.presentation.ui.type.ReplyStatus import com.sopt.clody.presentation.ui.home.screen.HomeRoute import com.sopt.clody.presentation.utils.navigation.Route import java.time.LocalDate diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt index 6af3aefb..fe4c4ee1 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt @@ -38,7 +38,6 @@ import com.sopt.clody.R import com.sopt.clody.core.review.InAppReviewManager import com.sopt.clody.data.remote.dto.response.DailyDiariesResponseDto import com.sopt.clody.data.remote.dto.response.MonthlyCalendarResponseDto -import com.sopt.clody.domain.model.ReplyStatus import com.sopt.clody.presentation.ui.component.FailureScreen import com.sopt.clody.presentation.ui.component.LoadingScreen import com.sopt.clody.presentation.ui.component.bottomsheet.DiaryDeleteSheet @@ -50,6 +49,7 @@ import com.sopt.clody.presentation.ui.component.toast.ClodyToastMessage import com.sopt.clody.presentation.ui.home.component.DailyStateButton import com.sopt.clody.presentation.ui.home.component.HomeTopAppBar import com.sopt.clody.presentation.ui.home.component.MonthlyCalendarAndDailyDiary +import com.sopt.clody.presentation.ui.type.ReplyStatus import com.sopt.clody.presentation.utils.amplitude.AmplitudeConstraints import com.sopt.clody.presentation.utils.amplitude.AmplitudeUtils import com.sopt.clody.presentation.utils.extension.toLocalizedMonthLabel diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt index de13a266..dffdc9e1 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt @@ -9,13 +9,13 @@ import com.sopt.clody.data.remote.dto.request.SendNotificationRequestDto import com.sopt.clody.data.remote.dto.response.DailyDiariesResponseDto import com.sopt.clody.data.remote.dto.response.MonthlyCalendarResponseDto import com.sopt.clody.data.remote.dto.response.NotificationInfoResponseDto -import com.sopt.clody.domain.model.ReplyStatus import com.sopt.clody.domain.repository.DiaryRepository import com.sopt.clody.domain.repository.DraftRepository import com.sopt.clody.domain.repository.NotificationRepository import com.sopt.clody.domain.repository.ReviewRepository import com.sopt.clody.presentation.ui.home.component.DiaryDateData import com.sopt.clody.presentation.ui.setting.notificationsetting.screen.NotificationChangeState +import com.sopt.clody.presentation.ui.type.ReplyStatus import com.sopt.clody.presentation.utils.network.ErrorMessageProvider import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/replydiary/ReplyDiaryScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/replydiary/ReplyDiaryScreen.kt index c391ff08..66e20c65 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/replydiary/ReplyDiaryScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/replydiary/ReplyDiaryScreen.kt @@ -35,7 +35,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import com.sopt.clody.R -import com.sopt.clody.domain.model.ReplyStatus +import com.sopt.clody.presentation.ui.type.ReplyStatus import com.sopt.clody.presentation.ui.component.FailureScreen import com.sopt.clody.presentation.ui.component.LoadingScreen import com.sopt.clody.presentation.utils.amplitude.AmplitudeConstraints diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/replydiary/navigation/ReplyDiaryNavigation.kt b/app/src/main/java/com/sopt/clody/presentation/ui/replydiary/navigation/ReplyDiaryNavigation.kt index 492ea5f2..6f23b20a 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/replydiary/navigation/ReplyDiaryNavigation.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/replydiary/navigation/ReplyDiaryNavigation.kt @@ -5,7 +5,7 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptionsBuilder import androidx.navigation.compose.composable import androidx.navigation.toRoute -import com.sopt.clody.domain.model.ReplyStatus +import com.sopt.clody.presentation.ui.type.ReplyStatus import com.sopt.clody.presentation.ui.replydiary.ReplyDiaryRoute import com.sopt.clody.presentation.utils.navigation.Route diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/replyloading/navigation/ReplyLoadingNavigation.kt b/app/src/main/java/com/sopt/clody/presentation/ui/replyloading/navigation/ReplyLoadingNavigation.kt index be1af5f9..86444c31 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/replyloading/navigation/ReplyLoadingNavigation.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/replyloading/navigation/ReplyLoadingNavigation.kt @@ -5,7 +5,7 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptionsBuilder import androidx.navigation.compose.composable import androidx.navigation.toRoute -import com.sopt.clody.domain.model.ReplyStatus +import com.sopt.clody.presentation.ui.type.ReplyStatus import com.sopt.clody.presentation.ui.replyloading.screen.ReplyLoadingRoute import com.sopt.clody.presentation.utils.navigation.Route diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/replyloading/screen/ReplyLoadingScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/replyloading/screen/ReplyLoadingScreen.kt index 5b931081..700aa1d9 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/replyloading/screen/ReplyLoadingScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/replyloading/screen/ReplyLoadingScreen.kt @@ -37,7 +37,7 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import com.airbnb.lottie.compose.LottieConstants import com.sopt.clody.R -import com.sopt.clody.domain.model.ReplyStatus +import com.sopt.clody.presentation.ui.type.ReplyStatus import com.sopt.clody.presentation.ui.component.FailureScreen import com.sopt.clody.presentation.ui.component.LoadingScreen import com.sopt.clody.presentation.ui.component.button.ClodyButton diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/type/DiaryCloverType.kt b/app/src/main/java/com/sopt/clody/presentation/ui/type/DailyCloverType.kt similarity index 94% rename from app/src/main/java/com/sopt/clody/presentation/ui/type/DiaryCloverType.kt rename to app/src/main/java/com/sopt/clody/presentation/ui/type/DailyCloverType.kt index bb02178a..d1eb6ead 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/type/DiaryCloverType.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/type/DailyCloverType.kt @@ -3,7 +3,6 @@ package com.sopt.clody.presentation.ui.type import androidx.annotation.DrawableRes import com.sopt.clody.R import com.sopt.clody.data.remote.dto.response.MonthlyCalendarResponseDto -import com.sopt.clody.domain.model.ReplyStatus /** * DiaryData를 기반으로 해당 날짜에 보여줄 클로버 아이콘 타입을 반환. @@ -17,7 +16,7 @@ import com.sopt.clody.domain.model.ReplyStatus * - 이 외의 경우 기본값 👉 [UNGIVEN_CLOVER] */ -enum class DiaryCloverType(@DrawableRes val iconRes: Int) { +enum class DailyCloverType(@DrawableRes val iconRes: Int) { TODAY_UNWRITTEN(R.drawable.ic_home_today_unwritten_clover), TODAY_WRITTEN(R.drawable.ic_home_today_written_clover), UNGIVEN_CLOVER(R.drawable.ic_home_ungiven_clover), @@ -32,7 +31,7 @@ enum class DiaryCloverType(@DrawableRes val iconRes: Int) { fun getCalendarCloverType( diaryData: MonthlyCalendarResponseDto.Diary, isToday: Boolean, - ): DiaryCloverType { + ): DailyCloverType { val count = diaryData.diaryCount val reply = diaryData.replyStatus diff --git a/app/src/main/java/com/sopt/clody/domain/model/ReplyStatus.kt b/app/src/main/java/com/sopt/clody/presentation/ui/type/ReplyStatus.kt similarity index 84% rename from app/src/main/java/com/sopt/clody/domain/model/ReplyStatus.kt rename to app/src/main/java/com/sopt/clody/presentation/ui/type/ReplyStatus.kt index f1c5885f..d8e3ec2e 100644 --- a/app/src/main/java/com/sopt/clody/domain/model/ReplyStatus.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/type/ReplyStatus.kt @@ -1,4 +1,4 @@ -package com.sopt.clody.domain.model +package com.sopt.clody.presentation.ui.type import kotlinx.serialization.Serializable diff --git a/app/src/main/java/com/sopt/clody/presentation/utils/navigation/Route.kt b/app/src/main/java/com/sopt/clody/presentation/utils/navigation/Route.kt index d00a7cff..44dc6af2 100644 --- a/app/src/main/java/com/sopt/clody/presentation/utils/navigation/Route.kt +++ b/app/src/main/java/com/sopt/clody/presentation/utils/navigation/Route.kt @@ -1,6 +1,6 @@ package com.sopt.clody.presentation.utils.navigation -import com.sopt.clody.domain.model.ReplyStatus +import com.sopt.clody.presentation.ui.type.ReplyStatus import kotlinx.serialization.Serializable /** From 94588bb92d7116b90bd397818a8d394af37a2106 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Sun, 17 Aug 2025 18:05:07 +0900 Subject: [PATCH 03/20] =?UTF-8?q?[CHORE/#323]=20updateYearMonthAndLoadData?= =?UTF-8?q?=20=ED=95=A8=EC=88=98=EB=A5=BC=20=ED=86=B5=EC=9D=BC=ED=95=A9?= =?UTF-8?q?=EB=8B=88=EB=8B=A4.=20-=20=ED=99=88=ED=99=94=EB=A9=B4=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=97=B0/=EC=9B=94=EC=9D=B4=20=EB=B3=80=EA=B2=BD?= =?UTF-8?q?=EB=90=A0=20=EB=95=8C=20=EB=B3=80=EA=B2=BD=EB=90=9C=20=EC=9B=94?= =?UTF-8?q?=EC=9D=98=201=EC=9D=BC=EC=9D=84=20=EA=B8=B0=EC=A4=80=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=BA=98=EB=A6=B0=EB=8D=94+=EB=8D=B0=EC=9D=BC?= =?UTF-8?q?=EB=A6=AC=20=EC=9D=BC=EA=B8=B0=20=EC=A0=95=EB=B3=B4=EB=A5=BC=20?= =?UTF-8?q?=EB=B6=88=EB=9F=AC=EC=98=A4=EB=8A=94=EB=8D=B0=20=EA=B7=B8?= =?UTF-8?q?=EA=B2=83=EB=A7=8C=EC=9D=84=20=EC=9C=84=ED=95=B4=20=ED=95=A8?= =?UTF-8?q?=EC=88=98=EA=B0=80=20=ED=95=98=EB=82=98=20=EB=8D=94=20=EC=9E=88?= =?UTF-8?q?=EC=96=B4=EC=84=9C=201=EC=9D=BC=EC=9D=84=20=EB=84=98=EA=B8=B0?= =?UTF-8?q?=EB=8A=94=20=EC=8B=9D=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= =?UTF-8?q?=ED=95=98=EC=97=AC=20=ED=86=B5=EC=9D=BC=ED=96=88=EC=8A=B5?= =?UTF-8?q?=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/ui/home/screen/HomeScreen.kt | 2 +- .../ui/home/screen/HomeViewModel.kt | 28 +------------------ 2 files changed, 2 insertions(+), 28 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt index fe4c4ee1..13eed820 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt @@ -431,7 +431,7 @@ fun HomeScreen( selectedYear = selectedYear, selectedMonth = selectedMonth, onYearMonthSelected = { year, month -> - homeViewModel.updateYearMonthAndLoadData(year, month) + homeViewModel.updateYearMonthAndLoadData(year, month, day = 1) }, ) } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt index dffdc9e1..550a04b9 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt @@ -107,6 +107,7 @@ class HomeViewModel @Inject constructor( val hasDraft: StateFlow get() = _hasDraft private var isInitialized = false + private val loadDataMutex = Mutex() init { initialize() @@ -192,33 +193,6 @@ class HomeViewModel @Inject constructor( } } - private val loadDataMutex = Mutex() - fun updateYearMonthAndLoadData(year: Int, month: Int) { - viewModelScope.launch { - loadDataMutex.withLock { - val sameYm = _selectedDiaryDate.value.year == year && - _selectedDiaryDate.value.month == month - - val calendarLoaded = calendarState.value is CalendarState.Success - val dailyLoaded = dailyDiariesState.value is DailyDiariesState.Success - val selectedIsFirst = _selectedDate.value.dayOfMonth == 1 - val alreadyLoaded = sameYm && calendarLoaded && (selectedIsFirst && dailyLoaded) - - if (alreadyLoaded) return@withLock - - _selectedDiaryDate.value = DiaryDateData(year, month) - _selectedDate.value = LocalDate.of(year, month, 1) - - coroutineScope { - awaitAll( - async { loadCalendarData(year, month) }, - async { loadDailyDiariesData(year, month, 1) }, - ) - } - } - } - } - fun updateYearMonthAndLoadData(year: Int, month: Int, day: Int) { viewModelScope.launch { loadDataMutex.withLock { From acebc0701a97d215ec95a766950f7642c40a9d84 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Sun, 17 Aug 2025 18:15:59 +0900 Subject: [PATCH 04/20] =?UTF-8?q?[MOVE/#323]=20domain=20type=20=ED=8C=A8?= =?UTF-8?q?=ED=82=A4=EC=A7=80=EB=A5=BC=20=EB=A7=8C=EB=93=AD=EB=8B=88?= =?UTF-8?q?=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../data/remote/dto/response/MonthlyCalendarResponseDto.kt | 2 +- .../clody/data/remote/dto/response/MonthlyDiaryResponseDto.kt | 2 +- .../main/java/com/sopt/clody/domain/{ => type}/Notification.kt | 2 +- .../sopt/clody/{presentation/ui => domain}/type/ReplyStatus.kt | 2 +- .../clody/presentation/ui/diarylist/component/DailyDiaryCard.kt | 2 +- .../presentation/ui/diarylist/component/MonthlyDiaryList.kt | 2 +- .../presentation/ui/diarylist/navigation/DiaryListNavigation.kt | 2 +- .../clody/presentation/ui/diarylist/screen/DiaryListScreen.kt | 2 +- .../clody/presentation/ui/home/component/MonthlyCalendar.kt | 2 +- .../ui/home/component/MonthlyCalendarAndDailyDiary.kt | 2 +- .../clody/presentation/ui/home/navigation/HomeNavigation.kt | 2 +- .../com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt | 2 +- .../com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt | 2 +- .../sopt/clody/presentation/ui/replydiary/ReplyDiaryScreen.kt | 2 +- .../ui/replydiary/navigation/ReplyDiaryNavigation.kt | 2 +- .../ui/replyloading/navigation/ReplyLoadingNavigation.kt | 2 +- .../presentation/ui/replyloading/screen/ReplyLoadingScreen.kt | 2 +- .../notificationsetting/screen/NotificationSettingScreen.kt | 2 +- .../notificationsetting/screen/NotificationSettingViewModel.kt | 2 +- .../java/com/sopt/clody/presentation/ui/type/DailyCloverType.kt | 1 + .../java/com/sopt/clody/presentation/utils/navigation/Route.kt | 2 +- 21 files changed, 21 insertions(+), 20 deletions(-) rename app/src/main/java/com/sopt/clody/domain/{ => type}/Notification.kt (60%) rename app/src/main/java/com/sopt/clody/{presentation/ui => domain}/type/ReplyStatus.kt (84%) diff --git a/app/src/main/java/com/sopt/clody/data/remote/dto/response/MonthlyCalendarResponseDto.kt b/app/src/main/java/com/sopt/clody/data/remote/dto/response/MonthlyCalendarResponseDto.kt index 9742f1dc..c78b2a9f 100644 --- a/app/src/main/java/com/sopt/clody/data/remote/dto/response/MonthlyCalendarResponseDto.kt +++ b/app/src/main/java/com/sopt/clody/data/remote/dto/response/MonthlyCalendarResponseDto.kt @@ -1,6 +1,6 @@ package com.sopt.clody.data.remote.dto.response -import com.sopt.clody.presentation.ui.type.ReplyStatus +import com.sopt.clody.domain.type.ReplyStatus import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable diff --git a/app/src/main/java/com/sopt/clody/data/remote/dto/response/MonthlyDiaryResponseDto.kt b/app/src/main/java/com/sopt/clody/data/remote/dto/response/MonthlyDiaryResponseDto.kt index bdad746d..2b3efa08 100644 --- a/app/src/main/java/com/sopt/clody/data/remote/dto/response/MonthlyDiaryResponseDto.kt +++ b/app/src/main/java/com/sopt/clody/data/remote/dto/response/MonthlyDiaryResponseDto.kt @@ -1,6 +1,6 @@ package com.sopt.clody.data.remote.dto.response -import com.sopt.clody.presentation.ui.type.ReplyStatus +import com.sopt.clody.domain.type.ReplyStatus import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable diff --git a/app/src/main/java/com/sopt/clody/domain/Notification.kt b/app/src/main/java/com/sopt/clody/domain/type/Notification.kt similarity index 60% rename from app/src/main/java/com/sopt/clody/domain/Notification.kt rename to app/src/main/java/com/sopt/clody/domain/type/Notification.kt index 5135ab84..d4675a90 100644 --- a/app/src/main/java/com/sopt/clody/domain/Notification.kt +++ b/app/src/main/java/com/sopt/clody/domain/type/Notification.kt @@ -1,4 +1,4 @@ -package com.sopt.clody.domain +package com.sopt.clody.domain.type enum class Notification { DIARY, DRAFT, REPLY diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/type/ReplyStatus.kt b/app/src/main/java/com/sopt/clody/domain/type/ReplyStatus.kt similarity index 84% rename from app/src/main/java/com/sopt/clody/presentation/ui/type/ReplyStatus.kt rename to app/src/main/java/com/sopt/clody/domain/type/ReplyStatus.kt index d8e3ec2e..ced774b3 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/type/ReplyStatus.kt +++ b/app/src/main/java/com/sopt/clody/domain/type/ReplyStatus.kt @@ -1,4 +1,4 @@ -package com.sopt.clody.presentation.ui.type +package com.sopt.clody.domain.type import kotlinx.serialization.Serializable diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/component/DailyDiaryCard.kt b/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/component/DailyDiaryCard.kt index 3a8a3974..6fd28d80 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/component/DailyDiaryCard.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/component/DailyDiaryCard.kt @@ -28,7 +28,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.sopt.clody.R import com.sopt.clody.data.remote.dto.response.MonthlyDiaryResponseDto -import com.sopt.clody.presentation.ui.type.ReplyStatus +import com.sopt.clody.domain.type.ReplyStatus import com.sopt.clody.presentation.ui.diarylist.screen.DiaryListViewModel import com.sopt.clody.ui.theme.ClodyTheme diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/component/MonthlyDiaryList.kt b/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/component/MonthlyDiaryList.kt index 46007358..9bd2ddde 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/component/MonthlyDiaryList.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/component/MonthlyDiaryList.kt @@ -9,7 +9,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.sopt.clody.data.remote.dto.response.MonthlyDiaryResponseDto -import com.sopt.clody.presentation.ui.type.ReplyStatus +import com.sopt.clody.domain.type.ReplyStatus import com.sopt.clody.presentation.ui.diarylist.screen.DiaryListViewModel import com.sopt.clody.presentation.utils.extension.getDayOfWeek diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/navigation/DiaryListNavigation.kt b/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/navigation/DiaryListNavigation.kt index 6574514c..5c0cd5a6 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/navigation/DiaryListNavigation.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/navigation/DiaryListNavigation.kt @@ -4,7 +4,7 @@ import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable import androidx.navigation.toRoute -import com.sopt.clody.presentation.ui.type.ReplyStatus +import com.sopt.clody.domain.type.ReplyStatus import com.sopt.clody.presentation.ui.diarylist.screen.DiaryListRoute import com.sopt.clody.presentation.utils.navigation.Route diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/screen/DiaryListScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/screen/DiaryListScreen.kt index 9afbf62c..318be8d8 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/screen/DiaryListScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/screen/DiaryListScreen.kt @@ -12,7 +12,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.res.stringResource import androidx.hilt.navigation.compose.hiltViewModel import com.sopt.clody.R -import com.sopt.clody.presentation.ui.type.ReplyStatus +import com.sopt.clody.domain.type.ReplyStatus import com.sopt.clody.presentation.ui.component.FailureScreen import com.sopt.clody.presentation.ui.component.LoadingScreen import com.sopt.clody.presentation.ui.component.bottomsheet.DiaryDeleteSheet diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/component/MonthlyCalendar.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/MonthlyCalendar.kt index 77542c82..e450a3e7 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/component/MonthlyCalendar.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/MonthlyCalendar.kt @@ -26,7 +26,7 @@ import androidx.compose.ui.unit.dp import com.sopt.clody.R import com.sopt.clody.data.remote.dto.response.MonthlyCalendarResponseDto import com.sopt.clody.presentation.ui.type.DailyCloverType -import com.sopt.clody.presentation.ui.type.ReplyStatus +import com.sopt.clody.domain.type.ReplyStatus import com.sopt.clody.presentation.utils.amplitude.AmplitudeConstraints import com.sopt.clody.presentation.utils.amplitude.AmplitudeUtils import com.sopt.clody.ui.theme.ClodyTheme diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/component/MonthlyCalendarAndDailyDiary.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/MonthlyCalendarAndDailyDiary.kt index 21b223a6..91886d7b 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/component/MonthlyCalendarAndDailyDiary.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/MonthlyCalendarAndDailyDiary.kt @@ -26,7 +26,7 @@ import com.sopt.clody.presentation.ui.component.FailureScreen import com.sopt.clody.presentation.ui.component.LoadingScreen import com.sopt.clody.presentation.ui.home.screen.DailyDiariesState import com.sopt.clody.presentation.ui.home.screen.HomeViewModel -import com.sopt.clody.presentation.ui.type.ReplyStatus +import com.sopt.clody.domain.type.ReplyStatus import com.sopt.clody.ui.theme.ClodyTheme import java.time.LocalDate import java.time.YearMonth diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/navigation/HomeNavigation.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/navigation/HomeNavigation.kt index 16fd8a1f..ba00b0bc 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/navigation/HomeNavigation.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/navigation/HomeNavigation.kt @@ -5,7 +5,7 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptionsBuilder import androidx.navigation.compose.composable import androidx.navigation.toRoute -import com.sopt.clody.presentation.ui.type.ReplyStatus +import com.sopt.clody.domain.type.ReplyStatus import com.sopt.clody.presentation.ui.home.screen.HomeRoute import com.sopt.clody.presentation.utils.navigation.Route import java.time.LocalDate diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt index 13eed820..990befff 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt @@ -49,7 +49,7 @@ import com.sopt.clody.presentation.ui.component.toast.ClodyToastMessage import com.sopt.clody.presentation.ui.home.component.DailyStateButton import com.sopt.clody.presentation.ui.home.component.HomeTopAppBar import com.sopt.clody.presentation.ui.home.component.MonthlyCalendarAndDailyDiary -import com.sopt.clody.presentation.ui.type.ReplyStatus +import com.sopt.clody.domain.type.ReplyStatus import com.sopt.clody.presentation.utils.amplitude.AmplitudeConstraints import com.sopt.clody.presentation.utils.amplitude.AmplitudeUtils import com.sopt.clody.presentation.utils.extension.toLocalizedMonthLabel diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt index 550a04b9..5537bd7c 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt @@ -15,7 +15,7 @@ import com.sopt.clody.domain.repository.NotificationRepository import com.sopt.clody.domain.repository.ReviewRepository import com.sopt.clody.presentation.ui.home.component.DiaryDateData import com.sopt.clody.presentation.ui.setting.notificationsetting.screen.NotificationChangeState -import com.sopt.clody.presentation.ui.type.ReplyStatus +import com.sopt.clody.domain.type.ReplyStatus import com.sopt.clody.presentation.utils.network.ErrorMessageProvider import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/replydiary/ReplyDiaryScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/replydiary/ReplyDiaryScreen.kt index 66e20c65..798b03a3 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/replydiary/ReplyDiaryScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/replydiary/ReplyDiaryScreen.kt @@ -35,7 +35,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import com.sopt.clody.R -import com.sopt.clody.presentation.ui.type.ReplyStatus +import com.sopt.clody.domain.type.ReplyStatus import com.sopt.clody.presentation.ui.component.FailureScreen import com.sopt.clody.presentation.ui.component.LoadingScreen import com.sopt.clody.presentation.utils.amplitude.AmplitudeConstraints diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/replydiary/navigation/ReplyDiaryNavigation.kt b/app/src/main/java/com/sopt/clody/presentation/ui/replydiary/navigation/ReplyDiaryNavigation.kt index 6f23b20a..26f66d47 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/replydiary/navigation/ReplyDiaryNavigation.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/replydiary/navigation/ReplyDiaryNavigation.kt @@ -5,7 +5,7 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptionsBuilder import androidx.navigation.compose.composable import androidx.navigation.toRoute -import com.sopt.clody.presentation.ui.type.ReplyStatus +import com.sopt.clody.domain.type.ReplyStatus import com.sopt.clody.presentation.ui.replydiary.ReplyDiaryRoute import com.sopt.clody.presentation.utils.navigation.Route diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/replyloading/navigation/ReplyLoadingNavigation.kt b/app/src/main/java/com/sopt/clody/presentation/ui/replyloading/navigation/ReplyLoadingNavigation.kt index 86444c31..0edacf64 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/replyloading/navigation/ReplyLoadingNavigation.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/replyloading/navigation/ReplyLoadingNavigation.kt @@ -5,7 +5,7 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptionsBuilder import androidx.navigation.compose.composable import androidx.navigation.toRoute -import com.sopt.clody.presentation.ui.type.ReplyStatus +import com.sopt.clody.domain.type.ReplyStatus import com.sopt.clody.presentation.ui.replyloading.screen.ReplyLoadingRoute import com.sopt.clody.presentation.utils.navigation.Route diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/replyloading/screen/ReplyLoadingScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/replyloading/screen/ReplyLoadingScreen.kt index 700aa1d9..4564bfe8 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/replyloading/screen/ReplyLoadingScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/replyloading/screen/ReplyLoadingScreen.kt @@ -37,7 +37,7 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import com.airbnb.lottie.compose.LottieConstants import com.sopt.clody.R -import com.sopt.clody.presentation.ui.type.ReplyStatus +import com.sopt.clody.domain.type.ReplyStatus import com.sopt.clody.presentation.ui.component.FailureScreen import com.sopt.clody.presentation.ui.component.LoadingScreen import com.sopt.clody.presentation.ui.component.button.ClodyButton diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingScreen.kt index ce2542af..49e178c5 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingScreen.kt @@ -22,7 +22,7 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import com.sopt.clody.R import com.sopt.clody.data.remote.dto.response.NotificationInfoResponseDto -import com.sopt.clody.domain.Notification +import com.sopt.clody.domain.type.Notification import com.sopt.clody.presentation.ui.component.FailureScreen import com.sopt.clody.presentation.ui.component.LoadingScreen import com.sopt.clody.presentation.ui.component.dialog.FailureDialog diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingViewModel.kt index dfdc1d8b..0b4adfdb 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingViewModel.kt @@ -6,7 +6,7 @@ import androidx.lifecycle.viewModelScope import com.sopt.clody.core.network.NetworkConnectivityObserver import com.sopt.clody.core.network.NetworkStatus import com.sopt.clody.data.remote.dto.request.SendNotificationRequestDto -import com.sopt.clody.domain.Notification +import com.sopt.clody.domain.type.Notification import com.sopt.clody.domain.repository.NotificationRepository import com.sopt.clody.presentation.utils.extension.TimePeriod import com.sopt.clody.presentation.utils.extension.convertUTZtoKST diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/type/DailyCloverType.kt b/app/src/main/java/com/sopt/clody/presentation/ui/type/DailyCloverType.kt index d1eb6ead..55f044de 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/type/DailyCloverType.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/type/DailyCloverType.kt @@ -3,6 +3,7 @@ package com.sopt.clody.presentation.ui.type import androidx.annotation.DrawableRes import com.sopt.clody.R import com.sopt.clody.data.remote.dto.response.MonthlyCalendarResponseDto +import com.sopt.clody.domain.type.ReplyStatus /** * DiaryData를 기반으로 해당 날짜에 보여줄 클로버 아이콘 타입을 반환. diff --git a/app/src/main/java/com/sopt/clody/presentation/utils/navigation/Route.kt b/app/src/main/java/com/sopt/clody/presentation/utils/navigation/Route.kt index 44dc6af2..77a0922b 100644 --- a/app/src/main/java/com/sopt/clody/presentation/utils/navigation/Route.kt +++ b/app/src/main/java/com/sopt/clody/presentation/utils/navigation/Route.kt @@ -1,6 +1,6 @@ package com.sopt.clody.presentation.utils.navigation -import com.sopt.clody.presentation.ui.type.ReplyStatus +import com.sopt.clody.domain.type.ReplyStatus import kotlinx.serialization.Serializable /** From c436359e51b2d00cd3e7f7c58856d926ecbc653e Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Sun, 17 Aug 2025 18:53:38 +0900 Subject: [PATCH 05/20] =?UTF-8?q?[FEAT/#323]=20=EC=9D=BC=EA=B8=B0=EB=A5=BC?= =?UTF-8?q?=20=EC=9E=AC=EC=9E=91=EC=84=B1=20=ED=95=A0=20=EC=8B=9C=20?= =?UTF-8?q?=EC=A7=84=ED=95=9C=20=ED=81=B4=EB=A1=9C=EB=B2=84=EB=A1=9C=20?= =?UTF-8?q?=ED=91=9C=EA=B8=B0=EB=90=A9=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/sopt/clody/presentation/ui/type/DailyCloverType.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/type/DailyCloverType.kt b/app/src/main/java/com/sopt/clody/presentation/ui/type/DailyCloverType.kt index 55f044de..a581acc0 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/type/DailyCloverType.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/type/DailyCloverType.kt @@ -41,12 +41,13 @@ enum class DailyCloverType(@DrawableRes val iconRes: Int) { val hasDraft = reply == ReplyStatus.HAS_DRAFT val draftExpired = reply == ReplyStatus.INVALID_DRAFT val hasUnreadOrNoReply = reply.isUnreadOrNotRead + val isDeleted = diaryData.isDeleted return when { hasDraft -> DRAFT_SAVED isToday && noDiary -> TODAY_UNWRITTEN isToday && hasUnreadOrNoReply -> TODAY_WRITTEN - draftExpired -> EXPIRED_WRITTEN + isDeleted || draftExpired -> EXPIRED_WRITTEN hasDiary && hasUnreadOrNoReply -> UNGIVEN_CLOVER count in 1..2 -> BOTTOM_CLOVER count in 3..4 -> MID_CLOVER From 3eb4945ecde1616414f2903f0c848fb10df5e800 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Mon, 18 Aug 2025 13:30:26 +0900 Subject: [PATCH 06/20] =?UTF-8?q?[FEAT/#323]=20=ED=81=B4=EB=A1=9C=EB=B2=84?= =?UTF-8?q?=EC=99=80=20=ED=95=98=EB=8B=A8=EB=B2=84=ED=8A=BC=EC=97=90=20?= =?UTF-8?q?=EB=8C=80=ED=95=9C=20=ED=83=80=EC=9E=85=EC=9D=84=20=EC=A0=95?= =?UTF-8?q?=EC=9D=98=ED=95=A9=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/diarylist/component/DailyDiaryCard.kt | 2 +- .../presentation/ui/type/DailyButtonType.kt | 5 ++++ .../presentation/ui/type/DailyCloverType.kt | 26 +++++++++---------- ....xml => ic_home_disabled_reply_clover.xml} | 0 ...r.xml => ic_home_enabled_diary_clover.xml} | 0 ...r.xml => ic_home_waiting_reply_clover.xml} | 0 6 files changed, 19 insertions(+), 14 deletions(-) create mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/type/DailyButtonType.kt rename app/src/main/res/drawable/{ic_home_expired_written_clover.xml => ic_home_disabled_reply_clover.xml} (100%) rename app/src/main/res/drawable/{ic_home_today_unwritten_clover.xml => ic_home_enabled_diary_clover.xml} (100%) rename app/src/main/res/drawable/{ic_home_today_written_clover.xml => ic_home_waiting_reply_clover.xml} (100%) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/component/DailyDiaryCard.kt b/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/component/DailyDiaryCard.kt index 6fd28d80..37c3ab17 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/component/DailyDiaryCard.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/diarylist/component/DailyDiaryCard.kt @@ -47,7 +47,7 @@ fun DailyDiaryCard( val iconRes = when { dailyDiary.replyStatus == ReplyStatus.READY_NOT_READ && dailyDiary.diaryCount > 0 -> R.drawable.ic_home_ungiven_clover dailyDiary.replyStatus == ReplyStatus.UNREADY && dailyDiary.diaryCount > 0 -> R.drawable.ic_home_ungiven_clover - dailyDiary.replyStatus == ReplyStatus.INVALID_DRAFT -> R.drawable.ic_home_expired_written_clover + dailyDiary.replyStatus == ReplyStatus.INVALID_DRAFT -> R.drawable.ic_home_disabled_reply_clover dailyDiary.diaryCount == 0 -> R.drawable.ic_home_ungiven_clover dailyDiary.diaryCount in 1..2 -> R.drawable.ic_home_bottom_clover dailyDiary.diaryCount in 3..4 -> R.drawable.ic_home_mid_clover diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/type/DailyButtonType.kt b/app/src/main/java/com/sopt/clody/presentation/ui/type/DailyButtonType.kt new file mode 100644 index 00000000..ee310b6e --- /dev/null +++ b/app/src/main/java/com/sopt/clody/presentation/ui/type/DailyButtonType.kt @@ -0,0 +1,5 @@ +package com.sopt.clody.presentation.ui.type + +enum class DailyButtonType { + DRAFT_ENABLED, REPLY_ENABLED, REPLY_DISABLED, DIARY_ENABLED, DIARY_DISABLED; +} diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/type/DailyCloverType.kt b/app/src/main/java/com/sopt/clody/presentation/ui/type/DailyCloverType.kt index a581acc0..94325fd3 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/type/DailyCloverType.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/type/DailyCloverType.kt @@ -8,24 +8,24 @@ import com.sopt.clody.domain.type.ReplyStatus /** * DiaryData를 기반으로 해당 날짜에 보여줄 클로버 아이콘 타입을 반환. * - * - 오늘이고 일기가 없으면 👉 [TODAY_UNWRITTEN] - * - 오늘이고 일기와 읽지 않은 답장이 있으면 👉 [TODAY_WRITTEN] + * - 오늘이고 일기가 없으면 👉 [ENABLED_DIARY] + * - 오늘이고 일기와 읽지 않은 답장이 있으면 👉 [WAITING_REPLY] * - 임시저장이 존재하면 👉 [DRAFT_SAVED] - * - 임시저장이 만료되었으면 👉 [EXPIRED_WRITTEN] + * - 임시저장이 만료되었으면 👉 [DISABLED_REPLY] * - 일기가 있고 답장이 없거나 읽지 않았으면 👉 [UNGIVEN_CLOVER] * - 일기 수에 따라 👉 [BOTTOM_CLOVER], [MID_CLOVER], [TOP_CLOVER] 구분 * - 이 외의 경우 기본값 👉 [UNGIVEN_CLOVER] */ enum class DailyCloverType(@DrawableRes val iconRes: Int) { - TODAY_UNWRITTEN(R.drawable.ic_home_today_unwritten_clover), - TODAY_WRITTEN(R.drawable.ic_home_today_written_clover), + DRAFT_SAVED(R.drawable.ic_home_draft_saved_clover), + DISABLED_REPLY(R.drawable.ic_home_disabled_reply_clover), + ENABLED_DIARY(R.drawable.ic_home_enabled_diary_clover), + WAITING_REPLY(R.drawable.ic_home_waiting_reply_clover), UNGIVEN_CLOVER(R.drawable.ic_home_ungiven_clover), BOTTOM_CLOVER(R.drawable.ic_home_bottom_clover), MID_CLOVER(R.drawable.ic_home_mid_clover), TOP_CLOVER(R.drawable.ic_home_top_clover), - DRAFT_SAVED(R.drawable.ic_home_draft_saved_clover), - EXPIRED_WRITTEN(R.drawable.ic_home_expired_written_clover), ; companion object { @@ -45,13 +45,13 @@ enum class DailyCloverType(@DrawableRes val iconRes: Int) { return when { hasDraft -> DRAFT_SAVED - isToday && noDiary -> TODAY_UNWRITTEN - isToday && hasUnreadOrNoReply -> TODAY_WRITTEN - isDeleted || draftExpired -> EXPIRED_WRITTEN + isDeleted || draftExpired -> DISABLED_REPLY + isToday && noDiary -> ENABLED_DIARY + isToday && hasUnreadOrNoReply -> WAITING_REPLY hasDiary && hasUnreadOrNoReply -> UNGIVEN_CLOVER - count in 1..2 -> BOTTOM_CLOVER - count in 3..4 -> MID_CLOVER - count >= 5 -> TOP_CLOVER + reply == ReplyStatus.READY_READ && count in 1..2 -> BOTTOM_CLOVER + reply == ReplyStatus.READY_READ && count in 3..4 -> MID_CLOVER + reply == ReplyStatus.READY_READ && count >= 5 -> TOP_CLOVER else -> UNGIVEN_CLOVER } } diff --git a/app/src/main/res/drawable/ic_home_expired_written_clover.xml b/app/src/main/res/drawable/ic_home_disabled_reply_clover.xml similarity index 100% rename from app/src/main/res/drawable/ic_home_expired_written_clover.xml rename to app/src/main/res/drawable/ic_home_disabled_reply_clover.xml diff --git a/app/src/main/res/drawable/ic_home_today_unwritten_clover.xml b/app/src/main/res/drawable/ic_home_enabled_diary_clover.xml similarity index 100% rename from app/src/main/res/drawable/ic_home_today_unwritten_clover.xml rename to app/src/main/res/drawable/ic_home_enabled_diary_clover.xml diff --git a/app/src/main/res/drawable/ic_home_today_written_clover.xml b/app/src/main/res/drawable/ic_home_waiting_reply_clover.xml similarity index 100% rename from app/src/main/res/drawable/ic_home_today_written_clover.xml rename to app/src/main/res/drawable/ic_home_waiting_reply_clover.xml From 9cb6ff45cc05180fe957cc7d4abda9ca89ff5b98 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Mon, 18 Aug 2025 14:22:53 +0900 Subject: [PATCH 07/20] =?UTF-8?q?[FEAT/#323]=20=EC=97=B0/=EC=9B=94?= =?UTF-8?q?=EC=9D=B4=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20=EB=90=98?= =?UTF-8?q?=EA=B1=B0=EB=82=98,=20=EC=9D=BC=EC=9E=90=EA=B0=80=20=ED=81=B4?= =?UTF-8?q?=EB=A6=AD=EB=90=98=EB=A9=B4=20=ED=99=88=20=EC=A0=84=EC=B2=B4?= =?UTF-8?q?=EC=9D=98=20=EC=A0=95=EB=B3=B4=EB=A5=BC=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=ED=95=A9=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../clody/domain/model/MonthlyCalendarInfo.kt | 20 +++ .../domain/usecase/LoadHomeDataUseCase.kt | 55 ++++++++ .../ui/home/component/DailyDiary.kt | 11 +- .../ui/home/component/MonthlyCalendar.kt | 5 +- .../component/MonthlyCalendarAndDailyDiary.kt | 38 ++---- .../ui/home/screen/CalendarState.kt | 8 -- .../ui/home/screen/DailyDiariesState.kt | 8 -- .../presentation/ui/home/screen/HomeScreen.kt | 41 +++--- .../ui/home/screen/HomeUiState.kt | 8 ++ .../ui/home/screen/HomeViewModel.kt | 125 ++++++------------ .../presentation/ui/type/DailyCloverType.kt | 3 +- 11 files changed, 163 insertions(+), 159 deletions(-) create mode 100644 app/src/main/java/com/sopt/clody/domain/model/MonthlyCalendarInfo.kt create mode 100644 app/src/main/java/com/sopt/clody/domain/usecase/LoadHomeDataUseCase.kt delete mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/home/screen/CalendarState.kt delete mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/home/screen/DailyDiariesState.kt create mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeUiState.kt diff --git a/app/src/main/java/com/sopt/clody/domain/model/MonthlyCalendarInfo.kt b/app/src/main/java/com/sopt/clody/domain/model/MonthlyCalendarInfo.kt new file mode 100644 index 00000000..c8c11d22 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/domain/model/MonthlyCalendarInfo.kt @@ -0,0 +1,20 @@ +package com.sopt.clody.domain.model + +import com.sopt.clody.domain.type.ReplyStatus +import java.time.LocalDate + +data class MonthlyCalendarInfo( + val year: Int = LocalDate.now().year, + val month: Int = LocalDate.now().monthValue, + val totalCloverCount: Int = 0, + val dailyDiaryInfoList: List = listOf() +) { + data class DailyDiaryInfo( + val diaryCount: Int = 0, + val replyStatus: ReplyStatus = ReplyStatus.UNREADY, + val date: String = "", + val diaryList: List = listOf(), + val isDeleted: Boolean = false, + val isDraft: Boolean = false + ) +} diff --git a/app/src/main/java/com/sopt/clody/domain/usecase/LoadHomeDataUseCase.kt b/app/src/main/java/com/sopt/clody/domain/usecase/LoadHomeDataUseCase.kt new file mode 100644 index 00000000..63ed73d4 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/domain/usecase/LoadHomeDataUseCase.kt @@ -0,0 +1,55 @@ +package com.sopt.clody.domain.usecase + +import com.sopt.clody.domain.model.MonthlyCalendarInfo +import com.sopt.clody.domain.repository.DiaryRepository +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope +import javax.inject.Inject + +class LoadHomeDataUseCase @Inject constructor( + private val diaryRepository: DiaryRepository, +) { + suspend operator fun invoke(year: Int, month: Int, day: Int): Result = + coroutineScope { + try { + val monthlyDiaryInfoDeferred = + async { diaryRepository.getMonthlyCalendarData(year, month) } + val dailyDiaryInfoDeferred = + async { diaryRepository.getDailyDiariesData(year, month, day) } + + val monthlyDiaryInfo = monthlyDiaryInfoDeferred.await() + val dailyDiaryInfo = dailyDiaryInfoDeferred.await() + + if (monthlyDiaryInfo.isSuccess && dailyDiaryInfo.isSuccess) { + Result.success( + MonthlyCalendarInfo( + year = year, + month = month, + totalCloverCount = monthlyDiaryInfo.getOrThrow().totalCloverCount, + dailyDiaryInfoList = monthlyDiaryInfo.getOrThrow().diaries.map { it -> + MonthlyCalendarInfo.DailyDiaryInfo( + diaryCount = it.diaryCount, + replyStatus = it.replyStatus, + date = it.date, + diaryList = dailyDiaryInfo.getOrThrow().diaries.map { it.content }, + isDeleted = dailyDiaryInfo.getOrThrow().isDeleted, + isDraft = dailyDiaryInfo.getOrThrow().isDraft + ) + } + ) + ) + } else { + Result.failure( + Exception( + "Failed to load monthly diary info or daily diary info." + + "monthlyDiaryInfo: ${monthlyDiaryInfo.exceptionOrNull()?.message}, " + + "dailyDiaryInfo: ${dailyDiaryInfo.exceptionOrNull()?.message}" + ) + ) + } + } catch (e: Exception) { + Result.failure(e) + } + } +} + diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/component/DailyDiary.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/DailyDiary.kt index 1766c06b..e67dba91 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/component/DailyDiary.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/DailyDiary.kt @@ -24,6 +24,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import com.sopt.clody.R import com.sopt.clody.data.remote.dto.response.DailyDiariesResponseDto +import com.sopt.clody.domain.model.MonthlyCalendarInfo import com.sopt.clody.ui.theme.ClodyTheme import kotlinx.datetime.DayOfWeek import java.time.LocalDate @@ -33,7 +34,7 @@ import java.time.format.TextStyle fun DailyDiary( date: LocalDate, dayOfWeek: DayOfWeek, - dailyDiary: DailyDiariesResponseDto, + dailyDiary: MonthlyCalendarInfo.DailyDiaryInfo, onShowDiaryDeleteStateChange: (Boolean) -> Unit, ) { Column( @@ -64,7 +65,7 @@ fun DailyDiary( modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp), ) Spacer(modifier = Modifier.weight(1f)) - if (dailyDiary.diaries.isNotEmpty()) { + if (dailyDiary.diaryList.isNotEmpty()) { Image( painter = painterResource(id = R.drawable.ic_home_kebab), contentDescription = "go to delete", @@ -92,7 +93,7 @@ fun DailyDiary( } } - dailyDiary.diaries.isEmpty() -> { + dailyDiary.diaryList.isEmpty() -> { Box( contentAlignment = Alignment.Center, modifier = Modifier @@ -109,8 +110,8 @@ fun DailyDiary( } else -> { - dailyDiary.diaries.forEachIndexed { index, diary -> - DiaryItem(index = index + 1, text = diary.content) + dailyDiary.diaryList.forEachIndexed { index, diary -> + DiaryItem(index = index + 1, text = diary) } } } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/component/MonthlyCalendar.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/MonthlyCalendar.kt index e450a3e7..13e3b125 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/component/MonthlyCalendar.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/MonthlyCalendar.kt @@ -25,6 +25,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import com.sopt.clody.R import com.sopt.clody.data.remote.dto.response.MonthlyCalendarResponseDto +import com.sopt.clody.domain.model.MonthlyCalendarInfo import com.sopt.clody.presentation.ui.type.DailyCloverType import com.sopt.clody.domain.type.ReplyStatus import com.sopt.clody.presentation.utils.amplitude.AmplitudeConstraints @@ -39,7 +40,7 @@ fun MonthlyCalendar( dateList: List, selectedDate: LocalDate, onDayClick: (LocalDate) -> Unit, - getDiaryDataForDate: (LocalDate) -> MonthlyCalendarResponseDto.Diary?, + getDiaryDataForDate: (LocalDate) -> MonthlyCalendarInfo.DailyDiaryInfo?, ) { val locale = LocalConfiguration.current.locales[0] val days = remember { @@ -140,7 +141,7 @@ fun DailyClover( date: LocalDate, onDayClick: (LocalDate) -> Unit, isSelected: Boolean, - diaryData: MonthlyCalendarResponseDto.Diary, + diaryData: MonthlyCalendarInfo.DailyDiaryInfo, modifier: Modifier = Modifier, ) { val today = LocalDate.now() diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/component/MonthlyCalendarAndDailyDiary.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/MonthlyCalendarAndDailyDiary.kt index 91886d7b..ac1c628f 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/component/MonthlyCalendarAndDailyDiary.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/MonthlyCalendarAndDailyDiary.kt @@ -20,13 +20,9 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import com.sopt.clody.R -import com.sopt.clody.data.remote.dto.response.DailyDiariesResponseDto -import com.sopt.clody.data.remote.dto.response.MonthlyCalendarResponseDto -import com.sopt.clody.presentation.ui.component.FailureScreen -import com.sopt.clody.presentation.ui.component.LoadingScreen -import com.sopt.clody.presentation.ui.home.screen.DailyDiariesState -import com.sopt.clody.presentation.ui.home.screen.HomeViewModel +import com.sopt.clody.domain.model.MonthlyCalendarInfo import com.sopt.clody.domain.type.ReplyStatus +import com.sopt.clody.presentation.ui.home.screen.HomeViewModel import com.sopt.clody.ui.theme.ClodyTheme import java.time.LocalDate import java.time.YearMonth @@ -37,12 +33,11 @@ fun MonthlyCalendarAndDailyDiary( selectedMonth: Int, cloverCount: Int, homeViewModel: HomeViewModel, - diaries: List, + diaries: List, onShowDiaryDeleteStateChange: (Boolean) -> Unit, selectedDate: LocalDate, onDiaryDataUpdated: (Int, ReplyStatus) -> Unit, modifier: Modifier = Modifier, - dailyDiariesState: DailyDiariesState, ) { val scrollState = rememberScrollState() val currentMonth = YearMonth.of(selectedYear, selectedMonth) @@ -107,27 +102,12 @@ fun MonthlyCalendarAndDailyDiary( .padding(top = 30.dp, bottom = 20.dp), ) - when (dailyDiariesState) { - is DailyDiariesState.Idle -> { - } - - is DailyDiariesState.Loading -> { - LoadingScreen() - } - - is DailyDiariesState.Success -> { - DailyDiary( - date = selectedDate, - dayOfWeek = initialDayOfWeek, - dailyDiary = dailyDiariesState.data, - onShowDiaryDeleteStateChange = onShowDiaryDeleteStateChange, - ) - } - - is DailyDiariesState.Error -> { - FailureScreen() - } - } + DailyDiary( + date = selectedDate, + dayOfWeek = initialDayOfWeek, + dailyDiary = diaries[selectedDate.dayOfMonth - 1], + onShowDiaryDeleteStateChange = onShowDiaryDeleteStateChange, + ) } } } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/CalendarState.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/CalendarState.kt deleted file mode 100644 index 054fecb9..00000000 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/CalendarState.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.sopt.clody.presentation.ui.home.screen - -sealed class CalendarState { - object Idle : CalendarState() - object Loading : CalendarState() - data class Success(val data: T) : CalendarState() - data class Error(val message: String) : CalendarState() -} diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/DailyDiariesState.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/DailyDiariesState.kt deleted file mode 100644 index 4cdc7409..00000000 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/DailyDiariesState.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.sopt.clody.presentation.ui.home.screen - -sealed class DailyDiariesState { - object Idle : DailyDiariesState() - object Loading : DailyDiariesState() - data class Success(val data: T) : DailyDiariesState() - data class Error(val message: String) : DailyDiariesState() -} diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt index 990befff..84765df1 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt @@ -38,6 +38,7 @@ import com.sopt.clody.R import com.sopt.clody.core.review.InAppReviewManager import com.sopt.clody.data.remote.dto.response.DailyDiariesResponseDto import com.sopt.clody.data.remote.dto.response.MonthlyCalendarResponseDto +import com.sopt.clody.domain.model.MonthlyCalendarInfo import com.sopt.clody.presentation.ui.component.FailureScreen import com.sopt.clody.presentation.ui.component.LoadingScreen import com.sopt.clody.presentation.ui.component.bottomsheet.DiaryDeleteSheet @@ -73,8 +74,7 @@ fun HomeRoute( ) -> Unit, homeViewModel: HomeViewModel = hiltViewModel(), ) { - val calendarState by homeViewModel.calendarState.collectAsStateWithLifecycle() - val dailyDiariesState by homeViewModel.dailyDiariesState.collectAsStateWithLifecycle() + val homeUiState by homeViewModel.homeUiState.collectAsStateWithLifecycle() val replyStatus by homeViewModel.replyStatus.collectAsStateWithLifecycle() val showFirstDraftPopup by homeViewModel.showFirstDraftPopup.collectAsStateWithLifecycle() val draftAlarmEnableToast by homeViewModel.draftAlarmEnableToast.collectAsStateWithLifecycle() @@ -131,7 +131,7 @@ fun HomeRoute( } LaunchedEffect(Unit) { - homeViewModel.updateYearMonthAndLoadData( + homeViewModel.loadMonthlyCalendarInfo( selectedDiaryDate.year, selectedDiaryDate.month, selectedDate.dayOfMonth, @@ -142,7 +142,7 @@ fun HomeRoute( FailureScreen( message = errorMessage, confirmAction = { - homeViewModel.updateYearMonthAndLoadData( + homeViewModel.loadMonthlyCalendarInfo( selectedDiaryDate.year, selectedDiaryDate.month, selectedDate.dayOfMonth, @@ -152,8 +152,7 @@ fun HomeRoute( } else { HomeScreen( homeViewModel = homeViewModel, - calendarState = calendarState, - dailyDiariesState = dailyDiariesState, + homeUiState = homeUiState, deleteDiaryState = deleteDiaryState, showYearMonthPickerState = showYearMonthPickerState, onClickDiaryList = navigateToDiaryList, @@ -318,8 +317,7 @@ fun HomeRoute( @Composable fun HomeScreen( homeViewModel: HomeViewModel, - calendarState: CalendarState, - dailyDiariesState: DailyDiariesState, + homeUiState: HomeUiState, deleteDiaryState: DeleteDiaryState, showYearMonthPickerState: Boolean, onClickDiaryList: (Int, Int) -> Unit, @@ -354,32 +352,31 @@ fun HomeScreen( }, containerColor = ClodyTheme.colors.white, content = { innerPadding -> - when (calendarState) { - is CalendarState.Idle -> {} + when (homeUiState) { + is HomeUiState.Idle -> {} - is CalendarState.Loading -> { + is HomeUiState.Loading -> { LoadingScreen() } - is CalendarState.Success -> { + is HomeUiState.Success -> { MonthlyCalendarAndDailyDiary( - selectedYear = selectedYear, - selectedMonth = selectedMonth, - cloverCount = calendarState.data.totalCloverCount, - diaries = calendarState.data.diaries, + selectedYear = homeUiState.data.year, + selectedMonth = homeUiState.data.month, + cloverCount = homeUiState.data.totalCloverCount, + diaries = homeUiState.data.dailyDiaryInfoList, homeViewModel = homeViewModel, onShowDiaryDeleteStateChange = { newState -> homeViewModel.setShowDiaryDeleteState(newState) }, selectedDate = selectedDate, onDiaryDataUpdated = { _, _ -> - homeViewModel.updateDiaryState(calendarState.data.diaries) + homeViewModel.updateDiaryState(homeUiState.data.dailyDiaryInfoList) }, - modifier = Modifier.padding(innerPadding), - dailyDiariesState = dailyDiariesState, + modifier = Modifier.padding(innerPadding) ) } - is CalendarState.Error -> { - homeViewModel.setErrorState(true, calendarState.message) + is HomeUiState.Error -> { + homeViewModel.setErrorState(true, homeUiState.message) } } @@ -431,7 +428,7 @@ fun HomeScreen( selectedYear = selectedYear, selectedMonth = selectedMonth, onYearMonthSelected = { year, month -> - homeViewModel.updateYearMonthAndLoadData(year, month, day = 1) + homeViewModel.loadMonthlyCalendarInfo(year, month, day = 1) }, ) } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeUiState.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeUiState.kt new file mode 100644 index 00000000..9adfacc6 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeUiState.kt @@ -0,0 +1,8 @@ +package com.sopt.clody.presentation.ui.home.screen + +sealed class HomeUiState { + data object Idle : HomeUiState() + data object Loading : HomeUiState() + data class Success(val data: T) : HomeUiState() + data class Error(val message: String) : HomeUiState() +} diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt index 5537bd7c..31ce2bfd 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt @@ -6,28 +6,25 @@ import com.sopt.clody.core.fcm.FcmTokenProvider import com.sopt.clody.core.network.NetworkConnectivityObserver import com.sopt.clody.core.network.NetworkStatus import com.sopt.clody.data.remote.dto.request.SendNotificationRequestDto -import com.sopt.clody.data.remote.dto.response.DailyDiariesResponseDto import com.sopt.clody.data.remote.dto.response.MonthlyCalendarResponseDto import com.sopt.clody.data.remote.dto.response.NotificationInfoResponseDto +import com.sopt.clody.domain.model.MonthlyCalendarInfo import com.sopt.clody.domain.repository.DiaryRepository import com.sopt.clody.domain.repository.DraftRepository import com.sopt.clody.domain.repository.NotificationRepository import com.sopt.clody.domain.repository.ReviewRepository +import com.sopt.clody.domain.type.ReplyStatus +import com.sopt.clody.domain.usecase.LoadHomeDataUseCase import com.sopt.clody.presentation.ui.home.component.DiaryDateData import com.sopt.clody.presentation.ui.setting.notificationsetting.screen.NotificationChangeState -import com.sopt.clody.domain.type.ReplyStatus import com.sopt.clody.presentation.utils.network.ErrorMessageProvider import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext import java.time.LocalDate import java.time.ZoneId @@ -42,14 +39,11 @@ class HomeViewModel @Inject constructor( private val reviewRepository: ReviewRepository, private val errorMessageProvider: ErrorMessageProvider, private val networkConnectivityObserver: NetworkConnectivityObserver, + private val loadHomeDataUseCase: LoadHomeDataUseCase, ) : ViewModel() { - private val _calendarState = MutableStateFlow>(CalendarState.Idle) - val calendarState: StateFlow> get() = _calendarState - - private val _dailyDiariesState = - MutableStateFlow>(DailyDiariesState.Idle) - val dailyDiariesState: StateFlow> get() = _dailyDiariesState + private val _homeUiState = MutableStateFlow>(HomeUiState.Idle) + val homeUiState: StateFlow> get() = _homeUiState private val _deleteDiaryState = MutableStateFlow(DeleteDiaryState.Idle) val deleteDiaryState: StateFlow get() = _deleteDiaryState @@ -107,7 +101,6 @@ class HomeViewModel @Inject constructor( val hasDraft: StateFlow get() = _hasDraft private var isInitialized = false - private val loadDataMutex = Mutex() init { initialize() @@ -126,49 +119,40 @@ class HomeViewModel @Inject constructor( _errorState.value = isError to message } - private suspend fun loadCalendarData(year: Int, month: Int) { - if (!isNetworkAvailable()) { - setErrorState(true, errorMessageProvider.getNetworkError()) - return - } - _calendarState.value = CalendarState.Loading - val result = withContext(Dispatchers.IO) { - diaryRepository.getMonthlyCalendarData(year, month) - } - _calendarState.value = result.fold( - onSuccess = { - setErrorState(false) - CalendarState.Success(it) - }, - onFailure = { - setErrorState(true, errorMessageProvider.getTemporaryError()) - CalendarState.Error(errorMessageProvider.getTemporaryError()) - }, - ) - } + fun loadMonthlyCalendarInfo(year: Int, month: Int, day: Int) { + _homeUiState.value = HomeUiState.Loading + viewModelScope.launch { + if (!isNetworkAvailable()) { + setErrorState(true, errorMessageProvider.getNetworkError()) + return@launch + } - private suspend fun loadDailyDiariesData(year: Int, month: Int, date: Int) { - if (!isNetworkAvailable()) { - setErrorState(true, errorMessageProvider.getNetworkError()) - return - } - _dailyDiariesState.value = DailyDiariesState.Loading - val result = withContext(Dispatchers.IO) { - diaryRepository.getDailyDiariesData(year, month, date) + val sameYmd = _selectedDiaryDate.value.year == year && + _selectedDiaryDate.value.month == month && + _selectedDate.value.dayOfMonth == day + val homeUiLoaded = _homeUiState.value is HomeUiState.Success + val alreadyLoaded = sameYmd && homeUiLoaded + + if (alreadyLoaded) return@launch + + _selectedDiaryDate.value = DiaryDateData(year, month) + _selectedDate.value = LocalDate.of(year, month, day) + + val result = withContext(Dispatchers.IO) { + loadHomeDataUseCase(year, month, day) + } + + _homeUiState.value = result.fold( + onSuccess = { + setErrorState(false) + HomeUiState.Success(it) + }, + onFailure = { + setErrorState(true, errorMessageProvider.getTemporaryError()) + HomeUiState.Error(errorMessageProvider.getTemporaryError()) + } + ) } - _dailyDiariesState.value = result.fold( - onSuccess = { dailyResponse -> - _hasDraft.value = dailyResponse.isDraft - _diaryCount.value = dailyResponse.diaries.size - _isDeleted.value = dailyResponse.isDeleted - setErrorState(false) - DailyDiariesState.Success(dailyResponse) - }, - onFailure = { - setErrorState(true, errorMessageProvider.getTemporaryError()) - DailyDiariesState.Error(errorMessageProvider.getTemporaryError()) - }, - ) } fun deleteDailyDiary(year: Int, month: Int, day: Int) { @@ -179,8 +163,7 @@ class HomeViewModel @Inject constructor( } _deleteDiaryResult.value = result.fold( onSuccess = { - loadCalendarData(year, month) - loadDailyDiariesData(year, month, day) + loadHomeDataUseCase(year, month, day) _diaryCount.value = 0 _isDeleted.value = false _replyStatus.value = ReplyStatus.UNREADY @@ -193,40 +176,14 @@ class HomeViewModel @Inject constructor( } } - fun updateYearMonthAndLoadData(year: Int, month: Int, day: Int) { - viewModelScope.launch { - loadDataMutex.withLock { - val sameYmd = _selectedDiaryDate.value.year == year && - _selectedDiaryDate.value.month == month && - _selectedDate.value.dayOfMonth == day - - val calendarLoaded = calendarState.value is CalendarState.Success - val dailyLoaded = dailyDiariesState.value is DailyDiariesState.Success - val alreadyLoaded = sameYmd && calendarLoaded && dailyLoaded - - if (alreadyLoaded) return@withLock - - _selectedDiaryDate.value = DiaryDateData(year, month) - _selectedDate.value = LocalDate.of(year, month, day) - - coroutineScope { - awaitAll( - async { loadCalendarData(year, month) }, - async { loadDailyDiariesData(year, month, day) }, - ) - } - } - } - } - fun updateSelectedDate(date: LocalDate) { _selectedDate.value = date viewModelScope.launch { - loadDailyDiariesData(date.year, date.monthValue, date.dayOfMonth) + loadHomeDataUseCase(date.year, date.monthValue, date.dayOfMonth) } } - fun updateDiaryState(diaries: List) { + fun updateDiaryState(diaries: List) { val selectedDiary = diaries.getOrNull(_selectedDate.value.dayOfMonth - 1) _diaryCount.value = selectedDiary?.diaryCount ?: 0 _replyStatus.value = selectedDiary?.replyStatus ?: ReplyStatus.UNREADY diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/type/DailyCloverType.kt b/app/src/main/java/com/sopt/clody/presentation/ui/type/DailyCloverType.kt index 94325fd3..05b0e7ac 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/type/DailyCloverType.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/type/DailyCloverType.kt @@ -3,6 +3,7 @@ package com.sopt.clody.presentation.ui.type import androidx.annotation.DrawableRes import com.sopt.clody.R import com.sopt.clody.data.remote.dto.response.MonthlyCalendarResponseDto +import com.sopt.clody.domain.model.MonthlyCalendarInfo import com.sopt.clody.domain.type.ReplyStatus /** @@ -30,7 +31,7 @@ enum class DailyCloverType(@DrawableRes val iconRes: Int) { companion object { fun getCalendarCloverType( - diaryData: MonthlyCalendarResponseDto.Diary, + diaryData: MonthlyCalendarInfo.DailyDiaryInfo, isToday: Boolean, ): DailyCloverType { val count = diaryData.diaryCount From c870328c1f385bf46eb00d5ef4eceeb5f147cfb5 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Mon, 18 Aug 2025 14:46:06 +0900 Subject: [PATCH 08/20] =?UTF-8?q?[FEAT/#323]=20=ED=99=88=ED=99=94=EB=A9=B4?= =?UTF-8?q?=20=EC=9D=BC=EC=9D=BC=20=EC=9D=BC=EA=B8=B0=20=EC=83=81=ED=83=9C?= =?UTF-8?q?=EC=97=90=20=EB=8C=80=ED=95=9C=20=ED=95=98=EB=8B=A8=20=EB=B2=84?= =?UTF-8?q?=ED=8A=BC=EC=9D=98=20=ED=83=80=EC=9E=85=EC=9D=84=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=ED=95=98=EA=B3=A0,=20=EB=B6=84=EA=B8=B0=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20=EB=A1=9C=EC=A7=81=EC=9D=84=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=ED=95=A9=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/ui/type/DailyButtonType.kt | 5 ---- .../ui/type/DailyStateButtonType.kt | 24 +++++++++++++++++++ 2 files changed, 24 insertions(+), 5 deletions(-) delete mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/type/DailyButtonType.kt create mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/type/DailyStateButtonType.kt diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/type/DailyButtonType.kt b/app/src/main/java/com/sopt/clody/presentation/ui/type/DailyButtonType.kt deleted file mode 100644 index ee310b6e..00000000 --- a/app/src/main/java/com/sopt/clody/presentation/ui/type/DailyButtonType.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.sopt.clody.presentation.ui.type - -enum class DailyButtonType { - DRAFT_ENABLED, REPLY_ENABLED, REPLY_DISABLED, DIARY_ENABLED, DIARY_DISABLED; -} diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/type/DailyStateButtonType.kt b/app/src/main/java/com/sopt/clody/presentation/ui/type/DailyStateButtonType.kt new file mode 100644 index 00000000..8b6294cd --- /dev/null +++ b/app/src/main/java/com/sopt/clody/presentation/ui/type/DailyStateButtonType.kt @@ -0,0 +1,24 @@ +package com.sopt.clody.presentation.ui.type + +import com.sopt.clody.domain.model.MonthlyCalendarInfo +import com.sopt.clody.domain.type.ReplyStatus + +enum class DailyStateButtonType { + DRAFT_ENABLED, REPLY_ENABLED, REPLY_DISABLED, DIARY_ENABLED, DIARY_DISABLED; + + companion object { + fun getButtonType( + info: MonthlyCalendarInfo.DailyDiaryInfo, + enableWriteDiary: Boolean, + ): DailyStateButtonType = with(info) { + when { + isDraft && replyStatus == ReplyStatus.HAS_DRAFT -> DRAFT_ENABLED + (isDeleted && diaryCount > 0) || replyStatus == ReplyStatus.INVALID_DRAFT -> REPLY_DISABLED + replyStatus in setOf(ReplyStatus.READY_READ, ReplyStatus.READY_NOT_READ) || + (replyStatus == ReplyStatus.UNREADY && diaryCount > 0) -> REPLY_ENABLED + enableWriteDiary -> DIARY_ENABLED + else -> DIARY_DISABLED + } + } + } +} From 1292c5edd1292918eaeaba07bcec466d185cda1a Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Tue, 19 Aug 2025 11:27:53 +0900 Subject: [PATCH 09/20] =?UTF-8?q?[FEAT/#323]=20=ED=99=88=ED=99=94=EB=A9=B4?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EC=82=AC=EC=9A=A9=ED=95=A0=20=EC=9B=94?= =?UTF-8?q?=EB=B3=84=20=EC=9D=BC=EA=B8=B0,=20=EC=9D=BC=EC=9D=BC=20?= =?UTF-8?q?=EC=9D=BC=EA=B8=B0=EC=97=90=20=EB=8C=80=ED=95=9C=20data=20class?= =?UTF-8?q?=EB=A5=BC=20=EC=A0=95=EC=9D=98=ED=95=A9=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../clody/domain/model/CalendarMonthlyInfo.kt | 29 +++++++++++++++++++ .../sopt/clody/domain/model/DailyDiaryInfo.kt | 6 ++++ .../clody/domain/model/MonthlyCalendarInfo.kt | 20 ------------- 3 files changed, 35 insertions(+), 20 deletions(-) create mode 100644 app/src/main/java/com/sopt/clody/domain/model/CalendarMonthlyInfo.kt create mode 100644 app/src/main/java/com/sopt/clody/domain/model/DailyDiaryInfo.kt delete mode 100644 app/src/main/java/com/sopt/clody/domain/model/MonthlyCalendarInfo.kt diff --git a/app/src/main/java/com/sopt/clody/domain/model/CalendarMonthlyInfo.kt b/app/src/main/java/com/sopt/clody/domain/model/CalendarMonthlyInfo.kt new file mode 100644 index 00000000..0c55e4d0 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/domain/model/CalendarMonthlyInfo.kt @@ -0,0 +1,29 @@ +package com.sopt.clody.domain.model + +import com.sopt.clody.domain.type.ReplyStatus +import java.time.LocalDate +import java.time.ZoneId + +data class CalendarMonthlyInfo( + val totalCloverCount: Int = 0, + val calendarDailyInfoList: List = listOf(), +) { + data class CalendarDailyInfo( + val diaryCount: Int = 0, + val replyStatus: ReplyStatus = ReplyStatus.UNREADY, + val date: String = "", + val isDeleted: Boolean = false, + ) { + fun enableWriteDiary(): Boolean { + val userTimeZone = ZoneId.systemDefault().id + val today = LocalDate.now().toString() + val yesterday = LocalDate.now().minusDays(1).toString() + val isAvailableDay = if (userTimeZone == "Asia/Seoul") { + date == today || date == yesterday + } else { + date == today + } + return diaryCount == 0 && isAvailableDay + } + } +} diff --git a/app/src/main/java/com/sopt/clody/domain/model/DailyDiaryInfo.kt b/app/src/main/java/com/sopt/clody/domain/model/DailyDiaryInfo.kt new file mode 100644 index 00000000..cc00960e --- /dev/null +++ b/app/src/main/java/com/sopt/clody/domain/model/DailyDiaryInfo.kt @@ -0,0 +1,6 @@ +package com.sopt.clody.domain.model + +data class DailyDiaryInfo( + val diaryList: List = listOf(), + val isDraft: Boolean = false, +) diff --git a/app/src/main/java/com/sopt/clody/domain/model/MonthlyCalendarInfo.kt b/app/src/main/java/com/sopt/clody/domain/model/MonthlyCalendarInfo.kt deleted file mode 100644 index c8c11d22..00000000 --- a/app/src/main/java/com/sopt/clody/domain/model/MonthlyCalendarInfo.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.sopt.clody.domain.model - -import com.sopt.clody.domain.type.ReplyStatus -import java.time.LocalDate - -data class MonthlyCalendarInfo( - val year: Int = LocalDate.now().year, - val month: Int = LocalDate.now().monthValue, - val totalCloverCount: Int = 0, - val dailyDiaryInfoList: List = listOf() -) { - data class DailyDiaryInfo( - val diaryCount: Int = 0, - val replyStatus: ReplyStatus = ReplyStatus.UNREADY, - val date: String = "", - val diaryList: List = listOf(), - val isDeleted: Boolean = false, - val isDraft: Boolean = false - ) -} From 9ad69008bae3249fe6ac2272c185a73504090fb6 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Tue, 19 Aug 2025 11:28:56 +0900 Subject: [PATCH 10/20] =?UTF-8?q?[FEAT/#323]=20=EC=83=88=EB=A1=AD=EA=B2=8C?= =?UTF-8?q?=20=EC=A0=95=EC=9D=98=ED=95=9C=20data=20class=EB=A5=BC=20?= =?UTF-8?q?=ED=86=B5=ED=95=B4=20=ED=81=B4=EB=A1=9C=EB=B2=84=EC=99=80=20?= =?UTF-8?q?=ED=99=88=ED=99=94=EB=A9=B4=20=ED=95=98=EB=8B=A8=20=EB=B2=84?= =?UTF-8?q?=ED=8A=BC=20=EB=B6=84=EA=B8=B0=20=EB=A1=9C=EC=A7=81=EC=9D=84=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=ED=95=A9=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../presentation/ui/type/DailyCloverType.kt | 33 +++++-------------- .../ui/type/DailyStateButtonType.kt | 25 +++++++------- 2 files changed, 23 insertions(+), 35 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/type/DailyCloverType.kt b/app/src/main/java/com/sopt/clody/presentation/ui/type/DailyCloverType.kt index 05b0e7ac..ccdca7b1 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/type/DailyCloverType.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/type/DailyCloverType.kt @@ -2,8 +2,7 @@ package com.sopt.clody.presentation.ui.type import androidx.annotation.DrawableRes import com.sopt.clody.R -import com.sopt.clody.data.remote.dto.response.MonthlyCalendarResponseDto -import com.sopt.clody.domain.model.MonthlyCalendarInfo +import com.sopt.clody.domain.model.CalendarMonthlyInfo import com.sopt.clody.domain.type.ReplyStatus /** @@ -30,29 +29,15 @@ enum class DailyCloverType(@DrawableRes val iconRes: Int) { ; companion object { - fun getCalendarCloverType( - diaryData: MonthlyCalendarInfo.DailyDiaryInfo, - isToday: Boolean, - ): DailyCloverType { - val count = diaryData.diaryCount - val reply = diaryData.replyStatus - - val hasDiary = count > 0 - val noDiary = count == 0 - val hasDraft = reply == ReplyStatus.HAS_DRAFT - val draftExpired = reply == ReplyStatus.INVALID_DRAFT - val hasUnreadOrNoReply = reply.isUnreadOrNotRead - val isDeleted = diaryData.isDeleted - + fun getType(info: CalendarMonthlyInfo.CalendarDailyInfo): DailyCloverType { return when { - hasDraft -> DRAFT_SAVED - isDeleted || draftExpired -> DISABLED_REPLY - isToday && noDiary -> ENABLED_DIARY - isToday && hasUnreadOrNoReply -> WAITING_REPLY - hasDiary && hasUnreadOrNoReply -> UNGIVEN_CLOVER - reply == ReplyStatus.READY_READ && count in 1..2 -> BOTTOM_CLOVER - reply == ReplyStatus.READY_READ && count in 3..4 -> MID_CLOVER - reply == ReplyStatus.READY_READ && count >= 5 -> TOP_CLOVER + info.replyStatus == ReplyStatus.HAS_DRAFT -> DRAFT_SAVED + info.replyStatus == ReplyStatus.INVALID_DRAFT || info.isDeleted -> DISABLED_REPLY + info.enableWriteDiary() && info.diaryCount == 0 -> ENABLED_DIARY + info.replyStatus == ReplyStatus.UNREADY && info.diaryCount > 0 -> WAITING_REPLY + info.replyStatus == ReplyStatus.READY_READ && info.diaryCount in 1..2 -> BOTTOM_CLOVER + info.replyStatus == ReplyStatus.READY_READ && info.diaryCount in 3..4 -> MID_CLOVER + info.replyStatus == ReplyStatus.READY_READ && info.diaryCount >= 5 -> TOP_CLOVER else -> UNGIVEN_CLOVER } } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/type/DailyStateButtonType.kt b/app/src/main/java/com/sopt/clody/presentation/ui/type/DailyStateButtonType.kt index 8b6294cd..dfc63a57 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/type/DailyStateButtonType.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/type/DailyStateButtonType.kt @@ -1,22 +1,25 @@ package com.sopt.clody.presentation.ui.type -import com.sopt.clody.domain.model.MonthlyCalendarInfo +import com.sopt.clody.domain.model.CalendarMonthlyInfo +import com.sopt.clody.domain.model.DailyDiaryInfo import com.sopt.clody.domain.type.ReplyStatus enum class DailyStateButtonType { DRAFT_ENABLED, REPLY_ENABLED, REPLY_DISABLED, DIARY_ENABLED, DIARY_DISABLED; companion object { - fun getButtonType( - info: MonthlyCalendarInfo.DailyDiaryInfo, - enableWriteDiary: Boolean, - ): DailyStateButtonType = with(info) { - when { - isDraft && replyStatus == ReplyStatus.HAS_DRAFT -> DRAFT_ENABLED - (isDeleted && diaryCount > 0) || replyStatus == ReplyStatus.INVALID_DRAFT -> REPLY_DISABLED - replyStatus in setOf(ReplyStatus.READY_READ, ReplyStatus.READY_NOT_READ) || - (replyStatus == ReplyStatus.UNREADY && diaryCount > 0) -> REPLY_ENABLED - enableWriteDiary -> DIARY_ENABLED + fun getType( + calendarDailyInfo: CalendarMonthlyInfo.CalendarDailyInfo, + dailyDiaryInfo: DailyDiaryInfo, + ): DailyStateButtonType { + return when { + dailyDiaryInfo.isDraft -> DRAFT_ENABLED + (calendarDailyInfo.isDeleted && calendarDailyInfo.diaryCount > 0) || + calendarDailyInfo.replyStatus == ReplyStatus.INVALID_DRAFT -> REPLY_DISABLED + calendarDailyInfo.replyStatus == ReplyStatus.READY_READ || + calendarDailyInfo.replyStatus == ReplyStatus.READY_NOT_READ || + (calendarDailyInfo.replyStatus == ReplyStatus.UNREADY && calendarDailyInfo.diaryCount > 0) -> REPLY_ENABLED + calendarDailyInfo.enableWriteDiary() -> DIARY_ENABLED else -> DIARY_DISABLED } } From 98d77e656078055561cc4dfe9520286bd5646559 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Tue, 19 Aug 2025 11:30:58 +0900 Subject: [PATCH 11/20] =?UTF-8?q?[REFACTOR/#323]=20=ED=99=88=ED=99=94?= =?UTF-8?q?=EB=A9=B4=EC=9D=84=20MVVM=20->=20MVI=20(Mavericks)=EB=A1=9C=20?= =?UTF-8?q?=EB=A7=88=EC=9D=B4=EA=B7=B8=EB=A0=88=EC=9D=B4=EC=85=98=ED=95=A9?= =?UTF-8?q?=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/usecase/LoadHomeDataUseCase.kt | 55 -- .../clody/presentation/di/ViewModelsModule.kt | 8 + .../ui/home/component/CalendarDate.kt | 7 - .../ui/home/component/DailyDiary.kt | 25 +- .../ui/home/component/DailyStateButton.kt | 32 +- .../ui/home/component/DiaryDateData.kt | 8 - .../ui/home/component/HomeTopAppBar.kt | 8 +- .../ui/home/component/MonthlyCalendar.kt | 84 ++- .../component/MonthlyCalendarAndDailyDiary.kt | 59 +- .../ui/home/screen/DeleteDiaryState.kt | 8 - .../ui/home/screen/HomeContract.kt | 83 +++ .../presentation/ui/home/screen/HomeScreen.kt | 545 ++++++++---------- .../ui/home/screen/HomeUiState.kt | 10 +- .../ui/home/screen/HomeViewModel.kt | 456 +++++++-------- .../screen/NotificationSettingViewModel.kt | 2 +- 15 files changed, 629 insertions(+), 761 deletions(-) delete mode 100644 app/src/main/java/com/sopt/clody/domain/usecase/LoadHomeDataUseCase.kt delete mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/home/component/CalendarDate.kt delete mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/home/component/DiaryDateData.kt delete mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/home/screen/DeleteDiaryState.kt create mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeContract.kt diff --git a/app/src/main/java/com/sopt/clody/domain/usecase/LoadHomeDataUseCase.kt b/app/src/main/java/com/sopt/clody/domain/usecase/LoadHomeDataUseCase.kt deleted file mode 100644 index 63ed73d4..00000000 --- a/app/src/main/java/com/sopt/clody/domain/usecase/LoadHomeDataUseCase.kt +++ /dev/null @@ -1,55 +0,0 @@ -package com.sopt.clody.domain.usecase - -import com.sopt.clody.domain.model.MonthlyCalendarInfo -import com.sopt.clody.domain.repository.DiaryRepository -import kotlinx.coroutines.async -import kotlinx.coroutines.coroutineScope -import javax.inject.Inject - -class LoadHomeDataUseCase @Inject constructor( - private val diaryRepository: DiaryRepository, -) { - suspend operator fun invoke(year: Int, month: Int, day: Int): Result = - coroutineScope { - try { - val monthlyDiaryInfoDeferred = - async { diaryRepository.getMonthlyCalendarData(year, month) } - val dailyDiaryInfoDeferred = - async { diaryRepository.getDailyDiariesData(year, month, day) } - - val monthlyDiaryInfo = monthlyDiaryInfoDeferred.await() - val dailyDiaryInfo = dailyDiaryInfoDeferred.await() - - if (monthlyDiaryInfo.isSuccess && dailyDiaryInfo.isSuccess) { - Result.success( - MonthlyCalendarInfo( - year = year, - month = month, - totalCloverCount = monthlyDiaryInfo.getOrThrow().totalCloverCount, - dailyDiaryInfoList = monthlyDiaryInfo.getOrThrow().diaries.map { it -> - MonthlyCalendarInfo.DailyDiaryInfo( - diaryCount = it.diaryCount, - replyStatus = it.replyStatus, - date = it.date, - diaryList = dailyDiaryInfo.getOrThrow().diaries.map { it.content }, - isDeleted = dailyDiaryInfo.getOrThrow().isDeleted, - isDraft = dailyDiaryInfo.getOrThrow().isDraft - ) - } - ) - ) - } else { - Result.failure( - Exception( - "Failed to load monthly diary info or daily diary info." + - "monthlyDiaryInfo: ${monthlyDiaryInfo.exceptionOrNull()?.message}, " + - "dailyDiaryInfo: ${dailyDiaryInfo.exceptionOrNull()?.message}" - ) - ) - } - } catch (e: Exception) { - Result.failure(e) - } - } -} - diff --git a/app/src/main/java/com/sopt/clody/presentation/di/ViewModelsModule.kt b/app/src/main/java/com/sopt/clody/presentation/di/ViewModelsModule.kt index 420e1f68..d7c244f5 100644 --- a/app/src/main/java/com/sopt/clody/presentation/di/ViewModelsModule.kt +++ b/app/src/main/java/com/sopt/clody/presentation/di/ViewModelsModule.kt @@ -4,6 +4,7 @@ import com.airbnb.mvrx.hilt.AssistedViewModelFactory import com.airbnb.mvrx.hilt.MavericksViewModelComponent import com.airbnb.mvrx.hilt.ViewModelKey import com.sopt.clody.presentation.ui.auth.signup.SignUpViewModel +import com.sopt.clody.presentation.ui.home.screen.HomeViewModel import com.sopt.clody.presentation.ui.login.LoginViewModel import com.sopt.clody.presentation.ui.splash.SplashViewModel import dagger.Binds @@ -35,4 +36,11 @@ interface ViewModelsModule { fun bindSignUpViewModelFactory( factory: SignUpViewModel.Factory, ): AssistedViewModelFactory<*, *> + + @Binds + @IntoMap + @ViewModelKey(HomeViewModel::class) + fun bindHomeViewModelFactory( + factory: HomeViewModel.Factory, + ): AssistedViewModelFactory<*, *> } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/component/CalendarDate.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/CalendarDate.kt deleted file mode 100644 index d8e638c7..00000000 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/component/CalendarDate.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.sopt.clody.presentation.ui.home.component - -data class CalendarDate( - val date: Int, - val month: Int, - val year: Int, -) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/component/DailyDiary.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/DailyDiary.kt index e67dba91..696f4829 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/component/DailyDiary.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/DailyDiary.kt @@ -23,19 +23,16 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import com.sopt.clody.R -import com.sopt.clody.data.remote.dto.response.DailyDiariesResponseDto -import com.sopt.clody.domain.model.MonthlyCalendarInfo +import com.sopt.clody.domain.model.DailyDiaryInfo import com.sopt.clody.ui.theme.ClodyTheme -import kotlinx.datetime.DayOfWeek import java.time.LocalDate import java.time.format.TextStyle @Composable fun DailyDiary( - date: LocalDate, - dayOfWeek: DayOfWeek, - dailyDiary: MonthlyCalendarInfo.DailyDiaryInfo, - onShowDiaryDeleteStateChange: (Boolean) -> Unit, + selectedDate: LocalDate, + selectedDailyInfo: DailyDiaryInfo, + onClickDiaryDelete: () -> Unit, ) { Column( modifier = Modifier @@ -50,13 +47,13 @@ fun DailyDiary( .padding(8.dp), ) { Text( - text = "${date.month.value}.${date.dayOfMonth}", + text = "${selectedDate.month.value}.${selectedDate.dayOfMonth}", style = ClodyTheme.typography.body2Medium, color = ClodyTheme.colors.gray04, modifier = Modifier.padding(vertical = 3.dp), ) Text( - text = dayOfWeek.getDisplayName( + text = selectedDate.dayOfWeek.getDisplayName( TextStyle.FULL, LocalConfiguration.current.locales.let { if (it.isEmpty) java.util.Locale.getDefault() else it[0] }, ), @@ -65,19 +62,19 @@ fun DailyDiary( modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp), ) Spacer(modifier = Modifier.weight(1f)) - if (dailyDiary.diaryList.isNotEmpty()) { + if (selectedDailyInfo.diaryList.isNotEmpty()) { Image( painter = painterResource(id = R.drawable.ic_home_kebab), contentDescription = "go to delete", modifier = Modifier .clip(RoundedCornerShape(12.dp)) - .clickable(onClick = { onShowDiaryDeleteStateChange(true) }), + .clickable(onClick = onClickDiaryDelete), ) } } when { - dailyDiary.isDraft -> { + selectedDailyInfo.isDraft -> { Box( contentAlignment = Alignment.Center, modifier = Modifier @@ -93,7 +90,7 @@ fun DailyDiary( } } - dailyDiary.diaryList.isEmpty() -> { + selectedDailyInfo.diaryList.isEmpty() -> { Box( contentAlignment = Alignment.Center, modifier = Modifier @@ -110,7 +107,7 @@ fun DailyDiary( } else -> { - dailyDiary.diaryList.forEachIndexed { index, diary -> + selectedDailyInfo.diaryList.forEachIndexed { index, diary -> DiaryItem(index = index + 1, text = diary) } } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/component/DailyStateButton.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/DailyStateButton.kt index 724f4837..67c426ba 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/component/DailyStateButton.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/DailyStateButton.kt @@ -4,34 +4,34 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import com.sopt.clody.R +import com.sopt.clody.domain.model.CalendarMonthlyInfo +import com.sopt.clody.domain.model.DailyDiaryInfo import com.sopt.clody.presentation.ui.component.button.ClodyButton import com.sopt.clody.presentation.ui.component.button.ClodyReplyButton +import com.sopt.clody.presentation.ui.type.DailyStateButtonType import com.sopt.clody.ui.theme.ClodyTheme @Composable fun DailyStateButton( - hasDraft: Boolean, - canWrite: Boolean, - canReply: Boolean, - isInvalidDraft: Boolean, - year: Int, - month: Int, - day: Int, - onClickWriteDiary: (Int, Int, Int) -> Unit, + calendarDailyInfo: CalendarMonthlyInfo.CalendarDailyInfo, + selectedDailyInfo: DailyDiaryInfo, + onClickWriteDiary: () -> Unit, onClickReplyDiary: () -> Unit, modifier: Modifier = Modifier, ) { - when { - hasDraft -> { + val type = DailyStateButtonType.getType(calendarDailyInfo, selectedDailyInfo) + + when (type) { + DailyStateButtonType.DRAFT_ENABLED -> { ClodyButton( - onClick = { onClickWriteDiary(year, month, day) }, + onClick = onClickWriteDiary, text = stringResource(R.string.home_btn_continue_draft), enabled = true, modifier = modifier, ) } - isInvalidDraft -> { + DailyStateButtonType.REPLY_DISABLED -> { ClodyButton( onClick = { /* no-action */ }, text = stringResource(R.string.home_btn_check_reply), @@ -42,7 +42,7 @@ fun DailyStateButton( ) } - canReply -> { + DailyStateButtonType.REPLY_ENABLED -> { ClodyReplyButton( onClick = onClickReplyDiary, text = stringResource(R.string.home_btn_check_reply), @@ -51,9 +51,9 @@ fun DailyStateButton( ) } - canWrite -> { + DailyStateButtonType.DIARY_ENABLED -> { ClodyButton( - onClick = { onClickWriteDiary(year, month, day) }, + onClick = onClickWriteDiary, text = stringResource(R.string.home_btn_write_diary), enabled = true, modifier = modifier, @@ -62,7 +62,7 @@ fun DailyStateButton( else -> { ClodyButton( - onClick = { onClickWriteDiary(year, month, day) }, + onClick = { /* no-action */ }, text = stringResource(R.string.home_btn_write_diary), enabled = false, modifier = modifier, diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/component/DiaryDateData.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/DiaryDateData.kt deleted file mode 100644 index e02accde..00000000 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/component/DiaryDateData.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.sopt.clody.presentation.ui.home.component - -import java.time.LocalDate - -data class DiaryDateData( - val year: Int = LocalDate.now().year, - val month: Int = LocalDate.now().monthValue, -) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/component/HomeTopAppBar.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/HomeTopAppBar.kt index 6bac1a49..c8c8a90a 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/component/HomeTopAppBar.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/HomeTopAppBar.kt @@ -24,11 +24,11 @@ import com.sopt.clody.ui.theme.ClodyTheme @OptIn(ExperimentalMaterial3Api::class) @Composable fun HomeTopAppBar( - onClickDiaryList: () -> Unit, - onClickSetting: () -> Unit, - onShowYearMonthPickerStateChange: (Boolean) -> Unit, selectedYear: String, selectedMonth: String, + onClickDiaryList: () -> Unit, + onClickYearMonth: () -> Unit, + onClickSetting: () -> Unit, ) { CenterAlignedTopAppBar( title = { @@ -38,7 +38,7 @@ fun HomeTopAppBar( ) { Row( modifier = Modifier.clickable( - onClick = { onShowYearMonthPickerStateChange(true) }, + onClick = onClickYearMonth, indication = null, interactionSource = remember { MutableInteractionSource() }, ), diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/component/MonthlyCalendar.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/MonthlyCalendar.kt index 13e3b125..8c75f0fc 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/component/MonthlyCalendar.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/MonthlyCalendar.kt @@ -24,10 +24,9 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import com.sopt.clody.R -import com.sopt.clody.data.remote.dto.response.MonthlyCalendarResponseDto -import com.sopt.clody.domain.model.MonthlyCalendarInfo -import com.sopt.clody.presentation.ui.type.DailyCloverType +import com.sopt.clody.domain.model.CalendarMonthlyInfo import com.sopt.clody.domain.type.ReplyStatus +import com.sopt.clody.presentation.ui.type.DailyCloverType import com.sopt.clody.presentation.utils.amplitude.AmplitudeConstraints import com.sopt.clody.presentation.utils.amplitude.AmplitudeUtils import com.sopt.clody.ui.theme.ClodyTheme @@ -37,15 +36,25 @@ import java.time.format.TextStyle @Composable fun MonthlyCalendar( - dateList: List, + year: Int, + month: Int, selectedDate: LocalDate, - onDayClick: (LocalDate) -> Unit, - getDiaryDataForDate: (LocalDate) -> MonthlyCalendarInfo.DailyDiaryInfo?, + calendarDailyInfoList: List, + onClickDay: (Int) -> Unit, ) { val locale = LocalConfiguration.current.locales[0] - val days = remember { - List(7) { i -> DayOfWeek.SUNDAY.plus(i.toLong()) } - } + val screenWidth = LocalConfiguration.current.screenWidthDp.dp + val cellWidth = remember(screenWidth) { (screenWidth - 40.dp) / 7 } + val days = remember { List(7) { i -> DayOfWeek.SUNDAY.plus(i.toLong()) } } + + val yearMonth = remember(year, month) { java.time.YearMonth.of(year, month) } + val monthDates = remember(yearMonth) { (1..yearMonth.lengthOfMonth()).map { day -> yearMonth.atDay(day) } } + val firstDayOfWeek = remember(yearMonth) { yearMonth.atDay(1).dayOfWeek } + val emptyDays = remember(firstDayOfWeek) { firstDayOfWeek.value % 7 } // Sunday=0, Monday=1, ... + val paddedDates: List = remember(monthDates, emptyDays) { List(emptyDays) { null } + monthDates } + + // 날짜 문자열(YYYY-MM-DD) → Info 매핑 (탐색 비용 절감) + val infoByDate = remember(calendarDailyInfoList) { calendarDailyInfoList.associateBy { it.date } } Column( horizontalAlignment = Alignment.CenterHorizontally, @@ -53,7 +62,7 @@ fun MonthlyCalendar( .fillMaxWidth() .padding(horizontal = 20.dp), ) { - // 요일 헤더 부분 + // 요일 헤더 Row( horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier @@ -62,11 +71,11 @@ fun MonthlyCalendar( ) { days.forEach { week -> Box( - modifier = Modifier.width((LocalConfiguration.current.screenWidthDp.dp - 40.dp) / 7), + modifier = Modifier.width(cellWidth), contentAlignment = Alignment.Center, ) { Text( - text = week.getDisplayName(TextStyle.NARROW, locale), + text = week.getDisplayName(java.time.format.TextStyle.NARROW, locale), color = ClodyTheme.colors.gray05, style = ClodyTheme.typography.detail1Medium, textAlign = TextAlign.Center, @@ -84,36 +93,30 @@ fun MonthlyCalendar( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.fillMaxWidth(), ) { - val firstDate = dateList.firstOrNull()?.let { LocalDate.of(it.year, it.month, it.date) } - val firstDayOfWeek = firstDate?.dayOfWeek ?: DayOfWeek.SUNDAY - val emptyDays = (firstDayOfWeek.value % 7) - - val paddedDateList = List(emptyDays) { null } + dateList - - paddedDateList.chunked(7).forEach { weekDates -> + paddedDates.chunked(7).forEach { weekDates -> Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, ) { - weekDates.forEach { date -> + weekDates.forEach { dateOrNull -> Box( modifier = Modifier .weight(1f) .padding(vertical = 2.dp), contentAlignment = Alignment.Center, ) { + val date = dateOrNull if (date != null) { - val localDate = LocalDate.of(date.year, date.month, date.date) - val diaryData = getDiaryDataForDate(localDate) - if (diaryData != null) { + val calendarDailyInfo = infoByDate[date.toString()] + if (calendarDailyInfo != null) { DailyClover( - date = localDate, - onDayClick = { clickedDate -> + localDate = date, + calendarDailyInfo = calendarDailyInfo, + onClickDay = { AmplitudeUtils.trackEvent(AmplitudeConstraints.HOME_CALENDAR_CLOVER) - onDayClick(clickedDate) + onClickDay(date.dayOfMonth) }, - isSelected = localDate == selectedDate, - diaryData = diaryData, + isSelected = date == selectedDate, modifier = Modifier.fillMaxWidth(), ) } @@ -124,7 +127,7 @@ fun MonthlyCalendar( repeat(7 - weekDates.size) { Box( modifier = Modifier - .width((LocalConfiguration.current.screenWidthDp.dp - 40.dp) / 7) + .width(cellWidth) .padding(vertical = 2.dp), ) } @@ -137,25 +140,20 @@ fun MonthlyCalendar( } @Composable -fun DailyClover( - date: LocalDate, - onDayClick: (LocalDate) -> Unit, +private fun DailyClover( + localDate: LocalDate, + calendarDailyInfo: CalendarMonthlyInfo.CalendarDailyInfo, + onClickDay: () -> Unit, isSelected: Boolean, - diaryData: MonthlyCalendarInfo.DailyDiaryInfo, modifier: Modifier = Modifier, ) { - val today = LocalDate.now() - val isToday = date == today - - val iconRes = DailyCloverType.getCalendarCloverType(diaryData, isToday).iconRes - Column( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, modifier = modifier .padding(8.dp) .clip(RoundedCornerShape(12.dp)) - .clickable { onDayClick(date) }, + .clickable { onClickDay() }, ) { Box( modifier = Modifier @@ -163,10 +161,10 @@ fun DailyClover( contentAlignment = Alignment.Center, ) { Image( - painter = painterResource(id = iconRes), + painter = painterResource(id = DailyCloverType.getType(calendarDailyInfo).iconRes), contentDescription = "Diary clover icon", ) - if (diaryData.replyStatus == ReplyStatus.READY_NOT_READ && diaryData.diaryCount > 0) { + if (calendarDailyInfo.replyStatus == ReplyStatus.READY_NOT_READ && calendarDailyInfo.diaryCount > 0) { Image( painter = painterResource(id = R.drawable.ic_home_unread_reply), contentDescription = "Unread replies icon", @@ -187,11 +185,11 @@ fun DailyClover( .padding(horizontal = 6.dp), ) { Text( - text = date.dayOfMonth.toString(), + text = localDate.dayOfMonth.toString(), style = ClodyTheme.typography.detail1SemiBold.copy( color = when { isSelected -> ClodyTheme.colors.white - isToday -> ClodyTheme.colors.gray02 + localDate == LocalDate.now() -> ClodyTheme.colors.gray02 else -> ClodyTheme.colors.gray05 }, ), diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/component/MonthlyCalendarAndDailyDiary.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/MonthlyCalendarAndDailyDiary.kt index ac1c628f..02d79bd2 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/component/MonthlyCalendarAndDailyDiary.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/MonthlyCalendarAndDailyDiary.kt @@ -12,51 +12,29 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import com.sopt.clody.R -import com.sopt.clody.domain.model.MonthlyCalendarInfo -import com.sopt.clody.domain.type.ReplyStatus -import com.sopt.clody.presentation.ui.home.screen.HomeViewModel +import com.sopt.clody.domain.model.CalendarMonthlyInfo +import com.sopt.clody.domain.model.DailyDiaryInfo import com.sopt.clody.ui.theme.ClodyTheme import java.time.LocalDate -import java.time.YearMonth @Composable fun MonthlyCalendarAndDailyDiary( - selectedYear: Int, - selectedMonth: Int, - cloverCount: Int, - homeViewModel: HomeViewModel, - diaries: List, - onShowDiaryDeleteStateChange: (Boolean) -> Unit, + year: Int, + month: Int, selectedDate: LocalDate, - onDiaryDataUpdated: (Int, ReplyStatus) -> Unit, + calendarMonthlyInfo: CalendarMonthlyInfo, + onClickDay: (Int) -> Unit, + selectedDailyInfo: DailyDiaryInfo, + onClickDiaryDelete: () -> Unit, modifier: Modifier = Modifier, ) { val scrollState = rememberScrollState() - val currentMonth = YearMonth.of(selectedYear, selectedMonth) - val dateList = remember(currentMonth.year, currentMonth.monthValue) { - (1..YearMonth.of(currentMonth.year, currentMonth.monthValue).lengthOfMonth()).map { day -> - CalendarDate(day, currentMonth.monthValue, currentMonth.year) - } - } - val initialDayOfWeek = selectedDate.dayOfWeek - - LaunchedEffect(selectedDate, diaries) { - if (selectedDate.year == selectedYear && selectedDate.monthValue == selectedMonth) { - homeViewModel.updateDiaryState(diaries) - onDiaryDataUpdated( - homeViewModel.diaryCount.value, - homeViewModel.replyStatus.value, - ) - } - } Column( modifier = modifier @@ -66,7 +44,7 @@ fun MonthlyCalendarAndDailyDiary( ) { // 클로버 총 갯수 Text( - text = stringResource(R.string.home_total_clover, cloverCount), + text = stringResource(R.string.home_total_clover, calendarMonthlyInfo.totalCloverCount), style = ClodyTheme.typography.detail1SemiBold, color = ClodyTheme.colors.darkGreen, modifier = Modifier @@ -83,15 +61,11 @@ fun MonthlyCalendarAndDailyDiary( modifier = Modifier.fillMaxSize(), ) { MonthlyCalendar( - dateList = dateList, + year = year, + month = month, selectedDate = selectedDate, - onDayClick = { date -> - homeViewModel.updateSelectedDate(date) - homeViewModel.updateDiaryState(diaries) - }, - getDiaryDataForDate = { date -> - diaries.getOrNull(date.dayOfMonth - 1) - }, + calendarDailyInfoList = calendarMonthlyInfo.calendarDailyInfoList, + onClickDay = onClickDay, ) HorizontalDivider( @@ -103,10 +77,9 @@ fun MonthlyCalendarAndDailyDiary( ) DailyDiary( - date = selectedDate, - dayOfWeek = initialDayOfWeek, - dailyDiary = diaries[selectedDate.dayOfMonth - 1], - onShowDiaryDeleteStateChange = onShowDiaryDeleteStateChange, + selectedDate = selectedDate, + selectedDailyInfo = selectedDailyInfo, + onClickDiaryDelete = onClickDiaryDelete, ) } } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/DeleteDiaryState.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/DeleteDiaryState.kt deleted file mode 100644 index 0539365d..00000000 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/DeleteDiaryState.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.sopt.clody.presentation.ui.home.screen - -sealed class DeleteDiaryState { - object Idle : DeleteDiaryState() - object Loading : DeleteDiaryState() - object Success : DeleteDiaryState() - data class Failure(val errorMessage: String) : DeleteDiaryState() -} diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeContract.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeContract.kt new file mode 100644 index 00000000..e735036d --- /dev/null +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeContract.kt @@ -0,0 +1,83 @@ +package com.sopt.clody.presentation.ui.home.screen + +import com.airbnb.mvrx.MavericksState +import com.sopt.clody.domain.model.CalendarMonthlyInfo +import com.sopt.clody.domain.model.DailyDiaryInfo +import java.time.LocalDate + +class HomeContract { + data class HomeState( + val homeUiState: HomeUiState = HomeUiState.Idle, + val errorMessage: String? = null, + + val year: Int = LocalDate.now().year, + val month: Int = LocalDate.now().monthValue, + val dayOfMonth: Int = LocalDate.now().dayOfMonth, + val calendarMonthlyInfo: CalendarMonthlyInfo = CalendarMonthlyInfo(), + val selectedDailyInfo: DailyDiaryInfo = DailyDiaryInfo(), + val showYearMonthPicker: Boolean = false, + + val showDiaryDeleteBottomSheet: Boolean = false, + val showDiaryDeleteDialog: Boolean = false, + + val showDraftExpiredDialog: Boolean = false, + + val showDraftNotificationPopup: Boolean = false, + val showDraftNotificationToast: Boolean = false, + + val showInAppReviewPopup: Boolean = false, + ) : MavericksState { + val selectedDate: LocalDate = LocalDate.of(year, month, dayOfMonth) + + fun isDraftExpired(): Boolean = selectedDate != LocalDate.now() + + // 현재 선택된 날짜의 CalendarDailyInfo를 안전하게 가져오기 + fun getCurrentCalendarDailyInfo(): CalendarMonthlyInfo.CalendarDailyInfo? { + return if (isCalendarDataLoaded()) { + calendarMonthlyInfo.calendarDailyInfoList[dayOfMonth - 1] + } else { + null + } + } + + // 캘린더 데이터가 안전하게 로드되었는지 확인 + private fun isCalendarDataLoaded(): Boolean { + return calendarMonthlyInfo.calendarDailyInfoList.isNotEmpty() && + dayOfMonth > 0 && + dayOfMonth <= calendarMonthlyInfo.calendarDailyInfoList.size + } + } + + sealed class HomeIntent { + data class LoadCalendarMonthlyInfo(val year: Int, val month: Int) : HomeIntent() + data class LoadDailyDiaryInfo(val year: Int, val month: Int, val dayOfMonth: Int) : HomeIntent() + + data object OnClickDiaryList : HomeIntent() + data object OnClickYearMonth : HomeIntent() + data class UpdateYearMonth(val newYear: Int, val newMonth: Int) : HomeIntent() + data object DismissYearMonthPicker : HomeIntent() + data object OnClickSetting : HomeIntent() + + data object OnClickDiaryDelete : HomeIntent() + data object ShowDiaryDeleteDialog : HomeIntent() + data class ConfirmDiaryDelete(val year: Int, val month: Int, val dayOfMonth: Int) : HomeIntent() + data object DismissDiaryDelete : HomeIntent() + + data object OnClickWriteDiary : HomeIntent() + data object ShowDraftExpiredDialog : HomeIntent() + + data object OnClickReplyDiary : HomeIntent() + + data object EnableDraftAlarm : HomeIntent() + data class SendNotification(val granted: Boolean) : HomeIntent() + data class UpdateInAppReview(val show: Boolean) : HomeIntent() + data class UpdateDraftPopup(val show: Boolean) : HomeIntent() + } + + sealed interface HomeSideEffect { + data object NavigateToDiaryList : HomeSideEffect + data object NavigateToSetting : HomeSideEffect + data object NavigateToWriteDiary : HomeSideEffect + data object NavigateToReplyLoading : HomeSideEffect + } +} diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt index 84765df1..fa86ab7a 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt @@ -32,13 +32,12 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.compose.LocalLifecycleOwner +import com.airbnb.mvrx.compose.collectAsState +import com.airbnb.mvrx.compose.mavericksViewModel import com.sopt.clody.R import com.sopt.clody.core.review.InAppReviewManager -import com.sopt.clody.data.remote.dto.response.DailyDiariesResponseDto -import com.sopt.clody.data.remote.dto.response.MonthlyCalendarResponseDto -import com.sopt.clody.domain.model.MonthlyCalendarInfo +import com.sopt.clody.domain.type.ReplyStatus import com.sopt.clody.presentation.ui.component.FailureScreen import com.sopt.clody.presentation.ui.component.LoadingScreen import com.sopt.clody.presentation.ui.component.bottomsheet.DiaryDeleteSheet @@ -50,54 +49,60 @@ import com.sopt.clody.presentation.ui.component.toast.ClodyToastMessage import com.sopt.clody.presentation.ui.home.component.DailyStateButton import com.sopt.clody.presentation.ui.home.component.HomeTopAppBar import com.sopt.clody.presentation.ui.home.component.MonthlyCalendarAndDailyDiary -import com.sopt.clody.domain.type.ReplyStatus import com.sopt.clody.presentation.utils.amplitude.AmplitudeConstraints import com.sopt.clody.presentation.utils.amplitude.AmplitudeUtils +import com.sopt.clody.presentation.utils.extension.repeatOnStarted import com.sopt.clody.presentation.utils.extension.toLocalizedMonthLabel import com.sopt.clody.presentation.utils.extension.toLocalizedYearLabel import com.sopt.clody.presentation.utils.navigation.Route import com.sopt.clody.ui.theme.ClodyTheme -import java.time.LocalDate @Composable fun HomeRoute( + viewModel: HomeViewModel = mavericksViewModel(), isFromReplyDiary: Boolean, navigateToDiaryList: (year: Int, month: Int) -> Unit, navigateToSetting: () -> Unit, navigateToWriteDiary: (year: Int, month: Int, date: Int) -> Unit, - navigateToReplyLoading: ( - year: Int, - month: Int, - date: Int, - from: Route.ReplyLoading.ReplyLoadingFrom, - replyStatus: ReplyStatus, - ) -> Unit, - homeViewModel: HomeViewModel = hiltViewModel(), + navigateToReplyLoading: (year: Int, month: Int, date: Int, from: Route.ReplyLoading.ReplyLoadingFrom, replyStatus: ReplyStatus) -> Unit, ) { - val homeUiState by homeViewModel.homeUiState.collectAsStateWithLifecycle() - val replyStatus by homeViewModel.replyStatus.collectAsStateWithLifecycle() - val showFirstDraftPopup by homeViewModel.showFirstDraftPopup.collectAsStateWithLifecycle() - val draftAlarmEnableToast by homeViewModel.draftAlarmEnableToast.collectAsStateWithLifecycle() + val state by viewModel.collectAsState() val context = LocalContext.current - val showInAppReviewPopup by homeViewModel.showInAppReviewPopup.collectAsStateWithLifecycle() - val showContinueDraftDialog by homeViewModel.showContinueDraftDialog.collectAsStateWithLifecycle() - val showDiaryDeleteState by homeViewModel.showDiaryDeleteState.collectAsStateWithLifecycle() - val showDiaryDeleteDialog by homeViewModel.showDiaryDeleteDialog.collectAsStateWithLifecycle() - val selectedDiaryDate by homeViewModel.selectedDiaryDate.collectAsStateWithLifecycle() - val selectedDate by homeViewModel.selectedDate.collectAsStateWithLifecycle() - val deleteDiaryState by homeViewModel.deleteDiaryState.collectAsStateWithLifecycle() - val (isError, errorMessage) = homeViewModel.errorState.collectAsStateWithLifecycle().value - val showYearMonthPickerState by homeViewModel.showYearMonthPickerState.collectAsStateWithLifecycle() - val hasDraft by homeViewModel.hasDraft.collectAsStateWithLifecycle() - var backPressedTime by remember { mutableLongStateOf(0L) } - val backPressThreshold = 2000 + val lifecycleOwner = LocalLifecycleOwner.current - val requestPermissionLauncher = rememberLauncherForActivityResult( - contract = ActivityResultContracts.RequestPermission(), - ) { isGranted: Boolean -> - homeViewModel.sendNotification(isGranted) + LaunchedEffect(viewModel) { + lifecycleOwner.repeatOnStarted { + viewModel.sideEffects.collect { effect -> + when (effect) { + is HomeContract.HomeSideEffect.NavigateToDiaryList -> navigateToDiaryList(state.year, state.month) + is HomeContract.HomeSideEffect.NavigateToSetting -> navigateToSetting() + is HomeContract.HomeSideEffect.NavigateToWriteDiary -> navigateToWriteDiary(state.year, state.month, state.dayOfMonth) + is HomeContract.HomeSideEffect.NavigateToReplyLoading -> { + val replyStatus = state.calendarMonthlyInfo.calendarDailyInfoList + .find { it.date == java.time.LocalDate.of(state.year, state.month, state.dayOfMonth).toString() } + ?.replyStatus ?: ReplyStatus.UNREADY + navigateToReplyLoading( + state.year, + state.month, + state.dayOfMonth, + Route.ReplyLoading.ReplyLoadingFrom.HOME, + replyStatus, + ) + } + } + } + } + } + + LaunchedEffect(Unit) { + // 먼저 월간 캘린더 데이터를 로드 + viewModel.postIntent(HomeContract.HomeIntent.LoadCalendarMonthlyInfo(state.year, state.month)) + // 월간 데이터 로드 완료 후 일간 데이터 로드 (ViewModel에서 순서 보장) } + // 백핸들러 조작 + var backPressedTime by remember { mutableLongStateOf(0L) } + val backPressThreshold = 2000 BackHandler { val currentTime = System.currentTimeMillis() if (currentTime - backPressedTime <= backPressThreshold) { @@ -107,208 +112,50 @@ fun HomeRoute( } } - LaunchedEffect(Unit) { - AmplitudeUtils.trackEvent(eventName = AmplitudeConstraints.HOME) - - if (showInAppReviewPopup && isFromReplyDiary) { - InAppReviewManager.showPopup(context as Activity) - homeViewModel.updateShowInAppReviewPopup(false) - } + // 알림 권한 신청 다이얼로그 + val requestPermissionLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.RequestPermission(), + ) { isGranted: Boolean -> + viewModel.postIntent(HomeContract.HomeIntent.SendNotification(isGranted)) } - - // 알림 권한 요청 LaunchedEffect(Unit) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { val notificationPermission = Manifest.permission.POST_NOTIFICATIONS if (ContextCompat.checkSelfPermission(context, notificationPermission) != PackageManager.PERMISSION_GRANTED) { requestPermissionLauncher.launch(notificationPermission) } else { - homeViewModel.sendNotification(true) + viewModel.postIntent(HomeContract.HomeIntent.SendNotification(true)) } } else { - homeViewModel.sendNotification(true) + viewModel.postIntent(HomeContract.HomeIntent.SendNotification(true)) } } + // 인앱리뷰 팝업 노출 LaunchedEffect(Unit) { - homeViewModel.loadMonthlyCalendarInfo( - selectedDiaryDate.year, - selectedDiaryDate.month, - selectedDate.dayOfMonth, - ) + AmplitudeUtils.trackEvent(eventName = AmplitudeConstraints.HOME) + if (state.showInAppReviewPopup && isFromReplyDiary) { + InAppReviewManager.showPopup(context as Activity) + viewModel.postIntent(HomeContract.HomeIntent.UpdateInAppReview(false)) + } } - if (isError) { - FailureScreen( - message = errorMessage, - confirmAction = { - homeViewModel.loadMonthlyCalendarInfo( - selectedDiaryDate.year, - selectedDiaryDate.month, - selectedDate.dayOfMonth, - ) - }, - ) - } else { - HomeScreen( - homeViewModel = homeViewModel, - homeUiState = homeUiState, - deleteDiaryState = deleteDiaryState, - showYearMonthPickerState = showYearMonthPickerState, - onClickDiaryList = navigateToDiaryList, - onClickSetting = navigateToSetting, - onClickWriteDiary = { year, month, day -> - AmplitudeUtils.trackEvent(eventName = AmplitudeConstraints.HOME_WRITING_DIARY) - if (hasDraft && !homeViewModel.isValidDraftDate()) { - homeViewModel.setShowContinueDraftDialog(true) - } else { - navigateToWriteDiary(year, month, day) - } - }, - onClickReplyDiary = { year, month, day, _ -> - AmplitudeUtils.trackEvent(eventName = AmplitudeConstraints.HOME_REPLY) - navigateToReplyLoading( - year, - month, - day, - Route.ReplyLoading.ReplyLoadingFrom.HOME, - replyStatus, - ) - }, - selectedYear = selectedDiaryDate.year, - selectedMonth = selectedDiaryDate.month, - selectedDate = selectedDate, - hasDraft = hasDraft, - canWrite = homeViewModel.canWriteDiary(), - canReply = homeViewModel.canReplyDiary(), - isInvalidDraft = replyStatus == ReplyStatus.INVALID_DRAFT, - ) - - if (showFirstDraftPopup) { - ClodyPopupBottomSheet( - onDismissRequest = { homeViewModel.updateFirstDraftUse(false) }, - content = { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier - .fillMaxWidth() - .padding(top = 20.dp) - .padding(horizontal = 16.dp), - ) { - Text( - text = stringResource(R.string.bottom_sheet_home_initial_draft_title), - color = ClodyTheme.colors.gray01, - textAlign = TextAlign.Center, - style = ClodyTheme.typography.head3, - ) - Spacer(modifier = Modifier.height(10.dp)) - Text( - text = stringResource(R.string.bottom_sheet_home_initial_draft_description), - color = ClodyTheme.colors.gray04, - textAlign = TextAlign.Center, - style = ClodyTheme.typography.body3Regular, - ) - Spacer(modifier = Modifier.height(4.dp)) - Text( - text = stringResource(R.string.bottom_sheet_home_initial_draft_guide), - color = ClodyTheme.colors.gray04, - textAlign = TextAlign.Center, - style = ClodyTheme.typography.body3Regular, - ) - Spacer(modifier = Modifier.height(28.dp)) - ClodyButton( - text = stringResource(R.string.bottom_sheet_home_initial_draft_accept), - onClick = { - homeViewModel.enableDraftAlarm() - homeViewModel.updateFirstDraftUse(false) - }, - enabled = true, - modifier = Modifier.fillMaxWidth(), - ) - Text( - text = stringResource(R.string.bottom_sheet_home_initial_draft_skip), - modifier = Modifier - .clickable(onClick = { homeViewModel.updateFirstDraftUse(false) }) - .padding(12.dp), - color = ClodyTheme.colors.gray05, - style = ClodyTheme.typography.body4Medium, - ) - Spacer(modifier = Modifier.height(4.dp)) - } - }, - ) + when (state.homeUiState) { + HomeUiState.Idle -> { } - - if (draftAlarmEnableToast) { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.BottomCenter, - content = { - ClodyToastMessage( - message = stringResource(R.string.toast_home_draft_alarm_enabled), - iconResId = R.drawable.ic_toast_check_on_18, - backgroundColor = ClodyTheme.colors.gray04, - contentColor = ClodyTheme.colors.white, - durationMillis = 3000, - onDismiss = { homeViewModel.resetDraftAlarmEnableToast() }, - modifier = Modifier - .navigationBarsPadding() - .padding(40.dp), - ) - }, - ) + HomeUiState.Loading -> { + LoadingScreen() } - - if (showContinueDraftDialog) { - ClodyDialog( - titleMassage = stringResource(R.string.dialog_home_continue_draft_title), - descriptionMassage = stringResource(R.string.dialog_home_continue_draft_description), - confirmOption = stringResource(R.string.dialog_home_continue_draft_confirm), - dismissOption = stringResource(R.string.dialog_home_continue_draft_dismiss), - confirmAction = { - homeViewModel.setShowContinueDraftDialog(false) - val date = homeViewModel.selectedDate.value - navigateToWriteDiary(date.year, date.monthValue, date.dayOfMonth) - }, - onDismiss = { - homeViewModel.setShowContinueDraftDialog(false) - }, - confirmButtonColor = ClodyTheme.colors.mainYellow, - confirmButtonTextColor = ClodyTheme.colors.gray01, + HomeUiState.Success -> { + HomeScreen( + state = state, + onIntent = { viewModel.postIntent(it) }, ) } - - if (showDiaryDeleteState) { - DiaryDeleteSheet( - onDismiss = { homeViewModel.setShowDiaryDeleteState(false) }, - showDiaryDeleteDialog = { - AmplitudeUtils.trackEvent(eventName = AmplitudeConstraints.HOME_DELETE_DIARY) - homeViewModel.setShowDiaryDeleteDialog(true) - }, - ) - } - - if (showDiaryDeleteDialog) { - ClodyDialog( - titleMassage = stringResource(R.string.dialog_diary_delete_title), - descriptionMassage = stringResource(R.string.dialog_diary_delete_description), - confirmOption = stringResource(R.string.dialog_diary_delete_confirm), - dismissOption = stringResource(R.string.dialog_diary_delete_dismiss), - confirmAction = { - val d = homeViewModel.selectedDate.value - homeViewModel.deleteDailyDiary( - d.year, - d.monthValue, - d.dayOfMonth, - ) - homeViewModel.setShowDiaryDeleteDialog(false) - }, - onDismiss = { - AmplitudeUtils.trackEvent(eventName = AmplitudeConstraints.HOME_NO_DELETE_DIARY) - homeViewModel.setShowDiaryDeleteDialog(false) - }, - confirmButtonColor = ClodyTheme.colors.red, - confirmButtonTextColor = ClodyTheme.colors.white, + HomeUiState.Error -> { + FailureScreen( + message = state.errorMessage ?: stringResource(R.string.error_unknown), + confirmAction = { viewModel.postIntent(HomeContract.HomeIntent.LoadCalendarMonthlyInfo(state.year, state.month)) }, ) } } @@ -316,121 +163,191 @@ fun HomeRoute( @Composable fun HomeScreen( - homeViewModel: HomeViewModel, - homeUiState: HomeUiState, - deleteDiaryState: DeleteDiaryState, - showYearMonthPickerState: Boolean, - onClickDiaryList: (Int, Int) -> Unit, - onClickSetting: () -> Unit, - onClickWriteDiary: (Int, Int, Int) -> Unit, - onClickReplyDiary: ( - year: Int, - month: Int, - date: Int, - replyStatus: Route.ReplyLoading.ReplyLoadingFrom, - ) -> Unit, - selectedYear: Int, - selectedMonth: Int, - selectedDate: LocalDate, - hasDraft: Boolean, - canWrite: Boolean, - canReply: Boolean, - isInvalidDraft: Boolean, + state: HomeContract.HomeState, + onIntent: (HomeContract.HomeIntent) -> Unit, ) { Scaffold( topBar = { HomeTopAppBar( + selectedYear = state.year.toLocalizedYearLabel(), + selectedMonth = state.month.toLocalizedMonthLabel(), onClickDiaryList = { AmplitudeUtils.trackEvent(eventName = AmplitudeConstraints.HOME_LIST_DIARY) - onClickDiaryList(selectedYear, selectedMonth) + onIntent(HomeContract.HomeIntent.OnClickDiaryList) + }, + onClickYearMonth = { + onIntent(HomeContract.HomeIntent.OnClickYearMonth) + }, + onClickSetting = { + onIntent(HomeContract.HomeIntent.OnClickSetting) }, - onClickSetting = onClickSetting, - onShowYearMonthPickerStateChange = { newState -> homeViewModel.setShowYearMonthPickerState(newState) }, - selectedYear = selectedYear.toLocalizedYearLabel(), - selectedMonth = selectedMonth.toLocalizedMonthLabel(), ) }, containerColor = ClodyTheme.colors.white, content = { innerPadding -> - when (homeUiState) { - is HomeUiState.Idle -> {} - - is HomeUiState.Loading -> { - LoadingScreen() - } - - is HomeUiState.Success -> { - MonthlyCalendarAndDailyDiary( - selectedYear = homeUiState.data.year, - selectedMonth = homeUiState.data.month, - cloverCount = homeUiState.data.totalCloverCount, - diaries = homeUiState.data.dailyDiaryInfoList, - homeViewModel = homeViewModel, - onShowDiaryDeleteStateChange = { newState -> homeViewModel.setShowDiaryDeleteState(newState) }, - selectedDate = selectedDate, - onDiaryDataUpdated = { _, _ -> - homeViewModel.updateDiaryState(homeUiState.data.dailyDiaryInfoList) - }, - modifier = Modifier.padding(innerPadding) - ) - } - - is HomeUiState.Error -> { - homeViewModel.setErrorState(true, homeUiState.message) - } - } - - when (deleteDiaryState) { - is DeleteDiaryState.Idle -> {} - - is DeleteDiaryState.Loading -> { - LoadingScreen() - } - - is DeleteDiaryState.Success -> {} - - is DeleteDiaryState.Failure -> { - homeViewModel.setErrorState(true, stringResource(R.string.home_error_delete_diary)) - } - } + MonthlyCalendarAndDailyDiary( + year = state.year, + month = state.month, + selectedDate = state.selectedDate, + calendarMonthlyInfo = state.calendarMonthlyInfo, + onClickDay = { dayOfMonth -> onIntent(HomeContract.HomeIntent.LoadDailyDiaryInfo(state.year, state.month, dayOfMonth)) }, + selectedDailyInfo = state.selectedDailyInfo, + onClickDiaryDelete = { onIntent(HomeContract.HomeIntent.OnClickDiaryDelete) }, + modifier = Modifier.padding(innerPadding), + ) }, bottomBar = { - DailyStateButton( - hasDraft = hasDraft, - canWrite = canWrite, - canReply = canReply, - isInvalidDraft = isInvalidDraft, - year = selectedYear, - month = selectedMonth, - day = selectedDate.dayOfMonth, - onClickWriteDiary = onClickWriteDiary, - onClickReplyDiary = { - onClickReplyDiary( - selectedYear, - selectedMonth, - selectedDate.dayOfMonth, - Route.ReplyLoading.ReplyLoadingFrom.HOME, - ) - }, - modifier = Modifier - .fillMaxWidth() - .navigationBarsPadding() - .background(ClodyTheme.colors.white) - .padding(horizontal = 16.dp, vertical = 14.dp), - ) + // 캘린더 데이터가 로드되었을 때만 DailyStateButton 표시 + state.getCurrentCalendarDailyInfo()?.let { calendarDailyInfo -> + DailyStateButton( + calendarDailyInfo = calendarDailyInfo, + selectedDailyInfo = state.selectedDailyInfo, + onClickWriteDiary = { + AmplitudeUtils.trackEvent(eventName = AmplitudeConstraints.HOME_WRITING_DIARY) + if (state.isDraftExpired()) { + onIntent(HomeContract.HomeIntent.ShowDraftExpiredDialog) + } else { onIntent(HomeContract.HomeIntent.OnClickWriteDiary) } + }, + onClickReplyDiary = { + AmplitudeUtils.trackEvent(eventName = AmplitudeConstraints.HOME_REPLY) + onIntent(HomeContract.HomeIntent.OnClickReplyDiary) + }, + modifier = Modifier + .fillMaxWidth() + .navigationBarsPadding() + .background(ClodyTheme.colors.white) + .padding(horizontal = 16.dp, vertical = 14.dp), + ) + } }, ) - if (showYearMonthPickerState) { - ClodyPopupBottomSheet(onDismissRequest = { homeViewModel.setShowYearMonthPickerState(false) }) { + if (state.showYearMonthPicker) { + ClodyPopupBottomSheet(onDismissRequest = { onIntent(HomeContract.HomeIntent.DismissYearMonthPicker) }) { YearMonthPicker( - onDismissRequest = { homeViewModel.setShowYearMonthPickerState(false) }, - selectedYear = selectedYear, - selectedMonth = selectedMonth, - onYearMonthSelected = { year, month -> - homeViewModel.loadMonthlyCalendarInfo(year, month, day = 1) + onDismissRequest = { onIntent(HomeContract.HomeIntent.DismissYearMonthPicker) }, + selectedYear = state.year, + selectedMonth = state.month, + onYearMonthSelected = { newYear, newMonth -> + onIntent(HomeContract.HomeIntent.UpdateYearMonth(newYear, newMonth)) }, ) } } + + if (state.showDraftNotificationPopup) { + ClodyPopupBottomSheet( + onDismissRequest = { onIntent(HomeContract.HomeIntent.UpdateDraftPopup(false)) }, + content = { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .fillMaxWidth() + .padding(top = 20.dp) + .padding(horizontal = 16.dp), + ) { + Text( + text = stringResource(R.string.bottom_sheet_home_initial_draft_title), + color = ClodyTheme.colors.gray01, + textAlign = TextAlign.Center, + style = ClodyTheme.typography.head3, + ) + Spacer(modifier = Modifier.height(10.dp)) + Text( + text = stringResource(R.string.bottom_sheet_home_initial_draft_description), + color = ClodyTheme.colors.gray04, + textAlign = TextAlign.Center, + style = ClodyTheme.typography.body3Regular, + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = stringResource(R.string.bottom_sheet_home_initial_draft_guide), + color = ClodyTheme.colors.gray04, + textAlign = TextAlign.Center, + style = ClodyTheme.typography.body3Regular, + ) + Spacer(modifier = Modifier.height(28.dp)) + ClodyButton( + text = stringResource(R.string.bottom_sheet_home_initial_draft_accept), + onClick = { + onIntent(HomeContract.HomeIntent.EnableDraftAlarm) + onIntent(HomeContract.HomeIntent.UpdateDraftPopup(false)) + }, + enabled = true, + modifier = Modifier.fillMaxWidth(), + ) + Text( + text = stringResource(R.string.bottom_sheet_home_initial_draft_skip), + modifier = Modifier + .clickable(onClick = { onIntent(HomeContract.HomeIntent.UpdateDraftPopup(false)) }) + .padding(12.dp), + color = ClodyTheme.colors.gray05, + style = ClodyTheme.typography.body4Medium, + ) + Spacer(modifier = Modifier.height(4.dp)) + } + }, + ) + } + + if (state.showDraftNotificationToast) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.BottomCenter, + content = { + ClodyToastMessage( + message = stringResource(R.string.toast_home_draft_alarm_enabled), + iconResId = R.drawable.ic_toast_check_on_18, + backgroundColor = ClodyTheme.colors.gray04, + contentColor = ClodyTheme.colors.white, + durationMillis = 3000, + onDismiss = { /* handled by state reset elsewhere if needed */ }, + modifier = Modifier + .navigationBarsPadding() + .padding(40.dp), + ) + }, + ) + } + + if (state.showDraftExpiredDialog) { + ClodyDialog( + titleMassage = stringResource(R.string.dialog_home_continue_draft_title), + descriptionMassage = stringResource(R.string.dialog_home_continue_draft_description), + confirmOption = stringResource(R.string.dialog_home_continue_draft_confirm), + dismissOption = stringResource(R.string.dialog_home_continue_draft_dismiss), + confirmAction = { onIntent(HomeContract.HomeIntent.OnClickWriteDiary) }, + onDismiss = { }, + confirmButtonColor = ClodyTheme.colors.mainYellow, + confirmButtonTextColor = ClodyTheme.colors.gray01, + ) + } + + if (state.showDiaryDeleteBottomSheet) { + DiaryDeleteSheet( + onDismiss = { onIntent(HomeContract.HomeIntent.DismissDiaryDelete) }, + showDiaryDeleteDialog = { + AmplitudeUtils.trackEvent(eventName = AmplitudeConstraints.HOME_DELETE_DIARY) + onIntent(HomeContract.HomeIntent.ShowDiaryDeleteDialog) + }, + ) + } + + if (state.showDiaryDeleteDialog) { + ClodyDialog( + titleMassage = stringResource(R.string.dialog_diary_delete_title), + descriptionMassage = stringResource(R.string.dialog_diary_delete_description), + confirmOption = stringResource(R.string.dialog_diary_delete_confirm), + dismissOption = stringResource(R.string.dialog_diary_delete_dismiss), + confirmAction = { + onIntent(HomeContract.HomeIntent.ConfirmDiaryDelete(state.year, state.month, state.dayOfMonth)) + }, + onDismiss = { + AmplitudeUtils.trackEvent(eventName = AmplitudeConstraints.HOME_NO_DELETE_DIARY) + onIntent(HomeContract.HomeIntent.DismissDiaryDelete) + }, + confirmButtonColor = ClodyTheme.colors.red, + confirmButtonTextColor = ClodyTheme.colors.white, + ) + } } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeUiState.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeUiState.kt index 9adfacc6..c073e47e 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeUiState.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeUiState.kt @@ -1,8 +1,8 @@ package com.sopt.clody.presentation.ui.home.screen -sealed class HomeUiState { - data object Idle : HomeUiState() - data object Loading : HomeUiState() - data class Success(val data: T) : HomeUiState() - data class Error(val message: String) : HomeUiState() +enum class HomeUiState { + Idle, + Loading, + Success, + Error, } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt index 31ce2bfd..5346de4f 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt @@ -1,37 +1,34 @@ package com.sopt.clody.presentation.ui.home.screen -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope +import com.airbnb.mvrx.MavericksViewModel +import com.airbnb.mvrx.MavericksViewModelFactory +import com.airbnb.mvrx.hilt.AssistedViewModelFactory +import com.airbnb.mvrx.hilt.hiltMavericksViewModelFactory import com.sopt.clody.core.fcm.FcmTokenProvider import com.sopt.clody.core.network.NetworkConnectivityObserver import com.sopt.clody.core.network.NetworkStatus -import com.sopt.clody.data.remote.dto.request.SendNotificationRequestDto -import com.sopt.clody.data.remote.dto.response.MonthlyCalendarResponseDto -import com.sopt.clody.data.remote.dto.response.NotificationInfoResponseDto -import com.sopt.clody.domain.model.MonthlyCalendarInfo +import com.sopt.clody.domain.model.CalendarMonthlyInfo +import com.sopt.clody.domain.model.DailyDiaryInfo import com.sopt.clody.domain.repository.DiaryRepository import com.sopt.clody.domain.repository.DraftRepository import com.sopt.clody.domain.repository.NotificationRepository import com.sopt.clody.domain.repository.ReviewRepository -import com.sopt.clody.domain.type.ReplyStatus -import com.sopt.clody.domain.usecase.LoadHomeDataUseCase -import com.sopt.clody.presentation.ui.home.component.DiaryDateData -import com.sopt.clody.presentation.ui.setting.notificationsetting.screen.NotificationChangeState import com.sopt.clody.presentation.utils.network.ErrorMessageProvider -import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.Channel.Factory.BUFFERED import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.withContext -import java.time.LocalDate -import java.time.ZoneId -import javax.inject.Inject -@HiltViewModel -class HomeViewModel @Inject constructor( +class HomeViewModel @AssistedInject constructor( + @Assisted initialState: HomeContract.HomeState, private val diaryRepository: DiaryRepository, private val notificationRepository: NotificationRepository, private val draftRepository: DraftRepository, @@ -39,272 +36,245 @@ class HomeViewModel @Inject constructor( private val reviewRepository: ReviewRepository, private val errorMessageProvider: ErrorMessageProvider, private val networkConnectivityObserver: NetworkConnectivityObserver, - private val loadHomeDataUseCase: LoadHomeDataUseCase, -) : ViewModel() { +) : MavericksViewModel(initialState) { - private val _homeUiState = MutableStateFlow>(HomeUiState.Idle) - val homeUiState: StateFlow> get() = _homeUiState - - private val _deleteDiaryState = MutableStateFlow(DeleteDiaryState.Idle) - val deleteDiaryState: StateFlow get() = _deleteDiaryState - - private val _deleteDiaryResult = MutableStateFlow(DeleteDiaryState.Idle) - val deleteDiaryResult: StateFlow get() = _deleteDiaryResult - - private val _selectedDiaryDate = MutableStateFlow(DiaryDateData()) - val selectedDiaryDate: StateFlow get() = _selectedDiaryDate - - private val _selectedDate = MutableStateFlow(LocalDate.now()) - val selectedDate: StateFlow get() = _selectedDate - - private val _diaryCount = MutableStateFlow(0) - val diaryCount: StateFlow get() = _diaryCount - - private val _replyStatus = MutableStateFlow(ReplyStatus.UNREADY) - val replyStatus: StateFlow get() = _replyStatus - - private val _isToday = MutableStateFlow(false) - val isToday: StateFlow get() = _isToday - - private val _isDeleted = MutableStateFlow(false) - val isDeleted: StateFlow get() = _isDeleted - - private val _showYearMonthPickerState = MutableStateFlow(false) - val showYearMonthPickerState: StateFlow get() = _showYearMonthPickerState - - private val _showDiaryDeleteState = MutableStateFlow(false) - val showDiaryDeleteState: StateFlow get() = _showDiaryDeleteState - - private val _showDiaryDeleteDialog = MutableStateFlow(false) - val showDiaryDeleteDialog: StateFlow get() = _showDiaryDeleteDialog - - private val _showContinueDraftDialog = MutableStateFlow(false) - val showContinueDraftDialog: StateFlow get() = _showContinueDraftDialog - - private val _showFirstDraftPopup = MutableStateFlow(draftRepository.getIsFirstUse()) - val showFirstDraftPopup: StateFlow = _showFirstDraftPopup - - private val _draftAlarmChangeState = - MutableStateFlow(NotificationChangeState.Idle) - val draftAlarmChangeState: StateFlow = _draftAlarmChangeState - - private val _draftAlarmEnableToast = MutableStateFlow(false) - val draftAlarmEnableToast: StateFlow = _draftAlarmEnableToast - - private val _showInAppReviewPopup = MutableStateFlow(reviewRepository.getShouldShowPopup()) - val showInAppReviewPopup: StateFlow get() = _showInAppReviewPopup - - private val _errorState = MutableStateFlow(false to "") - val errorState: StateFlow> = _errorState - - private val _hasDraft = MutableStateFlow(false) - val hasDraft: StateFlow get() = _hasDraft - - private var isInitialized = false + private val _intents = Channel(BUFFERED) + private val _sideEffects = Channel(BUFFERED) + val sideEffects = _sideEffects.receiveAsFlow() init { - initialize() + _intents + .receiveAsFlow() + .onEach(::handleIntent) + .launchIn(viewModelScope) } - private fun initialize() { - if (!isInitialized) { - val now = LocalDate.now() - _selectedDiaryDate.value = DiaryDateData(now.year, now.monthValue) - _selectedDate.value = now - isInitialized = true - } + fun postIntent(intent: HomeContract.HomeIntent) { + viewModelScope.launch { _intents.send(intent) } } - fun setErrorState(isError: Boolean, message: String = errorMessageProvider.getTemporaryError()) { - _errorState.value = isError to message + private suspend fun handleIntent(intent: HomeContract.HomeIntent) { + when (intent) { + is HomeContract.HomeIntent.LoadCalendarMonthlyInfo -> loadCalendarMonthlyInfo(intent.year, intent.month) + is HomeContract.HomeIntent.LoadDailyDiaryInfo -> loadDailyDiaryInfo(intent.year, intent.month, intent.dayOfMonth) + is HomeContract.HomeIntent.OnClickDiaryList -> _sideEffects.send(HomeContract.HomeSideEffect.NavigateToDiaryList) + is HomeContract.HomeIntent.OnClickYearMonth -> setState { copy(showYearMonthPicker = true) } + is HomeContract.HomeIntent.UpdateYearMonth -> updateYearMonth(intent.newYear, intent.newMonth) + is HomeContract.HomeIntent.DismissYearMonthPicker -> setState { copy(showYearMonthPicker = false) } + is HomeContract.HomeIntent.OnClickSetting -> _sideEffects.send(HomeContract.HomeSideEffect.NavigateToSetting) + + is HomeContract.HomeIntent.OnClickDiaryDelete -> setState { copy(showDiaryDeleteBottomSheet = true) } + is HomeContract.HomeIntent.ShowDiaryDeleteDialog -> setState { copy(showDiaryDeleteDialog = true, showDiaryDeleteBottomSheet = false) } + is HomeContract.HomeIntent.ConfirmDiaryDelete -> deleteDiary(intent.year, intent.month, intent.dayOfMonth) + is HomeContract.HomeIntent.DismissDiaryDelete -> setState { copy(showDiaryDeleteDialog = false) } + + is HomeContract.HomeIntent.OnClickWriteDiary -> _sideEffects.send(HomeContract.HomeSideEffect.NavigateToWriteDiary) + is HomeContract.HomeIntent.ShowDraftExpiredDialog -> setState { copy(showDraftExpiredDialog = true) } + + is HomeContract.HomeIntent.OnClickReplyDiary -> _sideEffects.send(HomeContract.HomeSideEffect.NavigateToReplyLoading) + + is HomeContract.HomeIntent.EnableDraftAlarm -> enableDraftAlarm() + is HomeContract.HomeIntent.SendNotification -> sendNotification(intent.granted) + is HomeContract.HomeIntent.UpdateInAppReview -> updateInAppReview(intent.show) + is HomeContract.HomeIntent.UpdateDraftPopup -> updateDraftPopup(intent.show) + } } - fun loadMonthlyCalendarInfo(year: Int, month: Int, day: Int) { - _homeUiState.value = HomeUiState.Loading - viewModelScope.launch { - if (!isNetworkAvailable()) { - setErrorState(true, errorMessageProvider.getNetworkError()) - return@launch - } - - val sameYmd = _selectedDiaryDate.value.year == year && - _selectedDiaryDate.value.month == month && - _selectedDate.value.dayOfMonth == day - val homeUiLoaded = _homeUiState.value is HomeUiState.Success - val alreadyLoaded = sameYmd && homeUiLoaded - - if (alreadyLoaded) return@launch - - _selectedDiaryDate.value = DiaryDateData(year, month) - _selectedDate.value = LocalDate.of(year, month, day) + private suspend fun isNetworkAvailable(): Boolean = + networkConnectivityObserver.networkStatus.first() == NetworkStatus.Available - val result = withContext(Dispatchers.IO) { - loadHomeDataUseCase(year, month, day) - } + private suspend fun loadCalendarMonthlyInfo(year: Int, month: Int) { + setState { copy(homeUiState = HomeUiState.Loading, year = year, month = month, dayOfMonth = 1) } - _homeUiState.value = result.fold( - onSuccess = { - setErrorState(false) - HomeUiState.Success(it) - }, - onFailure = { - setErrorState(true, errorMessageProvider.getTemporaryError()) - HomeUiState.Error(errorMessageProvider.getTemporaryError()) - } - ) + if (!isNetworkAvailable()) { + setState { copy(errorMessage = errorMessageProvider.getNetworkError()) } + return } - } - fun deleteDailyDiary(year: Int, month: Int, day: Int) { - viewModelScope.launch { - _deleteDiaryResult.value = DeleteDiaryState.Loading - val result = withContext(Dispatchers.IO) { - diaryRepository.deleteDailyDiary(year, month, day) - } - _deleteDiaryResult.value = result.fold( - onSuccess = { - loadHomeDataUseCase(year, month, day) - _diaryCount.value = 0 - _isDeleted.value = false - _replyStatus.value = ReplyStatus.UNREADY - DeleteDiaryState.Success - }, - onFailure = { - DeleteDiaryState.Failure(it.message ?: errorMessageProvider.getTemporaryError()) - }, - ) + val result = withContext(Dispatchers.IO) { + diaryRepository.getMonthlyCalendarData(year, month) } - } - fun updateSelectedDate(date: LocalDate) { - _selectedDate.value = date - viewModelScope.launch { - loadHomeDataUseCase(date.year, date.monthValue, date.dayOfMonth) - } + result.fold( + onSuccess = { data -> + setState { + copy( + homeUiState = HomeUiState.Success, + calendarMonthlyInfo = CalendarMonthlyInfo( + totalCloverCount = data.totalCloverCount, + calendarDailyInfoList = data.diaries.map { + CalendarMonthlyInfo.CalendarDailyInfo( + diaryCount = it.diaryCount, + replyStatus = it.replyStatus, + date = it.date, + isDeleted = it.isDeleted, + ) + }, + ), + ) + } + // 월간 데이터 로드 완료 후 일간 데이터 로드 + loadDailyDiaryInfo(year, month, 1) + }, + onFailure = { + setState { + copy( + homeUiState = HomeUiState.Error, + errorMessage = errorMessageProvider.getTemporaryError(), + ) + } + }, + ) } - fun updateDiaryState(diaries: List) { - val selectedDiary = diaries.getOrNull(_selectedDate.value.dayOfMonth - 1) - _diaryCount.value = selectedDiary?.diaryCount ?: 0 - _replyStatus.value = selectedDiary?.replyStatus ?: ReplyStatus.UNREADY - _isDeleted.value = selectedDiary?.isDeleted ?: false - } + private suspend fun loadDailyDiaryInfo(year: Int, month: Int, day: Int) { + setState { copy(homeUiState = HomeUiState.Loading, dayOfMonth = day) } - fun setShowYearMonthPickerState(state: Boolean) { - _showYearMonthPickerState.value = state - } + if (!isNetworkAvailable()) { + setState { copy(errorMessage = errorMessageProvider.getNetworkError()) } + return + } - fun setShowDiaryDeleteState(state: Boolean) { - _showDiaryDeleteState.value = state - } + val result = withContext(Dispatchers.IO) { + diaryRepository.getDailyDiariesData(year, month, day) + } - fun setShowDiaryDeleteDialog(state: Boolean) { - _showDiaryDeleteDialog.value = state + result.fold( + onSuccess = { data -> + setState { + copy( + homeUiState = HomeUiState.Success, + selectedDailyInfo = DailyDiaryInfo( + diaryList = data.diaries.map { it.content }, + isDraft = data.isDraft, + ), + ) + } + }, + onFailure = { + setState { + copy( + homeUiState = HomeUiState.Error, + errorMessage = errorMessageProvider.getTemporaryError(), + ) + } + }, + ) } - fun setShowContinueDraftDialog(state: Boolean) { - _showContinueDraftDialog.value = state + private suspend fun updateYearMonth(newYear: Int, newMonth: Int) { + loadCalendarMonthlyInfo(newYear, newMonth) } - fun updateFirstDraftUse(newState: Boolean) { - draftRepository.setIsFirstUse(false) - _showFirstDraftPopup.value = newState - } + private suspend fun deleteDiary(year: Int, month: Int, dayOfMonth: Int) { + setState { copy(homeUiState = HomeUiState.Loading) } - fun canWriteDiary(): Boolean { - val userTimeZone = ZoneId.systemDefault().id - val today = LocalDate.now() - val selected = _selectedDate.value - val isAvailableDay = if (userTimeZone == "Asia/Seoul") { - selected == today || selected == today.minusDays(1) - } else { - selected == today + val result = withContext(Dispatchers.IO) { + diaryRepository.deleteDailyDiary(year, month, dayOfMonth) } - return _diaryCount.value == 0 && isAvailableDay + result.fold( + onSuccess = { + loadCalendarMonthlyInfo(year, month) + setState { + copy( + homeUiState = HomeUiState.Success, + showDiaryDeleteDialog = false, + showDiaryDeleteBottomSheet = false, + ) + } + }, + onFailure = { + setState { + copy( + homeUiState = HomeUiState.Error, + errorMessage = errorMessageProvider.getTemporaryError(), + ) + } + }, + ) } - fun canReplyDiary(): Boolean { - return _diaryCount.value > 0 && !_isDeleted.value - } + private suspend fun enableDraftAlarm() { + setState { copy(homeUiState = HomeUiState.Loading) } - fun isValidDraftDate(): Boolean { - val today = LocalDate.now() - val selected = _selectedDate.value - return selected == today || selected == today.minusDays(1) - } + if (!isNetworkAvailable()) { + setState { copy(errorMessage = errorMessageProvider.getNetworkError()) } + return + } - fun enableDraftAlarm() { - viewModelScope.launch { - if (!isNetworkAvailable()) { - setErrorState(true, errorMessageProvider.getNetworkError()) - return@launch + val fcmToken = fcmTokenProvider.getToken().orEmpty() + val info = withContext(Dispatchers.IO) { notificationRepository.getNotificationInfo() } + .getOrElse { + setState { copy(errorMessage = errorMessageProvider.getTemporaryError()) } + return } - val fcmToken = fcmTokenProvider.getToken().orEmpty() - val notificationInfo = getNotificationInfo() ?: return@launch - val request = buildDraftAlarmRequest(notificationInfo, fcmToken) - sendDraftAlarmRequest(request) - } - } + val request = com.sopt.clody.data.remote.dto.request.SendNotificationRequestDto( + isDiaryAlarm = info.isDiaryAlarm, + isDraftAlarm = true, + isReplyAlarm = info.isReplyAlarm, + time = info.time, + fcmToken = fcmToken, + ) - private suspend fun isNetworkAvailable(): Boolean { - return networkConnectivityObserver.networkStatus.first() == NetworkStatus.Available + withContext(Dispatchers.IO) { notificationRepository.sendNotification(request) } + .fold( + onSuccess = { + setState { + copy( + homeUiState = HomeUiState.Success, + showDraftNotificationToast = true, + showDraftNotificationPopup = false, + ) + } + }, + onFailure = { + setState { + copy( + homeUiState = HomeUiState.Error, + errorMessage = errorMessageProvider.getTemporaryError(), + ) + } + }, + ) } - private suspend fun getNotificationInfo(): NotificationInfoResponseDto? { - return notificationRepository.getNotificationInfo().getOrElse { - _draftAlarmChangeState.value = NotificationChangeState.Failure(errorMessageProvider.getTemporaryError()) - null - } - } + private suspend fun sendNotification(granted: Boolean) { + setState { copy(homeUiState = HomeUiState.Loading) } - private fun buildDraftAlarmRequest( - info: NotificationInfoResponseDto, - fcmToken: String, - ): SendNotificationRequestDto = SendNotificationRequestDto( - isDiaryAlarm = info.isDiaryAlarm, - isDraftAlarm = true, - isReplyAlarm = info.isReplyAlarm, - time = info.time, - fcmToken = fcmToken, - ) - - private suspend fun sendDraftAlarmRequest(request: SendNotificationRequestDto) { - withContext(Dispatchers.IO) { - notificationRepository.sendNotification(request) - }.fold( - onSuccess = { - _draftAlarmEnableToast.value = true - _draftAlarmChangeState.value = NotificationChangeState.Success(it) - }, - onFailure = { - _draftAlarmChangeState.value = - NotificationChangeState.Failure(errorMessageProvider.getTemporaryError()) - }, + val fcmToken = fcmTokenProvider.getToken().orEmpty() + val info = withContext(Dispatchers.IO) { notificationRepository.getNotificationInfo() } + .getOrElse { return } + + val request = com.sopt.clody.data.remote.dto.request.SendNotificationRequestDto( + isDiaryAlarm = granted, + isDraftAlarm = info.isDraftAlarm, + isReplyAlarm = granted, + time = info.time, + fcmToken = fcmToken, ) + withContext(Dispatchers.IO) { notificationRepository.sendNotification(request) } + .fold( + onSuccess = { setState { copy(homeUiState = HomeUiState.Success) } }, + onFailure = { setState { copy(homeUiState = HomeUiState.Error) } }, + ) } - fun resetDraftAlarmEnableToast() { - _draftAlarmEnableToast.value = false + private fun updateInAppReview(show: Boolean) { + reviewRepository.setShouldShowPopup(show) + setState { copy(showInAppReviewPopup = show) } } - fun updateShowInAppReviewPopup(state: Boolean) { - reviewRepository.setShouldShowPopup(state) - _showInAppReviewPopup.value = state + private fun updateDraftPopup(show: Boolean) { + if (!show) draftRepository.setIsFirstUse(false) + setState { copy(showDraftNotificationPopup = show) } } - fun sendNotification(isGranted: Boolean) { - viewModelScope.launch { - val fcmToken = fcmTokenProvider.getToken().orEmpty() - val notificationInfo = getNotificationInfo() ?: return@launch - val requestDto = SendNotificationRequestDto( - isDiaryAlarm = isGranted, - isDraftAlarm = notificationInfo.isDraftAlarm, - isReplyAlarm = isGranted, - time = notificationInfo.time, - fcmToken = fcmToken, - ) - notificationRepository.sendNotification(requestDto) - } + @AssistedFactory + interface Factory : AssistedViewModelFactory { + override fun create(state: HomeContract.HomeState): HomeViewModel } + + companion object : + MavericksViewModelFactory by hiltMavericksViewModelFactory() } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingViewModel.kt index 0b4adfdb..b6453233 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/setting/notificationsetting/screen/NotificationSettingViewModel.kt @@ -6,8 +6,8 @@ import androidx.lifecycle.viewModelScope import com.sopt.clody.core.network.NetworkConnectivityObserver import com.sopt.clody.core.network.NetworkStatus import com.sopt.clody.data.remote.dto.request.SendNotificationRequestDto -import com.sopt.clody.domain.type.Notification import com.sopt.clody.domain.repository.NotificationRepository +import com.sopt.clody.domain.type.Notification import com.sopt.clody.presentation.utils.extension.TimePeriod import com.sopt.clody.presentation.utils.extension.convertUTZtoKST import com.sopt.clody.presentation.utils.network.ErrorMessageProvider From f399ab5de8f37cda19c21ae5b69c32795dc9ffd2 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Fri, 22 Aug 2025 13:48:00 +0900 Subject: [PATCH 12/20] =?UTF-8?q?[REFACTOR/#323]=20=EB=B6=80=EA=B0=80?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5(=EC=95=8C=EB=A6=BC=EA=B6=8C=ED=95=9C?= =?UTF-8?q?=EC=9A=94=EC=B2=AD,=20=EC=9D=B8=EC=95=B1=EB=A6=AC=EB=B7=B0,=20?= =?UTF-8?q?=EC=9D=B4=EC=96=B4=EC=93=B0=EA=B8=B0=20=EC=95=8C=EB=A6=BC)=20?= =?UTF-8?q?=EC=A0=9C=EC=99=B8=20=EA=B8=B0=EB=8A=A5=20=EC=A0=90=EA=B2=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../clody/domain/model/CalendarMonthlyInfo.kt | 18 ++ .../sopt/clody/domain/model/DailyDiaryInfo.kt | 7 + .../sopt/clody/domain/model/ExampleModel.kt | 6 - .../ui/home/component/DailyDiary.kt | 41 ++-- .../ui/home/component/DailyStateButton.kt | 5 +- .../ui/home/component/HomeTopAppBar.kt | 6 +- .../ui/home/component/MonthlyCalendar.kt | 18 +- .../ui/home/screen/HomeContract.kt | 48 ++-- .../presentation/ui/home/screen/HomeScreen.kt | 172 ++++++-------- .../ui/home/screen/HomeUiState.kt | 8 - .../ui/home/screen/HomeViewModel.kt | 218 ++++++++---------- .../presentation/ui/type/DailyCloverType.kt | 6 +- .../presentation/utils/base/UiLoadState.kt | 8 + 13 files changed, 258 insertions(+), 303 deletions(-) delete mode 100644 app/src/main/java/com/sopt/clody/domain/model/ExampleModel.kt delete mode 100644 app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeUiState.kt create mode 100644 app/src/main/java/com/sopt/clody/presentation/utils/base/UiLoadState.kt diff --git a/app/src/main/java/com/sopt/clody/domain/model/CalendarMonthlyInfo.kt b/app/src/main/java/com/sopt/clody/domain/model/CalendarMonthlyInfo.kt index 0c55e4d0..77e5de64 100644 --- a/app/src/main/java/com/sopt/clody/domain/model/CalendarMonthlyInfo.kt +++ b/app/src/main/java/com/sopt/clody/domain/model/CalendarMonthlyInfo.kt @@ -4,16 +4,34 @@ import com.sopt.clody.domain.type.ReplyStatus import java.time.LocalDate import java.time.ZoneId +/** + * 홈 화면의 월별 달력에서 사용되는 정보 + * + * @property totalCloverCount 해당 월에 받은 클로버(답장)의 개수 + * @property calendarDailyInfoList 해당 월의 일별 정보 + * + * */ data class CalendarMonthlyInfo( val totalCloverCount: Int = 0, val calendarDailyInfoList: List = listOf(), ) { + /** + * 홈 화면 월별 달력을 구성하는 일별 일기 정보 + * + * @property diaryCount 해당 일에 작성한 일기의 개수 + * @property replyStatus 해당 일에 작성한 일기의 답장 상태 + * @property date 해당 일의 날짜, "2025-08-21" 형식 + * @property isDeleted 해당 일에 작성한 일기가 삭제 이력의 여부 + * + * */ data class CalendarDailyInfo( val diaryCount: Int = 0, val replyStatus: ReplyStatus = ReplyStatus.UNREADY, val date: String = "", val isDeleted: Boolean = false, ) { + fun isToday(): Boolean = date == LocalDate.now().toString() + fun enableWriteDiary(): Boolean { val userTimeZone = ZoneId.systemDefault().id val today = LocalDate.now().toString() diff --git a/app/src/main/java/com/sopt/clody/domain/model/DailyDiaryInfo.kt b/app/src/main/java/com/sopt/clody/domain/model/DailyDiaryInfo.kt index cc00960e..e7860c9c 100644 --- a/app/src/main/java/com/sopt/clody/domain/model/DailyDiaryInfo.kt +++ b/app/src/main/java/com/sopt/clody/domain/model/DailyDiaryInfo.kt @@ -1,5 +1,12 @@ package com.sopt.clody.domain.model +/** + * 홈 화면에서 월별 달력 하단에 일별 일기 정보를 위한 데이터 클래스 + * + * @property diaryList 해당 일에 작성한 일기의 내용 + * @property isDraft 해당 일에 임시 저장 일기의 존재 여부 + * + */ data class DailyDiaryInfo( val diaryList: List = listOf(), val isDraft: Boolean = false, diff --git a/app/src/main/java/com/sopt/clody/domain/model/ExampleModel.kt b/app/src/main/java/com/sopt/clody/domain/model/ExampleModel.kt deleted file mode 100644 index 2f99de36..00000000 --- a/app/src/main/java/com/sopt/clody/domain/model/ExampleModel.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.sopt.clody.domain.model - -data class ExampleModel( - val id: String, - val name: String, -) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/component/DailyDiary.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/DailyDiary.kt index 696f4829..d6aa1d39 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/component/DailyDiary.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/DailyDiary.kt @@ -27,6 +27,7 @@ import com.sopt.clody.domain.model.DailyDiaryInfo import com.sopt.clody.ui.theme.ClodyTheme import java.time.LocalDate import java.time.format.TextStyle +import java.util.Locale @Composable fun DailyDiary( @@ -35,9 +36,7 @@ fun DailyDiary( onClickDiaryDelete: () -> Unit, ) { Column( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), + modifier = Modifier.padding(horizontal = 16.dp), verticalArrangement = Arrangement.spacedBy(10.dp), ) { Row( @@ -47,7 +46,7 @@ fun DailyDiary( .padding(8.dp), ) { Text( - text = "${selectedDate.month.value}.${selectedDate.dayOfMonth}", + text = "${selectedDate.monthValue}.${selectedDate.dayOfMonth}", style = ClodyTheme.typography.body2Medium, color = ClodyTheme.colors.gray04, modifier = Modifier.padding(vertical = 3.dp), @@ -55,7 +54,7 @@ fun DailyDiary( Text( text = selectedDate.dayOfWeek.getDisplayName( TextStyle.FULL, - LocalConfiguration.current.locales.let { if (it.isEmpty) java.util.Locale.getDefault() else it[0] }, + LocalConfiguration.current.locales.let { if (it.isEmpty) Locale.getDefault() else it[0] }, ), style = ClodyTheme.typography.body2SemiBold, color = ClodyTheme.colors.gray02, @@ -108,28 +107,20 @@ fun DailyDiary( else -> { selectedDailyInfo.diaryList.forEachIndexed { index, diary -> - DiaryItem(index = index + 1, text = diary) + Column( + modifier = Modifier + .fillMaxWidth() + .background(ClodyTheme.colors.gray08, shape = RoundedCornerShape(10.dp)) + .padding(18.dp), + ) { + Text( + text = "${index + 1}. $diary", + style = ClodyTheme.typography.body2Medium, + color = ClodyTheme.colors.gray01, + ) + } } } } } } - -@Composable -fun DiaryItem( - index: Int, - text: String, -) { - Column( - modifier = Modifier - .fillMaxWidth() - .background(ClodyTheme.colors.gray08, shape = RoundedCornerShape(10.dp)) - .padding(18.dp), - ) { - Text( - text = "$index. $text", - style = ClodyTheme.typography.body2Medium, - color = ClodyTheme.colors.gray01, - ) - } -} diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/component/DailyStateButton.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/DailyStateButton.kt index 67c426ba..429ceecc 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/component/DailyStateButton.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/DailyStateButton.kt @@ -10,12 +10,15 @@ import com.sopt.clody.presentation.ui.component.button.ClodyButton import com.sopt.clody.presentation.ui.component.button.ClodyReplyButton import com.sopt.clody.presentation.ui.type.DailyStateButtonType import com.sopt.clody.ui.theme.ClodyTheme +import timber.log.Timber +import java.time.LocalDate @Composable fun DailyStateButton( calendarDailyInfo: CalendarMonthlyInfo.CalendarDailyInfo, selectedDailyInfo: DailyDiaryInfo, onClickWriteDiary: () -> Unit, + onClickContinueDraft: () -> Unit, onClickReplyDiary: () -> Unit, modifier: Modifier = Modifier, ) { @@ -24,7 +27,7 @@ fun DailyStateButton( when (type) { DailyStateButtonType.DRAFT_ENABLED -> { ClodyButton( - onClick = onClickWriteDiary, + onClick = onClickContinueDraft, text = stringResource(R.string.home_btn_continue_draft), enabled = true, modifier = modifier, diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/component/HomeTopAppBar.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/HomeTopAppBar.kt index c8c8a90a..78b89ee9 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/component/HomeTopAppBar.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/HomeTopAppBar.kt @@ -52,14 +52,14 @@ fun HomeTopAppBar( Image( painter = painterResource(id = R.drawable.ic_home_under_arrow), contentDescription = "choose month", - modifier = Modifier.padding(horizontal = 6.dp, vertical = 6.dp), + modifier = Modifier.padding(6.dp), ) } } }, navigationIcon = { IconButton( - onClick = { onClickDiaryList() }, + onClick = onClickDiaryList, modifier = Modifier.padding(start = 8.dp), ) { Image( @@ -70,7 +70,7 @@ fun HomeTopAppBar( }, actions = { IconButton( - onClick = { onClickSetting() }, + onClick = onClickSetting, modifier = Modifier.padding(end = 8.dp), ) { Image( diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/component/MonthlyCalendar.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/MonthlyCalendar.kt index 8c75f0fc..24306ba2 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/component/MonthlyCalendar.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/MonthlyCalendar.kt @@ -42,10 +42,9 @@ fun MonthlyCalendar( calendarDailyInfoList: List, onClickDay: (Int) -> Unit, ) { - val locale = LocalConfiguration.current.locales[0] val screenWidth = LocalConfiguration.current.screenWidthDp.dp val cellWidth = remember(screenWidth) { (screenWidth - 40.dp) / 7 } - val days = remember { List(7) { i -> DayOfWeek.SUNDAY.plus(i.toLong()) } } + val dayOfWeeks = remember { List(7) { i -> DayOfWeek.SUNDAY.plus(i.toLong()) } } val yearMonth = remember(year, month) { java.time.YearMonth.of(year, month) } val monthDates = remember(yearMonth) { (1..yearMonth.lengthOfMonth()).map { day -> yearMonth.atDay(day) } } @@ -69,13 +68,13 @@ fun MonthlyCalendar( .fillMaxWidth() .padding(bottom = 8.dp), ) { - days.forEach { week -> + dayOfWeeks.forEach { dayOfWeek -> Box( modifier = Modifier.width(cellWidth), contentAlignment = Alignment.Center, ) { Text( - text = week.getDisplayName(java.time.format.TextStyle.NARROW, locale), + text = dayOfWeek.getDisplayName(TextStyle.NARROW, LocalConfiguration.current.locales[0]), color = ClodyTheme.colors.gray05, style = ClodyTheme.typography.detail1Medium, textAlign = TextAlign.Center, @@ -105,18 +104,17 @@ fun MonthlyCalendar( .padding(vertical = 2.dp), contentAlignment = Alignment.Center, ) { - val date = dateOrNull - if (date != null) { - val calendarDailyInfo = infoByDate[date.toString()] + if (dateOrNull != null) { + val calendarDailyInfo = infoByDate[dateOrNull.toString()] if (calendarDailyInfo != null) { DailyClover( - localDate = date, + localDate = dateOrNull, calendarDailyInfo = calendarDailyInfo, onClickDay = { AmplitudeUtils.trackEvent(AmplitudeConstraints.HOME_CALENDAR_CLOVER) - onClickDay(date.dayOfMonth) + onClickDay(dateOrNull.dayOfMonth) }, - isSelected = date == selectedDate, + isSelected = dateOrNull == selectedDate, modifier = Modifier.fillMaxWidth(), ) } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeContract.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeContract.kt index e735036d..3f3a4f84 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeContract.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeContract.kt @@ -3,36 +3,33 @@ package com.sopt.clody.presentation.ui.home.screen import com.airbnb.mvrx.MavericksState import com.sopt.clody.domain.model.CalendarMonthlyInfo import com.sopt.clody.domain.model.DailyDiaryInfo +import com.sopt.clody.domain.type.ReplyStatus +import com.sopt.clody.presentation.utils.base.UiLoadState import java.time.LocalDate class HomeContract { data class HomeState( - val homeUiState: HomeUiState = HomeUiState.Idle, - val errorMessage: String? = null, - val year: Int = LocalDate.now().year, val month: Int = LocalDate.now().monthValue, val dayOfMonth: Int = LocalDate.now().dayOfMonth, + val calendarLoadState: UiLoadState = UiLoadState.Idle, val calendarMonthlyInfo: CalendarMonthlyInfo = CalendarMonthlyInfo(), - val selectedDailyInfo: DailyDiaryInfo = DailyDiaryInfo(), + val dailyDiaryLoadState: UiLoadState = UiLoadState.Idle, + val dailyDiaryInfo: DailyDiaryInfo = DailyDiaryInfo(), val showYearMonthPicker: Boolean = false, - val showDiaryDeleteBottomSheet: Boolean = false, val showDiaryDeleteDialog: Boolean = false, + val diaryDeleteState: UiLoadState = UiLoadState.Idle, + val errorMessage: String? = null, - val showDraftExpiredDialog: Boolean = false, - + val showInAppReviewPopup: Boolean = false, val showDraftNotificationPopup: Boolean = false, val showDraftNotificationToast: Boolean = false, - - val showInAppReviewPopup: Boolean = false, + val showDraftExpiredDialog: Boolean = false, ) : MavericksState { val selectedDate: LocalDate = LocalDate.of(year, month, dayOfMonth) - fun isDraftExpired(): Boolean = selectedDate != LocalDate.now() - - // 현재 선택된 날짜의 CalendarDailyInfo를 안전하게 가져오기 - fun getCurrentCalendarDailyInfo(): CalendarMonthlyInfo.CalendarDailyInfo? { + fun getCalendarDailyInfo(): CalendarMonthlyInfo.CalendarDailyInfo? { return if (isCalendarDataLoaded()) { calendarMonthlyInfo.calendarDailyInfoList[dayOfMonth - 1] } else { @@ -40,7 +37,6 @@ class HomeContract { } } - // 캘린더 데이터가 안전하게 로드되었는지 확인 private fun isCalendarDataLoaded(): Boolean { return calendarMonthlyInfo.calendarDailyInfoList.isNotEmpty() && dayOfMonth > 0 && @@ -49,35 +45,35 @@ class HomeContract { } sealed class HomeIntent { - data class LoadCalendarMonthlyInfo(val year: Int, val month: Int) : HomeIntent() - data class LoadDailyDiaryInfo(val year: Int, val month: Int, val dayOfMonth: Int) : HomeIntent() - + data class InitializeInfo(val year: Int, val month: Int, val dayOfMonth: Int) : HomeIntent() data object OnClickDiaryList : HomeIntent() data object OnClickYearMonth : HomeIntent() - data class UpdateYearMonth(val newYear: Int, val newMonth: Int) : HomeIntent() + data class ConfirmYearMonthPicker(val newYear: Int, val newMonth: Int) : HomeIntent() data object DismissYearMonthPicker : HomeIntent() data object OnClickSetting : HomeIntent() - + data class OnClickDay(val year: Int, val month: Int, val dayOfMonth: Int) : HomeIntent() data object OnClickDiaryDelete : HomeIntent() data object ShowDiaryDeleteDialog : HomeIntent() data class ConfirmDiaryDelete(val year: Int, val month: Int, val dayOfMonth: Int) : HomeIntent() data object DismissDiaryDelete : HomeIntent() - data object OnClickWriteDiary : HomeIntent() + data class OnClickReplyDiary(val replyStatus: ReplyStatus) : HomeIntent() data object ShowDraftExpiredDialog : HomeIntent() + data object ConfirmDraftExpiredDialog : HomeIntent() + data object DismissDraftExpiredDialog : HomeIntent() - data object OnClickReplyDiary : HomeIntent() - + data class RequestNotificationPermission(val granted: Boolean) : HomeIntent() + data class UpdateInAppReviewFlag(val flag: Boolean) : HomeIntent() data object EnableDraftAlarm : HomeIntent() - data class SendNotification(val granted: Boolean) : HomeIntent() - data class UpdateInAppReview(val show: Boolean) : HomeIntent() - data class UpdateDraftPopup(val show: Boolean) : HomeIntent() + data class UpdateDraftPopupFlag(val show: Boolean) : HomeIntent() + data object DismissDraftNotificationToast : HomeIntent() + } sealed interface HomeSideEffect { data object NavigateToDiaryList : HomeSideEffect data object NavigateToSetting : HomeSideEffect data object NavigateToWriteDiary : HomeSideEffect - data object NavigateToReplyLoading : HomeSideEffect + data class NavigateToReplyLoading(val replyStatus: ReplyStatus) : HomeSideEffect } } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt index fa86ab7a..dcaf69ce 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt @@ -38,8 +38,6 @@ import com.airbnb.mvrx.compose.mavericksViewModel import com.sopt.clody.R import com.sopt.clody.core.review.InAppReviewManager import com.sopt.clody.domain.type.ReplyStatus -import com.sopt.clody.presentation.ui.component.FailureScreen -import com.sopt.clody.presentation.ui.component.LoadingScreen import com.sopt.clody.presentation.ui.component.bottomsheet.DiaryDeleteSheet import com.sopt.clody.presentation.ui.component.button.ClodyButton import com.sopt.clody.presentation.ui.component.dialog.ClodyDialog @@ -77,30 +75,17 @@ fun HomeRoute( is HomeContract.HomeSideEffect.NavigateToDiaryList -> navigateToDiaryList(state.year, state.month) is HomeContract.HomeSideEffect.NavigateToSetting -> navigateToSetting() is HomeContract.HomeSideEffect.NavigateToWriteDiary -> navigateToWriteDiary(state.year, state.month, state.dayOfMonth) - is HomeContract.HomeSideEffect.NavigateToReplyLoading -> { - val replyStatus = state.calendarMonthlyInfo.calendarDailyInfoList - .find { it.date == java.time.LocalDate.of(state.year, state.month, state.dayOfMonth).toString() } - ?.replyStatus ?: ReplyStatus.UNREADY - navigateToReplyLoading( - state.year, - state.month, - state.dayOfMonth, - Route.ReplyLoading.ReplyLoadingFrom.HOME, - replyStatus, - ) - } + is HomeContract.HomeSideEffect.NavigateToReplyLoading -> navigateToReplyLoading(state.year, state.month, state.dayOfMonth, Route.ReplyLoading.ReplyLoadingFrom.HOME, effect.replyStatus) } } } } LaunchedEffect(Unit) { - // 먼저 월간 캘린더 데이터를 로드 - viewModel.postIntent(HomeContract.HomeIntent.LoadCalendarMonthlyInfo(state.year, state.month)) - // 월간 데이터 로드 완료 후 일간 데이터 로드 (ViewModel에서 순서 보장) + AmplitudeUtils.trackEvent(eventName = AmplitudeConstraints.HOME) + viewModel.postIntent(HomeContract.HomeIntent.InitializeInfo(state.year, state.month, state.dayOfMonth)) } - // 백핸들러 조작 var backPressedTime by remember { mutableLongStateOf(0L) } val backPressThreshold = 2000 BackHandler { @@ -112,11 +97,10 @@ fun HomeRoute( } } - // 알림 권한 신청 다이얼로그 val requestPermissionLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.RequestPermission(), ) { isGranted: Boolean -> - viewModel.postIntent(HomeContract.HomeIntent.SendNotification(isGranted)) + viewModel.postIntent(HomeContract.HomeIntent.RequestNotificationPermission(isGranted)) } LaunchedEffect(Unit) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { @@ -124,41 +108,24 @@ fun HomeRoute( if (ContextCompat.checkSelfPermission(context, notificationPermission) != PackageManager.PERMISSION_GRANTED) { requestPermissionLauncher.launch(notificationPermission) } else { - viewModel.postIntent(HomeContract.HomeIntent.SendNotification(true)) + viewModel.postIntent(HomeContract.HomeIntent.RequestNotificationPermission(true)) } } else { - viewModel.postIntent(HomeContract.HomeIntent.SendNotification(true)) + viewModel.postIntent(HomeContract.HomeIntent.RequestNotificationPermission(true)) } } - // 인앱리뷰 팝업 노출 - LaunchedEffect(Unit) { - AmplitudeUtils.trackEvent(eventName = AmplitudeConstraints.HOME) + LaunchedEffect(state.showInAppReviewPopup, isFromReplyDiary) { if (state.showInAppReviewPopup && isFromReplyDiary) { InAppReviewManager.showPopup(context as Activity) - viewModel.postIntent(HomeContract.HomeIntent.UpdateInAppReview(false)) + viewModel.postIntent(HomeContract.HomeIntent.UpdateInAppReviewFlag(false)) } } - when (state.homeUiState) { - HomeUiState.Idle -> { - } - HomeUiState.Loading -> { - LoadingScreen() - } - HomeUiState.Success -> { - HomeScreen( - state = state, - onIntent = { viewModel.postIntent(it) }, - ) - } - HomeUiState.Error -> { - FailureScreen( - message = state.errorMessage ?: stringResource(R.string.error_unknown), - confirmAction = { viewModel.postIntent(HomeContract.HomeIntent.LoadCalendarMonthlyInfo(state.year, state.month)) }, - ) - } - } + HomeScreen( + state = state, + onIntent = { viewModel.postIntent(it) }, + ) } @Composable @@ -190,27 +157,32 @@ fun HomeScreen( month = state.month, selectedDate = state.selectedDate, calendarMonthlyInfo = state.calendarMonthlyInfo, - onClickDay = { dayOfMonth -> onIntent(HomeContract.HomeIntent.LoadDailyDiaryInfo(state.year, state.month, dayOfMonth)) }, - selectedDailyInfo = state.selectedDailyInfo, + onClickDay = { dayOfMonth -> onIntent(HomeContract.HomeIntent.OnClickDay(state.year, state.month, dayOfMonth)) }, + selectedDailyInfo = state.dailyDiaryInfo, onClickDiaryDelete = { onIntent(HomeContract.HomeIntent.OnClickDiaryDelete) }, modifier = Modifier.padding(innerPadding), ) }, bottomBar = { - // 캘린더 데이터가 로드되었을 때만 DailyStateButton 표시 - state.getCurrentCalendarDailyInfo()?.let { calendarDailyInfo -> + state.getCalendarDailyInfo()?.let { calendarDailyInfo -> DailyStateButton( calendarDailyInfo = calendarDailyInfo, - selectedDailyInfo = state.selectedDailyInfo, + selectedDailyInfo = state.dailyDiaryInfo, onClickWriteDiary = { AmplitudeUtils.trackEvent(eventName = AmplitudeConstraints.HOME_WRITING_DIARY) - if (state.isDraftExpired()) { + onIntent(HomeContract.HomeIntent.OnClickWriteDiary) + }, + onClickContinueDraft = { + if (calendarDailyInfo.enableWriteDiary()) { + AmplitudeUtils.trackEvent(eventName = AmplitudeConstraints.HOME_WRITING_DIARY) + onIntent(HomeContract.HomeIntent.OnClickWriteDiary) + } else { onIntent(HomeContract.HomeIntent.ShowDraftExpiredDialog) - } else { onIntent(HomeContract.HomeIntent.OnClickWriteDiary) } + } }, onClickReplyDiary = { AmplitudeUtils.trackEvent(eventName = AmplitudeConstraints.HOME_REPLY) - onIntent(HomeContract.HomeIntent.OnClickReplyDiary) + onIntent(HomeContract.HomeIntent.OnClickReplyDiary(calendarDailyInfo.replyStatus)) }, modifier = Modifier .fillMaxWidth() @@ -229,15 +201,58 @@ fun HomeScreen( selectedYear = state.year, selectedMonth = state.month, onYearMonthSelected = { newYear, newMonth -> - onIntent(HomeContract.HomeIntent.UpdateYearMonth(newYear, newMonth)) + onIntent(HomeContract.HomeIntent.ConfirmYearMonthPicker(newYear, newMonth)) }, ) } } + if (state.showDiaryDeleteBottomSheet) { + DiaryDeleteSheet( + onDismiss = { + onIntent(HomeContract.HomeIntent.DismissDiaryDelete) + }, + showDiaryDeleteDialog = { + AmplitudeUtils.trackEvent(eventName = AmplitudeConstraints.HOME_DELETE_DIARY) + onIntent(HomeContract.HomeIntent.ShowDiaryDeleteDialog) + }, + ) + } + + if (state.showDiaryDeleteDialog) { + ClodyDialog( + titleMassage = stringResource(R.string.dialog_diary_delete_title), + descriptionMassage = stringResource(R.string.dialog_diary_delete_description), + confirmOption = stringResource(R.string.dialog_diary_delete_confirm), + dismissOption = stringResource(R.string.dialog_diary_delete_dismiss), + confirmAction = { + onIntent(HomeContract.HomeIntent.ConfirmDiaryDelete(state.year, state.month, state.dayOfMonth)) + }, + onDismiss = { + AmplitudeUtils.trackEvent(eventName = AmplitudeConstraints.HOME_NO_DELETE_DIARY) + onIntent(HomeContract.HomeIntent.DismissDiaryDelete) + }, + confirmButtonColor = ClodyTheme.colors.red, + confirmButtonTextColor = ClodyTheme.colors.white, + ) + } + + if (state.showDraftExpiredDialog) { + ClodyDialog( + titleMassage = stringResource(R.string.dialog_home_continue_draft_title), + descriptionMassage = stringResource(R.string.dialog_home_continue_draft_description), + confirmOption = stringResource(R.string.dialog_home_continue_draft_confirm), + dismissOption = stringResource(R.string.dialog_home_continue_draft_dismiss), + confirmAction = { onIntent(HomeContract.HomeIntent.ConfirmDraftExpiredDialog) }, + onDismiss = { onIntent(HomeContract.HomeIntent.DismissDraftExpiredDialog) }, + confirmButtonColor = ClodyTheme.colors.mainYellow, + confirmButtonTextColor = ClodyTheme.colors.gray01, + ) + } + if (state.showDraftNotificationPopup) { ClodyPopupBottomSheet( - onDismissRequest = { onIntent(HomeContract.HomeIntent.UpdateDraftPopup(false)) }, + onDismissRequest = { onIntent(HomeContract.HomeIntent.UpdateDraftPopupFlag(false)) }, content = { Column( horizontalAlignment = Alignment.CenterHorizontally, @@ -271,7 +286,7 @@ fun HomeScreen( text = stringResource(R.string.bottom_sheet_home_initial_draft_accept), onClick = { onIntent(HomeContract.HomeIntent.EnableDraftAlarm) - onIntent(HomeContract.HomeIntent.UpdateDraftPopup(false)) + onIntent(HomeContract.HomeIntent.UpdateDraftPopupFlag(false)) }, enabled = true, modifier = Modifier.fillMaxWidth(), @@ -279,7 +294,7 @@ fun HomeScreen( Text( text = stringResource(R.string.bottom_sheet_home_initial_draft_skip), modifier = Modifier - .clickable(onClick = { onIntent(HomeContract.HomeIntent.UpdateDraftPopup(false)) }) + .clickable(onClick = { onIntent(HomeContract.HomeIntent.UpdateDraftPopupFlag(false)) }) .padding(12.dp), color = ClodyTheme.colors.gray05, style = ClodyTheme.typography.body4Medium, @@ -301,7 +316,7 @@ fun HomeScreen( backgroundColor = ClodyTheme.colors.gray04, contentColor = ClodyTheme.colors.white, durationMillis = 3000, - onDismiss = { /* handled by state reset elsewhere if needed */ }, + onDismiss = { onIntent(HomeContract.HomeIntent.DismissDraftNotificationToast) }, modifier = Modifier .navigationBarsPadding() .padding(40.dp), @@ -309,45 +324,4 @@ fun HomeScreen( }, ) } - - if (state.showDraftExpiredDialog) { - ClodyDialog( - titleMassage = stringResource(R.string.dialog_home_continue_draft_title), - descriptionMassage = stringResource(R.string.dialog_home_continue_draft_description), - confirmOption = stringResource(R.string.dialog_home_continue_draft_confirm), - dismissOption = stringResource(R.string.dialog_home_continue_draft_dismiss), - confirmAction = { onIntent(HomeContract.HomeIntent.OnClickWriteDiary) }, - onDismiss = { }, - confirmButtonColor = ClodyTheme.colors.mainYellow, - confirmButtonTextColor = ClodyTheme.colors.gray01, - ) - } - - if (state.showDiaryDeleteBottomSheet) { - DiaryDeleteSheet( - onDismiss = { onIntent(HomeContract.HomeIntent.DismissDiaryDelete) }, - showDiaryDeleteDialog = { - AmplitudeUtils.trackEvent(eventName = AmplitudeConstraints.HOME_DELETE_DIARY) - onIntent(HomeContract.HomeIntent.ShowDiaryDeleteDialog) - }, - ) - } - - if (state.showDiaryDeleteDialog) { - ClodyDialog( - titleMassage = stringResource(R.string.dialog_diary_delete_title), - descriptionMassage = stringResource(R.string.dialog_diary_delete_description), - confirmOption = stringResource(R.string.dialog_diary_delete_confirm), - dismissOption = stringResource(R.string.dialog_diary_delete_dismiss), - confirmAction = { - onIntent(HomeContract.HomeIntent.ConfirmDiaryDelete(state.year, state.month, state.dayOfMonth)) - }, - onDismiss = { - AmplitudeUtils.trackEvent(eventName = AmplitudeConstraints.HOME_NO_DELETE_DIARY) - onIntent(HomeContract.HomeIntent.DismissDiaryDelete) - }, - confirmButtonColor = ClodyTheme.colors.red, - confirmButtonTextColor = ClodyTheme.colors.white, - ) - } } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeUiState.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeUiState.kt deleted file mode 100644 index c073e47e..00000000 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeUiState.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.sopt.clody.presentation.ui.home.screen - -enum class HomeUiState { - Idle, - Loading, - Success, - Error, -} diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt index 5346de4f..ba3be3a4 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt @@ -7,12 +7,16 @@ import com.airbnb.mvrx.hilt.hiltMavericksViewModelFactory import com.sopt.clody.core.fcm.FcmTokenProvider import com.sopt.clody.core.network.NetworkConnectivityObserver import com.sopt.clody.core.network.NetworkStatus +import com.sopt.clody.data.remote.dto.request.SendNotificationRequestDto +import com.sopt.clody.data.remote.dto.response.NotificationInfoResponseDto +import com.sopt.clody.data.remote.dto.response.SendNotificationResponseDto import com.sopt.clody.domain.model.CalendarMonthlyInfo import com.sopt.clody.domain.model.DailyDiaryInfo import com.sopt.clody.domain.repository.DiaryRepository import com.sopt.clody.domain.repository.DraftRepository import com.sopt.clody.domain.repository.NotificationRepository import com.sopt.clody.domain.repository.ReviewRepository +import com.sopt.clody.presentation.utils.base.UiLoadState import com.sopt.clody.presentation.utils.network.ErrorMessageProvider import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -55,51 +59,49 @@ class HomeViewModel @AssistedInject constructor( private suspend fun handleIntent(intent: HomeContract.HomeIntent) { when (intent) { - is HomeContract.HomeIntent.LoadCalendarMonthlyInfo -> loadCalendarMonthlyInfo(intent.year, intent.month) - is HomeContract.HomeIntent.LoadDailyDiaryInfo -> loadDailyDiaryInfo(intent.year, intent.month, intent.dayOfMonth) + is HomeContract.HomeIntent.InitializeInfo -> loadCalendarMonthlyInfo(intent.year, intent.month, intent.dayOfMonth) + is HomeContract.HomeIntent.OnClickDiaryList -> _sideEffects.send(HomeContract.HomeSideEffect.NavigateToDiaryList) is HomeContract.HomeIntent.OnClickYearMonth -> setState { copy(showYearMonthPicker = true) } - is HomeContract.HomeIntent.UpdateYearMonth -> updateYearMonth(intent.newYear, intent.newMonth) + is HomeContract.HomeIntent.ConfirmYearMonthPicker -> loadCalendarMonthlyInfo(intent.newYear, intent.newMonth) is HomeContract.HomeIntent.DismissYearMonthPicker -> setState { copy(showYearMonthPicker = false) } is HomeContract.HomeIntent.OnClickSetting -> _sideEffects.send(HomeContract.HomeSideEffect.NavigateToSetting) - + is HomeContract.HomeIntent.OnClickDay -> loadDailyDiaryInfo(intent.year, intent.month, intent.dayOfMonth) is HomeContract.HomeIntent.OnClickDiaryDelete -> setState { copy(showDiaryDeleteBottomSheet = true) } - is HomeContract.HomeIntent.ShowDiaryDeleteDialog -> setState { copy(showDiaryDeleteDialog = true, showDiaryDeleteBottomSheet = false) } + is HomeContract.HomeIntent.ShowDiaryDeleteDialog -> setState { copy(showDiaryDeleteBottomSheet = false, showDiaryDeleteDialog = true) } is HomeContract.HomeIntent.ConfirmDiaryDelete -> deleteDiary(intent.year, intent.month, intent.dayOfMonth) - is HomeContract.HomeIntent.DismissDiaryDelete -> setState { copy(showDiaryDeleteDialog = false) } - + is HomeContract.HomeIntent.DismissDiaryDelete -> setState { copy(showDiaryDeleteBottomSheet = false, showDiaryDeleteDialog = false) } is HomeContract.HomeIntent.OnClickWriteDiary -> _sideEffects.send(HomeContract.HomeSideEffect.NavigateToWriteDiary) + is HomeContract.HomeIntent.OnClickReplyDiary -> _sideEffects.send(HomeContract.HomeSideEffect.NavigateToReplyLoading(intent.replyStatus)) is HomeContract.HomeIntent.ShowDraftExpiredDialog -> setState { copy(showDraftExpiredDialog = true) } + is HomeContract.HomeIntent.ConfirmDraftExpiredDialog -> { + _sideEffects.send(HomeContract.HomeSideEffect.NavigateToWriteDiary) + setState { copy(showDraftExpiredDialog = false) } + } + is HomeContract.HomeIntent.DismissDraftExpiredDialog -> setState { copy(showDraftExpiredDialog = false) } - is HomeContract.HomeIntent.OnClickReplyDiary -> _sideEffects.send(HomeContract.HomeSideEffect.NavigateToReplyLoading) - + is HomeContract.HomeIntent.RequestNotificationPermission -> sendNotification(intent.granted) + is HomeContract.HomeIntent.UpdateInAppReviewFlag -> updateInAppReviewFlag(intent.flag) is HomeContract.HomeIntent.EnableDraftAlarm -> enableDraftAlarm() - is HomeContract.HomeIntent.SendNotification -> sendNotification(intent.granted) - is HomeContract.HomeIntent.UpdateInAppReview -> updateInAppReview(intent.show) - is HomeContract.HomeIntent.UpdateDraftPopup -> updateDraftPopup(intent.show) + is HomeContract.HomeIntent.UpdateDraftPopupFlag -> updateDraftPopupFlag(intent.show) + is HomeContract.HomeIntent.DismissDraftNotificationToast -> setState { copy(showDraftNotificationToast = false) } + } } - private suspend fun isNetworkAvailable(): Boolean = - networkConnectivityObserver.networkStatus.first() == NetworkStatus.Available + private suspend fun loadCalendarMonthlyInfo(year: Int, month: Int, dayOfMonth: Int = 1) { + setState { copy(calendarLoadState = UiLoadState.Loading, year = year, month = month) } - private suspend fun loadCalendarMonthlyInfo(year: Int, month: Int) { - setState { copy(homeUiState = HomeUiState.Loading, year = year, month = month, dayOfMonth = 1) } - - if (!isNetworkAvailable()) { + if (networkConnectivityObserver.networkStatus.first() != NetworkStatus.Available) { setState { copy(errorMessage = errorMessageProvider.getNetworkError()) } return } - val result = withContext(Dispatchers.IO) { - diaryRepository.getMonthlyCalendarData(year, month) - } - - result.fold( + withContext(Dispatchers.IO) { diaryRepository.getMonthlyCalendarData(year, month) }.fold( onSuccess = { data -> setState { copy( - homeUiState = HomeUiState.Success, + calendarLoadState = UiLoadState.Success, calendarMonthlyInfo = CalendarMonthlyInfo( totalCloverCount = data.totalCloverCount, calendarDailyInfoList = data.diaries.map { @@ -113,38 +115,33 @@ class HomeViewModel @AssistedInject constructor( ), ) } - // 월간 데이터 로드 완료 후 일간 데이터 로드 - loadDailyDiaryInfo(year, month, 1) + loadDailyDiaryInfo(year, month, dayOfMonth) }, onFailure = { setState { copy( - homeUiState = HomeUiState.Error, - errorMessage = errorMessageProvider.getTemporaryError(), + calendarLoadState = UiLoadState.Error, + errorMessage = errorMessageProvider.getServerError(), ) } }, ) } - private suspend fun loadDailyDiaryInfo(year: Int, month: Int, day: Int) { - setState { copy(homeUiState = HomeUiState.Loading, dayOfMonth = day) } + private suspend fun loadDailyDiaryInfo(year: Int, month: Int, dayOfMonth: Int) { + setState { copy(dailyDiaryLoadState = UiLoadState.Loading, dayOfMonth = dayOfMonth) } - if (!isNetworkAvailable()) { + if (networkConnectivityObserver.networkStatus.first() != NetworkStatus.Available) { setState { copy(errorMessage = errorMessageProvider.getNetworkError()) } return } - val result = withContext(Dispatchers.IO) { - diaryRepository.getDailyDiariesData(year, month, day) - } - - result.fold( + withContext(Dispatchers.IO) { diaryRepository.getDailyDiariesData(year, month, dayOfMonth) }.fold( onSuccess = { data -> setState { copy( - homeUiState = HomeUiState.Success, - selectedDailyInfo = DailyDiaryInfo( + dailyDiaryLoadState = UiLoadState.Success, + dailyDiaryInfo = DailyDiaryInfo( diaryList = data.diaries.map { it.content }, isDraft = data.isDraft, ), @@ -154,120 +151,97 @@ class HomeViewModel @AssistedInject constructor( onFailure = { setState { copy( - homeUiState = HomeUiState.Error, - errorMessage = errorMessageProvider.getTemporaryError(), + dailyDiaryLoadState = UiLoadState.Error, + errorMessage = errorMessageProvider.getServerError(), ) } }, ) } - private suspend fun updateYearMonth(newYear: Int, newMonth: Int) { - loadCalendarMonthlyInfo(newYear, newMonth) - } - private suspend fun deleteDiary(year: Int, month: Int, dayOfMonth: Int) { - setState { copy(homeUiState = HomeUiState.Loading) } + setState { copy(diaryDeleteState = UiLoadState.Loading) } - val result = withContext(Dispatchers.IO) { - diaryRepository.deleteDailyDiary(year, month, dayOfMonth) + if (networkConnectivityObserver.networkStatus.first() != NetworkStatus.Available) { + setState { copy(errorMessage = errorMessageProvider.getNetworkError()) } + return } - result.fold( + + withContext(Dispatchers.IO) { diaryRepository.deleteDailyDiary(year, month, dayOfMonth) }.fold( onSuccess = { - loadCalendarMonthlyInfo(year, month) - setState { - copy( - homeUiState = HomeUiState.Success, - showDiaryDeleteDialog = false, - showDiaryDeleteBottomSheet = false, - ) - } + loadCalendarMonthlyInfo(year, month, dayOfMonth) + setState { copy(showDiaryDeleteDialog = false, diaryDeleteState = UiLoadState.Success) } }, onFailure = { - setState { - copy( - homeUiState = HomeUiState.Error, - errorMessage = errorMessageProvider.getTemporaryError(), - ) - } + setState { copy(diaryDeleteState = UiLoadState.Error, errorMessage = errorMessageProvider.getServerError()) } }, ) } - private suspend fun enableDraftAlarm() { - setState { copy(homeUiState = HomeUiState.Loading) } - - if (!isNetworkAvailable()) { - setState { copy(errorMessage = errorMessageProvider.getNetworkError()) } - return - } - - val fcmToken = fcmTokenProvider.getToken().orEmpty() - val info = withContext(Dispatchers.IO) { notificationRepository.getNotificationInfo() } + // 공통 유틸: 알림 설정(info) 조회 실패 시 에러 상태 세팅 후 null 반환 + private suspend fun getNotificationInfoOrNull(): NotificationInfoResponseDto? = + withContext(Dispatchers.IO) { notificationRepository.getNotificationInfo() } .getOrElse { setState { copy(errorMessage = errorMessageProvider.getTemporaryError()) } - return + null } - val request = com.sopt.clody.data.remote.dto.request.SendNotificationRequestDto( - isDiaryAlarm = info.isDiaryAlarm, - isDraftAlarm = true, - isReplyAlarm = info.isReplyAlarm, - time = info.time, - fcmToken = fcmToken, - ) - - withContext(Dispatchers.IO) { notificationRepository.sendNotification(request) } - .fold( - onSuccess = { - setState { - copy( - homeUiState = HomeUiState.Success, - showDraftNotificationToast = true, - showDraftNotificationPopup = false, - ) - } - }, - onFailure = { - setState { - copy( - homeUiState = HomeUiState.Error, - errorMessage = errorMessageProvider.getTemporaryError(), - ) - } - }, - ) + // 공통 유틸: 요청 생성 람다만 넘기면 전송까지 수행 + private suspend fun buildAndSendNotification( + build: (info: NotificationInfoResponseDto, fcmToken: String) -> SendNotificationRequestDto + ): Result { + val token = fcmTokenProvider.getToken().orEmpty() + val info = getNotificationInfoOrNull() ?: return Result.failure(IllegalStateException("notification info null")) + val request = build(info, token) + return withContext(Dispatchers.IO) { notificationRepository.sendNotification(request) } } + // 권한 부여/해제에 따른 전송 (결과는 기존처럼 별도 처리 없이 호출만) private suspend fun sendNotification(granted: Boolean) { - setState { copy(homeUiState = HomeUiState.Loading) } + buildAndSendNotification { info, token -> + SendNotificationRequestDto( + isDiaryAlarm = granted, + isDraftAlarm = info.isDraftAlarm, + isReplyAlarm = granted, + time = info.time, + fcmToken = token, + ) + } + } - val fcmToken = fcmTokenProvider.getToken().orEmpty() - val info = withContext(Dispatchers.IO) { notificationRepository.getNotificationInfo() } - .getOrElse { return } + // 임시저장(draft) 알림 활성화 + private suspend fun enableDraftAlarm() { + if (networkConnectivityObserver.networkStatus.first() != NetworkStatus.Available) { + setState { copy(errorMessage = errorMessageProvider.getNetworkError()) } + return + } - val request = com.sopt.clody.data.remote.dto.request.SendNotificationRequestDto( - isDiaryAlarm = granted, - isDraftAlarm = info.isDraftAlarm, - isReplyAlarm = granted, - time = info.time, - fcmToken = fcmToken, - ) - withContext(Dispatchers.IO) { notificationRepository.sendNotification(request) } - .fold( - onSuccess = { setState { copy(homeUiState = HomeUiState.Success) } }, - onFailure = { setState { copy(homeUiState = HomeUiState.Error) } }, + buildAndSendNotification { info, token -> + SendNotificationRequestDto( + isDiaryAlarm = info.isDiaryAlarm, + isDraftAlarm = true, + isReplyAlarm = info.isReplyAlarm, + time = info.time, + fcmToken = token, ) + }.fold( + onSuccess = { + setState { copy(showDraftNotificationPopup = false, showDraftNotificationToast = true) } + }, + onFailure = { + setState { copy(errorMessage = errorMessageProvider.getTemporaryError()) } + }, + ) } - private fun updateInAppReview(show: Boolean) { - reviewRepository.setShouldShowPopup(show) - setState { copy(showInAppReviewPopup = show) } + private fun updateDraftPopupFlag(flag: Boolean) { + draftRepository.setIsFirstUse(flag) + setState { copy(showDraftNotificationPopup = flag) } } - private fun updateDraftPopup(show: Boolean) { - if (!show) draftRepository.setIsFirstUse(false) - setState { copy(showDraftNotificationPopup = show) } + private fun updateInAppReviewFlag(flag: Boolean) { + reviewRepository.setShouldShowPopup(flag) + setState { copy(showInAppReviewPopup = flag) } } @AssistedFactory diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/type/DailyCloverType.kt b/app/src/main/java/com/sopt/clody/presentation/ui/type/DailyCloverType.kt index ccdca7b1..58258707 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/type/DailyCloverType.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/type/DailyCloverType.kt @@ -32,9 +32,9 @@ enum class DailyCloverType(@DrawableRes val iconRes: Int) { fun getType(info: CalendarMonthlyInfo.CalendarDailyInfo): DailyCloverType { return when { info.replyStatus == ReplyStatus.HAS_DRAFT -> DRAFT_SAVED - info.replyStatus == ReplyStatus.INVALID_DRAFT || info.isDeleted -> DISABLED_REPLY - info.enableWriteDiary() && info.diaryCount == 0 -> ENABLED_DIARY - info.replyStatus == ReplyStatus.UNREADY && info.diaryCount > 0 -> WAITING_REPLY + info.replyStatus == ReplyStatus.INVALID_DRAFT || (info.isDeleted && info.diaryCount > 0) -> DISABLED_REPLY + info.isToday() && info.diaryCount == 0 -> ENABLED_DIARY + info.isToday() && info.replyStatus == ReplyStatus.UNREADY && info.diaryCount > 0 -> WAITING_REPLY info.replyStatus == ReplyStatus.READY_READ && info.diaryCount in 1..2 -> BOTTOM_CLOVER info.replyStatus == ReplyStatus.READY_READ && info.diaryCount in 3..4 -> MID_CLOVER info.replyStatus == ReplyStatus.READY_READ && info.diaryCount >= 5 -> TOP_CLOVER diff --git a/app/src/main/java/com/sopt/clody/presentation/utils/base/UiLoadState.kt b/app/src/main/java/com/sopt/clody/presentation/utils/base/UiLoadState.kt new file mode 100644 index 00000000..e059ae47 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/presentation/utils/base/UiLoadState.kt @@ -0,0 +1,8 @@ +package com.sopt.clody.presentation.utils.base + +enum class UiLoadState { + Idle, + Loading, + Success, + Error +} From 5568ba8e10b7dd86264b234f8c7f8ef1ab20eb3c Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Fri, 22 Aug 2025 15:46:01 +0900 Subject: [PATCH 13/20] =?UTF-8?q?[REFACTOR/#323]=20=EB=B6=80=EA=B0=80?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5(=EC=95=8C=EB=A6=BC=EA=B6=8C=ED=95=9C?= =?UTF-8?q?=EC=9A=94=EC=B2=AD,=20=EC=9D=B8=EC=95=B1=EB=A6=AC=EB=B7=B0,=20?= =?UTF-8?q?=EC=9D=B4=EC=96=B4=EC=93=B0=EA=B8=B0=20=EC=95=8C=EB=A6=BC)=20?= =?UTF-8?q?=EC=A0=90=EA=B2=80=ED=95=A9=EB=8B=88=EB=8B=A4.=20-=20=EC=9D=B8?= =?UTF-8?q?=EC=95=B1=EB=A6=AC=EB=B7=B0=EB=8A=94=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=95=B1=EC=9D=B4=EB=9D=BC=20=ED=94=8C=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EC=8A=A4=ED=86=A0=EC=96=B4=20=EC=9A=94=EC=B2=AD?= =?UTF-8?q?=EC=9D=B4=20=EB=B6=88=EA=B0=80=ED=95=98=EC=97=AC=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=EA=B0=80=20=EC=96=B4=EB=A0=B5=EC=8A=B5?= =?UTF-8?q?=EB=8B=88=EB=8B=A4.=20=EC=BD=98=EC=86=94=EC=97=90=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=EC=95=B1=EC=9D=84=20=EB=94=B0=EB=A1=9C=20?= =?UTF-8?q?=EC=97=85=EB=A1=9C=EB=93=9C=20=ED=95=B4=EC=95=BC=ED=95=A8.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/home/screen/HomeContract.kt | 15 ++++----- .../presentation/ui/home/screen/HomeScreen.kt | 8 ++--- .../ui/home/screen/HomeViewModel.kt | 32 +++++++++++-------- 3 files changed, 29 insertions(+), 26 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeContract.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeContract.kt index 3f3a4f84..1a9d40d1 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeContract.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeContract.kt @@ -20,12 +20,11 @@ class HomeContract { val showDiaryDeleteBottomSheet: Boolean = false, val showDiaryDeleteDialog: Boolean = false, val diaryDeleteState: UiLoadState = UiLoadState.Idle, - val errorMessage: String? = null, - + val showDraftExpiredDialog: Boolean = false, val showInAppReviewPopup: Boolean = false, val showDraftNotificationPopup: Boolean = false, val showDraftNotificationToast: Boolean = false, - val showDraftExpiredDialog: Boolean = false, + val errorMessage: String? = null, ) : MavericksState { val selectedDate: LocalDate = LocalDate.of(year, month, dayOfMonth) @@ -56,24 +55,22 @@ class HomeContract { data object ShowDiaryDeleteDialog : HomeIntent() data class ConfirmDiaryDelete(val year: Int, val month: Int, val dayOfMonth: Int) : HomeIntent() data object DismissDiaryDelete : HomeIntent() - data object OnClickWriteDiary : HomeIntent() + data class OnClickWriteDiary(val year: Int, val month: Int, val dayOfMonth: Int) : HomeIntent() data class OnClickReplyDiary(val replyStatus: ReplyStatus) : HomeIntent() data object ShowDraftExpiredDialog : HomeIntent() - data object ConfirmDraftExpiredDialog : HomeIntent() + data class ConfirmDraftExpiredDialog(val year: Int, val month: Int, val dayOfMonth: Int) : HomeIntent() data object DismissDraftExpiredDialog : HomeIntent() - data class RequestNotificationPermission(val granted: Boolean) : HomeIntent() - data class UpdateInAppReviewFlag(val flag: Boolean) : HomeIntent() data object EnableDraftAlarm : HomeIntent() data class UpdateDraftPopupFlag(val show: Boolean) : HomeIntent() data object DismissDraftNotificationToast : HomeIntent() - + data class UpdateInAppReviewFlag(val newValue: Boolean) : HomeIntent() } sealed interface HomeSideEffect { data object NavigateToDiaryList : HomeSideEffect data object NavigateToSetting : HomeSideEffect - data object NavigateToWriteDiary : HomeSideEffect + data class NavigateToWriteDiary(val year: Int, val month: Int, val dayOfMonth: Int) : HomeSideEffect data class NavigateToReplyLoading(val replyStatus: ReplyStatus) : HomeSideEffect } } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt index dcaf69ce..9701afc1 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt @@ -74,7 +74,7 @@ fun HomeRoute( when (effect) { is HomeContract.HomeSideEffect.NavigateToDiaryList -> navigateToDiaryList(state.year, state.month) is HomeContract.HomeSideEffect.NavigateToSetting -> navigateToSetting() - is HomeContract.HomeSideEffect.NavigateToWriteDiary -> navigateToWriteDiary(state.year, state.month, state.dayOfMonth) + is HomeContract.HomeSideEffect.NavigateToWriteDiary -> navigateToWriteDiary(effect.year, effect.month, effect.dayOfMonth) is HomeContract.HomeSideEffect.NavigateToReplyLoading -> navigateToReplyLoading(state.year, state.month, state.dayOfMonth, Route.ReplyLoading.ReplyLoadingFrom.HOME, effect.replyStatus) } } @@ -170,12 +170,12 @@ fun HomeScreen( selectedDailyInfo = state.dailyDiaryInfo, onClickWriteDiary = { AmplitudeUtils.trackEvent(eventName = AmplitudeConstraints.HOME_WRITING_DIARY) - onIntent(HomeContract.HomeIntent.OnClickWriteDiary) + onIntent(HomeContract.HomeIntent.OnClickWriteDiary(state.year, state.month, state.dayOfMonth)) }, onClickContinueDraft = { if (calendarDailyInfo.enableWriteDiary()) { AmplitudeUtils.trackEvent(eventName = AmplitudeConstraints.HOME_WRITING_DIARY) - onIntent(HomeContract.HomeIntent.OnClickWriteDiary) + onIntent(HomeContract.HomeIntent.OnClickWriteDiary(state.year, state.month, state.dayOfMonth)) } else { onIntent(HomeContract.HomeIntent.ShowDraftExpiredDialog) } @@ -243,7 +243,7 @@ fun HomeScreen( descriptionMassage = stringResource(R.string.dialog_home_continue_draft_description), confirmOption = stringResource(R.string.dialog_home_continue_draft_confirm), dismissOption = stringResource(R.string.dialog_home_continue_draft_dismiss), - confirmAction = { onIntent(HomeContract.HomeIntent.ConfirmDraftExpiredDialog) }, + confirmAction = { onIntent(HomeContract.HomeIntent.ConfirmDraftExpiredDialog(state.year, state.month, state.dayOfMonth)) }, onDismiss = { onIntent(HomeContract.HomeIntent.DismissDraftExpiredDialog) }, confirmButtonColor = ClodyTheme.colors.mainYellow, confirmButtonTextColor = ClodyTheme.colors.gray01, diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt index ba3be3a4..5ecb4c1a 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt @@ -51,6 +51,7 @@ class HomeViewModel @AssistedInject constructor( .receiveAsFlow() .onEach(::handleIntent) .launchIn(viewModelScope) + initialize() } fun postIntent(intent: HomeContract.HomeIntent) { @@ -60,7 +61,6 @@ class HomeViewModel @AssistedInject constructor( private suspend fun handleIntent(intent: HomeContract.HomeIntent) { when (intent) { is HomeContract.HomeIntent.InitializeInfo -> loadCalendarMonthlyInfo(intent.year, intent.month, intent.dayOfMonth) - is HomeContract.HomeIntent.OnClickDiaryList -> _sideEffects.send(HomeContract.HomeSideEffect.NavigateToDiaryList) is HomeContract.HomeIntent.OnClickYearMonth -> setState { copy(showYearMonthPicker = true) } is HomeContract.HomeIntent.ConfirmYearMonthPicker -> loadCalendarMonthlyInfo(intent.newYear, intent.newMonth) @@ -71,24 +71,29 @@ class HomeViewModel @AssistedInject constructor( is HomeContract.HomeIntent.ShowDiaryDeleteDialog -> setState { copy(showDiaryDeleteBottomSheet = false, showDiaryDeleteDialog = true) } is HomeContract.HomeIntent.ConfirmDiaryDelete -> deleteDiary(intent.year, intent.month, intent.dayOfMonth) is HomeContract.HomeIntent.DismissDiaryDelete -> setState { copy(showDiaryDeleteBottomSheet = false, showDiaryDeleteDialog = false) } - is HomeContract.HomeIntent.OnClickWriteDiary -> _sideEffects.send(HomeContract.HomeSideEffect.NavigateToWriteDiary) + is HomeContract.HomeIntent.OnClickWriteDiary -> _sideEffects.send(HomeContract.HomeSideEffect.NavigateToWriteDiary(intent.year, intent.month, intent.dayOfMonth)) is HomeContract.HomeIntent.OnClickReplyDiary -> _sideEffects.send(HomeContract.HomeSideEffect.NavigateToReplyLoading(intent.replyStatus)) is HomeContract.HomeIntent.ShowDraftExpiredDialog -> setState { copy(showDraftExpiredDialog = true) } is HomeContract.HomeIntent.ConfirmDraftExpiredDialog -> { - _sideEffects.send(HomeContract.HomeSideEffect.NavigateToWriteDiary) + _sideEffects.send(HomeContract.HomeSideEffect.NavigateToWriteDiary(intent.year, intent.month, intent.dayOfMonth)) setState { copy(showDraftExpiredDialog = false) } } is HomeContract.HomeIntent.DismissDraftExpiredDialog -> setState { copy(showDraftExpiredDialog = false) } - is HomeContract.HomeIntent.RequestNotificationPermission -> sendNotification(intent.granted) - is HomeContract.HomeIntent.UpdateInAppReviewFlag -> updateInAppReviewFlag(intent.flag) is HomeContract.HomeIntent.EnableDraftAlarm -> enableDraftAlarm() is HomeContract.HomeIntent.UpdateDraftPopupFlag -> updateDraftPopupFlag(intent.show) is HomeContract.HomeIntent.DismissDraftNotificationToast -> setState { copy(showDraftNotificationToast = false) } - + is HomeContract.HomeIntent.UpdateInAppReviewFlag -> updateInAppReviewFlag(intent.newValue) } } + private fun initialize() { + setState { copy( + showInAppReviewPopup = reviewRepository.getShouldShowPopup(), + showDraftNotificationPopup = draftRepository.getIsFirstUse() + ) } + } + private suspend fun loadCalendarMonthlyInfo(year: Int, month: Int, dayOfMonth: Int = 1) { setState { copy(calendarLoadState = UiLoadState.Loading, year = year, month = month) } @@ -178,7 +183,6 @@ class HomeViewModel @AssistedInject constructor( ) } - // 공통 유틸: 알림 설정(info) 조회 실패 시 에러 상태 세팅 후 null 반환 private suspend fun getNotificationInfoOrNull(): NotificationInfoResponseDto? = withContext(Dispatchers.IO) { notificationRepository.getNotificationInfo() } .getOrElse { @@ -186,7 +190,6 @@ class HomeViewModel @AssistedInject constructor( null } - // 공통 유틸: 요청 생성 람다만 넘기면 전송까지 수행 private suspend fun buildAndSendNotification( build: (info: NotificationInfoResponseDto, fcmToken: String) -> SendNotificationRequestDto ): Result { @@ -196,8 +199,12 @@ class HomeViewModel @AssistedInject constructor( return withContext(Dispatchers.IO) { notificationRepository.sendNotification(request) } } - // 권한 부여/해제에 따른 전송 (결과는 기존처럼 별도 처리 없이 호출만) private suspend fun sendNotification(granted: Boolean) { + if (networkConnectivityObserver.networkStatus.first() != NetworkStatus.Available) { + setState { copy(errorMessage = errorMessageProvider.getNetworkError()) } + return + } + buildAndSendNotification { info, token -> SendNotificationRequestDto( isDiaryAlarm = granted, @@ -209,7 +216,6 @@ class HomeViewModel @AssistedInject constructor( } } - // 임시저장(draft) 알림 활성화 private suspend fun enableDraftAlarm() { if (networkConnectivityObserver.networkStatus.first() != NetworkStatus.Available) { setState { copy(errorMessage = errorMessageProvider.getNetworkError()) } @@ -239,9 +245,9 @@ class HomeViewModel @AssistedInject constructor( setState { copy(showDraftNotificationPopup = flag) } } - private fun updateInAppReviewFlag(flag: Boolean) { - reviewRepository.setShouldShowPopup(flag) - setState { copy(showInAppReviewPopup = flag) } + private fun updateInAppReviewFlag(newValue: Boolean) { + reviewRepository.setShouldShowPopup(newValue) + setState { copy(showInAppReviewPopup = newValue) } } @AssistedFactory From 264bca23d50d556c10af547648be633ae34fc36b Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Fri, 22 Aug 2025 15:56:21 +0900 Subject: [PATCH 14/20] [CHORE/#323] ktlint Format --- .../ui/home/component/DailyStateButton.kt | 2 -- .../presentation/ui/home/screen/HomeScreen.kt | 8 +++++++- .../presentation/ui/home/screen/HomeViewModel.kt | 16 ++++++++++------ .../clody/presentation/utils/base/UiLoadState.kt | 2 +- 4 files changed, 18 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/component/DailyStateButton.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/DailyStateButton.kt index 429ceecc..c1f3e049 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/component/DailyStateButton.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/DailyStateButton.kt @@ -10,8 +10,6 @@ import com.sopt.clody.presentation.ui.component.button.ClodyButton import com.sopt.clody.presentation.ui.component.button.ClodyReplyButton import com.sopt.clody.presentation.ui.type.DailyStateButtonType import com.sopt.clody.ui.theme.ClodyTheme -import timber.log.Timber -import java.time.LocalDate @Composable fun DailyStateButton( diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt index 9701afc1..e93f5935 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt @@ -75,7 +75,13 @@ fun HomeRoute( is HomeContract.HomeSideEffect.NavigateToDiaryList -> navigateToDiaryList(state.year, state.month) is HomeContract.HomeSideEffect.NavigateToSetting -> navigateToSetting() is HomeContract.HomeSideEffect.NavigateToWriteDiary -> navigateToWriteDiary(effect.year, effect.month, effect.dayOfMonth) - is HomeContract.HomeSideEffect.NavigateToReplyLoading -> navigateToReplyLoading(state.year, state.month, state.dayOfMonth, Route.ReplyLoading.ReplyLoadingFrom.HOME, effect.replyStatus) + is HomeContract.HomeSideEffect.NavigateToReplyLoading -> navigateToReplyLoading( + state.year, + state.month, + state.dayOfMonth, + Route.ReplyLoading.ReplyLoadingFrom.HOME, + effect.replyStatus, + ) } } } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt index 5ecb4c1a..3e43e2dd 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt @@ -71,7 +71,9 @@ class HomeViewModel @AssistedInject constructor( is HomeContract.HomeIntent.ShowDiaryDeleteDialog -> setState { copy(showDiaryDeleteBottomSheet = false, showDiaryDeleteDialog = true) } is HomeContract.HomeIntent.ConfirmDiaryDelete -> deleteDiary(intent.year, intent.month, intent.dayOfMonth) is HomeContract.HomeIntent.DismissDiaryDelete -> setState { copy(showDiaryDeleteBottomSheet = false, showDiaryDeleteDialog = false) } - is HomeContract.HomeIntent.OnClickWriteDiary -> _sideEffects.send(HomeContract.HomeSideEffect.NavigateToWriteDiary(intent.year, intent.month, intent.dayOfMonth)) + is HomeContract.HomeIntent.OnClickWriteDiary -> _sideEffects.send( + HomeContract.HomeSideEffect.NavigateToWriteDiary(intent.year, intent.month, intent.dayOfMonth), + ) is HomeContract.HomeIntent.OnClickReplyDiary -> _sideEffects.send(HomeContract.HomeSideEffect.NavigateToReplyLoading(intent.replyStatus)) is HomeContract.HomeIntent.ShowDraftExpiredDialog -> setState { copy(showDraftExpiredDialog = true) } is HomeContract.HomeIntent.ConfirmDraftExpiredDialog -> { @@ -88,10 +90,12 @@ class HomeViewModel @AssistedInject constructor( } private fun initialize() { - setState { copy( - showInAppReviewPopup = reviewRepository.getShouldShowPopup(), - showDraftNotificationPopup = draftRepository.getIsFirstUse() - ) } + setState { + copy( + showInAppReviewPopup = reviewRepository.getShouldShowPopup(), + showDraftNotificationPopup = draftRepository.getIsFirstUse(), + ) + } } private suspend fun loadCalendarMonthlyInfo(year: Int, month: Int, dayOfMonth: Int = 1) { @@ -191,7 +195,7 @@ class HomeViewModel @AssistedInject constructor( } private suspend fun buildAndSendNotification( - build: (info: NotificationInfoResponseDto, fcmToken: String) -> SendNotificationRequestDto + build: (info: NotificationInfoResponseDto, fcmToken: String) -> SendNotificationRequestDto, ): Result { val token = fcmTokenProvider.getToken().orEmpty() val info = getNotificationInfoOrNull() ?: return Result.failure(IllegalStateException("notification info null")) diff --git a/app/src/main/java/com/sopt/clody/presentation/utils/base/UiLoadState.kt b/app/src/main/java/com/sopt/clody/presentation/utils/base/UiLoadState.kt index e059ae47..e0bc4313 100644 --- a/app/src/main/java/com/sopt/clody/presentation/utils/base/UiLoadState.kt +++ b/app/src/main/java/com/sopt/clody/presentation/utils/base/UiLoadState.kt @@ -4,5 +4,5 @@ enum class UiLoadState { Idle, Loading, Success, - Error + Error, } From 64cd81df87ec8b33545d512e2418df171653ca16 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Wed, 10 Sep 2025 11:16:37 +0900 Subject: [PATCH 15/20] =?UTF-8?q?[CHORE/#323]=20=EC=9D=BC=EA=B8=B0=20?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=20=ED=9B=84=20=EC=9E=84=EC=8B=9C=EC=A0=80?= =?UTF-8?q?=EC=9E=A5=ED=95=9C=20=EC=9D=BC=EA=B8=B0=EB=A5=BC=20=EB=B3=B4?= =?UTF-8?q?=EB=82=BC=20=EA=B2=BD=EC=9A=B0,=20=EC=82=AD=EC=A0=9C=EB=90=9C?= =?UTF-8?q?=20=EC=9D=BC=EA=B8=B0=EB=A1=9C=20=EC=B7=A8=EA=B8=89=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=9A=B0=EC=84=A0=EC=88=9C=EC=9C=84?= =?UTF-8?q?=EB=A5=BC=20=EB=B3=80=EA=B2=BD=ED=95=A9=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../clody/domain/model/CalendarMonthlyInfo.kt | 2 +- .../ui/home/component/DailyDiary.kt | 40 +++++++++---------- .../ui/type/DailyStateButtonType.kt | 4 +- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/domain/model/CalendarMonthlyInfo.kt b/app/src/main/java/com/sopt/clody/domain/model/CalendarMonthlyInfo.kt index 77e5de64..28d3b0df 100644 --- a/app/src/main/java/com/sopt/clody/domain/model/CalendarMonthlyInfo.kt +++ b/app/src/main/java/com/sopt/clody/domain/model/CalendarMonthlyInfo.kt @@ -7,7 +7,7 @@ import java.time.ZoneId /** * 홈 화면의 월별 달력에서 사용되는 정보 * - * @property totalCloverCount 해당 월에 받은 클로버(답장)의 개수 + * @property totalCloverCount 지금까지 모은 클로버(답장)의 개수. 연 단위로 카운트 * @property calendarDailyInfoList 해당 월의 일별 정보 * * */ diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/component/DailyDiary.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/DailyDiary.kt index d6aa1d39..a322f451 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/component/DailyDiary.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/DailyDiary.kt @@ -73,11 +73,28 @@ fun DailyDiary( } when { + selectedDailyInfo.diaryList.isNotEmpty() -> { + selectedDailyInfo.diaryList.forEachIndexed { index, diary -> + Column( + modifier = Modifier + .fillMaxWidth() + .background(ClodyTheme.colors.gray08, shape = RoundedCornerShape(10.dp)) + .padding(18.dp), + ) { + Text( + text = "${index + 1}. $diary", + style = ClodyTheme.typography.body2Medium, + color = ClodyTheme.colors.gray01, + ) + } + } + } + selectedDailyInfo.isDraft -> { Box( contentAlignment = Alignment.Center, modifier = Modifier - .fillMaxSize() + .fillMaxWidth() .padding(vertical = 44.dp), ) { Text( @@ -89,11 +106,11 @@ fun DailyDiary( } } - selectedDailyInfo.diaryList.isEmpty() -> { + else -> { Box( contentAlignment = Alignment.Center, modifier = Modifier - .fillMaxSize() + .fillMaxWidth() .padding(vertical = 44.dp), ) { Text( @@ -104,23 +121,6 @@ fun DailyDiary( ) } } - - else -> { - selectedDailyInfo.diaryList.forEachIndexed { index, diary -> - Column( - modifier = Modifier - .fillMaxWidth() - .background(ClodyTheme.colors.gray08, shape = RoundedCornerShape(10.dp)) - .padding(18.dp), - ) { - Text( - text = "${index + 1}. $diary", - style = ClodyTheme.typography.body2Medium, - color = ClodyTheme.colors.gray01, - ) - } - } - } } } } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/type/DailyStateButtonType.kt b/app/src/main/java/com/sopt/clody/presentation/ui/type/DailyStateButtonType.kt index dfc63a57..aced5045 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/type/DailyStateButtonType.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/type/DailyStateButtonType.kt @@ -13,9 +13,9 @@ enum class DailyStateButtonType { dailyDiaryInfo: DailyDiaryInfo, ): DailyStateButtonType { return when { + calendarDailyInfo.isDeleted && calendarDailyInfo.diaryCount > 0 -> REPLY_DISABLED dailyDiaryInfo.isDraft -> DRAFT_ENABLED - (calendarDailyInfo.isDeleted && calendarDailyInfo.diaryCount > 0) || - calendarDailyInfo.replyStatus == ReplyStatus.INVALID_DRAFT -> REPLY_DISABLED + calendarDailyInfo.replyStatus == ReplyStatus.INVALID_DRAFT -> REPLY_DISABLED calendarDailyInfo.replyStatus == ReplyStatus.READY_READ || calendarDailyInfo.replyStatus == ReplyStatus.READY_NOT_READ || (calendarDailyInfo.replyStatus == ReplyStatus.UNREADY && calendarDailyInfo.diaryCount > 0) -> REPLY_ENABLED From e388bba08d27c8bf307dcd594a0b312205f05fae Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Thu, 9 Oct 2025 20:36:54 +0900 Subject: [PATCH 16/20] =?UTF-8?q?[FIX/#323]=20=EB=A7=8C=EB=A3=8C=EB=90=9C?= =?UTF-8?q?=20=EC=9E=84=EC=8B=9C=EC=A0=80=EC=9E=A5=20=EC=9D=BC=EA=B8=B0?= =?UTF-8?q?=EB=A5=BC=20=EC=9E=91=EC=84=B1=ED=95=98=EB=A0=A4=20=ED=95=98?= =?UTF-8?q?=EB=8A=94=EB=8D=B0,=20=EC=A0=84=EB=82=A0=EB=A1=9C=20convert=20?= =?UTF-8?q?=EB=90=98=EC=96=B4=20=EC=98=A4=EB=A5=98=EA=B0=80=20=EB=B0=9C?= =?UTF-8?q?=EC=83=9D=ED=95=98=EB=8A=94=20=EB=AC=B8=EC=A0=9C=EB=A5=BC=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0=ED=96=88=EC=8A=B5=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../utils/extension/TimeZoneExt.kt | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/utils/extension/TimeZoneExt.kt b/app/src/main/java/com/sopt/clody/presentation/utils/extension/TimeZoneExt.kt index 4314fdb4..91476516 100644 --- a/app/src/main/java/com/sopt/clody/presentation/utils/extension/TimeZoneExt.kt +++ b/app/src/main/java/com/sopt/clody/presentation/utils/extension/TimeZoneExt.kt @@ -1,6 +1,7 @@ package com.sopt.clody.presentation.utils.extension import java.time.LocalDate +import java.time.LocalDateTime import java.time.LocalTime import java.time.ZoneId import java.time.ZonedDateTime @@ -66,17 +67,17 @@ fun convertUTZtoKST(timePeriod: TimePeriod, hour: String, minute: String, refere * @param day 작성된 일기의 일 * */ fun convertDateToKstDateTime(year: Int, month: Int, day: Int): String { - val localNowDate = LocalDate.now() - val targetDate = LocalDate.of(year, month, day) + val userZone = ZoneId.systemDefault() val kstZone = ZoneId.of("Asia/Seoul") - val nowKst = ZonedDateTime.now(kstZone) - val targetZonedDateTime = if (targetDate == localNowDate) { - nowKst - } else { - nowKst.minusDays(1) - } + val userNow = ZonedDateTime.now(userZone) + val userDateTime = LocalDateTime.of( + LocalDate.of(year, month, day), + userNow.toLocalTime() + ).atZone(userZone) + + val kstDateTime = userDateTime.withZoneSameInstant(kstZone) val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss") - return targetZonedDateTime.format(formatter) + return kstDateTime.format(formatter) } From e5c09d4cb03da0291c7a744721076f26fac6d259 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Thu, 9 Oct 2025 20:38:05 +0900 Subject: [PATCH 17/20] =?UTF-8?q?[REFACTOR/#323]=20=EC=97=90=EB=9F=AC?= =?UTF-8?q?=EB=8C=80=EC=9D=91(=EB=8B=A4=EC=9D=B4=EC=96=BC=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8,=20=EC=97=90=EB=9F=AC=EC=8A=A4=ED=81=AC=EB=A6=B0)?= =?UTF-8?q?=EC=9D=84=20=EC=B6=94=EA=B0=80=ED=95=A9=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ui/home/screen/HomeContract.kt | 4 ++- .../presentation/ui/home/screen/HomeScreen.kt | 25 ++++++++++++++++--- .../ui/home/screen/HomeViewModel.kt | 21 ++++++++-------- 3 files changed, 35 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeContract.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeContract.kt index 1a9d40d1..905d21f9 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeContract.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeContract.kt @@ -24,7 +24,8 @@ class HomeContract { val showInAppReviewPopup: Boolean = false, val showDraftNotificationPopup: Boolean = false, val showDraftNotificationToast: Boolean = false, - val errorMessage: String? = null, + val errorDialogMessage: String? = null, + val errorScreenMessage: String? = null, ) : MavericksState { val selectedDate: LocalDate = LocalDate.of(year, month, dayOfMonth) @@ -65,6 +66,7 @@ class HomeContract { data class UpdateDraftPopupFlag(val show: Boolean) : HomeIntent() data object DismissDraftNotificationToast : HomeIntent() data class UpdateInAppReviewFlag(val newValue: Boolean) : HomeIntent() + data object ResetErrorDialogMessage : HomeIntent() } sealed interface HomeSideEffect { diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt index e93f5935..328704ba 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt @@ -38,9 +38,11 @@ import com.airbnb.mvrx.compose.mavericksViewModel import com.sopt.clody.R import com.sopt.clody.core.review.InAppReviewManager import com.sopt.clody.domain.type.ReplyStatus +import com.sopt.clody.presentation.ui.component.FailureScreen import com.sopt.clody.presentation.ui.component.bottomsheet.DiaryDeleteSheet import com.sopt.clody.presentation.ui.component.button.ClodyButton import com.sopt.clody.presentation.ui.component.dialog.ClodyDialog +import com.sopt.clody.presentation.ui.component.dialog.FailureDialog import com.sopt.clody.presentation.ui.component.popup.ClodyPopupBottomSheet import com.sopt.clody.presentation.ui.component.timepicker.YearMonthPicker import com.sopt.clody.presentation.ui.component.toast.ClodyToastMessage @@ -128,10 +130,17 @@ fun HomeRoute( } } - HomeScreen( - state = state, - onIntent = { viewModel.postIntent(it) }, - ) + if (state.errorScreenMessage != null) { + FailureScreen( + message = state.errorScreenMessage!!, + confirmAction = { viewModel.postIntent(HomeContract.HomeIntent.InitializeInfo(state.year, state.month, state.dayOfMonth)) } + ) + } else { + HomeScreen( + state = state, + onIntent = { viewModel.postIntent(it) }, + ) + } } @Composable @@ -330,4 +339,12 @@ fun HomeScreen( }, ) } + + if (state.errorDialogMessage != null) { + FailureDialog( + message = state.errorDialogMessage, + confirmAction = { onIntent(HomeContract.HomeIntent.ResetErrorDialogMessage) }, + onDismiss = { onIntent(HomeContract.HomeIntent.ResetErrorDialogMessage) }, + ) + } } diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt index 3e43e2dd..7ad7855c 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt @@ -86,6 +86,7 @@ class HomeViewModel @AssistedInject constructor( is HomeContract.HomeIntent.UpdateDraftPopupFlag -> updateDraftPopupFlag(intent.show) is HomeContract.HomeIntent.DismissDraftNotificationToast -> setState { copy(showDraftNotificationToast = false) } is HomeContract.HomeIntent.UpdateInAppReviewFlag -> updateInAppReviewFlag(intent.newValue) + is HomeContract.HomeIntent.ResetErrorDialogMessage -> setState { copy(errorDialogMessage = null) } } } @@ -102,7 +103,7 @@ class HomeViewModel @AssistedInject constructor( setState { copy(calendarLoadState = UiLoadState.Loading, year = year, month = month) } if (networkConnectivityObserver.networkStatus.first() != NetworkStatus.Available) { - setState { copy(errorMessage = errorMessageProvider.getNetworkError()) } + setState { copy(errorScreenMessage = errorMessageProvider.getNetworkError()) } return } @@ -130,7 +131,7 @@ class HomeViewModel @AssistedInject constructor( setState { copy( calendarLoadState = UiLoadState.Error, - errorMessage = errorMessageProvider.getServerError(), + errorScreenMessage = errorMessageProvider.getServerError(), ) } }, @@ -141,7 +142,7 @@ class HomeViewModel @AssistedInject constructor( setState { copy(dailyDiaryLoadState = UiLoadState.Loading, dayOfMonth = dayOfMonth) } if (networkConnectivityObserver.networkStatus.first() != NetworkStatus.Available) { - setState { copy(errorMessage = errorMessageProvider.getNetworkError()) } + setState { copy(errorScreenMessage = errorMessageProvider.getNetworkError()) } return } @@ -161,7 +162,7 @@ class HomeViewModel @AssistedInject constructor( setState { copy( dailyDiaryLoadState = UiLoadState.Error, - errorMessage = errorMessageProvider.getServerError(), + errorScreenMessage = errorMessageProvider.getServerError(), ) } }, @@ -172,7 +173,7 @@ class HomeViewModel @AssistedInject constructor( setState { copy(diaryDeleteState = UiLoadState.Loading) } if (networkConnectivityObserver.networkStatus.first() != NetworkStatus.Available) { - setState { copy(errorMessage = errorMessageProvider.getNetworkError()) } + setState { copy(errorDialogMessage = errorMessageProvider.getNetworkError()) } return } @@ -182,7 +183,7 @@ class HomeViewModel @AssistedInject constructor( setState { copy(showDiaryDeleteDialog = false, diaryDeleteState = UiLoadState.Success) } }, onFailure = { - setState { copy(diaryDeleteState = UiLoadState.Error, errorMessage = errorMessageProvider.getServerError()) } + setState { copy(diaryDeleteState = UiLoadState.Error, errorDialogMessage = errorMessageProvider.getServerError()) } }, ) } @@ -190,7 +191,7 @@ class HomeViewModel @AssistedInject constructor( private suspend fun getNotificationInfoOrNull(): NotificationInfoResponseDto? = withContext(Dispatchers.IO) { notificationRepository.getNotificationInfo() } .getOrElse { - setState { copy(errorMessage = errorMessageProvider.getTemporaryError()) } + setState { copy(errorDialogMessage = errorMessageProvider.getTemporaryError()) } null } @@ -205,7 +206,7 @@ class HomeViewModel @AssistedInject constructor( private suspend fun sendNotification(granted: Boolean) { if (networkConnectivityObserver.networkStatus.first() != NetworkStatus.Available) { - setState { copy(errorMessage = errorMessageProvider.getNetworkError()) } + setState { copy(errorDialogMessage = errorMessageProvider.getNetworkError()) } return } @@ -222,7 +223,7 @@ class HomeViewModel @AssistedInject constructor( private suspend fun enableDraftAlarm() { if (networkConnectivityObserver.networkStatus.first() != NetworkStatus.Available) { - setState { copy(errorMessage = errorMessageProvider.getNetworkError()) } + setState { copy(errorDialogMessage = errorMessageProvider.getNetworkError()) } return } @@ -239,7 +240,7 @@ class HomeViewModel @AssistedInject constructor( setState { copy(showDraftNotificationPopup = false, showDraftNotificationToast = true) } }, onFailure = { - setState { copy(errorMessage = errorMessageProvider.getTemporaryError()) } + setState { copy(errorDialogMessage = errorMessageProvider.getTemporaryError()) } }, ) } From 6e447fcc5ee2f2d1b683df06b0eb4a9a8b3ca34d Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Thu, 9 Oct 2025 20:38:32 +0900 Subject: [PATCH 18/20] =?UTF-8?q?[FEAT/#323]=20AirBridge=20=EB=94=A5?= =?UTF-8?q?=EB=A7=81=ED=81=AC=20=EC=97=B0=EA=B2=B0=EC=9D=84=20=EC=9C=84?= =?UTF-8?q?=ED=95=9C=20=EC=84=B8=ED=8C=85=EC=9E=85=EB=8B=88=EB=8B=A4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/AndroidManifest.xml | 34 +++++++++++++++++++ .../com/sopt/clody/ClodyDeeplinkActivity.kt | 23 +++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 app/src/main/java/com/sopt/clody/ClodyDeeplinkActivity.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 95012ebc..82b9aef7 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -64,6 +64,40 @@ android:scheme="${kakaoRedirectUri}" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/java/com/sopt/clody/ClodyDeeplinkActivity.kt b/app/src/main/java/com/sopt/clody/ClodyDeeplinkActivity.kt new file mode 100644 index 00000000..4a7a1b07 --- /dev/null +++ b/app/src/main/java/com/sopt/clody/ClodyDeeplinkActivity.kt @@ -0,0 +1,23 @@ +package com.sopt.clody + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import co.ab180.airbridge.Airbridge + +class ClodyDeeplinkActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + } + + override fun onResume() { + super.onResume() + + val isFirstCalled = Airbridge.handleDeferredDeeplink { uri -> + // when handleDeferredDeeplink is called firstly after install + if (uri != null) { + // show proper content using uri (YOUR_SCHEME://...) + } + } + } +} From 84632789a4c0a0b74179b744bd6a2d6d91c86127 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Thu, 9 Oct 2025 20:39:42 +0900 Subject: [PATCH 19/20] [CHORE/#323] KtlintFormat --- .../com/sopt/clody/presentation/ui/home/component/DailyDiary.kt | 1 - .../com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt | 2 +- .../com/sopt/clody/presentation/utils/extension/TimeZoneExt.kt | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/component/DailyDiary.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/DailyDiary.kt index a322f451..06ac528a 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/component/DailyDiary.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/DailyDiary.kt @@ -8,7 +8,6 @@ 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.padding import androidx.compose.foundation.shape.RoundedCornerShape diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt index 328704ba..2a638d39 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeScreen.kt @@ -133,7 +133,7 @@ fun HomeRoute( if (state.errorScreenMessage != null) { FailureScreen( message = state.errorScreenMessage!!, - confirmAction = { viewModel.postIntent(HomeContract.HomeIntent.InitializeInfo(state.year, state.month, state.dayOfMonth)) } + confirmAction = { viewModel.postIntent(HomeContract.HomeIntent.InitializeInfo(state.year, state.month, state.dayOfMonth)) }, ) } else { HomeScreen( diff --git a/app/src/main/java/com/sopt/clody/presentation/utils/extension/TimeZoneExt.kt b/app/src/main/java/com/sopt/clody/presentation/utils/extension/TimeZoneExt.kt index 91476516..92ba5137 100644 --- a/app/src/main/java/com/sopt/clody/presentation/utils/extension/TimeZoneExt.kt +++ b/app/src/main/java/com/sopt/clody/presentation/utils/extension/TimeZoneExt.kt @@ -73,7 +73,7 @@ fun convertDateToKstDateTime(year: Int, month: Int, day: Int): String { val userNow = ZonedDateTime.now(userZone) val userDateTime = LocalDateTime.of( LocalDate.of(year, month, day), - userNow.toLocalTime() + userNow.toLocalTime(), ).atZone(userZone) val kstDateTime = userDateTime.withZoneSameInstant(kstZone) From a48077e042b78806e18666847e02f8caba308e54 Mon Sep 17 00:00:00 2001 From: SYAAINN Date: Thu, 9 Oct 2025 21:11:15 +0900 Subject: [PATCH 20/20] =?UTF-8?q?[CHORE/#323]=20=EC=BD=94=EB=93=9C?= =?UTF-8?q?=EB=9E=98=EB=B9=97=20=EB=A6=AC=EB=B7=B0=20=EC=82=AC=ED=95=AD=20?= =?UTF-8?q?=EB=B0=98=EC=98=81=20-=20=EC=97=90=EC=96=B4=EB=B8=8C=EB=A6=BF?= =?UTF-8?q?=EC=A7=80=20scheme=20=EC=84=A4=EC=A0=95=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?-=20=EC=97=90=EB=9F=AC=EC=8A=A4=ED=81=AC=EB=A6=B0=20=EB=A9=94?= =?UTF-8?q?=EC=8B=9C=EC=A7=80=20=EC=B4=88=EA=B8=B0=ED=99=94=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 --- app/src/main/AndroidManifest.xml | 2 +- .../clody/presentation/ui/home/component/MonthlyCalendar.kt | 2 +- .../com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 82b9aef7..b48f35ee 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -75,7 +75,7 @@ - + diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/component/MonthlyCalendar.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/MonthlyCalendar.kt index 24306ba2..8dea7ce4 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/component/MonthlyCalendar.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/component/MonthlyCalendar.kt @@ -30,7 +30,7 @@ import com.sopt.clody.presentation.ui.type.DailyCloverType import com.sopt.clody.presentation.utils.amplitude.AmplitudeConstraints import com.sopt.clody.presentation.utils.amplitude.AmplitudeUtils import com.sopt.clody.ui.theme.ClodyTheme -import kotlinx.datetime.DayOfWeek +import java.time.DayOfWeek import java.time.LocalDate import java.time.format.TextStyle diff --git a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt index 7ad7855c..f9ee614f 100644 --- a/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt +++ b/app/src/main/java/com/sopt/clody/presentation/ui/home/screen/HomeViewModel.kt @@ -123,6 +123,7 @@ class HomeViewModel @AssistedInject constructor( ) }, ), + errorScreenMessage = null, ) } loadDailyDiaryInfo(year, month, dayOfMonth) @@ -155,6 +156,7 @@ class HomeViewModel @AssistedInject constructor( diaryList = data.diaries.map { it.content }, isDraft = data.isDraft, ), + errorScreenMessage = null, ) } },